İçeriğe geç
academia.sh

Ders 08 / 19

Durum Yönetimi Yaklaşımları

Paylaşılan durum bir bileşenin isteğini değiştirir, genişliğini kimin belirlediğini değiştirmez. Ders bir sayaçtan beslenen rozetin genişliğini ölçer ve satırın sınırsız kısıtında rozetin seçilen kaldığını gösterir.

Yayın Güncelleme

Bu derste

  • Sağlayıcı, olay tabanlı ve tepkisel durum yönetimi yaklaşımlarını birbirinden ayırır
  • Paylaşılan bir sayacın rozetin isteğini nasıl değiştirdiğini ölçer
  • Satırın esnek olmayan çocuğuna verilen sınırsız kısıtta rozetin seçilen kaldığını gösterir
  • Çocukların toplamının satırı aştığı durumda modelin taşmayı göstermediğini açıklar
İçindekiler

Önceki ders, platforma göre değişen bileşen ailelerinin eylem düğmesine farklı genişlikler istetse de sabit bir yuvada aynı sonucu verdiğini ölçtü. Bu ders isteğin kaynağını uygulamanın durumuna taşır. Bir sayaç, bir oturum bilgisi ya da bir sepet toplamı birden çok bileşenin birlikte okuduğu değerdir ve böyle bir değer bileşenin isteğini değiştirebilir. Kursun sorusu aynı kalır: bu bileşenin genişliğini kim belirliyor? Ders yanıtı, paylaşılan bir sayaçtan beslenen bir rozet üzerinde arar.

Durumu Paylaşmanın Üç Yolu

Uygulamanın durumu tek bir bileşenin içine sığmadığında durum, ağacın üstünde ortak bir yerde tutulur ve alttaki bileşenler onu okur. Bu paylaşımın üç biçimi vardır. Kitaplık adları bu derste yazılmaz; her biri işleviyle anılır.

Sağlayıcı (provider) yaklaşımında değer ağacın üst bir noktasında durur. Altındaki bileşenler değeri bağlamdan okur ve değer değişince yalnız onu okuyan bileşenler yeniden kurulur. Paylaşılan durum kitaplıkları bu kalıbı kullanır.

Olay tabanlı (event-based) yaklaşımda bileşen durumu doğrudan değiştirmez, bir olay gönderir. Bir işleyici olayı alır ve yeni durumu üretir. Olay tabanlı durum kitaplıkları değişikliğin yolunu bu biçimde tek bir noktaya toplar.

Tepkisel (reactive) yaklaşımda durum dinlenebilir bir değerdir. Dinleyiciler değere abone olur ve her değişimde haber alır; değer bir akış ya da bir bildirim nesnesi olabilir.

Üç yaklaşım da aynı yere varır: durum değişir, ilgili bileşenin build yöntemi yeni durumdan yeni bir bileşen alt ağacı üretir ve çerçeve düzeni yeniden koşturur.

Çekirdek

Her ders aynı çekirdeği kullanır. measure ağacı ekran genişliğinde sıkı bir kısıtla dolaşır, her düğüm için bir kayıt üretir ve kayıttaki boyutlanmayı bir kural verir: kısıt sıkıysa dayatılan, isteğin aralık içinde kaldığı durumda seçilen, istek aralığın dışındaysa ya da yoksa ve üst sınır yoksa kırpılan, istek yok ve üst sınır sonluysa esnetilen. Çıktıda dört ad ASCII yazılır: dayatilan, secilen, kirpilan, esnetilen. ebeveyn: satırı dayatılan, kırpılan ve esnetilenin toplamıdır; genişliği bileşenin kendi isteği olmayan kayıtları sayar.

// sizing.dart — bir bilesenin genisligini kimin belirledigini siniflandirir
const sizings = ['dayatilan', 'secilen', 'kirpilan', 'esnetilen'];

class Limit {
  final int min;
  final int? max; // null: sinirsiz
  const Limit(this.min, this.max);

  bool get tight => max == min;
  Limit loosen() => Limit(0, max);
  Limit deflate(int by) => Limit(_floor(min - by), max == null ? null : _floor(max! - by));
  String bounds() => '$min..${max ?? 'sonsuz'}';
}

int _floor(int value) => value < 0 ? 0 : value;

class Part {
  final String kind;
  final String name;
  final int? width; // null: alabildigi kadar genis
  final List<Part> children;
  final int factor;
  final bool tightFlex;
  const Part(this.kind, this.name, this.width, this.children,
      {this.factor = 0, this.tightFlex = true});
}

Part leaf(String name, int width) => Part('leaf', name, width, const []);
Part fill(String name) => Part('leaf', name, null, const []);
Part pad(String name, int padding, Part child) => Part('pad', name, padding, [child]);
Part center(String name, Part child) => Part('center', name, null, [child]);
Part fixed(String name, int width, Part child) => Part('fixed', name, width, [child]);
Part column(String name, List<Part> children) => Part('column', name, null, children);
Part row(String name, List<Part> children) => Part('row', name, null, children);
Part flex(String name, int factor, Part child, {bool tight = true}) =>
    Part('flex', name, null, [child], factor: factor, tightFlex: tight);

class Sizing {
  final String sizing;
  final String path;
  final int width;
  final Limit limit;
  const Sizing(this.sizing, this.path, this.width, this.limit);
}

Sizing classify(String path, int? desire, Limit limit) {
  if (limit.tight) return Sizing('dayatilan', path, limit.min, limit);
  if (desire == null) {
    return limit.max == null
        ? Sizing('kirpilan', path, limit.min, limit)
        : Sizing('esnetilen', path, limit.max!, limit);
  }
  if (desire < limit.min) return Sizing('kirpilan', path, limit.min, limit);
  if (limit.max != null && desire > limit.max!) {
    return Sizing('kirpilan', path, limit.max!, limit);
  }
  return Sizing('secilen', path, desire, limit);
}

int layout(Part part, Limit limit, List<Sizing> log, [String path = '']) {
  final here = '$path/${part.name}';
  final below = <Sizing>[];
  if (part.kind == 'fixed') {
    final own = classify(here, part.width, limit);
    layout(part.children.first, Limit(own.width, own.width), below, here);
    log..add(own)..addAll(below);
    return own.width;
  }
  int? desire;
  switch (part.kind) {
    case 'leaf':
      desire = part.width;
    case 'pad':
      final space = 2 * part.width!;
      desire = layout(part.children.first, limit.deflate(space), below, here) + space;
    case 'center':
      final inner = layout(part.children.first, limit.loosen(), below, here);
      desire = limit.max == null ? inner : null;
    case 'flex':
      desire = layout(part.children.first, limit, below, here);
    case 'column':
      var widest = 0;
      for (final child in part.children) {
        final width = layout(child, limit.loosen(), below, here);
        if (width > widest) widest = width;
      }
      desire = widest;
    case 'row':
      final logs = [for (final _ in part.children) <Sizing>[]];
      final bounded = limit.max != null;
      var used = 0;
      var factors = 0;
      for (var i = 0; i < part.children.length; i++) {
        final child = part.children[i];
        if (child.kind == 'flex' && bounded) {
          factors += child.factor;
        } else {
          used += layout(child, const Limit(0, null), logs[i], here);
        }
      }
      final left = bounded ? _floor(limit.max! - used) : 0;
      var given = 0;
      var seen = 0;
      for (var i = 0; i < part.children.length; i++) {
        final child = part.children[i];
        if (child.kind != 'flex' || !bounded) continue;
        seen += child.factor;
        final share = (seen == factors ? left : left * seen ~/ factors) - given;
        given += share;
        final room = child.tightFlex ? Limit(share, share) : Limit(0, share);
        used += layout(child, room, logs[i], here);
      }
      for (final entries in logs) {
        below.addAll(entries);
      }
      desire = bounded ? null : used;
  }
  final own = classify(here, desire, limit);
  log..add(own)..addAll(below);
  return own.width;
}

List<Sizing> measure(Part root, int screen) {
  final log = <Sizing>[];
  layout(root, Limit(screen, screen), log);
  return log;
}

void printSizes(List<Sizing> log) {
  for (final entry in log) {
    print('${entry.sizing}: ${entry.path} genislik=${entry.width} kisit=${entry.limit.bounds()}');
  }
}

void countSizings(List<Sizing> log) {
  for (final name in sizings) {
    print('$name: ${log.where((entry) => entry.sizing == name).length}');
  }
  print('ebeveyn: ${log.where((entry) => entry.sizing != 'secilen').length}');
}

Çerçeve Tarafı

Aşağıdaki parça sağlayıcı kalıbının çerçeve tarafını gösterir. Parça ölçülmez; model çerçevenin kodunu çalıştırmaz.

// Platform kodu: saglayici bileseni
class CounterScope extends InheritedNotifier<ValueNotifier<int>> {
  const CounterScope({super.key, required ValueNotifier<int> counter, required super.child})
      : super(notifier: counter);

  static int of(BuildContext context) {
    final scope = context.dependOnInheritedWidgetOfExactType<CounterScope>();
    return scope!.notifier!.value;
  }
}

Parçada sayaç değeri ağacın üstünde bir bildirim nesnesinde durur. of çağrısı yapan bileşen o değere bağımlı olur; değer değişince çerçeve yalnız bu bağımlı bileşenleri yeniden kurar. Hangi yaklaşım seçilirse seçilsin düzen aşamasının girdisi değişmez: kısıt aşağıdan gelir, bileşenin isteği ise artık durumdan hesaplanır.

Sayaçtan Rozete

Modelin durumu paylaşılan bir sayaçtır. Sayacı bir yardımcı dosya tutar: değeri saklar, dinleyicileri çağırır ve çıktıya hiçbir şey basmaz. Bu, tepkisel kalıbın en küçük hâlidir.

// store.dart — paylasilan sayac deposu; her degisimde dinleyicilere haber verir
class CounterStore {
  int count;
  final List<void Function(int)> _listeners = [];
  CounterStore(this.count);

  void listen(void Function(int) listener) => _listeners.add(listener);

  void update(int value) {
    count = value;
    for (final listener in _listeners) {
      listener(count);
    }
  }
}

Rozetin isteği sayacın basamak sayısına bağlıdır: her basamak 12 birim ekler ve rozet 18 birimlik sabit bir boşlukla başlar. buildHeader depodan ağaç üretir: ekranın içinde bir sütun, sütunun içinde başlık ile rozeti taşıyan bir satır. Genişlikler modelin birimidir.

// main.dart
import 'sizing.dart';
import 'store.dart';

Part buildHeader(CounterStore store, {int title = 120}) {
  final digits = '${store.count}'.length;
  return pad('Screen', 16, column('Body', [
    row('Header', [
      leaf('Title', title),
      leaf('Badge', 18 + 12 * digits),
    ]),
  ]));
}

void main() {
  final store = CounterStore(9);
  final log = measure(buildHeader(store), 360);
  printSizes(log);
  countSizings(log);
}
dayatilan: /Screen genislik=360 kisit=360..360
dayatilan: /Screen/Body genislik=328 kisit=328..328
esnetilen: /Screen/Body/Header genislik=328 kisit=0..328
secilen: /Screen/Body/Header/Title genislik=120 kisit=0..sonsuz
secilen: /Screen/Body/Header/Badge genislik=30 kisit=0..sonsuz
dayatilan: 2
secilen: 2
kirpilan: 0
esnetilen: 1
ebeveyn: 3

Sayaç 9 iken rozet tek basamaklıdır ve 30 birim ister. Satır sınırlı bir kısıt aldığı için isteğini bildirmez; ama esnek olmayan iki çocuğuna sınırsız kısıt verir. Title 120, Badge 30 ister ve ikisi de sınırsız aralığın içinde kaldığından seçilendir. Genişliği başlık ve rozet kendileri belirler; ebeveyn yalnız ekran, sütun ve satır kayıtlarında sayılır.

Sayaç Değişince

Şimdi sayaca abone olan bir dinleyici ekleyelim. Dinleyici her değişimde ağacı yeniden üretir, yeniden ölçer ve yalnız rozetin kaydını basar. Sayaç 9’dan 1000’e ilerlerken 10 ve 99 aynı genişliği verecek, 100 ve 1000 farklı basamak sayısına geçecek.

// main.dart
import 'sizing.dart';
import 'store.dart';

Part buildHeader(CounterStore store, {int title = 120}) {
  final digits = '${store.count}'.length;
  return pad('Screen', 16, column('Body', [
    row('Header', [
      leaf('Title', title),
      leaf('Badge', 18 + 12 * digits),
    ]),
  ]));
}

void main() {
  final store = CounterStore(9);
  store.listen((value) {
    final log = measure(buildHeader(store), 360);
    final badge = log.firstWhere((entry) => entry.path.endsWith('/Badge'));
    print('sayac=$value ${badge.sizing} genislik=${badge.width} kisit=${badge.limit.bounds()}');
  });
  for (final value in [9, 10, 99, 100, 1000]) {
    store.update(value);
  }
}
sayac=9 secilen genislik=30 kisit=0..sonsuz
sayac=10 secilen genislik=42 kisit=0..sonsuz
sayac=99 secilen genislik=42 kisit=0..sonsuz
sayac=100 secilen genislik=54 kisit=0..sonsuz
sayac=1000 secilen genislik=66 kisit=0..sonsuz

Rozet 9’da 30, 99’a kadar 42, 100’de 54 ve 1000’de 66 birim genişliğindedir. Her satırda boyutlanma aynı kalır: secilen. Genişlik büyüdü, çünkü rozetin isteği büyüdü; ama genişliği hâlâ rozet belirliyor. Kısıt 0..sonsuz olduğu için isteği hiçbir değer aralığın dışına itemez. Kural durumun kendisini değil, durumun isteğe yansıyan kısmını görür: 10 ile 99 farklı sayılardır, ama aynı basamak sayısını verdikleri için aynı kayıt çıkar.

Olay tabanlı bir kitaplıkla da tepkisel bir kitaplıkla da sonuç aynıdır. Yaklaşımlar durumun nasıl taşındığını değiştirir; düzen aşamasına giren şey yine kısıt ve istektir.

Sınırlayıcı Ölçüm

Satır esnek olmayan çocuklarına sınırsız kısıt verdiği için rozetin büyümesini hiçbir şey durdurmaz. Çocukların toplamı satırın kendi aralığını aşabilir. Bunu görmek için ekranı 200 birime daraltalım ve başlığa 160 birim isteyelim. Sayaç 1000’de, rozet 66 birimdir. Model taşmayı göstermez: kaydın dili boyutlanmadır ve her kayıt yalnız kendi kısıtına bakar. Çocukların toplamını ders ayrı bir satırda hesaplar ve basar.

// main.dart
import 'sizing.dart';
import 'store.dart';

Part buildHeader(CounterStore store, {int title = 120}) {
  final digits = '${store.count}'.length;
  return pad('Screen', 16, column('Body', [
    row('Header', [
      leaf('Title', title),
      leaf('Badge', 18 + 12 * digits),
    ]),
  ]));
}

void main() {
  final store = CounterStore(1000);
  final log = measure(buildHeader(store, title: 160), 200);
  printSizes(log);
  final header = log.firstWhere((entry) => entry.path == '/Screen/Body/Header');
  var total = 0;
  for (final entry in log.where((entry) => entry.path.startsWith('/Screen/Body/Header/'))) {
    total += entry.width;
  }
  print('cocuklar toplami: $total satir: ${header.width}');
}
dayatilan: /Screen genislik=200 kisit=200..200
dayatilan: /Screen/Body genislik=168 kisit=168..168
esnetilen: /Screen/Body/Header genislik=168 kisit=0..168
secilen: /Screen/Body/Header/Title genislik=160 kisit=0..sonsuz
secilen: /Screen/Body/Header/Badge genislik=66 kisit=0..sonsuz
cocuklar toplami: 226 satir: 168

Odak düğüm /Screen/Body/Header/Badge’tır. Kaydı secilen, genişliği 66, kısıtı 0..sonsuz’dur: ekran daralmış, başlık büyümüş ve satır 168 birime inmiştir, ama rozet yine kendi isteğini seçmiştir. Son satır ölçüme değil, toplama aittir: iki çocuk birlikte 226 birim ister, satırın aralığı ise 168 birimdir. Taşma buradadır; gerçek çerçevede satırın çocukları sınırsız kısıtla ölçüldüğü için bu toplam bir taşma uyarısına dönüşür. Kural bunu görmez: ne taşmanın uyarısını, ne ekranda görünen şeridini, ne de hangi çocuğun kesileceğini kaydeder.

UY1. Model yalnız genişliği ölçer. Rozetin yüksekliği, metnin satıra kırılması ve bir durum değişiminin kaç kez yeniden kurmaya yol açtığı modelde yoktur. Model, bu boyutlanmanın kuralını kurar; model çerçevenin kendisi değildir. Basamak sayısından genişlik hesabı dersin varsayımıdır; gerçek bir rozet genişliğini yazı tipinin ölçülerinden alır.

Bu tür taşmayı önlemenin yolu durum yönetiminde değil, düzendedir: rozeti esnek bir çocuğa sarmak ya da başlığı kırpılabilir bir alana koymak gerekir. Durum yaklaşımları isteği değiştirir; isteğin sığıp sığmadığını kısıt belirler.

Özet

  • Bu dersin boyutlanması: seçilen. Odak düğüm /Screen/Body/Header/Badge’tır: satırın sınırsız kısıtında 66 birim isteyen rozet kendi isteğini seçmiştir.
  • Sağlayıcı, olay tabanlı ve tepkisel yaklaşımlar durumun nasıl paylaşıldığını değiştirir; düzen aşamasına giren kısıt ve istek aynı kalır.
  • Sayaç 9’dan 1000’e çıkınca rozetin genişliği 30’dan 66’ya büyüdü, boyutlanma her adımda secilen kaldı; aynı basamak sayısını veren sayılar aynı kaydı verdi.
  • Satır esnek olmayan çocuklarına sınırsız kısıt verir; çocukların toplamı (226) satırın aralığını (168) aşabilir ve model bunu göstermez.
  • Kural isteğe girmeyen değeri görmez: durumun genişliğe yansımayan farkları kaydı değiştirmez.

Sonraki Adım

Bu derste genişliği durumdan gelen bir değer değiştirdi, ama kısıt hep sınırsız ya da gevşekti. Sıradaki ders, Gezinme ve Yönlendirme, bir sayfanın genişliğini kimin verdiğini sorar: hangi rota açılırsa açılsın gezinme kabının sayfaya ne dayattığını ölçer.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat