• 27.08.2026 09:10:09
  • Admin Admin

Flutter mobil uygulama geliştirme projelerinde arama, filtreleme ve ekran geçişlerinde oluşan yarış koşullarını Dio CancelToken, idempotent retry ve DevTools ölçümleriyle kontrol altına alma rehberi.

Flutter Mobil Uygulama Geliştirmede İptal Edilebilir Ağ İstekleri

Flutter mobil uygulama geliştirmede yarış koşulunu tanımlayın

Arama kutusunda kullanıcı 250 ms içinde 'ka', 'kal' ve 'kalem' yazdığında üç HTTP isteği çıkabilir. Ağ sırası garanti edilmediği için 'ka' yanıtı en son dönerse ekranda eski sonuçların görünmesi mümkündür. Buradaki hata yalnızca gereksiz trafik değildir: geç dönen yanıt, daha yeni state'i ezebilir. Her yeni sorguda önceki Dio isteğini iptal edin ve ek olarak monotonic bir epoch değeriyle yanıtın güncelliğini doğrulayın. Epoch kontrolü, platform adaptörünün iptal sinyalini geç ilettiği veya yanıtın iptalden hemen önce tamamlandığı sınır durumunu kapatır.

sealed class SearchOutcome {
  const SearchOutcome();
}

final class SearchData extends SearchOutcome {
  const SearchData(this.products);
  final List<Product> products;
}

final class SearchCancelled extends SearchOutcome {
  const SearchCancelled();
}

class ProductSearchGateway {
  ProductSearchGateway(this._dio);

  final Dio _dio;
  CancelToken? _activeToken;
  int _epoch = 0;

  Future<SearchOutcome> search(String query) async {
    _activeToken?.cancel('Superseded by a newer query');
    final token = CancelToken();
    _activeToken = token;
    final requestEpoch = ++_epoch;

    try {
      final response = await _dio.get<List<dynamic>>(
        '/products/search',
        queryParameters: {'q': query},
        cancelToken: token,
      );

      if (requestEpoch != _epoch) return const SearchCancelled();
      final products = response.data!
          .map((json) => Product.fromJson(json as Map<String, dynamic>))
          .toList(growable: false);
      return SearchData(products);
    } on DioException catch (error) {
      if (CancelToken.isCancel(error)) return const SearchCancelled();
      rethrow;
    } finally {
      if (identical(_activeToken, token)) _activeToken = null;
    }
  }
}

CancelToken tek kullanımlıktır. İptal edilmiş bir token'ı sonraki isteğe vermek, yeni isteğin anında iptal edilmesine neden olur. Ayrıca iptali boş listeye dönüştürmeyin: boş liste geçerli bir domain sonucu olabilir ve UI yanlışlıkla 'sonuç yok' gösterebilir. Yukarıdaki sealed outcome, Flutter state management katmanının iptal, başarı ve hata durumlarını farklı işlemesini zorunlu kılar.

Flutter state management katmanında istek sahipliğini kurun

İstek ömrünü WidgetState içine dağınık biçimde koymak yerine, state sağlayıcısının yaşam döngüsüne bağlayın. Riverpod kullanılıyorsa autoDispose provider kapandığında CancelToken iptal edilmelidir. Böylece kullanıcı sonuç ekranından ayrıldığında, artık ekrana yazılmayacak JSON ayrıştırması ve state bildirimi yapılmaz. CancelToken'ı immutable UI state içine koymayın; bu nesne serileştirilebilir değildir ve state karşılaştırmalarını da anlamsızlaştırır.

final productSearchProvider =
    FutureProvider.autoDispose.family<List<Product>, String>((ref, query) async {
  final dio = ref.watch(dioProvider);
  final token = CancelToken();

  ref.onDispose(() {
    if (!token.isCancelled) {
      token.cancel('Search provider disposed');
    }
  });

  try {
    final response = await dio.get<List<dynamic>>(
      '/products/search',
      queryParameters: {'q': query},
      cancelToken: token,
    );
    return response.data!
        .map((item) => Product.fromJson(item as Map<String, dynamic>))
        .toList(growable: false);
  } on DioException catch (error) {
    if (CancelToken.isCancel(error)) {
      throw const SearchRequestDisposed();
    }
    rethrow;
  }
});

UI tarafında her tuş vuruşunda provider ailesini değiştirmek yerine debounce uygulayın. Örneğin TextEditingController listener'ında Timer ile 250 ms bekleyip yalnızca son değeri query state'ine yazın. Debounce sunucuya giden istek sayısını azaltır, CancelToken ise debounce penceresi dışında başlayan isteğin ekranı bozmasını engeller. İkisini birbirinin alternatifi saymak yaygın hatadır; biri istek üretimini, diğeri üretilmiş isteğin yaşam döngüsünü yönetir.

İptal edilen isteği retry mekanizmasından ayırın

Genel bir retry interceptor, CancelToken ile iptal edilmiş isteği yeniden başlatırsa kullanıcı ekranı kapatmış olsa bile trafik üretmeye devam eder. Retry yalnızca idempotent operasyonlarda uygulanmalıdır: GET, HEAD ve sunucunun idempotency key desteklediği belirli POST istekleri. Multipart upload veya tek kullanımlık stream body içeren RequestOptions tekrar gönderilemez; stream tüketildiği için ikinci deneme boş ya da bozuk gövdeyle gider.

class RetryInterceptor extends Interceptor {
  RetryInterceptor(this._dio);

  final Dio _dio;

  @override
  Future<void> onError(
    DioException error,
    ErrorInterceptorHandler handler,
  ) async {
    final options = error.requestOptions;
    final attempt = (options.extra['retryAttempt'] as int?) ?? 0;
    final status = error.response?.statusCode;
    final isCancelled = CancelToken.isCancel(error);
    final isIdempotent = options.method == 'GET' || options.method == 'HEAD';
    final isRetryableStatus = status == 429 || (status != null && status >= 500);
    final isRetryableTransport = error.type == DioExceptionType.connectionTimeout ||
        error.type == DioExceptionType.connectionError ||
        error.type == DioExceptionType.receiveTimeout;

    if (isCancelled || !isIdempotent || attempt >= 2 ||
        (!isRetryableStatus && !isRetryableTransport)) {
      return handler.next(error);
    }

    final delay = Duration(milliseconds: 300 * (1 << attempt));
    await Future<void>.delayed(delay);
    options.extra['retryAttempt'] = attempt + 1;

    try {
      final response = await _dio.fetch<dynamic>(options);
      handler.resolve(response);
    } on DioException catch (retryError) {
      handler.next(retryError);
    }
  }
}

Bu interceptor'ı Dio oluşturulurken ekleyin ve retry denemesi sayısını `RequestOptions.extra` altında taşıyın. Gerçek sistemde 429 için sunucunun Retry-After başlığını da parse edin; körlemesine 300 ms beklemek rate limit penceresini uzatabilir. Jitter eklemek, aynı anda hata alan binlerce istemcinin 300, 600 ms anlarında tekrar yük bindirmesini önler. Örneğin gecikmeye `Random().nextInt(100)` ms eklenebilir.

DevTools ile önce-sonra ağ isteği maliyetini ölçün

Bu değişikliği performans iddiasıyla değil, tekrarlanabilir bir senaryoyla doğrulayın. Önce debounce ve iptal olmadan bir profile build üzerinde 30 kez hızlı arama akışı çalıştırın; ardından aynı akışı iki mekanizma açıkken çalıştırın. Karşılaştırılacak metrikler şunlardır: kullanıcı başına başlatılan istek sayısı, iptal edilen istek sayısı, ekrana commit edilen eski yanıt sayısı ve arama sonucunun p95 tamamlanma süresi. Flutter DevTools Performance görünümünde repository çağrılarını TimelineTask ile işaretleyerek frame yükü ile ağ sonucunun ekrana işlendiği anı aynı zaman çizelgesinde inceleyin.

Future<T> tracedRequest<T>(
  String name,
  Future<T> Function() request,
) async {
  final task = TimelineTask(filterKey: 'network');
  final watch = Stopwatch()..start();
  task.start(name);

  try {
    return await request();
  } finally {
    watch.stop();
    task.finish(arguments: {
      'elapsedMs': watch.elapsedMilliseconds,
    });
  }
}

final result = await tracedRequest(
  'product_search',
  () => gateway.search(query),
);

Test cihazında release optimizasyonlarının etkisini korurken teşhis verisi almak için profile mod kullanın:

flutter run --profile
Ardından DevTools Performance kaydını başlatın, aynı metin girişlerini uygulayın ve JSON exportlarını iki commit arasında saklayın. Ölçüm sırasında LogInterceptor açmayın; her yanıt gövdesini debug konsoluna yazmak büyük payload'larda CPU ve I/O maliyetini değiştirir. Ağ gövdelerini görmek gerekiyorsa yalnızca geliştirme flavor'ında sınırlı boyutlu redacted log üretin.

Cross platform mobil uygulama geliştirme için iptal testleri

Android ve iOS ağ yığınları aynı gecikme düzeninde davranmayabilir; bu yüzden yalnızca elle cihaz testi yeterli değildir. Repository katmanını Dio'dan küçük bir arayüzle ayırın ve unit testte iki kontrollü Future kullanarak eski yanıtın yeni state'i ezemediğini kanıtlayın. `flutter test` komutunu CI içinde çalıştırın; test, fiziksel ağ kalitesine bağlı olmamalıdır.

test('newer query wins when older response completes last', () async {
  final oldResponse = Completer<List<Product>>();
  final newResponse = Completer<List<Product>>();
  final gateway = FakeSearchGateway([oldResponse, newResponse]);

  final first = gateway.search('ka');
  final second = gateway.search('kalem');

  newResponse.complete([Product(id: '2', name: 'Kalem')]);
  oldResponse.complete([Product(id: '1', name: 'Kase')]);

  expect(await second, isA<SearchData>());
  expect(await first, isA<SearchCancelled>());
});

Bir flutter eğitimi, flutter kursu veya dart programlama eğitimi içinde bu örneği yalnızca Dio API'si olarak değil, sahiplik ve zamanlama problemi olarak ele almak daha değerlidir. Teste ayrıca ekran dispose olurken token iptali, 429 sonrası iptalin retry'ı durdurması ve boş arama sorgusunda hiç HTTP çağrısı yapılmaması senaryolarını ekleyin. Bu dört test, üretimde sık rastlanan ama normal gecikmeli geliştirme ortamında görünmeyen yarış koşullarını kapsar.

İlgili Eğitim

Flutter Eğitimi

Sık Sorulan Sorular

Flutter state management içinde Dio CancelToken nereye konulmalı?

CancelToken'ı Riverpod autoDispose provider, Bloc event handler veya repository çağrısının sahip olduğu geçici nesne olarak oluşturun. Immutable state içine koymayın. Provider kullanıyorsanız `ref.onDispose(() => token.cancel())` ile ekran artık dinlemiyorken isteği kapatın.

Flutter mobil uygulama geliştirmede debounce varken istek iptali gerekli mi?

Evet. Debounce sadece belirlenen süre içindeki yeni isteklerin oluşmasını engeller. Debounce süresi dolduktan sonra başlayan bir istek, kullanıcı yeni sorgu yazdığında veya ekranı terk ettiğinde h'l' devam eder. Bu nedenle debounce ile CancelToken birlikte kullanılmalı, ayrıca geç yanıt için epoch kontrolü yapılmalıdır.

Cross platform mobil uygulama geliştirmede Dio retry iptal edilen isteği neden tekrarlar?

Retry interceptor `DioExceptionType` veya HTTP durum kodunu kontrol edip `CancelToken.isCancel(error)` kontrolü yapmıyorsa, iptali geçici ağ hatası sanabilir. Retry koşulunun ilk maddesi iptal kontrolü olmalı; ayrıca sadece GET ve HEAD gibi idempotent istekler tekrar gönderilmelidir.

Flutter eğitimi için ağ isteği iptalinin çalıştığını nasıl ölçerim?

Uygulamayı `flutter run --profile` ile başlatın, repository çağrılarını `TimelineTask` ile işaretleyin ve Flutter DevTools Performance görünümünde 30 tekrarlı arama akışını kaydedin. İyileştirme öncesi ve sonrası için başlatılan istek, iptal sayısı, stale response commit sayısı ve p95 sonuç süresini aynı cihazda karşılaştırın.

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