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:
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".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.
Bola o'z o'lchamini parentga qaytaradi
Faqat
Size(width, height)— joylashuv (position) haqida hech qanday ma'lumot yo'q.Parent bolaning pozitsiyasini belgilaydi
Bola o'z o'lchamini bilgach, parent (
RenderObject.paintbosqichida)offsetorqali 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),
],
)- Avval
Expanded/Flexiblebo'lmagan bolalar o'z tabiiy o'lchamiga moslashtiriladi (masalan,Icon— o'zining standart o'lchami). - Qolgan bo'sh joy hisoblab chiqiladi va
Expanded/Flexiblebolalar orasida ularningflexqiymati nisbatida bo'linadi.
| Widget | Xatti-harakat |
|---|---|
Expanded | Berilgan 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 |
Spacer | Ko'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 100pxmainAxisSize (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#
| Belgi | Ehtimoliy sabab | To'g'ri yechim |
|---|---|---|
RenderFlex overflowed by N pixels | Row/Column ichidagi kontent mavjud joydan katta, Expanded/Flexible yo'q | Kontentni Expanded bilan o'rash yoki Wrap/scroll qo'shish |
BoxConstraints forces an infinite width/height | Cheksiz constraint beruvchi widget (Column) ichiga cheksiz talab qiluvchi widget (ListView) to'g'ridan-to'g'ri qo'yilgan | Expanded yoki SizedBox bilan chegara belgilash |
| Ro'yxat scroll qilinganda "jerky" (silliq bo'lmagan) ishlaydi | Har itemBuilderda og'ir hisob-kitob yoki rasm decode qilinmoqda | Natijalarni keshlash, RepaintBoundary, rasm o'lchamlarini oldindan cheklash |
| Animatsiya butun ekranni "miltillatadi" | RepaintBoundary yo'q, animatsion widget katta statik daraxt ichida | Animatsion qismni alohida RepaintBoundary bilan izolyatsiya qilish |
Amaliyot#
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.