İçeriğe geç
academia.sh

Ders 04 / 19

Durumsuz ve Durumlu Bileşenler

Durumsuz bileşen tarifini bir kez verir, durumlu bileşen kendi durumunu ayrı bir nesnede taşır. Ders durum değişince tarifin yeniden kurulduğunu ve bir arama kutusunun genişliğini yeniden düzenden sonra kimin verdiğini ölçer.

Yayın Güncelleme

Bu derste

  • Durumsuz ve durumlu bileşenin farkını, durumun nerede durduğu üzerinden açıklar
  • Durum değişince tarifin yeniden kurulduğunu ve yeni tarifin yeni bir ölçüm verdiğini gösterir
  • Aynı arama kutusunun kapalıyken seçilen, açıkken esnetilen olduğunu ölçer
İçindekiler

Bileşen değişmez bir tariftir. Peki bir arama kutusu açılıp kapandığında, ya da bir sayaç arttığında ne değişir? Tarif değişmediğine göre değişen bir şey başka bir yerde durmalıdır. Bu yer durumdur ve bileşenler durumu nerede tuttuklarına göre ikiye ayrılır.

Durumsuz Bileşen

Durumsuz bileşen yalnız aldığı parametrelerden kurulur. Aynı parametreyle her çağrıda aynı tarifi verir ve kendi başına hiçbir şey hatırlamaz. Başlık etiketi, simge ve bir satırın metni böyledir. Dersin çekirdeği yine ilk dersin sizing.dart dosyasıdır.

// 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}');
}

Durumsuz bir bileşeni modelde, parametre alıp ağaç döndüren sıradan bir işlev olarak yazabiliriz. Aşağıdaki işlev bir arama kutusunun tarifini üretir; kutu açıksa genişliği boş yere yayılır, kapalıysa sabit bir istek bildirir.

DB1. Modelde durum, bir sınıfın alanıdır ve değişiklik toggle gibi sıradan bir yöntemle yapılır. Çerçevenin durum bildirme düzeni (setState) ölçülmez, KAYNAK parça olarak gösterilir.

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

class SearchModel {
  bool expanded = false;
  void toggle() {
    expanded = !expanded;
  }
}

Part buildSearch(bool expanded) {
  final search = expanded ? fill('Search') : leaf('Search', 120);
  return pad('Screen', 16, column('Body', [search]));
}

void main() {
  final model = SearchModel();
  print('kapali');
  printSizes(measure(buildSearch(model.expanded), 360));
  model.toggle();
  print('acik');
  printSizes(measure(buildSearch(model.expanded), 360));
}
kapali
dayatilan: /Screen genislik=360 kisit=360..360
dayatilan: /Screen/Body genislik=328 kisit=328..328
secilen: /Screen/Body/Search genislik=120 kisit=0..328
acik
dayatilan: /Screen genislik=360 kisit=360..360
dayatilan: /Screen/Body genislik=328 kisit=328..328
esnetilen: /Screen/Body/Search genislik=328 kisit=0..328

buildSearch durumsuzdur: expanded değeri aynıysa her çağrı aynı ağacı verir. Durum SearchModel içinde durur ve işlevin dışındadır. Kutu kapalıyken Search 120 ister ve gevşek kısıtta 120 alır. Açıkken istek yoktur, üst sınır sonludur ve Search 328’in tamamını alır.

Durumlu Bileşen

Durumlu bileşen iki nesneden oluşur: değişmez tarifi taşıyan bileşen ve durumu taşıyan, ağaçta yaşamaya devam eden durum nesnesi. Çerçeve, durum nesnesinin build yöntemini her çağırdığında yeni bir tarif alır. Durumu değiştirmek için setState çağrılır; bu çağrı durumun değiştiğini bildirir ve çerçeveyi build’i yeniden çağırmaya yönlendirir.

// Platform kodu: durumlu bilesen
class SearchBox extends StatefulWidget {
  const SearchBox({super.key});

  @override
  State<SearchBox> createState() => _SearchBoxState();
}

class _SearchBoxState extends State<SearchBox> {
  bool expanded = false;

  void toggle() {
    setState(() {
      expanded = !expanded;
    });
  }

  @override
  Widget build(BuildContext context) {
    return expanded ? const TextField() : const SizedBox(width: 120);
  }
}

Bu parçada tarif (SearchBox) hiç değişmez; expanded değeri durum nesnesinde yaşar. toggle çağrıldığında build yeni bir alt ağaç döndürür. Modeldeki karşılığı SearchModel ile buildSearch ikilisidir.

Tarif Değişince Ne Değişir

Durum değiştiğinde yeniden kurulan şey tariftir. Modelde boyutu etkilemeyen bir durum, örneğin kutunun içindeki ipucu metni değiştiğinde, ağacın yapısı ve istekleri aynı kalır. Bu durumda ölçüm de aynı kalır.

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

Part buildSearch(bool expanded, String hint) {
  final search = expanded ? fill('Search') : leaf('Search', 120);
  return pad('Screen', 16, column('Body', [search]));
}

void main() {
  for (final hint in ['Ara', 'Ders ara']) {
    print('ipucu: $hint');
    printSizes(measure(buildSearch(true, hint), 360));
  }
}
ipucu: Ara
dayatilan: /Screen genislik=360 kisit=360..360
dayatilan: /Screen/Body genislik=328 kisit=328..328
esnetilen: /Screen/Body/Search genislik=328 kisit=0..328
ipucu: Ders ara
dayatilan: /Screen genislik=360 kisit=360..360
dayatilan: /Screen/Body genislik=328 kisit=328..328
esnetilen: /Screen/Body/Search genislik=328 kisit=0..328

hint işleve verilir ama ağaca girmez; iki çağrının çıktısı birebir aynıdır. Bir durum değişikliği yeniden kurmayı tetikler, fakat yeniden kurulan tarif aynı isteği bildiriyorsa genişliklerin hiçbiri oynamaz.

DB2. Modelde ipucu metni genişliği etkilemez; gerçek bir kutuda çok uzun bir ipucu metni kutunun isteğini değiştirebilir. Burada o durum modellenmez, çünkü model metin ölçmez.

SwiftUI ile Arayüz kursunda durum değişince arayüzün yeniden hesaplanmasını neyin tetiklediği sorulur: değişen değer, ona bağlı görünümler ve yeniden hesaplama zinciri. Bu derste soru o değildir. Burada “neden yeniden kuruldu” sorusu sorulmaz; yeniden kurulan ağaçta genişliği kimin verdiği sorulur. Tetikleyici aynı olsa da sonuç sahibi değişebilir: yukarıdaki kutu kapalıyken kendi isteğini, açıkken ebeveyninin kısıtını izler.

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

Odak düğüm arama kutusudur: /Screen/Body/Search. Kutu açıkken istek bildirmez ve üstteki aralık sonludur. İki ekran genişliğinde ölçelim.

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

Part buildSearch(bool expanded) {
  final search = expanded ? fill('Search') : leaf('Search', 120);
  return pad('Screen', 16, column('Body', [search]));
}

void main() {
  for (final width in [480, 320]) {
    print('ekran: $width');
    printSizes(measure(buildSearch(true), width));
  }
}
ekran: 480
dayatilan: /Screen genislik=480 kisit=480..480
dayatilan: /Screen/Body genislik=448 kisit=448..448
esnetilen: /Screen/Body/Search genislik=448 kisit=0..448
ekran: 320
dayatilan: /Screen genislik=320 kisit=320..320
dayatilan: /Screen/Body genislik=288 kisit=288..288
esnetilen: /Screen/Body/Search genislik=288 kisit=0..288

Bu dersin boyutlanması: esnetilen. /Screen/Body/Search açıkken istek bildirmez, üst sınır 448 ya da 288’dir ve düğüm onu doldurur. Kutunun genişliği ekran genişliğiyle birlikte değişir; kapalı hâlinde ise hiçbir ekranda oynamayan 120’dir. Aynı düğümün iki durumu iki farklı sonuç sahibine denk gelir: kapalıyken kendi isteği (seçilen), açıkken ebeveynin üst sınırı (esnetilen).

Özet

  • Bu dersin boyutlanması: esnetilen. Açık arama kutusu istek bildirmez, ebeveynin üst sınırını doldurur ve genişliği 480 ekranda 448, 320 ekranda 288’dir.
  • Durumsuz bileşen yalnız parametrelerden kurulur ve aynı parametreyle aynı tarifi verir.
  • Durumlu bileşen, değişmez tarifini ve değişen durumunu iki ayrı nesnede taşır; durum değişince tarif yeniden kurulur.
  • Yeniden kurulan tarif aynı isteği bildiriyorsa genişlikler oynamaz; ipucu metni değişse de iki ölçüm aynıdır.
  • Aynı kutu kapalıyken seçilen, açıkken esnetilen olur: durum değişince sonucun sahibi de değişebilir.

Sonraki Adım

Bir bileşenin durumu değiştiğinde çerçeve ağacı yeniden kurar, ama ekrandaki nesneleri baştan yaratmaz. Üç Ağaç Modeli dersi, tarifin, yaşayan nesnenin ve ekrandaki düzenin üç ayrı ağaç olarak nasıl birbirine bağlandığını anlatır.

İ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