Flutter state management katmanında route kapanırken ve arama sorgusu değişirken HTTP isteklerini gerçekten iptal etmeyi ele alıyoruz. Dio, Riverpod, DevTools ve testlerle stale response kaynaklı hataları engelleyin.
Flutter State Management'de Route Bazlı İstek İptali Rehberi
Flutter state management için iptal sahipliğini tanımlayın
Bir isteğin sahipliği, onu başlatan UI nesnesinden değil, isteğin anlamsal ömründen gelmelidir. Örneğin ürün detay isteği ProductPage route'una; yazarken önerilen ürün listesi ise aktif query değerine aittir. flutter mobil uygulama geliştirme projelerinde repository içinde global bir CancelToken tutmak yaygın ama hatalı bir yaklaşımdır: iki farklı ekranda aynı repository kullanıldığında ikinci ekran birincinin isteğini iptal edebilir. Her use-case çağrısı kendi token'ini almalı, token'in dispose sorumluluğu ise provider, bloc veya controller scope'unda kalmalıdır.
Dio ile her yeni arama sorgusunda önceki HTTP çağrısı ve eski yanıtın state'i ezmesi birlikte engellenebilir. Buradaki requestId kontrolü gereklidir; iptal sinyali sokete geç ulaşırsa veya mock, proxy ya da custom adapter Future'i tamamlamaya devam ederse eski sonuç yine dönebilir. Token sadece taşıma katmanını, monoton artan requestId ise state yazımını korur.
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
class SearchController extends ChangeNotifier {
SearchController(this._dio);
final Dio _dio;
CancelToken? _activeToken;
int _requestId = 0;
List<String> results = const [];
bool loading = false;
Future<void> search(String query) async {
final int requestId = ++_requestId;
_activeToken?.cancel('Superseded by a newer query');
final token = CancelToken();
_activeToken = token;
loading = true;
notifyListeners();
try {
final response = await _dio.get<List<dynamic>>(
'/products/search',
queryParameters: {'q': query},
cancelToken: token,
);
if (requestId != _requestId) return;
results = response.data!.cast<String>();
} on DioException catch (error) {
if (CancelToken.isCancel(error)) return;
rethrow;
} finally {
if (requestId == _requestId) {
loading = false;
notifyListeners();
}
}
}
@override
void dispose() {
_activeToken?.cancel('Search page disposed');
super.dispose();
}
}Riverpod autoDispose ile route ömrüne bağlı Dio iptali
Riverpod kullanan ekiplerde FutureProvider.autoDispose, route'dan çıkıldığında provider'i yok ederek iptal için doğru ömür sınırını verir. ref.onDispose içinde CancelToken.cancel çağrısı kayıtlı değilse autoDispose sadece Dart tarafındaki provider state'ini kaldırır; çalışmakta olan Dio isteği ise socket ve decode maliyetiyle devam eder. Bu ayrım, flutter eğitimi içeriklerinde sıklıkla atlanır.
Aşağıdaki provider family, her productId için ayrı bir token üretir. Ekran yeniden build edilse bile provider dinlenmeye devam ediyorsa token korunur; son listener kaldırıldığında onDispose çalışır. keepAlive çağrısı bu davranışı değiştirir: keepAlive kullanılan provider için ayrıca zaman asımlı kapatma politikası tanımlanmalıdır.
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final dioProvider = Provider<Dio>((ref) => Dio());
final productProvider =
FutureProvider.autoDispose.family<Map<String, dynamic>, String>(
(ref, productId) async {
final token = CancelToken();
ref.onDispose(() => token.cancel('Product route left'));
final dio = ref.read(dioProvider);
try {
final response = await dio.get<Map<String, dynamic>>(
'/products/$productId',
cancelToken: token,
);
return response.data!;
} on DioException catch (error) {
if (CancelToken.isCancel(error)) {
throw StateError('Request cancelled because its provider was disposed');
}
rethrow;
}
},
);UI katmaninda AsyncValue.error durumunu koruyan bir hata ekrani olarak gostermek yerine iptal hatasini gozlemlenebilirlik katmaninda ayirmak daha temizdir. Sentry veya OpenTelemetry interceptor'iniz varsa CancelToken.isCancel(error) olan olaylari error severity ile gondermeyin; aksi halde kullanicinin geri tusuna basmasi yapay hata orani uretir. Buna karsilik timeout, 401 ve 5xx durumlarini ayri tag'lerle kaydetmek icin Dio Interceptor kullanin.
Debounce, switchMap ve gercek HTTP iptali farki
Debounce sadece yeni bir istek baslatmayi geciktirir, baslatilmis istegi durdurmaz. RxDart'taki switchMap ise eski stream subscription'ini iptal eder, fakat icerde await edilen Dio Future'ine CancelToken verilmemisse TCP baglantisi veya HTTP/2 stream'i devam edebilir. cross platform mobil uygulama geliştirme yapan ekipler bu farki hem Android hem iOS agirlikli ag kosullarinda test etmelidir; platform fark etmeksizin uygulama state'i eski yaniti kabul etmemelidir.
Bloc kullaniyorsaniz bloc_concurrency paketindeki restartable transformer ile event handler omrunu yonetin, ancak network iptalini acikca baglayin. Asagidaki ornekte operation yerel degiskene atanir; _operation alanini await sonrasinda okumak hatalidir, cunku o alan bu sirada yeni sorgunun operation'i olabilir.
import 'package:async/async.dart';
import 'package:bloc/bloc.dart';
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:dio/dio.dart';
on<QueryChanged>((event, emit) async {
await _operation?.cancel();
final token = CancelToken();
final operation = CancelableOperation<List<String>>.fromFuture(
api.search(event.query, cancelToken: token),
onCancel: () => token.cancel('New query event'),
);
_operation = operation;
final value = await operation.valueOrCancellation();
if (value != null && identical(_operation, operation)) {
emit(SearchReady(value));
}
}, transformer: restartable());Buradaki edge case, bos sorgudur. Bos sorguda sadece UI listesini temizlemek yerine aktif token'i de iptal edin; aksi halde kullanici arama alanini temizledikten sonra gec kalan onceki sonuc tekrar gorunur. API katmaninda minimum sorgu boyu 2 ise controller'da da ayni esigi uygulayin ve token iptalinden sonra emit edilen loading state'ini requestId veya identical kontroluyle kapatin.
Flutter mobil uygulama gelistirmede iptal maliyetini olcun
İstek iptalini performans iyileştirmesi olarak kabul etmeden önce iki profil build'iyle aynı senaryoyu ölçün: arama alanına 150 ms aralıklarla 12 karakter girin, sonra route'u geri tuşuyla kapatın. flutter run --profile ile uygulamayı çalıştırın, Flutter DevTools Performance ekranında TimelineTask işaretlerini arayın ve Dio loglarından başlatılan, tamamlanan ve iptal edilen istek sayılarını alın. Önce durumunda tamamlanan istek sayısı, p95 yanıt boyutu ve JSON decode süresi; sonra durumunda aynı üç metriyi karşılaştırın. Sadece build sayısını ölçmek yeterli değildir, çünkü asıl tasarruf genellikle gereksiz response body transferi ve deserialize edilmemiş veridedir.
Dio interceptor'i ve dart:developer TimelineTask birlikte kullanıldığında DevTools zaman çizelgesinde her endpoint görünür. production loglarında query metnini yazmak yerine uzunluk veya hash kullanın; arama metni kişisel veri içerebilir.
import 'dart:developer';
import 'package:dio/dio.dart';
class NetworkTimelineInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
final task = TimelineTask(filterKey: 'network');
task.start('GET ${options.path}', arguments: {'requestId': options.hashCode});
options.extra['timelineTask'] = task;
handler.next(options);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
(response.requestOptions.extra['timelineTask'] as TimelineTask?)?.finish();
handler.next(response);
}
@override
void onError(DioException error, ErrorInterceptorHandler handler) {
final task = error.requestOptions.extra['timelineTask'] as TimelineTask?;
task?.finish(arguments: {'cancelled': CancelToken.isCancel(error)});
handler.next(error);
}
}Sunucu etkisini de ayrı ölçün. Dio iptali istemci tarafında Future'i hata ile bitirir ve taşıma katmanını kesmeye çalışır, fakat backend handler'ı veritabanı sorgusunu çoktan başlatmış olabilir. API gateway loglarında request ID ile bağlantısı kapanan istekleri, backend tracing'de ise DB span'larını eşleştirin. Uzun sorgular için PostgreSQL tarafında statement_timeout, backend tarafında request context cancellation ve idempotent yazma endpoint'lerinde Idempotency-Key kullanımı ayrı savunma katmanlarıdır.
Dart programlama eğitimi seviyesinde yarışmayı test edin
Gerçek ağ ile yazılan widget testleri yarışmayı deterministik biçimde üretmez. Repository'yi Future dönen bir arayüz olarak soyutlayın, iki Completer ile önce eski sonra yeni sorguyu başlatın ve eski Future'i en son tamamlayın. Beklenen sonuç eski listenin ekranda görünmemesidir. Bu test, dart programlama eğitimi alan geliştiricilerin öğrendiği async sıralama varsayımının neden yanlış olduğunu somutlaştırır: Future'ler başlama sırasıyla tamamlanmaz.
Aşağıdaki unit test, token iptalini değil state yazma kapısını test eder. Her ikisi gereklidir: token ağ kaynağını azaltır, gate ise iptal edilemeyen adapter veya backend gecikmesinde veri tutarlılığını korur.
import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
class LatestOnly<T> {
int _version = 0;
T? value;
Future<void> run(Future<T> future) async {
final version = ++_version;
final result = await future;
if (version == _version) value = result;
}
}
void main() {
test('late old response cannot overwrite new response', () async {
final oldResponse = Completer<String>();
final newResponse = Completer<String>();
final gate = LatestOnly<String>();
final oldRun = gate.run(oldResponse.future);
final newRun = gate.run(newResponse.future);
newResponse.complete('new');
await newRun;
oldResponse.complete('old');
await oldRun;
expect(gate.value, 'new');
});
}Bir flutter kursu için code review kontrol listesine şu iki maddeyi eklemek pratiktir: dispose, ref.onDispose veya Bloc.close içinde aktif subscription ve CancelToken kapatılıyor mu; her sonucun state'e yazılmasından önce route veya request versiyonu doğrulanıyor mu? Özellikle pagination'da tek global token kullanmayın. Bir sonraki sayfa isteği ile pull-to-refresh isteğinin iptal semantiği farklıdır; refresh yeni bir liste oluştururken sayfalama aynı listenin devamıdır ve ayrı operation anahtarları gerektirir.
İlgili Eğitim
Sık Sorulan Sorular
Flutter state management içinde Dio CancelToken kullanmak stale response sorununu tek başına çözer mi?
Hayır. CancelToken Dio isteğini iptal etmeye çalışır, ancak eski Future adapter tarafında tamamlanabilir veya iptal yeni istekten sonra yarışmaya girebilir. Her isteğe artan requestId verin ya da operation referansını identical ile kontrol edin; state sadece en güncel istekten yazılmalıdır.
Flutter mobil uygulama geliştirme projesinde Riverpod autoDispose HTTP isteğini otomatik iptal eder mi?
autoDispose provider state'ini yok eder, Dio isteğini kendiliğinden bilmez. Provider içinde CancelToken oluşturup ref.onDispose(() => token.cancel()) kaydı ekleyin. keepAlive kullanıyorsanız provider dispose olmayacağı için token'in ömrünü ayrıca tasarlayın.
Cross platform mobil uygulama geliştirme için debounce mi switchMap mi kullanmalıyım?
Metin aramasında debounce yeni istek sayısını azaltır, switchMap eski event handler sonucunu geçersiz kılmaya yardım eder. Aktif HTTP isteğini de sonlandırmak istiyorsanız switchMap veya restartable akışını Dio CancelToken.cancel ile bağlayın. DevTools Timeline ve Dio interceptor loglarıyla tamamlanan istek sayısını önce-sonra ölçün.
Flutter eğitimi sırasında iptal edilen HTTP isteklerini hata olarak raporlamalı mıyım?
Hayır. DioException yakalandığında CancelToken.isCancel(error) kontrolü yapın ve bu olayı Sentry veya OpenTelemetry'ye application error olarak göndermeyin. Timeout, TLS, 401 ve 5xx hatalarını ise endpoint, status code ve request ID etiketleriyle ayrı raporlayı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.



