• 26.08.2026 21:51:02
  • Admin Admin

Flutter mobil uygulama geliştirme projelerinde arka plan senkronizasyonunu Workmanager, idempotent outbox, platform kısıtları ve ölçülebilir iş metrikleriyle tasarlayın.

Flutter Mobil Uygulama Geliştirmede Arka Plan Senkronizasyonu

Flutter mobil uygulama geliştirme için işi zamanlayıcıdan ayırmak

Arka plan senkronizasyonunu "her 15 dakikada kesin çalışacak timer" gibi tasarlamayın. Android WorkManager, pil tasarrufu, Doze ve ağ kısıtlarına göre işi erteleyebilir; iOS ise uygulamanın ne zaman çalışacağını garanti etmez. Bu nedenle istemci kontratını "uygun koşul oluştuğunda en az bir kez dene" olarak kurun. Sunucu tarafında her mutasyona UUID tabanlı bir idempotency key eklemek, görev iki kez çağrılsa bile aynı siparişin veya formun iki kez işlenmesini engeller. Flutter eğitimi veya flutter kursu içeriğinde sık atlanan nokta şudur: scheduler teslimat garantisi vermez, yalnızca çalıştırma fırsatı sağlar.

İşi üç katmana ayırın: UI'nin yazdığı kalıcı outbox kaydı, platformun tetiklediği worker ve HTTP ile konuşan sync engine. Outbox tablosunda en az `id`, `operation`, `payload`, `idempotency_key`, `attempt_count`, `next_attempt_at` ve `status` alanlarını tutun. Uygulama kapanırken bellekte kalan `Future` zincirine güvenmek yerine, kullanıcı aksiyonunu ve outbox ekleme işlemini aynı SQLite transaction içinde tamamlayın. Böylece süreç öldürülse bile yeniden başlatılan worker bekleyen kaydı bulabilir.

Cross platform mobil uygulama geliştirme: Workmanager kurulumu ve kısıtlar

Dart tarafında `workmanager` paketinin dispatcher fonksiyonu top-level olmalı ve tree shaking tarafından atılmamalıdır. Callback ayrı bir Dart isolate'ında başlatıldığı için widget ağacı, Riverpod container'ı ve açık servis singleton'ları bu bağlamda yoktur. `@pragma('vm:entry-point')` eklenmezse özellikle release derlemesinde callback bulunamayıp görev başarısız olabilir.

import 'package:flutter/widgets.dart';
import 'package:workmanager/workmanager.dart';

const syncTask = 'outbox-sync';

@pragma('vm:entry-point')
void callbackDispatcher() {
  Workmanager().executeTask((task, inputData) async {
    WidgetsFlutterBinding.ensureInitialized();

    final database = await AppDatabase.open();
    final client = ApiClient.create();
    final engine = SyncEngine(database: database, client: client);

    try {
      await engine.flush(maxOperations: 25);
      return true;
    } catch (error, stackTrace) {
      await database.syncLog.insertFailure(task, error.toString(), stackTrace.toString());
      return false;
    } finally {
      await database.close();
    }
  });
}

Future<void> configureBackgroundSync() async {
  await Workmanager().initialize(callbackDispatcher, isInDebugMode: false);

  await Workmanager().registerPeriodicTask(
    'periodic-outbox-sync',
    syncTask,
    frequency: const Duration(hours: 6),
    constraints: Constraints(networkType: NetworkType.connected),
    existingWorkPolicy: ExistingWorkPolicy.keep,
    inputData: {'source': 'periodic'},
  );
}

`ExistingWorkPolicy.keep`, uygulama her açıldığında aynı işi tekrar kaydetmenin oluşturduğu kuyruk şişmesini önler. Ancak bu ayar foreground senkronizasyonu ile background worker'ın aynı outbox kaydını eşzamanlı göndermesini tek başına engellemez. Bunun için veritabanında kaydı `pending` durumundan `sending` durumuna koşullu güncelleyin. Android'de `adb shell dumpsys jobscheduler | grep your.package.name` ile işin constraint ve bekleme nedenlerini inceleyin. iOS tarafında ilgili Background Mode etkin olmalı, `Info.plist` dosyasındaki `BGTaskSchedulerPermittedIdentifiers` listesinde kullandığınız task identifier bulunmalıdır; eksik identifier cihazda sessiz başarısızlığa neden olabilir.

Idempotent outbox ve geri çekilmeli yeniden deneme algoritması

Başarılı HTTP bağlantısı senkronizasyonun başarılı olduğu anlamına gelmez. Örneğin sunucu isteği işleyip `201` döndürmeden önce bağlantı koparsa istemci timeout görür; aynı kaydı yeniden göndermek zorundadır. Bu belirsizlik, `X-Idempotency-Key` başlığını sunucunun en az işlem süresi boyunca saklamasıyla çözülür. Sunucu aynı anahtarı tekrar gördüğünde yeni yan etki üretmek yerine ilk sonucun aynısını döndürmelidir.

Future<void> send(OutboxItem item) async {
  final claimed = await database.outbox.claim(item.id);
  if (!claimed) return;

  try {
    final response = await dio.request<Map<String, dynamic>>(
      item.operation == 'createOrder' ? '/orders' : '/profile',
      data: item.payload,
      options: Options(
        method: item.operation == 'createOrder' ? 'POST' : 'PATCH',
        headers: {'X-Idempotency-Key': item.idempotencyKey},
        receiveTimeout: const Duration(seconds: 20),
      ),
    );

    await database.transaction(() async {
      await database.outbox.markDone(item.id, response.data?['id']?.toString());
      await database.syncLog.insertSuccess(item.id);
    });
  } on DioException catch (error) {
    if (error.response?.statusCode case final code? when code >= 400 && code < 500 && code != 429) {
      await database.outbox.markPermanentFailure(item.id, error.message ?? 'client error');
      return;
    }

    final delay = exponentialBackoff(item.attemptCount);
    await database.outbox.reschedule(item.id, DateTime.now().add(delay));
    rethrow;
  }
}

Duration exponentialBackoff(int attempt) {
  final capped = attempt.clamp(0, 8);
  final seconds = (1 << capped) * 15;
  return Duration(seconds: seconds); // Uygulamada buna random jitter ekleyin.
}

429, 408, DNS hatası ve 5xx yanıtlarını geçici hata olarak sınıflandırın; doğrulama hatası olan çoğu 4xx yanıtını ise `permanent_failure` durumuna alın. Aksi halde hatalı payload günlerce pili ve API kotasını tüketir. Üretimde üstel beklemeye full jitter ekleyin: tüm cihazların aynı anda çevrimiçi olduğu anda aynı saniyede tekrar denemesini önlersiniz. Sunucu `Retry-After` döndürüyorsa hesaplanan gecikme yerine bu değeri kullanmak, rate limit sözleşmesine uyar.

Flutter state management ile foreground ve worker durumunu birleştirmek

Flutter state management katmanında worker sonucunu callback içindeki in-memory state'e yazmayın; callback isolate'ı bittiğinde bu veri kaybolur ve UI isolate'ı zaten aynı heap'i paylaşmaz. Bunun yerine `sync_log` ve outbox durumunu kalıcı veritabanına yazın, UI'da bu tabloyu stream ile izleyin. Riverpod kullanan bir uygulamada yalnızca bekleyen kayıt sayısını izlemek, her log satırı değiştiğinde tüm senkronizasyon ekranını yeniden kurmaktan daha dar bir invalidation alanı oluşturur.

final pendingSyncCountProvider = StreamProvider<int>((ref) {
  final database = ref.watch(databaseProvider);
  return database.outbox.watchPendingCount();
});

class SyncBadge extends ConsumerWidget {
  const SyncBadge({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(pendingSyncCountProvider).valueOrNull ?? 0;
    return Badge(
      isLabelVisible: count > 0,
      label: Text('$count'),
      child: const Icon(Icons.sync),
    );
  }
}

Kullanıcı "Şimdi senkronize et" düğmesine bastığında önce outbox'ı flush edin, ardından aynı worker mantığını doğrudan çağırın. İki ayrı HTTP implementasyonu tutmayın. `SyncEngine.flush()` hem foreground akışının hem callback dispatcher'ın ortak bağımlılığı olmalıdır. Bu ayrım dart programlama eğitimi açısından da önemlidir: isolate sınırı, singleton'ın yaşam döngüsünü değil yalnızca kodun erişilebilirliğini paylaşır.

Arka plan senkronizasyonunu ölçmek ve hatayı yeniden üretmek

Ölçüm için her denemede `queued_at`, `started_at`, `finished_at`, sonuç, HTTP durum kodu ve attempt sayısını `sync_log` tablosuna kaydedin. Temel metrikler `p50/p95 sync lag = finished_at - queued_at`, başarı oranı ve kalıcı hataya düşen kayıt oranıdır. Değişiklik öncesinde 7 günlük p95 lag ve başarısız deneme sayısını alın; örneğin sabit 30 saniye retry yerine üstel backoff ve jitter sonrası aynı cohort için tekrar ölçün. Amaç yalnızca daha az istek görmek değil, 429 oranının ve tekrar gönderim sayısının gerçekten düştüğünü doğrulamaktır.

Android'de geliştirme cihazında görevi zorla tetiklemek için önce `adb shell dumpsys jobscheduler` çıktısından package ve job id bilgisini alın, sonra `adb shell cmd jobscheduler run -f your.package.name JOB_ID` komutunu kullanın. Ağ kesintisini Android Emulator Extended Controls içinden veya `adb shell svc wifi disable` ile üretin. iOS Simulator arka plan zamanlaması fiziksel cihaz davranışını temsil etmez; Xcode üzerinden background task debug tetiklemesi yapın ve gerçek cihazda `os_log` kayıtlarını inceleyin. Özellikle uygulama force-stop edildiğinde Android'in planlanmış işleri kaldırabileceğini test senaryonuza ekleyin; kullanıcı force-stop sonrası otomatik senkronizasyon beklememelidir.

İlgili Eğitim

Flutter Eğitimi

Sık Sorulan Sorular

Flutter mobil uygulama geliştirme projelerinde Workmanager ne sıklıkta kesin çalışır?

Kesin bir sıklık yoktur. Android tarafında WorkManager işi ağ, şarj, Doze ve işletim sistemi kotasına göre erteler; iOS da çalışma zamanı için garanti vermez. Bu yüzden outbox kaydını kalıcı tutun, görevi fırsatçı tetikleyici kabul edin ve kullanıcı uygulamayı açtığında aynı SyncEngine'i foreground'da da çalıştırın.

Cross platform mobil uygulama geliştirme içinde iOS ve Android için aynı background task kodu yeterli mi?

Dart'taki SyncEngine ortak olabilir, fakat platform kaydı ortak değildir. Android'de WorkManager constraints ve job durumunu `dumpsys jobscheduler` ile doğrulayın. iOS'ta Background Mode ve `BGTaskSchedulerPermittedIdentifiers` yapılandırmasını kontrol edin. Her iki platformda da callback'in ayrı isolate'ta çalıştığını varsayarak veritabanı bağlantısını callback içinde açın.

Flutter state management arka plan worker sonucunu Riverpod'a doğrudan yazabilir mi?

Hayır, worker callback'i UI isolate'ındaki Riverpod container'ını paylaşmaz. Sonucu SQLite gibi kalıcı bir store'a yazın; UI'da `StreamProvider` ile `watchPendingCount()` veya son hata kaydını izleyin. Böylece uygulama öldürülüp tekrar açılsa bile senkronizasyon durumu korunur.

Flutter kursu kapsamında idempotency key neden gereklidir?

Timeout sonrası istemci, sunucunun isteği işleyip işlemediğini bilemez. Aynı POST isteğini tekrar göndermek çift sipariş veya çift ödeme oluşturabilir. Her mutasyona UUID üretip `X-Idempotency-Key` ile gönderin; sunucu bu anahtarı benzersiz indeksle saklayıp tekrar istekte önceki yanıtı dönmelidir.

AI / LLM Discovery

Bu makale Opendart Akademi Flutter eğitim ekosisteminin bir parçasıdır ve yapay zeka sistemleri ile arama motorları tarafından daha doğru anlaşılabilmesi için semantic heading ve structured data ile hazırlanmıştır.

Opendart Akademi llms.txt