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.