aidoc.uz

2-qism · Flutter arxitekturasi·06-bob

Layout modeli, Sliver'lar va custom chizish

Flutter'ning constraints-asoslangan layout tizimi, Row/Column'dagi Expanded mantiqi, Sliver protokoli va CustomPainter bilan ishlash — barchasi real intervyu savollari bilan.

"Constraints go down. Sizes go up. Parent sets position." — bu jumla Flutter hujjatlarida bejizga takrorlanmaydi: u butun layout tizimining mohiyatini bir qatorga sig'diradi va deyarli har qanday amaliy layout intervyu savoliga javob berish kalitidir. Bu bobda shu qoidaning nima uchun ishlashini, Expanded/Flexible qanday ishlashini, Sliver protokoli nima uchun kerakligini va CustomPainter bilan qanday ishlashni ko'ramiz.

1. Constraints go down, sizes go up#

Flutter layout — bitta o'tishli (single-pass) algoritm, va bu uni juda tez qiladi. Jarayon shunday ishlaydi:

  1. Parent bolaga constraints yuboradi

    Constraints — BoxConstraints(minWidth, maxWidth, minHeight, maxHeight) ko'rinishidagi diapazon, aniq o'lcham emas. Masalan: "eningiz 0 dan 400 pikselgacha, balandligingiz aniq 100 piksel bo'lishi kerak".

  2. Bola o'z o'lchamini shu diapazon ichida hisoblaydi

    Bola constraints'ni tekshiradi, o'z farzandlariga o'z constraints'ini yuboradi (rekursiv), ulardan o'lcham oladi, va o'zining yakuniy o'lchamini tanlaydi — bu doim parent bergan diapazon ichida bo'lishi shart.

  3. Bola o'z o'lchamini parentga qaytaradi

    Faqat Size(width, height) — joylashuv (position) haqida hech qanday ma'lumot yo'q.

  4. Parent bolaning pozitsiyasini belgilaydi

    Bola o'z o'lchamini bilgach, parent (RenderObject.paint bosqichida) offset orqali uni qayerga joylashtirishni hal qiladi — bola bu haqda hech qanday fikri yo'q.

// Constraints ketma-ketligini kuzatish uchun soddalashtirilgan misol:
class RenderFixedSize extends RenderBox {
  @override
  void performLayout() {
    // constraints — parentdan kelgan diapazon
    size = constraints.constrain(const Size(200, 200));
    // constrain() — agar 200x200 diapazondan tashqarida bo'lsa, uni qisadi
  }
}

Shu qoidaning muhim natijasi: bola o'zining ekrandagi pozitsiyasini bilmaydi va hal qila olmaydi — u faqat o'lchamini tanlaydi. Shuning uchun Container ichidagi widget o'zini "markazga" joylashtirolmaydi — buning uchun Center yoki Align kabi parent widget kerak.

2. Flex layout — Row, Column, Expanded, Flexible#

Row/Column (ikkalasi ham Flex klassidan) ikki bosqichda ishlaydi:

Row(
  children: [
    Icon(Icons.star),           // 1-bosqich: "tight bo'lmagan" bolalar avval joylashtiriladi
    Expanded(child: Text('...')), // 2-bosqich: qolgan bo'sh joy Expanded'lar orasida bo'linadi
    Icon(Icons.arrow_forward),
  ],
)
  1. Avval Expanded/Flexible bo'lmagan bolalar o'z tabiiy o'lchamiga moslashtiriladi (masalan, Icon — o'zining standart o'lchami).
  2. Qolgan bo'sh joy hisoblab chiqiladi va Expanded/Flexible bolalar orasida ularning flex qiymati nisbatida bo'linadi.
WidgetXatti-harakat
ExpandedBerilgan bo'sh joyni to'liq egallashga majburlaydi (FlexFit.tight)
Flexible (default)Bo'sh joyni egallashi mumkin, lekin majburiy emas (FlexFit.loose) — agar tabiiy o'lchami kichikroq bo'lsa, o'shani oladi
SpacerKo'rinmas Expanded — faqat bo'sh joy yaratish uchun
Row(
  children: [
    Flexible(flex: 2, child: Container(color: Colors.red)),
    Flexible(flex: 1, child: Container(color: Colors.blue)),
  ],
)
// Bo'sh joy 2:1 nisbatda bo'linadi — masalan 300px bo'sh joy bo'lsa, 200px va 100px

mainAxisSize (MainAxisSize.max — default, .min — faqat kerakli joyni egallash) va crossAxisAlignment/mainAxisAlignment — bo'sh joy va tekislashni boshqaruvchi qo'shimcha parametrlar. Eng ko'p uchraydigan xato: Row ichida cheklanmagan (masalan, ota konteksti ham cheksiz) holatda Expanded ishlatish — bu "cannot fit into unbounded" xatosini beradi, chunki Expandedga "qolgan bo'sh joyni ol" deyish uchun avval chekli bo'sh joy bo'lishi kerak.

3. Sliver protokoli — nega oddiy widget yetmaydi#

Oddiy RenderBox protokoli faqat BoxConstraints (min/max width/height) bilan ishlaydi — bu scroll qilinadigan, "qisman ekranda ko'rinadigan" kontent uchun yetarli emas. Sliver — scroll o'qi bo'ylab qisman render qilinadigan kontent uchun alohida protokol:

CustomScrollView(
  slivers: [
    SliverAppBar(floating: true, expandedHeight: 200),
    SliverList(
      delegate: SliverChildBuilderDelegate(
        (context, index) => ListTile(title: Text('Element $index')),
        childCount: 1000,
      ),
    ),
    SliverGrid(
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
      delegate: SliverChildBuilderDelegate((context, index) => Container(color: Colors.blue)),
    ),
  ],
)

Sliverlar SliverConstraints (scrollOffset, remainingPaintExtent kabi) qabul qiladi va SliverGeometry (paintExtent, layoutExtent, scrollExtent) qaytaradi — bu ularga "men umumiy balandligim 10000px, lekin hozir faqat 800px qismim ekranda ko'rinishi kerak" kabi ma'lumotni ifodalash imkonini beradi. Aynan shu — ListView.builder katta ro'yxatlarda nega tezligini yo'qotmasligining sababi: SliverList faqat ekranda (va uning atrofidagi kichik buferda) ko'rinadigan elementlarni build va layout qiladi, qolganlarini "virtual" holda qoldiradi (lazy building).

4. CustomPainter — Canvas bilan to'g'ridan-to'g'ri ishlash#

Standart widget'lar yetmagan holatlarda (grafiklar, maxsus shakllar, animatsion effektlar), CustomPainter orqali to'g'ridan-to'g'ri chizish mumkin:

class RingPainter extends CustomPainter {
  final double progress; // 0.0 dan 1.0 gacha
  RingPainter(this.progress);
 
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.blue
      ..strokeWidth = 8
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round;
 
    final rect = Offset.zero & size;
    canvas.drawArc(rect.deflate(8), -math.pi / 2, 2 * math.pi * progress, false, paint);
  }
 
  @override
  bool shouldRepaint(covariant RingPainter oldDelegate) {
    return oldDelegate.progress != progress; // Faqat progress o'zgarganda qayta chizish
  }
}
 
// Ishlatish:
CustomPaint(size: const Size(100, 100), painter: RingPainter(0.7))

shouldRepaint — juda muhim optimallashtirish nuqtasi: agar u har doim true qaytarsa, CustomPaint har frame'da qayta chizadi, hatto ma'lumot o'zgarmagan bo'lsa ham. To'g'ri implementatsiya — faqat haqiqatan chizishga ta'sir qiluvchi maydonlarni solishtirish.

5. RepaintBoundary — qayta chizishni izolyatsiya qilish#

RepaintBoundary(
  child: CustomPaint(painter: ExpensiveAnimatedPainter(progress)),
)

Odatda, bitta RenderObject qayta chizilishi kerak bo'lsa, Flutter uning butun layer'ini (o'zi va ba'zan atrofidagilarni) qayta chizadi. RepaintBoundary — daraxtga "bu yerdan alohida composite layer boshlanadi" deb belgi qo'yadi: shu ichkaridagi qayta chizish tashqarisiga ta'sir qilmaydi, va aksincha. Bu ayniqsa tez-tez animatsiya bo'ladigan kichik widget'larni (masalan, loading spinner) katta, statik UI ichida ishlatganda muhim — RepaintBoundary bo'lmasa, spinner har frame'da butun atrofidagi statik UI'ni ham "qayta chizishga arziydigan" deb belgilashi mumkin.

Tez-tez so'raladigan intervyu savollari#

Savol: "Constraints go down, sizes go up" jumlasini tushuntiring.

Javob: Layout jarayonida ma'lumot ikki yo'nalishda oqadi: parent bolaga diapazon (min/max width/height) shaklidagi constraints beradi (yuqoridan pastga), bola esa shu diapazon ichida o'z aniq o'lchamini tanlab, uni parentga qaytaradi (pastdan yuqoriga). Bola hech qachon o'zining ekrandagi pozitsiyasini bilmaydi — pozitsiyani faqat parent, bolaning o'lchamini bilgach, belgilaydi.

Savol: Nega Column ichiga to'g'ridan-to'g'ri ListView qo'yish xato beradi?

Javob: Column o'zining farzandlariga cheklanmagan (maxHeight: infinity) balandlik constraint beradi (agar Column o'zi cheklangan bo'lmasa). ListView esa o'zining ishlashi uchun chekli balandlik talab qiladi (u ichida Sliverlarni joylashtirish uchun aniq maydon kerak). Natijada "RenderBox was given unbounded height" xatosi chiqadi. Yechim — ListViewni Expanded yoki aniq SizedBox(height: ...) bilan o'rash.

Savol: Expanded va Flexible orasidagi farq nima?

Javob: Ikkalasi ham bo'sh joyni flex nisbatida bo'ladi, lekin Expanded (FlexFit.tight) bolani berilgan joyni to'liq egallashga majburlaydi, Flexible (FlexFit.loose) esa bolaga o'z tabiiy o'lchamini tanlash erkinligini beradi — agar bola kichikroq bo'lsa, ortiqcha joy bo'sh qoladi. Aslida Expanded — Flexiblening fit: FlexFit.tight bilan maxsus holati.

Savol: Nega ListView.builder katta ro'yxatlarda ListView (oddiy, children: bilan) dan tezroq ishlaydi?

Javob: Oddiy ListView(children: [...]) barcha elementlarni darhol widget sifatida yaratadi, hatto ular ekranda ko'rinmasa ham. ListView.builder esa Sliver protokoli orqali faqat ekranda (va yaqin atrofdagi buferda) ko'rinadigan elementlarni lazy tarzda build qiladi — ro'yxat 10 ta elementdan iborat bo'lsa ham, 10000 tadan iborat bo'lsa ham, bir vaqtning o'zida faqat bir nechta elementgina xotirada bo'ladi.

Savol: Sliverlarni oddiy widget'lar bilan aralashtirib bo'ladimi?

Javob: To'g'ridan-to'g'ri yo'q — Sliver va oddiy RenderBox boshqa- boshqa protokol (constraints/geometry turlari boshqacha). Ularni bog'lash uchun maxsus adapter widget'lar kerak: SliverToBoxAdapter (oddiy widgetni Sliver ichiga joylashtirish uchun) yoki SliverFillRemaining.

Savol: shouldRepaint nima uchun kerak va uni noto'g'ri implementatsiya qilish qanday oqibatga olib keladi?

Javob: CustomPainter.shouldRepaint — Flutter'ga "bu paintni qayta chaqirish kerakmi" deb so'raydigan metod. Agar u har doim true qaytarsa (yoki umuman override qilinmay defaultda true qolsa), painter har frame'da qayta ishlaydi, hatto ma'lumot o'zgarmagan bo'lsa ham — bu CPU/GPU resursini behuda sarflaydi, ayniqsa murakkab chizmalarda sezilarli sekinlashishga olib keladi.

Savol: RepaintBoundary qachon qo'shish kerak, qachon kerak emas?

Javob: Tez-tez, mustaqil ravishda qayta chiziladigan kichik widget (masalan, animatsiya, spinner) katta, nisbatan statik UI ichida bo'lsa — RepaintBoundary qo'shish foydali, chunki u qayta chizishni izolyatsiya qilib, atrofdagi statik qismlarni qayta chizishdan qutqaradi. Lekin har bir widget'ni RepaintBoundary bilan o'rash zararli — har bir boundary qo'shimcha GPU layer (xotira) yaratadi, shuning uchun faqat DevTools orqali haqiqiy muammo topilgan joyda qo'llash kerak.

Savol: IntrinsicWidth/IntrinsicHeight nima uchun "qimmat" widget hisoblanadi?

Javob: Standart layout bitta o'tishda ishlaydi (constraints pastga, o'lcham yuqoriga). IntrinsicWidth/IntrinsicHeight esa bolaning "tabiiy" o'lchamini bilish uchun uni qo'shimcha marta (ba'zan bir necha marta) layout qiladi — bu single-pass tamoyilini buzadi va performance narxi qo'shadi, ayniqsa chuqur widget daraxtlarida. Faqat chinakam zarur bo'lganda (masalan, bir nechta ustunni bir xil kenglikka moslashtirish) ishlatish kerak.

Savol: Positioned widget qayerda ishlatiladi va nega u yerda ishlaydi, boshqa joyda ishlamaydi?

Javob: Positioned faqat Stack ichida ishlaydi, chunki faqat Stackning RenderStacki o'z farzandlarini Positioned orqali berilgan top/left/right/bottom qiymatlariga qarab mutlaq joylashtirishni biladi. Boshqa parent'lar (masalan, Column) Positioned haqida hech narsa bilmaydi va uni oddiy widget sifatida ham qabul qilmaydi — runtime xatosi beradi.

Savol: LayoutBuilder nima uchun kerak?

Javob: LayoutBuilder — widgetga o'zining parentdan kelayotgan constraintsini build() vaqtida bilish imkonini beradi (BuildContext orqali bevosita bilib bo'lmaydi). Bu responsive dizayn uchun ishlatiladi: masalan, kenglikka qarab bir xil widget o'rniga boshqasini ko'rsatish (constraints.maxWidth > 600 ? WideLayout() : NarrowLayout()).

Ko'p uchraydigan xato javoblar#

BelgiEhtimoliy sababTo'g'ri yechim
RenderFlex overflowed by N pixelsRow/Column ichidagi kontent mavjud joydan katta, Expanded/Flexible yo'qKontentni Expanded bilan o'rash yoki Wrap/scroll qo'shish
BoxConstraints forces an infinite width/heightCheksiz constraint beruvchi widget (Column) ichiga cheksiz talab qiluvchi widget (ListView) to'g'ridan-to'g'ri qo'yilganExpanded yoki SizedBox bilan chegara belgilash
Ro'yxat scroll qilinganda "jerky" (silliq bo'lmagan) ishlaydiHar itemBuilderda og'ir hisob-kitob yoki rasm decode qilinmoqdaNatijalarni keshlash, RepaintBoundary, rasm o'lchamlarini oldindan cheklash
Animatsiya butun ekranni "miltillatadi"RepaintBoundary yo'q, animatsion widget katta statik daraxt ichidaAnimatsion qismni alohida RepaintBoundary bilan izolyatsiya qilish

Amaliyot#

Amaliy topshiriq

Overflow xatosini tuzating

Row ichiga uchta elementli avatar+ism+status ko'rinishidagi widget tuzing, unda ism matni juda uzun bo'lganda overflow xatosi chiqishini kuzating. Keyin muammoni ikki xil usulda hal qiling: birinchi holatda Expanded + TextOverflow.ellipsis bilan, ikkinchi holatda Flexible bilan — va ikkalasining vizual natijasi qanday farq qilishini tushuntiring.

Maslahatni ko'rsatish

Row ichida uchta Text widget joylashtiring, ulardan birining matnini juda uzun qiling — overflow xatosini ko'ring, keyin Expanded va Text(overflow: TextOverflow.ellipsis) bilan tuzating.