aidoc.uz

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#

  1. constructor

    State obyekti yaratiladi, lekin hali BuildContextga ega emas — contextga bog'liq hech narsa shu yerda qilib bo'lmaydi.

  2. initState()

    Birinchi marta va faqat bir marta chaqiriladi. contextga bog'liq bo'lmagan boshlang'ich sozlash shu yerda: AnimationController yaratish, Streamga obuna bo'lish, TextEditingController yaratish. Har doim super.initState()ni birinchi qatorda chaqirish shart.

  3. didChangeDependencies()

    initState()dan keyin darhol, va keyin har safar bu State bog'liq bo'lgan InheritedWidget o'zgarganda chaqiriladi. contextga bog'liq narsalar (masalan, Theme.of(context), MediaQuery.of(context)) shu yerda o'qilishi mumkin — initState()da emas, chunki u yerda InheritedWidget zanjiri hali to'liq bog'lanmagan bo'lishi mumkin.

  4. build()

    UI qayta quriladi. setState() chaqirilganda, didUpdateWidget()dan keyin, yoki didChangeDependencies()dan keyin chaqiriladi. Ko'p marta chaqirilishi mumkin — shuning uchun bu yerda og'ir hisob-kitob yoki yon ta'sir (side effect) qilmaslik kerak.

  5. didUpdateWidget(oldWidget)

    Parent qayta build bo'lib, bir xil Element uchun yangi widget konfiguratsiyasi kelganda chaqiriladi (canUpdate true qaytargan holat). Eski va yangi widget'ni solishtirib, agar konfiguratsiya o'zgargan bo'lsa (masalan, yangi controller berilgan bo'lsa), State'ni shunga moslashtirish kerak bo'ladi.

  6. deactivate()

    Element daraxtdan vaqtincha olib tashlanganda (masalan, GlobalKey bilan ko'chirilayotganda). Ko'pincha bo'sh qoldiriladi.

  7. 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 turiQachon ishlatiladi
ValueKey<T>Qiyoslanadigan qiymat mavjud bo'lganda (masalan, ID, string) — eng ko'p ishlatiladigan tur
ObjectKeyQiymat emas, butun obyekt identifikatorini solishtirish kerak bo'lganda (== orqali)
UniqueKeyHar 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#

VaziyatJunior darajadagi xatoSenior darajadagi to'g'ri yondashuv
Ro'yxat elementi o'chirilganda checkbox holati "sakraydi"Buni "Flutter bug'i" deb hisoblashkey: ValueKey(item.id) qo'shish kerakligini tushuntirish
StreamSubscription dispose()da bekor qilinmagan"Dart avtomatik tozalaydi" deb o'ylashHar bir listen() uchun dispose()da cancel() chaqirish shart
Asinxron callback'da setState chaqirilib, xato chiqadiXatoni e'tiborsiz qoldirish yoki try/catch bilan yashirishif (!mounted) return; tekshiruvi qo'shish
Har bir widget'ga "xavfsizlik uchun" GlobalKey qo'yishBu "yaxshi amaliyot" deb o'ylashGlobalKeyni faqat tashqi State murojaati zarur bo'lgandagina ishlatish, aks holda ValueKey yoki key umuman kerak emasligini tushunish

Amaliyot#

Amaliy topshiriq

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.