2-qism · Flutter arxitekturasi·05-bob
Widget turlari, State lifecycle va Keys
StatelessWidget va StatefulWidget farqi, State obyektining to'liq hayot davri, va Keys — Flutter intervyularidagi eng ko'p chalkashtiriladigan mavzu.
Agar oldingi bobda Element va reconciliation qanday ishlashini tushungan
bo'lsangiz, bu bob shuning ustiga quriladi: StatefulWidgetning State
obyekti aniq qachon yaratiladi va yo'q qilinadi, va Keylar bu jarayonga
qanday ta'sir qiladi. Bu — amaliy Flutter intervyularida eng ko'p "nega bu
bug chiqdi" tarzida so'raladigan mavzu, chunki noto'g'ri tushunilgan Keys
production'da ko'rinadigan, aniqlash qiyin bo'lgan xatolarga olib keladi.
1. StatelessWidget vs StatefulWidget — haqiqiy farq#
Ko'pchilik "StatelessWidget — o'zgarmaydi, StatefulWidget — o'zgaradi" deb javob beradi. Bu noto'liq. Haqiqiy farq — kim holatni saqlaydi:
class Greeting extends StatelessWidget {
final String name;
const Greeting({super.key, required this.name});
@override
Widget build(BuildContext context) => Text('Salom, $name!');
}StatelessWidgetning o'zi ham, uni ifodalovchi StatelessElement ham
hech qanday mutatsiyalanadigan holatni saqlamaydi — ekranda ko'rinadigan
narsa to'liq uning konstruktor parametrlariga bog'liq. U qayta chizilishi
uchun tashqaridan yangi parametrlar bilan qayta yaratilishi kerak.
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _count = 0; // Bu widget'ning o'zida emas, alohida State obyektida yashaydi
@override
Widget build(BuildContext context) => Text('$_count');
}StatefulWidgetning o'zi ham aslida immutable (_count widget klassida
emas!) — u faqat createState() orqali alohida, mutatsiyalanadigan State
obyektini yaratadi. Shu State obyekti — Element daraxtida yashaydigan,
widget qayta build bo'lganda ham saqlanadigan narsa. Widget o'zi har build
safar qayta yaratilishi mumkin, lekin State bitta bo'lib qoladi (agar
reconciliation "bu bir xil Element" deb topsa — bu aynan 3-bobda ko'rgan
canUpdate mantig'i).
2. State lifecycle — to'liq ketma-ketlik#
constructor
Stateobyekti yaratiladi, lekin haliBuildContextga ega emas —contextga bog'liq hech narsa shu yerda qilib bo'lmaydi.initState()
Birinchi marta va faqat bir marta chaqiriladi.
contextga bog'liq bo'lmagan boshlang'ich sozlash shu yerda:AnimationControlleryaratish,Streamga obuna bo'lish,TextEditingControlleryaratish. Har doimsuper.initState()ni birinchi qatorda chaqirish shart.didChangeDependencies()
initState()dan keyin darhol, va keyin har safar bu State bog'liq bo'lganInheritedWidgeto'zgarganda chaqiriladi.contextga bog'liq narsalar (masalan,Theme.of(context),MediaQuery.of(context)) shu yerda o'qilishi mumkin —initState()da emas, chunki u yerdaInheritedWidgetzanjiri hali to'liq bog'lanmagan bo'lishi mumkin.build()
UI qayta quriladi.
setState()chaqirilganda,didUpdateWidget()dan keyin, yokididChangeDependencies()dan keyin chaqiriladi. Ko'p marta chaqirilishi mumkin — shuning uchun bu yerda og'ir hisob-kitob yoki yon ta'sir (side effect) qilmaslik kerak.didUpdateWidget(oldWidget)
Parent qayta build bo'lib, bir xil Element uchun yangi widget konfiguratsiyasi kelganda chaqiriladi (
canUpdatetrue qaytargan holat). Eski va yangi widget'ni solishtirib, agar konfiguratsiya o'zgargan bo'lsa (masalan, yangicontrollerberilgan bo'lsa), State'ni shunga moslashtirish kerak bo'ladi.deactivate()
Element daraxtdan vaqtincha olib tashlanganda (masalan,
GlobalKeybilan ko'chirilayotganda). Ko'pincha bo'sh qoldiriladi.dispose()
State butunlay yo'q qilinganda, bir marta chaqiriladi. Barcha resurslarni tozalash shart:
AnimationController.dispose(),StreamSubscription.cancel(),TextEditingController.dispose(). Bu qadamni o'tkazib yuborish — Flutter'da eng keng tarqalgan memory leak sababi.
class _VideoPlayerState extends State<VideoPlayer> {
late final AnimationController _controller;
StreamSubscription? _sub;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: const Duration(seconds: 1));
_sub = widget.stream.listen(_onData);
}
@override
void didUpdateWidget(covariant VideoPlayer oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.stream != widget.stream) {
_sub?.cancel();
_sub = widget.stream.listen(_onData); // Eski obuna bekor qilinmasa — memory leak va ikki marta ma'lumot kelishi mumkin
}
}
@override
void dispose() {
_controller.dispose();
_sub?.cancel();
super.dispose();
}
void _onData(dynamic data) => setState(() {});
@override
Widget build(BuildContext context) => const Placeholder();
}3. Keys — nega ular umuman kerak#
Keys — reconciliation algoritmiga qo'shimcha "identifikatsiya" ma'lumoti beradi. Standart holatda Flutter widget'larni turi va pozitsiyasi bo'yicha solishtiradi. Bu quyidagi holatlarda yetarli emas:
// Key'siz — vaziyat: ro'yxatdan birinchi elementni o'chirish
ListView(
children: [
TodoTile(todo: todos[0]), // pozitsiya 0
TodoTile(todo: todos[1]), // pozitsiya 1
TodoTile(todo: todos[2]), // pozitsiya 2
],
)Agar todos[0] ro'yxatdan o'chirilsa, yangi ro'yxat [todos[1], todos[2]]
bo'ladi. Flutter buni pozitsiya bo'yicha solishtiradi: eski pozitsiya 0
(todos[0] uchun TodoTile) endi yangi pozitsiya 0'dagi (todos[1] uchun
TodoTile) bilan "bir xil Element" deb hisoblanadi (chunki turi bir xil,
key yo'q) — va shunchaki update() qilinadi. Agar TodoTile ichida
o'zining lokal State'i bo'lsa (masalan, Checkbox holati alohida
saqlangan bo'lsa), bu holat noto'g'ri elementga o'tib qoladi — vizual
"State sakrashi" bug'i.
// Key bilan — to'g'ri yechim
ListView(
children: todos.map((todo) => TodoTile(
key: ValueKey(todo.id), // Endi identifikatsiya ma'lumot, pozitsiya emas
todo: todo,
)).toList(),
)Endi Flutter Element'larni key qiymati bo'yicha moslashtiradi:
todos[0] o'chirilganda, ValueKey(todos[0].id)ga mos Element to'g'ridan-
to'g'ri unmount() qilinadi, qolgan ikkitasi esa o'z State'lari bilan
birga saqlanib qoladi, faqat pozitsiyasi almashadi.
4. Key turlari va qachon ishlatish kerak#
| Key turi | Qachon ishlatiladi |
|---|---|
ValueKey<T> | Qiyoslanadigan qiymat mavjud bo'lganda (masalan, ID, string) — eng ko'p ishlatiladigan tur |
ObjectKey | Qiymat emas, butun obyekt identifikatorini solishtirish kerak bo'lganda (== orqali) |
UniqueKey | Har doim boshqacha bo'lishi kerak bo'lgan holatlar — masalan, animatsiyani majburan qayta ishga tushirish uchun elementni "yangidan yaratilgan" deb ko'rsatish |
GlobalKey<T> | Elementni butun ilova bo'ylab topish yoki uning Stateiga tashqaridan murojaat qilish kerak bo'lganda (masalan, Form validatsiyasi, animatsiyani boshqa widget'dan boshqarish) |
final formKey = GlobalKey<FormState>();
Form(
key: formKey,
child: TextFormField(validator: (v) => v!.isEmpty ? 'Bo\'sh bo\'lmasin' : null),
)
// Boshqa joyda, masalan tugma bosilganda:
if (formKey.currentState!.validate()) {
// ...
}5. InheritedWidget — daraxt bo'ylab ma'lumot uzatishning asosi#
Provider, Riverpod, Bloc kabi barcha state management kutubxonalari
tagida InheritedWidgetga tayanadi:
class ThemeData2 extends InheritedWidget {
final Color primaryColor;
const ThemeData2({super.key, required this.primaryColor, required super.child});
static ThemeData2 of(BuildContext context) {
final result = context.dependOnInheritedWidgetOfExactType<ThemeData2>();
assert(result != null, 'ThemeData2 topilmadi');
return result!;
}
@override
bool updateShouldNotify(ThemeData2 oldWidget) => primaryColor != oldWidget.primaryColor;
}of(context) ichidagi dependOnInheritedWidgetOfExactType — oddiy
findAncestorWidgetOfExactTypedan farqli o'laroq, chaqirayotgan Element'ni
InheritedElementning bog'liqlar ro'yxatiga yozib qo'yadi. Shu sabab
ThemeData2.of(context) faqat build() ichida chaqirilishi kerak —
initState() ichida chaqirilsa, obuna to'g'ri ro'yxatga yozilmaydi (chunki
u yerda context hali to'liq joylashtirilmagan).
InheritedModel — InheritedWidgetning kengaytmasi bo'lib, bog'liqlarni
aspekt (masalan, faqat "rang" yoki faqat "shrift o'lchami") bo'yicha
ajratish imkonini beradi, shunda faqat tegishli aspektga bog'liq widget'lar
qayta build bo'ladi — bu katta ilovalarda keraksiz rebuild'larni kamaytiradi.
6. const widget'lar — eng arzon optimallashtirish#
// Har build()da yangi obyekt yaratiladi:
Icon(Icons.star, color: Colors.amber)
// Bir marta yaratiladi, keyin qayta ishlatiladi:
const Icon(Icons.star, color: Colors.amber)const widget — Dart compile vaqtida yaratilgan, o'zgarmas obyekt. Parent
qayta build bo'lganda, Flutter const widget uchun bir xil obyekt
identifikatori (identical()) ekanini ko'radi va uni umuman qayta
tekshirmasdan, qayta ishlatadi — hatto canUpdate solishtiruvi ham
qisqartiriladi. Bu — Flutter'da eng arzon, eng ko'p e'tiborsiz qoldiriladigan
performance yaxshilash usuli.
Tez-tez so'raladigan intervyu savollari#
Savol: StatelessWidget umuman "state"ga ega emasmi?
Javob: Widget'ning o'zi mutatsiyalanadigan holatga ega emas, lekin bu
uning ekrani hech qachon o'zgarmasligini anglatmaydi — u shunchaki
tashqaridan (parent'dan yangi parametrlar bilan) qayta yaratilishi
orqali o'zgaradi. Masalan, Text(counter.toString()) — StatelessWidget,
lekin counter o'zgarganda parent uni qayta yaratadi.
Savol: initState() ichida context.dependOnInheritedWidgetOfExactType
yoki Theme.of(context) chaqirsa bo'ladimi?
Javob: Xavfli. initState() chaqirilganda InheritedWidget zanjiri
hali to'liq o'rnatilmagan bo'lishi mumkin, va bu yerdagi obuna to'g'ri
ishlamaydi. Buning o'rniga didChangeDependencies()da chaqirish tavsiya
etiladi — u InheritedWidget o'zgarganda ham avtomatik qayta chaqiriladi.
Savol: Nega dispose()ni chaqirishni unutish xavfli?
Javob: AnimationController, StreamSubscription, TextEditingController
kabi obyektlar ichki resurslarni (masalan, ticker, native handle) ushlab
turadi. Agar dispose() chaqirilmasa, ular widget daraxtdan olib
tashlangandan keyin ham xotirada qolib, memory leak yasaydi — vaqt o'tishi
bilan ilova sekinlashadi yoki qulaydi.
Savol: mounted tekshiruvi nima uchun kerak?
Javob: Asinxron operatsiya (masalan, tarmoq so'rovi) tugagach, State
allaqachon dispose() qilingan bo'lishi mumkin. setState()ni dispose
qilingan State'da chaqirish xato beradi. if (!mounted) return; — shu
holatni oldini oladi, chunki mounted — State hali daraxtga bog'liqligini
bildiruvchi flag.
Savol: Ro'yxatda key bermasangiz nima bo'ladi?
Javob: Flutter elementlarni faqat pozitsiya va tur bo'yicha solishtiradi.
Ro'yxat o'rtasidan element qo'shilsa/o'chirilsa, keyingi barcha
elementlarning State'lari "siljib" ketishi mumkin — masalan, bitta
elementning ochiq/yopiq holati boshqasiga o'tib qolishi. Bu ayniqsa
Checkbox, TextField, ExpansionTile kabi lokal holat saqlaydigan
widget'larda ko'rinadi.
Savol: ValueKey va UniqueKey orasidagi amaliy farq nima?
Javob: ValueKey(id) — bir xil id bo'lsa, bir xil Element deb
hisoblanadi (State saqlanadi). UniqueKey() — har chaqirilganda yangi,
takrorlanmas qiymat yaratadi, shuning uchun Flutter uni har doim yangi
element deb hisoblaydi va eskisini yo'q qilib, yangisini yaratadi — bu
odatda animatsiyani yoki State'ni majburan reset qilish uchun ishlatiladi,
lekin performance narxi bor (State qayta yaratiladi).
Savol: GlobalKeyni haddan tashqari ko'p ishlatish nega yomon?
Javob: GlobalKeylar global reyestrda saqlanadi va har frame'da
noyobligi (assertion) tekshiriladi — bu qo'shimcha xotira va CPU narxi.
Bundan tashqari, GlobalKey orqali State'ga tashqaridan to'g'ridan-to'g'ri
murojaat qilish — widget'ning "faqat o'z parametrlar orqali boshqariladigan"
deklarativ tabiatini buzadi va kodni kuzatish (test qilish, debug qilish)
qiyinlashtiradi. Faqat haqiqiy ehtiyoj bo'lganda (Form, Scaffold,
animatsiyalarni tashqaridan boshqarish) ishlating.
Savol: didUpdateWidget qachon chaqiriladi va uni qachon override
qilish kerak?
Javob: Parent qayta build bo'lib, shu Element saqlanib qolgan holda
yangi widget konfiguratsiyasi kelganda chaqiriladi. Agar State'ingiz
widget'ning biror parametriga (masalan, controller yoki stream) bog'liq
resurs saqlasa va bu parametr o'zgarishi mumkin bo'lsa — didUpdateWidgetda
eski resursni tozalab, yangisini sozlash kerak, aks holda eski va yangi
resurslar ikkalasi ham faol qolib, xatolarga olib kelishi mumkin.
Savol: InheritedWidget orqali ma'lumot yangilanganda, qanday
widget'lar qayta build bo'ladi?
Javob: Faqat dependOnInheritedWidgetOfExactType orqali obuna
bo'lgan Element'lar — bularning barchasi InheritedElementning ichki
_dependents ro'yxatida saqlanadi. Agar biror widget shunchaki
findAncestorWidgetOfExactType (obunasiz o'qish) ishlatgan bo'lsa, u
qayta build bo'lmaydi — bu ikkalasining asosiy farqi.
Savol: const konstruktor ishlatish uchun qanday shartlar bajarilishi
kerak?
Javob: Klassning barcha maydonlari final bo'lishi va konstruktor
const deb belgilanishi kerak; barcha argumentlar ham compile-time
constant bo'lishi kerak (masalan, boshqa const widget'lar). Runtime'da
hisoblanadigan qiymat (masalan, DateTime.now()) const widget ichida
argument sifatida berilolmaydi.
Savol: deactivate() va dispose()ni bir xil deb hisoblash mumkinmi?
Javob: Yo'q. deactivate() — vaqtinchalik holat, ko'pincha bir necha
mikrosekund ichida activate() orqali qaytariladi (masalan, GlobalKey
bilan boshqa joyga ko'chirish). dispose() — yakuniy, qaytarib
bo'lmaydigan holat. Resurslarni tozalashni har doim dispose()da qilish
kerak, deactivate()da emas — aks holda hali ishlatilayotgan resurs
tasodifan yopilib qolishi mumkin.
Ko'p uchraydigan xato javoblar#
| Vaziyat | Junior darajadagi xato | Senior darajadagi to'g'ri yondashuv |
|---|---|---|
| Ro'yxat elementi o'chirilganda checkbox holati "sakraydi" | Buni "Flutter bug'i" deb hisoblash | key: ValueKey(item.id) qo'shish kerakligini tushuntirish |
StreamSubscription dispose()da bekor qilinmagan | "Dart avtomatik tozalaydi" deb o'ylash | Har bir listen() uchun dispose()da cancel() chaqirish shart |
Asinxron callback'da setState chaqirilib, xato chiqadi | Xatoni e'tiborsiz qoldirish yoki try/catch bilan yashirish | if (!mounted) return; tekshiruvi qo'shish |
Har bir widget'ga "xavfsizlik uchun" GlobalKey qo'yish | Bu "yaxshi amaliyot" deb o'ylash | GlobalKeyni faqat tashqi State murojaati zarur bo'lgandagina ishlatish, aks holda ValueKey yoki key umuman kerak emasligini tushunish |
Amaliyot#
Key bilan State saqlanishini isbotlang
StatefulWidget ichida lokal ranga ega uch nechta "karta" komponentini
ro'yxat sifatida ko'rsating. Ulardan birini bossangiz, faqat o'sha
kartaning fon rangi o'zgarsin (lokal State orqali). Endi ro'yxatning
o'rtasidagi elementni dasturiy ravishda o'chiring: birinchi holatda
key bermasdan, ikkinchi holatda ValueKey bilan. Qaysi holatda
ranglar "noto'g'ri" kartalarga o'tib qolishini kuzating va buni
reconciliation nuqtai nazaridan tushuntiring.
Maslahatni ko'rsatish
Uchta rangli Container yarating, har biriga bosilganda rang o'zgaradigan lokal State bering. Ro'yxatdan o'rtadagi elementni o'chiring — avval key'siz, keyin ValueKey bilan, va natijani solishtiring.