Ders 01 / 19
Dart Temelleri
Bir bileşenin genişliğini kimin belirlediğini sınıflayan küçük bir Dart çekirdeği. Ders Dart'ın tiplerini, null güvenliğini ve sınıflarını bu çekirdek üzerinden anlatır ve dört boyutlanmayı tanıtır: dayatılan, seçilen, kırpılan, esnetilen.
Yayın Güncelleme
Bu derste
- Dart'ın tip, null güvenliği ve sınıf yapılarını bir düzen çekirdeği üzerinde okur
- Düzen sözleşmesini açıklar; kısıtlar aşağı iner, boyutlar yukarı çıkar, ebeveyn konumu belirler
- Bir bileşenin genişliğini dayatılan, seçilen, kırpılan ve esnetilen olarak sınıflar
- Aynı ağacın farklı ekran genişliklerinde farklı boyutlanma verdiğini ölçer
İçindekiler
Bu kurs bir arayüz çerçevesini tek bir soruyla açar: bu bileşenin genişliğini kim belirliyor? Burada bileşen, ekranda yer kaplayan arayüz parçasıdır: bir metin, bir görsel, bir düğme ya da başka bileşenleri içeren bir kapsayıcı. Sözcük kurs boyunca hep bu anlamda kalır; yazılım mimarisindeki modül ya da servis anlamına gelmez.
Her bileşene ebeveyninden bir kısıt iner: kabul edilen en küçük ve en büyük genişlik. Bileşenin kendi isteği olabilir ya da olmayabilir. Kısıt ile isteğin birleşimi genişliği verir ve genişliğin sahibi dört taraftan biridir. Adları kurs boyunca değişmez; çıktıda ASCII yazılır, düzyazıda Türkçe karşılığıyla geçer.
- dayatılan (
dayatilan): kısıt sıkıdır, yani en küçük ve en büyük genişlik aynıdır. Bileşenin isteği sonucu değiştiremez. - seçilen (
secilen): bileşenin isteği kısıtın içinde kalır ve genişliği bileşen seçer. - kırpılan (
kirpilan): istek kısıtın dışına taşar ya da kısıt sınırsızken istek yoktur. Genişliği kısıt keser. - esnetilen (
esnetilen): istek yoktur, kısıtın üst sınırı vardır ve bileşen o sınıra kadar genişler.
Seçilen dışındaki üçünde genişliği bileşen değil, üstten gelen kısıt belirler. Bu üçünün toplamına ebeveyn denir; çıktının son satırı onu sayar.
Düzen Sözleşmesi
Kısıtın nereden geldiğini üç cümle söyler. Kısıtlar aşağı iner: ebeveyn, çocuğa kabul edeceği aralığı verir. Boyutlar yukarı çıkar: çocuk o aralığın içinde bir genişlik seçer ve ebeveyne bildirir. Ebeveyn konumu belirler: çocuğun nerede duracağına çocuk değil, ebeveyn karar verir. Bu kurs yalnız ilk iki cümlenin genişlik ekseniyle ilgilenir.
DB1. Model, düzen sözleşmesini yalnız genişlik ekseninde kurar: her düğüm tek bir min..max aralığı alır ve tek bir tamsayı genişlik döndürür. Yükseklik, konum ve çizim modelde yoktur; model çerçevenin kendisi değildir.
Çekirdek Model
Sınıflayıcı tek bir Dart dosyasıdır, sizing.dart. Kurs boyunca her ders onu ilk Dart bloğu olarak taşır ve içeriği hiç değişmez; sonraki derslerde yeniden okumak yerine yalnız üstüne eklenen ağaçlara bakılır. Bu ders dosyayı satır satır açar, çünkü Dart’ın temel yapıları burada birlikte çalışı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}');
}
Tipler ve null güvenliği. Limit sınıfının min alanı int, max alanı int? tipindedir. Dart 3.13.5’te bir tip null kabul etmez; kabul ediyorsa adının sonunda ? taşır. max’ın null olması “üst sınır yok” demektir ve bounds() bunu sonsuz diye yazar. null olabilen bir değeri null olamayan bir yere vermek derleme hatasıdır. Değerin sonuna konan ünlem işareti derleyiciye “bu değer burada null değil” der; çekirdek bunu yalnız önce null denetimi yapılmış yerlerde kullanır. max ?? 'sonsuz' ise max null ise sağ tarafı seçer.
Sınıflar ve değişmezlik. Limit ve Part sınıflarının alanları final, kurucuları const olduğundan nesneler kurulduktan sonra değişmez. loosen() ve deflate() mevcut kısıtı değiştirmez, yenisini döndürür. bool get tight => max == min; bir getter’dır: çağrılırken parantez almaz, limit.tight diye okunur.
Parametreler. Part kurucusunun ilk dört parametresi konumsaldır. Küme parantezi içindeki factor ve tightFlex adlandırılmış parametrelerdir, varsayılan değerleri vardır ve çağrıda adıyla verilir: flex('Note', 1, fill('Text'), tight: false). Yardımcı işlevler leaf, fill, pad gibi kısa adlarla ağaç kurar. fill, genişlik isteğini null bırakır: “alabildiğim kadar genişlerim”.
Koleksiyonlar ve ifadeler. [for (final _ in part.children) <Sizing>[]] her çocuk için boş bir liste kurar. log..add(own)..addAll(below) bir zincirleme çağrıdır; her .. aynı nesneye yeni bir işlem uygular. ~/ tamsayı bölmedir. switch içindeki case dalları break taşımaz; her dal kendi bloğunda biter.
Kural sırası
classify işlevi sınıflamanın tek yeridir ve soruları şu sırayla sorar:
- Kısıt sıkıysa sonuç dayatılandır.
- İstek yoksa: üst sınır sonluysa esnetilen, sınırsızsa kırpılan.
- İstek en küçük genişliğin altındaysa ya da üst sınırın üstündeyse kırpılan.
- Aksi hâlde seçilen.
DB2. Sıkı kısıt ilk sorulur, çünkü kısıt tek bir genişliğe izin veriyorsa bileşenin isteği sonucu değiştiremez. Bu sıra modelin kararıdır; sınıflamayı başka bir sırayla da kurmak mümkündür, ama çıktıdaki ad ancak bu sırayla anlam taşır.
Açılış Düzeni
Aşağıdaki ağaç kursun bağlayıcı açılış örneğidir. Kök Screen, her yandan 16 iç boşluk bırakır ve içinde Body sütunu durur. Sütunda 120 isteyen Title, 400 isteyen Banner ve bir satır olan Toolbar vardır. Satırda 48 isteyen Back ve Menu ile, aralarında esneklik çarpanı 1 olan Search yer alır; Search içindeki Field genişlik istemez.
// main.dart
import 'sizing.dart';
void main() {
final screen = pad('Screen', 16, column('Body', [
leaf('Title', 120),
leaf('Banner', 400),
row('Toolbar', [
leaf('Back', 48),
flex('Search', 1, fill('Field')),
leaf('Menu', 48),
]),
]));
final log = measure(screen, 360);
printSizes(log);
countSizings(log);
}
dayatilan: /Screen genislik=360 kisit=360..360 dayatilan: /Screen/Body genislik=328 kisit=328..328 secilen: /Screen/Body/Title genislik=120 kisit=0..328 kirpilan: /Screen/Body/Banner genislik=328 kisit=0..328 esnetilen: /Screen/Body/Toolbar genislik=328 kisit=0..328 secilen: /Screen/Body/Toolbar/Back genislik=48 kisit=0..sonsuz dayatilan: /Screen/Body/Toolbar/Search genislik=232 kisit=232..232 dayatilan: /Screen/Body/Toolbar/Search/Field genislik=232 kisit=232..232 secilen: /Screen/Body/Toolbar/Menu genislik=48 kisit=0..sonsuz dayatilan: 4 secilen: 3 kirpilan: 1 esnetilen: 1 ebeveyn: 6
Çıktı kuralları sırayla işletir. Screen 360 sıkı kısıt aldığı için dayatılandır. pad iki yandan 16 çıkarır; Body‘ye 328 sıkı kısıt iner ve Body da dayatılandır. Sütun kısıtı gevşetir: çocuklar 0..328 alır. Title 120 ister ve aralığın içinde kaldığından seçilendir. Banner 400 ister, üst sınır 328’dir; genişliğini kısıt keser ve kırpılandır. Toolbar genişlik istemez, üst sınırı 328 olduğundan esnetilendir. Satır, esnek olmayan çocuklara 0..sonsuz verir; Back ve Menu bu aralıkta 48’i seçer. Kalan 328 - 48 - 48 = 232, çarpanı 1 olan Search’e sıkı kısıt olarak iner ve Search ile Field dayatılan olur.
Sayım dört sınıfı ve ebeveyn toplamını verir: 4 dayatılan, 3 seçilen, 1 kırpılan, 1 esnetilen; ebeveyn 4 + 1 + 1 = 6. Bir ayrıntı şimdiden not edilmeli: Search esnek çocuktur, ama esnetilen değil dayatılandır. Esnetilen olan Toolbar’dır. İkisi aynı şey değildir; altıncı ders bu ayrımı açar.
Bileşen Sınıfı
Dart’ta bir bileşen bir sınıftır. Aşağıdaki parça çerçevenin durumsuz bileşen sınıfından türeyen en küçük bileşeni gösterir. Parça ölçülmez; model çerçevenin kodunu çalıştırmaz.
// Platform kodu: bilesen sinifi
class TitleLabel extends StatelessWidget {
const TitleLabel({super.key, required this.text});
final String text;
@override
Widget build(BuildContext context) {
return Text(text);
}
}
Sınıf StatelessWidget’ten türer (extends). required adlandırılmış parametreyi zorunlu yapar; text verilmeden bileşen kurulamaz. build yöntemi bileşenin ne göstereceğini başka bir bileşen olarak döndürür, burada bir Text. @override üst sınıftaki yöntemi yeniden tanımladığını bildirir.
Sınıfın kendisi genişlik hakkında hiçbir şey söylemez. build yalnız ne gösterileceğini tarif eder; genişliği bileşenin ağaçta durduğu yer ve ona inen kısıt belirler. Bu kursun işi o ikinci yarıyı ölçmektir.
İsteğin Yokluğu
Çekirdekteki int? yalnız bir kolaylık değildir; “istek yok” ile “istek sıfır” ayrımını taşır. Aşağıdaki blok iki yaprak kurar, isteklerini yazdırır ve ikisini bir sütunda ölçer.
// main.dart
import 'sizing.dart';
void main() {
final label = leaf('Label', 120);
final space = fill('Space');
print('Label istegi: ${label.width}');
print('Space istegi: ${space.width}');
final screen = pad('Screen', 16, column('Body', [label, space]));
printSizes(measure(screen, 360));
}
Label istegi: 120 Space istegi: null dayatilan: /Screen genislik=360 kisit=360..360 dayatilan: /Screen/Body genislik=328 kisit=328..328 secilen: /Screen/Body/Label genislik=120 kisit=0..328 esnetilen: /Screen/Body/Space genislik=328 kisit=0..328
Label 120 ister ve seçilendir. Space hiçbir şey istemez; isteği null olduğundan kısıt 0..328 iken üst sınıra kadar genişler ve esnetilendir. null ve 0 aynı şey olsaydı bu iki satır ayırt edilemezdi.
Sınırlayıcı Ölçüm
Bir ilke ancak çürütebilecek bir ölçümle sınanır. Açılış ağacını bu kez iki ekran genişliğinde ölçüyoruz ve yalnız odak düğümü, /Screen/Body/Banner‘ı süzüp yazdırıyoruz. Ağaç değişmez; yalnız kök kısıt değişir. Son satırlar 480’deki sayımı verir.
// main.dart
import 'sizing.dart';
Part buildScreen() {
return pad('Screen', 16, column('Body', [
leaf('Title', 120),
leaf('Banner', 400),
row('Toolbar', [
leaf('Back', 48),
flex('Search', 1, fill('Field')),
leaf('Menu', 48),
]),
]));
}
void main() {
for (final screenWidth in [480, 360]) {
print('ekran=$screenWidth');
final log = measure(buildScreen(), screenWidth);
printSizes(log.where((entry) => entry.path == '/Screen/Body/Banner').toList());
}
print('sayim ekran=480');
countSizings(measure(buildScreen(), 480));
}
ekran=480 secilen: /Screen/Body/Banner genislik=400 kisit=0..448 ekran=360 kirpilan: /Screen/Body/Banner genislik=328 kisit=0..328 sayim ekran=480 dayatilan: 4 secilen: 4 kirpilan: 0 esnetilen: 1 ebeveyn: 5
Bu dersin boyutlanması: kırpılan. 480 genişlikte Banner‘ın 400 isteği 0..448 aralığının içinde kalır ve genişliği Banner seçer. 360’ta aynı 400 isteği 0..328 aralığına sığmaz ve genişliği kısıt keser. Bileşen, ağaç ve istek aynıdır; sonucun sahibi yalnız kök kısıt değiştiği için değişir. 480’deki sayım da bunu doğrular: kırpılan sıfırdır, seçilen dörde çıkar.
DB3. Banner’ın 400 isteği modelde hazır bir sayıdır. Gerçek bir görselin genişliği çoğu zaman yüklendikten sonra belli olur; model bunu önceden bilinen bir istek sayar. Genişliğin sonradan geldiği durum bir sonraki dersin konusudur.
Özet
- Bu dersin boyutlanması: kırpılan.
/Screen/Body/Banner400 ister, 360 ekranda kısıt0..328verir ve genişliği kısıt keser. - Dart’ta
int?“değer yok” ihtimalini tipin içine yazar;nullolamayan tipenullvermek derleme hatasıdır ve çekirdekint?ile “istek yok” durumunu taşır. - Düzen sözleşmesi üç cümledir: kısıtlar aşağı iner, boyutlar yukarı çıkar, ebeveyn konumu belirler.
- Genişliğin sahibi dört taraftan biridir: dayatılan, seçilen, kırpılan, esnetilen. Açılış ağacında 4 dayatılan, 3 seçilen, 1 kırpılan, 1 esnetilen vardır; ebeveyn 6’dır.
- Aynı ağaç 480’de seçilen, 360’ta kırpılan sonuç verir: genişliğin sahibi bileşenin tek başına özelliği değil, bileşenle kısıtın birleşimidir.
Sonraki Adım
Banner’ın genişliği bu derste hazır bir sayıydı. Gerçek uygulamalarda böyle bir değer çoğu zaman hemen gelmez. Eşzamansız Dart dersi, genişliğin sonradan geldiği durumu Future ve await ile kurar ve aynı bileşenin değer gelmeden önce ve geldikten sonra hangi boyutlanmayı aldığını ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.