• 3.09.2026 21:36:09
  • Admin Admin

Flutter mobil uygulama geliştirme projelerinde gereksiz widget rebuild'lerini ölçmeyi, state dilimlemeyi ve rasterizasyon maliyetini ayrı incelemeyi öğrenin. Hedef, profile modda p95 frame süresini ölçülebilir bir bütçede tutmaktır.

Flutter Mobil Uygulama Geliştirmede Rebuild Bütçesi ve İzleme

Flutter mobil uygulama geliştirmede rebuild bütçesini ölçmek

Bir ekranın yavaş olduğunu hissetmek yerine, önce tekrarlanabilir bir senaryo tanımlayın: 200 satırlı ürün listesinde 20 kez scroll, 10 kez adet artırma ve filtre aç-kapat. Bunu emülatörde değil, fiziksel cihazda profile mod ile çalıştırın. Debug moddaki assertion'lar, JIT ve servis protokolü maliyeti frame sürelerini temsil etmez; release mod ise DevTools bağlantısı vermediği için ilk teşhis için uygun değildir. Frame bütçesi 60 Hz ekranda yaklaşık 16.67 ms, 120 Hz ekranda yaklaşık 8.33 ms'dir. Bu bütçe build ve raster sürelerinin ayrı ayrı incelenmesini gerektirir.

Uygulama içine geçici bir FrameTiming toplayıcısı ekleyin. Ölçümü yalnızca hedef akış sırasında başlatın ve ilk açılıştaki shader derleme etkisini sonuçtan çıkarın. DevTools Performance görünümünde aynı zaman aralığını açarak bu logdaki p95 ile frame grafiğini karşılaştırın.

import 'dart:math';
import 'package:flutter/scheduler.dart';

final List<double> buildMs = [];
final List<double> rasterMs = [];

void startFrameMeasurement() {
  SchedulerBinding.instance.addTimingsCallback((timings) {
    for (final timing in timings) {
      buildMs.add(timing.buildDuration.inMicroseconds / 1000);
      rasterMs.add(timing.rasterDuration.inMicroseconds / 1000);
    }
  });
}

double percentile95(List<double> values) {
  final sorted = [...values]..sort();
  final index = max(0, (sorted.length * 0.95).ceil() - 1);
  return sorted[index];
}

void printFrameSummary() {
  print('build p95: ${percentile95(buildMs).toStringAsFixed(2)} ms');
  print('raster p95: ${percentile95(rasterMs).toStringAsFixed(2)} ms');
}

Bu ayrım kök nedeni değiştirir: build p95 yüksek, raster p95 düşükse widget ağacı, inherited dependency veya state yayılımı incelenir. Raster p95 yüksekse önce CustomPainter, clip, opacity, saveLayer ve görsel çözünürlüğü incelenir. Sadece ortalama frame süresini raporlamak yaygın bir hatadır; tek tük 45 ms frame'ler ortalamayı makul gösterirken kullanıcı scroll sırasında takılmayı açıkça görür.

Flutter state management ile bağımlılığı state dilimine indirmek

flutter state management katmanında temel kural, ekrana tüm store'u dinletmemektir. Örneğin sepetteki bir ürünün adedi değiştiğinde sadece rozet ve ilgili satır değişmelidir; uygulama çubuğu, filtre çipi ve görünür olmayan satırlar yeniden build edilmemelidir. Provider kullanılıyorsa `context.watch<CartStore>()` yerine `Selector` veya `context.select` kullanın. Riverpod tarafındaki karşılığı `ref.watch(cartProvider.select((cart) => cart.itemCount))` yaklaşımıdır.

class CartBadge extends StatelessWidget {
  const CartBadge({super.key});

  @override
  Widget build(BuildContext context) {
    return Selector<CartStore, int>(
      selector: (_, cart) => cart.itemCount,
      builder: (_, itemCount, __) {
        return Badge(label: Text('$itemCount'));
      },
    );
  }
}

class ProductRow extends StatelessWidget {
  const ProductRow({required this.productId, super.key});
  final String productId;

  @override
  Widget build(BuildContext context) {
    final quantity = context.select<CartStore, int>(
      (cart) => cart.quantityOf(productId),
    );
    return ListTile(
      key: ValueKey(productId),
      title: Text(productId),
      trailing: Text('Adet: $quantity'),
    );
  }
}

Buradaki incelik selector sonucunun değişmez olmasıdır. `selector: (_, cart) => cart.items` ile her mutasyonda aynı `List` nesnesini yerinde değiştirmek, equality tabanlı yeniden build kararını bozar: bazı UI alanları eski veriyle kalabilir veya gereksiz rebuild oluşabilir. Store güncellemesinde yeni koleksiyon üretin ve alanları `final` olan immutable state taşıyın. Örneğin `items = List.unmodifiable([...items, updatedItem])` kullanın. Bu yaklaşım, dart programlama eğitimi sırasında sık görülen 'notifyListeners çağırdım ama satır güncellenmedi' sorununu referans kimliği üzerinden açıklayan pratik bir örnektir.

Değişikliğin etkisini doğrulamak için geliştirme derlemesinde sadece teşhis süresince rebuild logunu açın. Çıktıyı ürün adedi artırma senaryosundan önce ve sonra karşılaştırın; hedef, tüm liste yerine bir `ProductRow` ve `CartBadge` görmektir.

void main() {
  assert(() {
    debugPrintRebuildDirtyWidgets = true;
    debugProfileBuildsEnabled = true;
    return true;
  }());
  runApp(const App());
}

Rebuild ile repaint maliyetini karıştırmadan Flutter çizimini izlemek

Bir widget'ın rebuild olması, piksellerinin mutlaka yeniden rasterize edildiği anlamına gelmez; tersi de doğrudur. `RepaintBoundary`, alt ağacı ayrı compositing layer olarak tutar ve komşu alanın repaint'inden ayırabilir. Ancak state değişimi nedeniyle o alt ağacın `build` metodu yine çalışabilir. Bu nedenle önce DevTools Performance'da build ve raster şeritlerini, sonra Flutter Inspector'da `Repaint Rainbow` seçeneğini birlikte inceleyin. Hızla renk değiştiren alanlar repaint edilen bölgelerdir.

Scroll eden bir liste üzerinde canlı fiyat grafiği varsa grafiği boundary içine alın, fakat listenin tamamını her satırda boundary yapmak için ölçüm olmadan karar vermeyin. Her boundary layer ağacını büyütür; büyük bir offscreen layer GPU belleği tüketir ve compositing maliyeti yaratır. Aşağıdaki örnekte grafik verisi değiştiğinde yalnızca painter yeniden çizilir, `DashboardPage` için `setState` çağrılmaz.

class PriceChart extends StatelessWidget {
  const PriceChart({required this.samples, super.key});
  final ValueNotifier<List<double>> samples;

  @override
  Widget build(BuildContext context) {
    return RepaintBoundary(
      child: ValueListenableBuilder<List<double>>(
        valueListenable: samples,
        builder: (_, points, __) => CustomPaint(
          size: const Size(double.infinity, 180),
          painter: PricePainter(points),
        ),
      ),
    );
  }
}

class PricePainter extends CustomPainter {
  PricePainter(this.points);
  final List<double> points;

  @override
  void paint(Canvas canvas, Size size) {
    final path = Path();
    for (var i = 0; i < points.length; i++) {
      final x = i * size.width / (points.length - 1);
      final y = size.height * (1 - points[i]);
      i == 0 ? path.moveTo(x, y) : path.lineTo(x, y);
    }
    canvas.drawPath(path, Paint()..style = PaintingStyle.stroke);
  }

  @override
  bool shouldRepaint(covariant PricePainter old) => old.points != points;
}

Önce-sonra ölçümünde aynı scroll senaryosunda raster p95'i kaydedin. Boundary eklenince raster p95 düşmüyor, ama layer sayısı yükseliyorsa geri alın. Ayrıca `Opacity(opacity: 0.5, child: karmaşıkAlan)` ve anti-aliased `ClipPath` bazı ağaçlarda ara yüzey oluşturabilir. DevTools'ta raster yükselişi görmeden bunları topluca değiştirmek yerine, ilgili frame'in timeline event'ini açıp `saveLayer` maliyetini doğrulayın.

Yüksek frekanslı olayları frame sınırında birleştirmek

WebSocket fiyat akışı, konum güncellemesi veya yazı editöründeki analiz sonucu saniyede onlarca event üretebilir. Her event'te `notifyListeners()` çağırmak, cihazın ekran yenileme hızından daha sık build planlar. Debounce kullanmak her zaman doğru değildir; örneğin son olayı 300 ms geciktirmek canlı grafiği yanlış hissettirir. Bunun yerine en son değeri bir sonraki frame'e taşıyan coalescing uygulayın. Böylece 60 Hz ekranda aynı frame aralığına düşen 20 event en fazla bir UI güncellemesi üretir.

class FrameCoalescer<T extends Object> {
  FrameCoalescer(this.onFrame);
  final void Function(T value) onFrame;

  T? _latest;
  bool _scheduled = false;

  void add(T value) {
    _latest = value;
    if (_scheduled) return;

    _scheduled = true;
    SchedulerBinding.instance.addPostFrameCallback((_) {
      _scheduled = false;
      final value = _latest;
      _latest = null;
      if (value != null) onFrame(value);
    });
    SchedulerBinding.instance.scheduleFrame();
  }
}

Bu sınıfı store sınırında kullanın: ağ event'i `coalescer.add(tick)` çağırır, `onFrame` içinde immutable state güncellenir. `scheduleFrame()` önemlidir; uygulama boşta iken yalnızca post-frame callback eklemek yeni frame üretmeyebilir. Ölçüm için event sayacı ve store bildirim sayacı tutun. Örneğin 10 saniyede 600 socket mesajına karşı 590 bildirim yerine ekran yenileme hızına bağlı olarak yaklaşık 600 veya 1200 üst sınırında değil, görünür frame sayısı kadar bildirim görmelisiniz. Bu teknik, cross platform mobil uygulama geliştirme sırasında Android ve iOS cihazların farklı yenileme hızlarına sabit milisaniyelik throttle yazmadan uyum sağlar.

Flutter eğitimi ekiplerinde performans regresyonunu CI'a taşımak

Manuel DevTools oturumu teşhis içindir, regresyon koruması için yeterli değildir. Kritik kullanıcı yolunu bir integration test ile sabitleyin ve profile modda aynı fiziksel test cihazında timeline üretin. Flutter sürücüsü kullanan mevcut projelerde aşağıdaki komut timeline özetini alabilir; ekipteki cihaz modeli, işletim sistemi, güç tasarrufu modu ve test verisi CI artefact'ına yazılmalıdır. Farklı cihazların mutlak milisaniyeleri doğrudan karşılaştırılmamalıdır.

flutter drive   --profile   --target=test_driver/app.dart   --driver=test_driver/perf_test.dart   --trace-startup   --no-dds

CI kuralını ortalama yerine p95 sınırıyla yazın: örneğin referans cihazda ürün listesi akışının build p95'i 6 ms iken değişiklik sonrası 10 ms olduysa, test yeşil olsa bile inceleme isteyin. Timeline JSON içindeki frame olaylarını küçük bir Dart veya Python betiğiyle ayrıştırıp önceki başarılı pipeline'ın artefact'ı ile kıyaslayın. Bu, bir flutter kursu içeriğinde gösterilecek basit bir metrik değil, kod incelemesinde 'bu selector neden eklendi' sorusunu sayısal veriye bağlayan ekip pratiğidir.

İlgili Eğitim

Flutter Eğitimi

Sık Sorulan Sorular

Flutter state management kullanırken gereksiz rebuild nasıl bulunur?

Önce fiziksel cihazda profile modda DevTools Performance ile build p95'i kaydedin. Sonra geliştirme derlemesinde `debugPrintRebuildDirtyWidgets = true` açın ve tek bir kullanıcı aksiyonunda hangi widget'ların dirty olduğunu listeleyin. Store'un tamamını dinleyen `watch` çağrılarını `Selector`, `context.select` veya Riverpod `select` ile alan bazlı bağımlılığa indirin.

Flutter mobil uygulama geliştirmede RepaintBoundary ne zaman kullanılmalı?

Inspector'daki Repaint Rainbow ile sık yeniden boyanan ama çevresinden bağımsız çizilebilen alanı doğrulayın: animasyonlu grafik, harita üstü katman veya canlı sayaç tipik adaylardır. Boundary ekledikten sonra aynı akışta raster p95'i tekrar ölçün. Liste içindeki her satıra körlemesine eklemeyin; fazla boundary layer sayısını ve GPU bellek kullanımını artırabilir.

Cross platform mobil uygulama geliştirme sırasında WebSocket event'leri Flutter UI'ını nasıl yavaşlatır?

Saniyede ekran yenileme hızından fazla event geldiğinde her event için state bildirimi yapmak aynı frame içinde birden fazla build tetikleyebilir. Event'leri `SchedulerBinding.addPostFrameCallback` ile frame başına son değer olacak biçimde birleştirin, ardından build ve raster p95 değerlerini önce-sonra ölçün. Sabit 250 ms debounce yerine frame tabanlı yaklaşım, 60 Hz ve 120 Hz cihazlarda doğal olarak farklı bütçeye uyar.

Dart programlama eğitimi kapsamında immutable state neden selector sonuçlarını etkiler?

Selector mekanizmaları çoğunlukla seçilen değerin önceki sonuçla eşitliğine bakar. Bir `List` nesnesini yerinde değiştirirseniz referans aynı kalır ve selector değişikliği kaçırabilir. Her güncellemede yeni, değişmez koleksiyon üretmek ve state alanlarını `final` tutmak, equality kararını öngörülebilir hale getirir.

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