Task-uri în Fundal în .NET MAUI 2026: Ghid WorkManager și BGTaskScheduler
Ghid practic pentru task-uri în fundal în .NET MAUI: WorkManager pe Android 14+ cu foregroundServiceType, BGTaskScheduler pe iOS cu BGAppRefreshTask și BGProcessingTask, plus cod C# funcțional și configurări corecte de manifest.
Task-urile în fundal în .NET MAUI nu au un API unificat. Pe Android folosești WorkManager (opțional cu un Foreground Service pentru execuție imediată), iar pe iOS folosești BGTaskScheduler cu BGAppRefreshTask sau BGProcessingTask. Microsoft.Extensions.Hosting.BackgroundServicenu funcționează când aplicația este în background pe iOS, așa că orice soluție reală implică cod separat sub #if ANDROID și #if IOS, plus configurări în AndroidManifest.xml și Info.plist. Am spart destule cicluri de sincronizare la 2 dimineața ca să pot spune cu certitudine: dacă ignori regulile fiecărei platforme, OS-ul îți omoară task-ul înainte să se conecteze la server.
Pe Android 14+ (API 34) trebuie să declari foregroundServiceType în manifest, altfel primești MissingForegroundServiceTypeException la startForeground().
Pe iOS, identificatorii task-urilor merg obligatoriu în BGTaskSchedulerPermittedIdentifiers din Info.plist, iar înregistrarea se face în FinishedLaunching, nu mai târziu.
BGAppRefreshTask primește aproximativ 30 de secunde de execuție. BGProcessingTask primește câteva minute, dar rulează doar când device-ul e la încărcare și pe Wi-Fi.
WorkManager pe Android este soluția default pentru muncă deferabilă. Folosește Foreground Service doar când utilizatorul așteaptă rezultatul imediat (playback, transfer în curs, tracking GPS).
Serviciile DI înregistrate în MauiProgram.cs nu se rezolvă automat dintr-un Worker pornit când aplicația e killed. Trebuie să reconstruiești graful sau să folosești un ServiceProvider static.
De ce BackgroundService din .NET nu funcționează pe iOS
Prima greșeală pe care o văd la echipele care vin din ASP.NET e să înregistreze un IHostedService sau să extindă Microsoft.Extensions.Hosting.BackgroundService în MauiProgram.cs și să presupună că va rula când aplicația e minimizată. Nu va rula. Pe iOS, procesul aplicației este suspendat aproape imediat după ce utilizatorul revine la home screen, iar toate thread-urile managed sunt înghețate. Pe Android situația e ceva mai permisivă istoric, dar începând cu API 26 (Android 8), Google a impus restricții stricte pentru serviciile de fundal care nu au notificare vizibilă.
Ce înseamnă asta practic? Un Timer sau un Task.Delay(...).ContinueWith(...) pornit din codul share-uit nu supraviețuiește backgrounding-ului. Nici măcar un handler de push notification silent nu îți garantează execuție, iar iOS îl poate ignora dacă nu ești "un app pe care userul îl deschide des". Singurele API-uri care chiar funcționează sunt cele oferite de OS: WorkManager (Android), BGTaskScheduler (iOS) și, pentru Windows, BackgroundService normal din Microsoft.Extensions.Hosting (Windows nu killează procesele desktop la fel de agresiv).
Concluzia arhitecturală, sinceră: scrie logica de business share-uită într-un serviciu C# obișnuit (ex. ISyncService.RunAsync(CancellationToken)) și apelează-l din wrappers platform-specifice care rulează sub API-ul nativ potrivit. Așa reușești să testezi logica cu xUnit fără să ai nevoie de emulator.
Android: WorkManager cu worker C# și constrângeri
WorkManager este API-ul recomandat de Google pentru orice muncă deferabilă, precum sincronizare, upload de log-uri, backup zilnic. Se instalează prin NuGet:
Un Worker în C# derivează din AndroidX.Work.Worker, care la rândul lui derivează din Java.Lang.Object. Runtime-ul .NET for Android generează în spate un shim Java care mapează metodele virtuale înapoi la codul managed prin JNI. E și motivul pentru care constructorul trebuie să respecte semnătura așteptată de WorkManager (context și WorkerParameters). Iată un worker complet de sincronizare:
using Android.Content;
using AndroidX.Work;
using Microsoft.Extensions.DependencyInjection;
namespace MyApp.Platforms.Android.Background;
public class SyncWorker : Worker
{
public const string WorkTag = "sync-worker";
public SyncWorker(Context context, WorkerParameters workerParams)
: base(context, workerParams) { }
public override Result DoWork()
{
try
{
// Serviciile DI din MauiProgram NU sunt disponibile automat
// aici cand aplicatia e killed. Reconstruieste-le sau expune-le
// printr-un ServiceProvider static.
var services = IPlatformApplication.Current?.Services
?? throw new InvalidOperationException("ServiceProvider missing");
var sync = services.GetRequiredService<ISyncService>();
sync.RunAsync(CancellationToken.None).GetAwaiter().GetResult();
return Result.InvokeSuccess();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"SyncWorker failed: {ex}");
return Result.InvokeRetry(); // WorkManager va reincerca cu backoff exponential
}
}
}
Enqueue-ul se face din codul platform-specific, cu constrângeri clare pentru rețea și baterie:
public static class SyncScheduler
{
public static void ScheduleDaily()
{
var constraints = new Constraints.Builder()
.SetRequiredNetworkType(NetworkType.Unmetered) // doar Wi-Fi
.SetRequiresBatteryNotLow(true)
.Build();
var request = PeriodicWorkRequest.Builder.From<SyncWorker>(
repeatInterval: TimeSpan.FromHours(6))
.SetConstraints(constraints)
.SetBackoffCriteria(BackoffPolicy.Exponential,
OneTimeWorkRequest.MinBackoffMillis,
Java.Util.Concurrent.TimeUnit.Milliseconds!)
.AddTag(SyncWorker.WorkTag)
.Build();
WorkManager
.GetInstance(Microsoft.Maui.ApplicationModel.Platform.AppContext)
.EnqueueUniquePeriodicWork(
SyncWorker.WorkTag,
ExistingPeriodicWorkPolicy.Keep,
request);
}
}
PeriodicWorkRequest are un interval minim de 15 minute. Dacă îi ceri mai puțin, Android îl ridică tăcut. Pentru task-uri one-shot (ex. upload imediat după ce userul apasă "Save"), folosește OneTimeWorkRequest. Constrângerea SetRequiredNetworkType(NetworkType.Unmetered) e cea pe care majoritatea o omit, apoi se plâng că utilizatorii ard datele mobile pe sincronizări nedorite.
Android 14+: Foreground Service și foregroundServiceType
WorkManager amână execuția până când sistemul decide că e momentul potrivit. Dar dacă ai nevoie de execuție imediată și continuă (streaming muzical, tracking GPS în timpul unei livrări, apel VoIP), ai nevoie de un Foreground Service. Începând cu Android 14 (API 34), declararea unui foregroundServiceType este obligatorie, altfel primești MissingForegroundServiceTypeException în momentul apelării startForeground().
Iată un serviciu minimal pentru location tracking în MAUI, pus în Platforms/Android/Services/LocationForegroundService.cs:
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.OS;
using AndroidX.Core.App;
namespace MyApp.Platforms.Android.Services;
[Service(
Exported = false,
ForegroundServiceType = ForegroundService.TypeLocation)]
public class LocationForegroundService : Service
{
const int NotificationId = 1001;
const string ChannelId = "location_channel";
public override IBinder? OnBind(Intent? intent) => null;
public override StartCommandResult OnStartCommand(
Intent? intent, StartCommandFlags flags, int startId)
{
CreateChannelIfNeeded();
var notification = new NotificationCompat.Builder(this, ChannelId)
.SetContentTitle("Livrare in curs")
.SetContentText("Urmarim locatia pentru navigare.")
.SetSmallIcon(Resource.Drawable.ic_delivery)
.SetOngoing(true)
.Build();
// TREBUIE apelat in sub 10 secunde de la OnStartCommand,
// altfel: ForegroundServiceDidNotStartInTimeException
if (OperatingSystem.IsAndroidVersionAtLeast(34))
{
StartForeground(
NotificationId,
notification,
ForegroundService.TypeLocation);
}
else
{
StartForeground(NotificationId, notification);
}
// Porneste logica de tracking aici (nu blocking!)
_ = Task.Run(TrackLocationLoop);
return StartCommandResult.Sticky;
}
void CreateChannelIfNeeded()
{
if (!OperatingSystem.IsAndroidVersionAtLeast(26)) return;
var manager = (NotificationManager)GetSystemService(NotificationService)!;
if (manager.GetNotificationChannel(ChannelId) is not null) return;
var channel = new NotificationChannel(
ChannelId, "Location tracking", NotificationImportance.Low);
manager.CreateNotificationChannel(channel);
}
async Task TrackLocationLoop() { /* ... */ }
}
În AndroidManifest.xml declari permisiunea de tip corespunzătoare, altfel startForeground() aruncă SecurityException:
iOS: BGTaskScheduler, BGAppRefreshTask și BGProcessingTask
Pe iOS, framework-ul BackgroundTasks (introdus la WWDC 2019, iOS 13+) este singura cale sancționată de Apple pentru muncă recurentă în absența utilizatorului. Are două clase principale:
BGAppRefreshTask, pentru fetch-uri scurte (feed, mail, cursuri). Buget: circa 30 de secunde. Limită: un singur task refresh programat în orice moment.
BGProcessingTask, pentru muncă grea (indexare, curățare DB, antrenament Core ML). Buget: câteva minute. Poate cere RequiresExternalPower și RequiresNetworkConnectivity. Limită: până la 10 task-uri programate simultan.
Codul se pune în Platforms/iOS/AppDelegate.cs pentru că înregistrarea trebuie să se termine înainte ca FinishedLaunching să returneze. Documentația Apple pentru BGTaskScheduler e explicită aici. Înregistrare duplicată pentru același identifier crashează procesul cu mesajul "Launch handler for task with identifier ... has already been registered".
using BackgroundTasks;
using Foundation;
using UIKit;
namespace MyApp;
[Register(nameof(AppDelegate))]
public class AppDelegate : MauiUIApplicationDelegate
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
const string RefreshId = "com.myapp.refresh";
const string ProcessingId = "com.myapp.cleanup";
public override bool FinishedLaunching(
UIApplication application, NSDictionary launchOptions)
{
BGTaskScheduler.Shared.Register(
RefreshId, null, task => HandleRefresh((BGAppRefreshTask)task));
BGTaskScheduler.Shared.Register(
ProcessingId, null, task => HandleProcessing((BGProcessingTask)task));
return base.FinishedLaunching(application, launchOptions);
}
void HandleRefresh(BGAppRefreshTask task)
{
ScheduleNextRefresh(); // reprogrameaza IMEDIAT, altfel nu se mai repeta
var cts = new CancellationTokenSource();
task.ExpirationHandler = () => cts.Cancel();
Task.Run(async () =>
{
try
{
var services = IPlatformApplication.Current!.Services;
var sync = services.GetRequiredService<ISyncService>();
await sync.QuickPullAsync(cts.Token);
task.SetTaskCompleted(true);
}
catch
{
task.SetTaskCompleted(false);
}
});
}
static void ScheduleNextRefresh()
{
var request = new BGAppRefreshTaskRequest(RefreshId)
{
EarliestBeginDate = NSDate.FromTimeIntervalSinceNow(15 * 60) // 15 min
};
BGTaskScheduler.Shared.Submit(request, out _);
}
void HandleProcessing(BGProcessingTask task) { /* similar */ }
}
iOS 26 a adăugat BGContinuedProcessingTask, un tip nou care pornește din foreground și continuă în background. E util pentru upload-uri lungi începute după un tap al utilizatorului. Bindings-urile pentru .NET MAUI apar în net10.0-ios.
Configurarea corectă a Info.plist și AndroidManifest.xml
Configurarea corectă a fișierelor de manifest este partea unde majoritatea build-urilor pică la App Store Review. Sincer, cam jumătate din bug-urile pe care le-am prins în review-uri au fost aici. Pentru iOS, în Platforms/iOS/Info.plist, adaugă atât modul de fundal cât și lista de identificatori permiși:
Eroarea clasică "Missing Info.plist value. The Info.plist key BGTaskSchedulerPermittedIdentifiers must contain..." apare când ai UIBackgroundModes setat pe processing dar nu ai adăugat identificatorii. Identificatorii trebuie să corespundă exact cu string-urile pasate la BGTaskScheduler.Shared.Register(). Un typo aici înseamnă că task-ul nu se înregistrează.
Pentru Android, în Platforms/Android/AndroidManifest.xml, dacă folosești WorkManager și Android 14+, trebuie să suprascrii tipul serviciului implicit al WorkManager:
În codul workerului, când vrei să promovezi un worker la foreground (pentru upload-uri lungi), folosești setForegroundAsync cu un ForegroundInfo care conține și serviceType. Obligatoriu pe API 34+.
Abstracție cross-platform pentru scheduling
Din experiența mea, cel mai curat pattern e un serviciu IBackgroundJobScheduler în proiectul share-uit, cu implementări separate. Codul share-uit apelează metode agnostice, iar restul e izolat sub #if. Interfața minimală arată așa:
Această separare îți permite să testezi ViewModel-urile fără să atingi API-ul nativ. Când integrezi cu sincronizarea offline-first cu SQLite în .NET MAUI, singurul cod share-uit este ISyncService.RunAsync, iar scheduling-ul rămâne curat pe fiecare platformă.
Un detaliu subtil, pe care l-am ratat în primul proiect care a folosit MAUI: când WorkManager pornește un worker cu aplicația în stare killed, IPlatformApplication.Current e populat de runtime dar MauiApp-ul nu are ciclu de viață complet. Serviciile scoped nu funcționează, așa că folosește doar singletons și transients pure. Dacă ai nevoie de un logger persistent, injectează unul care nu depinde de IServiceScope.
Cum testezi și debugghezi task-urile de fundal
Task-urile de fundal sunt notoriu greu de testat pentru că sistemul decide când rulează. Ambele platforme oferă instrumente specifice.
Android: WorkManager Inspector și adb
Android Studio (>= Flamingo) include WorkManager Inspector. Vezi în timp real ce workere sunt Enqueued, Running sau Finished, împreună cu constrângerile care blochează execuția. Pentru testare din CLI:
# Forteaza workerele sa ruleze indiferent de constrangeri
adb shell cmd jobscheduler run -f com.myapp.package 999
# Vezi log-urile WorkManager
adb logcat -s WM-WorkerWrapper WM-WorkSpec
iOS: BGTaskScheduler debug hooks
Xcode oferă două comenzi debugger care declanșează task-urile la cerere. Rulezi aplicația din Xcode, pauzezi debugger-ul, și tastezi în consolă:
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.myapp.refresh"]
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.myapp.refresh"]
Prima simulează lansarea (utilă pentru a testa cazul happy-path), a doua simulează expirarea (testezi cleanup-ul și ExpirationHandler). Aceste hook-uri funcționează doar pe device real. Simulatorul nu execută BGTaskScheduler în mod real.
Când să nu folosești task-uri de fundal
Task-urile de fundal sunt scumpe. Consumă baterie, ard trafic mobil, și, cel mai important, nu au SLA de execuție. Dacă utilizatorul trebuie să vadă un rezultat la un moment specific, nu te baza pe ele. Alternativele mai bune:
Sincronizare la deschiderea aplicației: mult mai fiabil decât un fetch background care poate să nu se execute niciodată dacă userul nu deschide app-ul des.
Server-side scheduled jobs: dacă poți muta logica în backend (cron pe Azure Functions, EventBridge pe AWS), scapi de tot dansul cu OS-ul mobil.
Regula mea empirică, învățată la costuri: dacă operațiunea nu necesită accesul la fișiere locale ale device-ului sau la senzori, o mut pe server. Task-urile de fundal client-side sunt pentru sincronizarea datelor generate local (foto-uri, măsurători de senzori, log-uri offline). Nu sunt pentru "poll-uri la 5 minute". Pentru optimizări suplimentare de baterie și memorie, consultă și ghidul complet de optimizare a performanței în .NET MAUI.
Întrebări frecvente
Cât timp poate rula o aplicație .NET MAUI în fundal pe iOS?
Depinde de tipul task-ului. Un BGAppRefreshTask primește aproximativ 30 de secunde de execuție înainte ca sistemul să apeleze ExpirationHandler. Un BGProcessingTask primește câteva minute (Apple nu documentează un număr exact, dar în practică 4-10 minute), și doar dacă device-ul e la încărcare și pe Wi-Fi în cazul în care ai setat RequiresExternalPower.
De ce nu funcționează BackgroundService din Microsoft.Extensions.Hosting pe iOS?
Pentru că iOS suspendă procesul aplicației aproape imediat după ce trece în background, iar toate thread-urile managed sunt înghețate de sistemul de operare. BackgroundService este proiectat pentru procese long-running de tip server (Kestrel, Windows Service) unde OS-ul nu suspendă procesul. Pe iOS trebuie obligatoriu să folosești BGTaskScheduler, care e API-ul sancționat de Apple.
Care este diferența între BGAppRefreshTask și BGProcessingTask?
BGAppRefreshTask este pentru fetch-uri scurte (feed, mail, prețuri) cu buget aproximativ 30 secunde. Poți avea un singur task refresh programat. BGProcessingTask este pentru muncă grea (indexare, ML training, backup) cu câteva minute buget, poate cere sursă de alimentare și rețea specifică, și poți avea până la 10 astfel de task-uri programate simultan.
Cum configurez foregroundServiceType pe Android 14?
Începând cu API 34, în AndroidManifest.xml declari atributul android:foregroundServiceType pe elementul <service> (ex. location, mediaPlayback, dataSync, shortService). În plus adaugi permisiunea corespunzătoare FOREGROUND_SERVICE_<TYPE>. La apelul StartForeground() pasezi și tipul explicit ca al treilea argument. Fără declararea corectă, primești MissingForegroundServiceTypeException sau SecurityException.
Pot folosi WorkManager pentru muncă imediată în .NET MAUI?
WorkManager e proiectat pentru muncă deferabilă. Sistemul decide când rulează, respectând constrângerile setate. Pentru execuție imediată și continuă (streaming, GPS, apel VoIP), folosește un Foreground Service dedicat cu notificare vizibilă. Un OneTimeWorkRequest fără constrângeri rulează de obicei rapid, dar nu ai garanție de timing sub 10 secunde.
Ghid practic 2026 pentru accesibilitatea .NET MAUI: SemanticProperties, VoiceOver, TalkBack, WCAG 2.2, Dynamic Type și target-uri tactile, cu exemple XAML gata de folosit pentru iOS și Android.
Ghid complet pentru custom handlers în .NET MAUI 2026: învață cum funcționează arhitectura pe două straturi, cum modifici mappers, cum construiești un handler nou pe iOS și Android și cum eviți memory leaks în producție.