Flutter mobil uygulama geliştirmede binlerce satırlık akışları Sliver, sabit extent, sayfalama ve DevTools ölçümleriyle tasarlayın. Bu rehber, gereksiz layout ve raster işini kod düzeyinde azaltır.
Flutter Mobil Uygulama Geliştirmede Büyük Listeler için Sliver Mimarisi
Flutter mobil uygulama geliştirmede liste darboğazını ölçerek bulun
Büyük liste sorunu genellikle widget sayısından değil, her frame'de yapılan layout, paint ve image decode işinden çıkar. Ölçümü profile modda yapın; debug moddaki assertion ve observatory maliyetleri sonucu geçersiz kılar. Gerçek cihazda akıcı bir scroll kaydı alın:
flutter run --profile --trace-skia
flutter devtools DevTools Performance ekranında scroll sırasında UI ve Raster thread frame'lerini inceleyin. 60 Hz cihazda 16.67 ms, 120 Hz cihazda 8.33 ms sınırdır. Timeline'da UI tarafında uzun Layout/Build blokları, Raster tarafında uzun Paint veya ImageDecode blokları ayrı problemlerdir; ikisini aynı optimizasyonla çözmeye çalışmayın.Önce-sonra karşılaştırmasını tekrarlanabilir yapın: aynı cihazda, aynı API yanıtı ile 2.000 kayıt açın, listenin başından 1.500. kayda hızlı fling yapın ve 10 tekrarın p50/p95 frame sürelerini kaydedin. Flutter DevTools'ta Performance Overlay'i açmak için geçici olarak
MaterialApp(
showPerformanceOverlay: true,
home: const FeedPage(),
) kullanabilirsiniz. Yeşil grafik GPU, mavi grafik UI iş yükünü gösterir; örneğin UI p95 24 ms iken `itemExtent` sonrası 9 ms'ye iniyorsa kazanımın mekanizması Flutter'ın çocuk yüksekliklerini tek tek ölçmek yerine scroll offset'i aritmetik olarak hesaplamasıdır.Bir flutter eğitimi veya flutter kursu içinde liste performansını yalnızca `ListView.builder` ile anlatmak eksik kalır. Çalışan ekipler için önemli ayrım şudur: builder görünür ve cache alanındaki öğeleri tembel üretir, fakat değişken yüksekliğe sahip her çocuk yine layout maliyeti doğurur. Bu ayrım, cross platform mobil uygulama geliştirme sırasında düşük güçlü Android cihazlardaki CPU sınırını ve yüksek yenileme hızlı cihazlardaki daha dar frame bütçesini birlikte ele almayı gerektirir.
SliverFixedExtentList ile ölçülebilir layout maliyetini azaltın
Kartların yüksekliği tasarım gereği sabitse `ListView.builder` üzerinde `itemExtent` tanımlayın veya karma bir sayfada `SliverFixedExtentList` seçin. Flutter, sabit extent ile toplam scroll extent'ini ve görünür indeks aralığını doğrudan hesaplar; değişken çocuk boyutlarında yaptığı probe/layout geçişlerini azaltır. `prototypeItem` yalnızca tüm satırların gerçekten aynı ölçüde olduğu durumda güvenlidir; dinamik font ölçeği veya çevrilmiş metin yüksekliği bunu bozuyorsa kullanmayın.
class ActivityFeed extends StatelessWidget {
const ActivityFeed({super.key, required this.items});
final List items;
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
const SliverAppBar(pinned: true, title: Text('Aktivite')),
SliverFixedExtentList(
itemExtent: 72,
delegate: SliverChildBuilderDelegate(
(context, index) => ActivityTile(
key: ValueKey(items[index].id),
item: items[index],
),
childCount: items.length,
addAutomaticKeepAlives: false,
addRepaintBoundaries: true,
),
),
],
);
}
} Buradaki `ValueKey`, sayfalama sonucu listenin başına yeni kayıt eklendiğinde stateful satırların yanlış veriyle eşleşmesini engeller. Sadece indeks anahtarı kullanılırsa örneğin satır içindeki `TextField` veya animasyon state'i bir sonraki kayda taşınabilir. `addAutomaticKeepAlives: false` bellek baskısını düşürür, ancak satırda yazı girişi, video oynatma veya genişletilmiş panel state'i varsa o alt widget'ın `AutomaticKeepAliveClientMixin` ile bilinçli olarak keep-alive istemesi gerekir.
Flutter state management ile satır güncellemelerini görünür alana sınırlayın
Bir satırdaki begeni değişince tüm feed'i yeniden oluşturan tek bir `ChangeNotifier` dinleyicisi, model listesi büyüdükçe gereksiz build üretir. flutter state management katmanında state'i satır kimliği etrafında bölün ve yalnızca değişen alanı dinleyin. Riverpod kullanıyorsanız `select` ile immutable state'in sadece gerekli parçasını izleyin; `Activity` nesnesini mutation ile değiştirmek yerine yeni nesne üretin, aksi halde seçici karşılaştırma değişikliği göremez.
final feedProvider = NotifierProvider<FeedNotifier, Map<String, Activity>>(
FeedNotifier.new,
);
class LikeButton extends ConsumerWidget {
const LikeButton({super.key, required this.id});
final String id;
@override
Widget build(BuildContext context, WidgetRef ref) {
final liked = ref.watch(feedProvider.select(
(items) => items[id]?.isLiked ?? false,
));
return IconButton(
icon: Icon(liked ? Icons.favorite : Icons.favorite_border),
onPressed: () => ref.read(feedProvider.notifier).toggleLike(id),
);
}
}Bu tasarımda `ActivityTile`'ın tamamını değil yalnızca `LikeButton`'ı rebuild edersiniz. DevTools Inspector'da `Track widget rebuilds` seçeneğini açıp begeni aksiyonunu 20 kez çalıştırın; değişiklikten önce görünür tüm satırlar, sonrasında yalnızca hedef düğme işaretlenmelidir. Kritik edge case: sıralama, filtre veya server refresh işlemi `Map` içeriğini değiştirirken ekranda görünen ID silinmiş olabilir. Bu nedenle selector'da `items[id]?.isLiked ?? false` gibi null güvenli bir varsayılan kullanın ve asenkron aksiyon sonucunu eski indeksle uygulamayın.
Sayfalama, cacheExtent ve iptal stratejisini birlikte tasarlayın
Scroll sonuna `extentAfter` ile yaklaşmak, index tabanlı `if (index == items.length - 1)` kontrolünden daha güvenilirdir; farklı ekran boyutları, header sliver'ları ve cache alanı indeks varsayımını bozar. Ağ isteğini tekilleştirin, cursor tabanlı API kullanın ve ekran kapanınca isteği iptal edin. `dio` ile `CancelToken`, hızlı filtre değişiminde eski yanıtın yeni listeyi ezmesini önler.
class FeedPager {
bool _loading = false;
String? _cursor;
CancelToken? _token;
Future<void> loadNext() async {
if (_loading || _cursor == null && hasLoadedFirstPage) return;
_loading = true;
_token?.cancel('new pagination request');
final token = _token = CancelToken();
try {
final response = await dio.get('/activities', queryParameters: {
'cursor': _cursor,
'limit': 30,
}, cancelToken: token);
_cursor = response.data['nextCursor'] as String?;
appendPage(response.data['items']);
} on DioException catch (e) {
if (!CancelToken.isCancel(e)) rethrow;
} finally {
if (identical(_token, token)) _loading = false;
}
}
}`cacheExtent` değerini körlemesine büyütmeyin. Bu değer viewport dışındaki çocukların önceden build/layout edilmesine yol açar; hızlı scroll'da boşluk riskini azaltırken bellek ve UI thread maliyetini yükseltir. Örneğin 72 px satırda varsayılan davranışla p95 UI süresini ölçün, ardından `CustomScrollView(cacheExtent: 360)` ile yaklaşık 5 ek satır önbelleğe alın ve aynı fling senaryosunu tekrar edin. Raster p95 artıyor veya bellek grafiğinde sürekli büyüme görülüyorsa değer cihaz ve içerik için fazladır. Ağ görselleri için `cached_network_image` kullanılsa bile decode edilmiş bitmaplerin RAM'de kaldığını ayrıca doğrulayın.
Dart programlama eğitimi perspektifiyle async yarışlarını test edin
Sayfalı listelerde en pahalı üretim hatalarından biri, kullanıcı filtreyi değiştirince önceki isteğin geç dönüp yeni sonuçları ezmesidir. Sadece `mounted` kontrolü bunu çözmez; widget hala mounted olabilir ama sorgu artık geçersizdir. Bir `generation` sayacı tutup her yeni sorguda artırın, yanıtı yalnızca isteğin üretimi güncelse state'e yazın. Bu desen, dart programlama eğitimi içinde `Future` sıralamasının başlama sırasını korumadığını somut biçimde gösteren iyi bir örnektir.
int _generation = 0;
Future<void> applyFilter(String query) async {
final requestGeneration = ++_generation;
final page = await repository.search(query);
if (!mounted || requestGeneration != _generation) return;
setState(() {
items = page.items;
nextCursor = page.nextCursor;
});
}Bu davranışı `fake_async` veya repository'nin kontrol edilen `Completer` döndürdüğü bir widget testiyle doğrulayın: 'a' sorgusunu başlatın, 'ab' sorgusunu başlatın, önce 'ab' yanıtını sonra 'a' yanıtını tamamlayın ve ekranda yalnızca 'ab' verisinin kaldığını bekleyin. Böyle bir test, cross platform mobil uygulama geliştirme projelerinde cihaz hızına göre arada bir görünen ve loglarda yeniden üretmesi zor olan yarış hatasını deterministik hale getirir.
İlgili Eğitim
YTÜSEM İlgili Eğitim
Sık Sorulan Sorular
Flutter mobil uygulama geliştirmede ListView.builder ne zaman yetersiz kalır?
Görünür satırlar variable height nedeniyle pahalı layout yapıyorsa veya header, grid, boş durum ve feed aynı scroll yüzeyindeyse `CustomScrollView` ve sliver kullanın. Sabit 72 px satırlar için `SliverFixedExtentList` veya `ListView.builder(itemExtent: 72)` ile profile modda UI p95 frame süresini karşılaştırın.
Flutter state management ile listede sadece değişen satırı nasıl rebuild ederim?
Riverpod'da satır kimliğine göre `ref.watch(provider.select((s) => s[id]?.field))` kullanın ve state'i immutable güncelleyin. DevTools Inspector'daki `Track widget rebuilds` ile bir aksiyonda yalnızca hedef alt widget'ın rebuild edildiğini doğrulayın; tüm `ListView` işaretleniyorsa üst seviyede geniş bir provider izleniyordur.
Flutter kursu projelerinde cacheExtent için hangi değer seçilmeli?
Sabit bir evrensel değer yoktur. Satır yüksekliği 72 px ise 360 px yaklaşık 5 satır ön hazırlar. Profile modda varsayılan değer ve 360 px için aynı fling testinin p95 UI/Raster sürelerini ve DevTools Memory grafiğini kaydedin. Frame süresi veya heap kalıcı olarak yükseliyorsa değeri azaltın.
Dart programlama eğitimi sırasında sayfalama yarış hatası nasıl engellenir?
Her filtre veya yenileme isteğine monoton artan bir generation değeri atayın; yanıt geldiğinde değer güncel değilse state yazmayın. Dio kullanıyorsanız buna ek olarak eski isteği `CancelToken` ile iptal edin. Generation kontrolü, iptal edilemeyen bir transport veya iptalden sonra dönebilen bir katman için ikinci korumadır.
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.



