3-qism · Amaliy Flutter·07-bob
State management — Provider, Riverpod, Bloc va GetX qiyosi
Flutter'da holatni boshqarish yondashuvlarining barchasi: setState'dan Riverpod'gacha, har birining kuchli va zaif tomonlari, va intervyuda qaysi vaziyatda qaysi yechimni tanlashni qanday asoslash kerak.
"Qaysi state management kutubxonasini ishlatasiz?" — bu Flutter intervyusida deyarli har doim so'raladigan savol. Lekin senior darajadagi javob bitta nom aytish emas — har bir yechimning qanday muammoni hal qilishi, qanday narxi borligini va jamoa kontekstiga qarab qanday tanlov qilishni tushuntirish. Bu bobda aynan shu chuqurlikda boramiz.
1. Muammo nima — nega "faqat setState" yetarli emas#
StatefulWidget va setState — Flutter'ning eng boshlang'ich state boshqaruv
mexanizmi. U mukammal ishlaydi, lekin faqat lokal, bitta widget ichidagi
holat uchun:
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _count = 0;
void _increment() => setState(() => _count++);
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: _increment,
child: Text('Bosildi: $_count'),
);
}
}Muammo — dastur kattalashganda paydo bo'ladi:
- Umumiy holat (masalan, foydalanuvchi profili, savat, autentifikatsiya holati) ko'plab widget'larga kerak bo'ladi — ularni "yuqoridan pastga" konstruktor orqali uzatish ("prop drilling") tez orada boshqarib bo'lmas holga keladi.
- Biznes-mantiq widget kodi bilan aralashib ketadi —
build()metodi ham UI, ham hisoblash, ham tarmoq chaqiruvini o'z ichiga oladi, bu test qilishni qiyinlashtiradi. - Rebuild chegarasini nazorat qilib bo'lmaydi — bitta
setStatebutun katta widget daraxtini qayta quradi, hatto faqat kichik qism o'zgargan bo'lsa ham.
Shu uchta muammoni hal qilish uchun ekotizimda bir nechta yondashuv rivojlangan. Ularning barchasi bitta g'oyaga tayanadi: holatni widget daraxtidan tashqariga chiqarish va uni deklarativ tarzda "obuna bo'lish" orqali iste'mol qilish.
2. InheritedWidget — hammasining asosi#
Provider ham, Riverpod ham, hatto Bloc'ning BlocProvider'i ham tagida
InheritedWidget'ga tayanadi (4-bobda batafsil ko'rilgan). Buni tushunish
— "Provider qanday ishlaydi?" degan savolga chuqur javob berish imkonini beradi:
class CounterInherited extends InheritedWidget {
const CounterInherited({
super.key,
required this.count,
required super.child,
});
final int count;
static CounterInherited of(BuildContext context) {
final result = context.dependOnInheritedWidgetOfExactType<CounterInherited>();
assert(result != null, 'CounterInherited topilmadi');
return result!;
}
@override
bool updateShouldNotify(CounterInherited oldWidget) => count != oldWidget.count;
}context.dependOnInheritedWidgetOfExactType chaqirilganda, joriy Element
o'sha InheritedElement'ga "obunachi" sifatida ro'yxatdan o'tadi. count
o'zgarganda va updateShouldNotify true qaytarganda, faqat obuna bo'lgan
elementlar qayta quriladi — butun daraxt emas. Aynan shu mexanizm barcha state
management kutubxonalarining "selektiv rebuild" ta'minotining negizida yotadi.
3. Provider — jamiyat standarti bo'lgan qatlam#
Provider paketi InheritedWidget'ni qo'lda yozish og'irligini olib tashlaydi
va ChangeNotifier bilan reaktiv holatni qulay qiladi:
class CartModel extends ChangeNotifier {
final List<Item> _items = [];
List<Item> get items => List.unmodifiable(_items);
double get total => _items.fold(0, (sum, i) => sum + i.price);
void add(Item item) {
_items.add(item);
notifyListeners();
}
}ChangeNotifierProvider(
create: (_) => CartModel(),
child: const MyApp(),
)Iste'mol qilish uch xil usulda mumkin, va farqini bilish intervyuda ko'p so'raladi:
// 1) Consumer — faqat shu widget qayta quriladi
Consumer<CartModel>(
builder: (context, cart, child) => Text('${cart.items.length} ta mahsulot'),
)
// 2) context.watch — build() ichida, o'zgarishga obuna bo'ladi
final cart = context.watch<CartModel>();
// 3) context.read — faqat bir martalik o'qish, obuna bo'lmaydi (masalan onPressed ichida)
onPressed: () => context.read<CartModel>().add(item),context.watch'ni onPressed kabi event handler ichida ishlatish — eng ko'p
uchraydigan Provider xatosi: bu widget'ni notiflar orqali cheksiz qayta
qurishga emas, balki noto'g'ri lifecycle bosqichida InheritedWidget'ga
bog'lanishga urinishga olib keladi va context.watch() should not be called outside build() xatosini beradi.
Selector esa granularlikni yanada oshiradi — faqat kerakli qismni
kuzatadi:
Selector<CartModel, int>(
selector: (_, cart) => cart.items.length,
builder: (_, count, __) => Text('$count ta mahsulot'),
)Provider'ning zaif tomoni: bog'liqlik runtime'da (context orqali)
qidiriladi. Agar Provider.of<X> chaqirilgan joyda X mavjud bo'lmasa, xato
faqat ishga tushirilganda, compile vaqtida emas, runtime'da paydo bo'ladi.
4. Riverpod — Provider'ning compile-time xavfsiz davomi#
Riverpod'ni Provider paketining muallifi (Remi Rousselet) xuddi shu
muammolarni — ayniqsa runtime bog'liqlik qidiruvini — hal qilish uchun yozgan.
Asosiy farq: providerlar BuildContext'ga emas, global, lekin test qilinadigan
ProviderContainer'ga bog'lanadi.
final counterProvider = StateProvider<int>((ref) => 0);
final cartProvider = NotifierProvider<CartNotifier, List<Item>>(CartNotifier.new);
class CartNotifier extends Notifier<List<Item>> {
@override
List<Item> build() => [];
void add(Item item) => state = [...state, item];
}class CartScreen extends ConsumerWidget {
const CartScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final items = ref.watch(cartProvider);
return Text('${items.length} ta mahsulot');
}
}Asinxron holat uchun AsyncNotifier va FutureProvider alohida ahamiyatga
ega — ular loading/error/data holatlarini avtomatik AsyncValue sifatida
modellaydi:
final userProvider = FutureProvider<User>((ref) async {
final id = ref.watch(currentUserIdProvider);
return ref.watch(apiClientProvider).fetchUser(id);
});ref.watch(userProvider).when(
data: (user) => Text(user.name),
loading: () => const CircularProgressIndicator(),
error: (err, stack) => Text('Xato: $err'),
);Riverpod nima uchun "Provider'ni tuzatadi":
| Provider muammosi | Riverpod yechimi |
|---|---|
BuildContext orqali runtime qidiruv, topilmasa runtime xato | Global Provider obyektlari, compile vaqtida tekshiriladi |
Bir xil turdagi ikkita provider'ni ajratish qiyin (ProviderNotFoundException) | Har bir provider — o'ziga xos top-level konstanta |
| Providerlar orasidagi bog'liqlikni test qilish uchun butun widget daraxti kerak | ProviderContainer bilan widget'siz test qilish mumkin |
Asinxron holatni qo'lda FutureBuilder bilan boshqarish kerak | AsyncValue/AsyncNotifier bu holatni standartlashtiradi |
Narxi: bir oz ko'proq boilerplate va yangi mental model (ref.watch vs
ref.read vs ref.listen) — jamoa buni o'rganishi kerak.
5. Bloc / Cubit — qat'iy intizomli, oqim asosidagi yondashuv#
Bloc pattern holatni event → state oqimi sifatida modellaydi. Cubit —
Bloc'ning soddalashtirilgan varianti (event class'larsiz, to'g'ridan-to'g'ri
metod chaqiruvi):
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
}To'liq Bloc esa har bir o'zgarishni aniq nomlangan event sifatida
talab qiladi — bu katta jamoalarda "nima uchun bu holat o'zgardi" degan
savolga aniq javob beradi (audit qilinadigan, test qilinadigan oqim):
sealed class CartEvent {}
class ItemAdded extends CartEvent {
ItemAdded(this.item);
final Item item;
}
class CartBloc extends Bloc<CartEvent, List<Item>> {
CartBloc() : super([]) {
on<ItemAdded>((event, emit) => emit([...state, event.item]));
}
}BlocBuilder<CartBloc, List<Item>>(
builder: (context, items) => Text('${items.length} ta mahsulot'),
)
// Yozish:
context.read<CartBloc>().add(ItemAdded(item));Bloc'ning kuchli tomoni — qat'iy struktura: UI faqat event yuboradi, faqat
Bloc ichida mantiq bo'ladi, bu katta jamoalarda kodni bir xil qilib saqlaydi
va bloc_test paketi bilan state o'tishlarini aniq test qilish oson bo'ladi.
Kamchiligi — kichik funksiyalar uchun ham event class yozish og'irlik qilishi
mumkin (shuning uchun ko'pincha Cubit bilan boshlanadi).
6. GetX — tezkor, lekin bahsli#
GetX reaktivlikni .obs orqali minimal boilerplate bilan taklif qiladi:
class CartController extends GetxController {
final items = <Item>[].obs;
void add(Item item) => items.add(item);
}Obx(() => Text('${controller.items.length} ta mahsulot'))GetX shuningdek dependency injection (Get.put, Get.find) va navigatsiyani
(Get.to()) ham o'z ichiga oladi — bitta paket bilan uchta muammoni hal
qiladi. Bu tezkor prototiplash uchun jozibali, lekin senior muhandislar orasida
bahsli: BuildContext'siz global holatga tayanish test qilishni qiyinlashtiradi,
va "hamma narsani qiladigan" arxitektura katta, uzoq muddatli loyihalarda
chegaralarni loyihalashni qiyinlashtiradi.
7. Qaysi birini tanlash kerak — qiyosiy jadval#
| Yondashuv | Eng yaxshi mos keladi | Asosiy narx |
|---|---|---|
setState | Bitta widget ichidagi, boshqa hech kim bilmaydigan lokal holat | Umumiy holat uchun umuman mos emas |
Provider | O'rta hajmdagi loyihalar, Flutter jamoasi ham tavsiya qiladigan "rasmiy" yo'l | Runtime bog'liqlik xatolari, katta loyihada boilerplate ko'payadi |
Riverpod | Katta, test qilinishi muhim, ko'p asinxron holatli loyihalar | O'rganish egri chizig'i tikroq, ko'proq kontseptual investitsiya |
Bloc/Cubit | Katta jamoalar, qat'iy me'yor va audit qilinadigan holat o'tishlari kerak bo'lgan (masalan, bank, fintech) loyihalar | Ko'p boilerplate, kichik ekranlar uchun ortiqcha bo'lishi mumkin |
GetX | Kichik jamoa, MVP, tez yetkazib berish ustuvor | Test qilish va katta kodbazada chegaralash qiyinroq |
Intervyuda "eng yaxshisi qaysi?" degan savolga to'g'ri javob yo'q — to'g'ri javob savolni qaytarish: "Jamoa hajmi qancha? Loyiha necha yil yashaydi? Test qamrovi qanchalik muhim?" Shu savollarga javob berish orqali tanlovni asoslash — aynan senior darajani ko'rsatadigan narsa.
8. Ko'p uchraydigan xato javoblar#
| Belgi / savol | Junior darajadagi javob | Senior darajadagi javob |
|---|---|---|
| "Nega Provider ishlatasiz?" | "Chunki ko'pchilik ishlatadi" | Runtime lookup, InheritedWidget ustidan qulay qatlam, Selector bilan granular rebuild ekanini tushuntiradi |
context.watch() initState ichida chaqirilganda xato | "Flutter buggy" deb o'ylaydi | InheritedWidget faqat build() paytida bog'lanishi mumkinligini, didChangeDependencies to'g'ri joy ekanini tushuntiradi |
Bloc'da nega emit faqat on<Event> handler ichida chaqiriladi | E'tibor bermaydi | Bloc holatni faqat bitta joyda, bitta oqim orqali o'zgartirishga majburlashini, bu esa race condition'larni oldini olishini tushuntiradi |
| "GetX'ni nega ishlatmaysiz?" | Hech qanday asos ko'rsatmasdan rad etadi | Test qilinuvchanlik va katta kodbazada global holat muammolarini aniq misollar bilan tushuntiradi |
Tez-tez so'raladigan intervyu savollari#
Savol: Provider va Riverpod orasidagi eng muhim farq nima?
Javob: Provider BuildContext orqali runtime'da bog'liqlikni qidiradi —
agar provider topilmasa, xato faqat ilova ishga tushganda (yoki hatto
foydalanuvchi muayyan ekranga o'tganda) paydo bo'ladi. Riverpod providerlarni
BuildContext'dan mustaqil, global, kompilyatsiya vaqtida tekshiriladigan
obyektlar sifatida belgilaydi — noto'g'ri turdagi provider ishlatilsa,
kompilyator xato beradi, runtime emas. Bundan tashqari Riverpod
ProviderContainer orqali widget daraxtisiz test qilishni ancha osonlashtiradi.
Savol: context.watch va context.read orasidagi farqni tushuntiring.
Javob: context.watch<T>() joriy widget'ni T obyektining
notifyListeners() chaqiruvlariga obuna qiladi — T o'zgarganda widget
qayta quriladi. context.read<T>() esa faqat joriy qiymatni bir martalik
o'qiydi, hech qanday obunani ro'yxatdan o'tkazmaydi. watch faqat build()
ichida chaqirilishi kerak; onPressed kabi event handler'larda read
ishlatiladi, chunki u yerda obuna ma'nosiz — widget allaqachon qurilgan.
Savol: ChangeNotifier'da notifyListeners()'ni chaqirishni unutib
qo'ysangiz nima bo'ladi?
Javob: Model holati o'zgaradi (masalan, list'ga element qo'shiladi), lekin
Flutter buni bilmaydi, chunki InheritedWidget'ga bog'liq bo'lgan hech qaysi
element qayta qurish uchun signal olmaydi. UI eski holatni ko'rsatishda davom
etadi, garchi underlying data to'g'ri bo'lsa ham — bu "UI yangilanmayapti"
degan bug'larning eng keng tarqalgan sababi.
Savol: Nega Bloc'da emit() faqat event handler ichida chaqirilishi
mumkin, tashqarida chaqirsa xato beradi?
Javob: Bloc holatni faqat bitta, tartiblangan oqim orqali o'zgartirishga
majburlaydi — har bir emit aynan qaysi event sabab bo'lganini kuzatib
borish mumkin bo'ladi. Agar emit'ni istalgan joydan chaqirish mumkin
bo'lganida, bir vaqtning o'zida bir nechta manba holatni o'zgartirishi va
race condition yoki tartibsiz holat o'tishlari paydo bo'lishi mumkin edi. Bu
cheklov — atayin qilingan arxitektura qarori, xato emas.
Savol: Selector Consumer'dan nima bilan farq qiladi va qachon
ishlatiladi?
Javob: Consumer<T> T obyektining istalgan o'zgarishida qayta
quriladi. Selector<T, S> esa T'dan faqat kerakli qismni (S) tanlab
oladi va faqat o'sha qism o'zgarganda qayta quradi — masalan, butun CartModel
o'zgarganda emas, faqat items.length o'zgarganda. Katta model ko'p maydonga
ega bo'lganda, Selector keraksiz rebuild'larni sezilarli kamaytiradi.
Savol: Riverpod'da StateProvider va NotifierProvider orasidagi farq
qachon muhim bo'ladi?
Javob: StateProvider oddiy, primitiv qiymatlar (masalan, bitta int
yoki bool filter) uchun mo'ljallangan — u holatni to'g'ridan-to'g'ri
tashqaridan almashtirish imkonini beradi (ref.read(provider.notifier).state = x). NotifierProvider esa murakkab mantiq (bir nechta metod, ichki
validatsiya, boshqa providerlarga bog'liqlik) kerak bo'lganda ishlatiladi —
holatni o'zgartirish faqat class ichidagi metodlar orqali bo'ladi, bu
mantiqni inkapsulyatsiya qiladi va test qilishni osonlashtiradi.
Savol: AsyncValue/FutureProvider qanday muammoni hal qiladi?
Javob: Asinxron ma'lumot (masalan, tarmoqdan foydalanuvchi ma'lumoti)
har doim uchta holatga ega bo'ladi: yuklanmoqda, muvaffaqiyatli keldi, yoki
xato bo'ldi. Bu uch holatni qo'lda bool isLoading, T? data, String? error kabi alohida o'zgaruvchilar bilan boshqarish xatoga moyil (masalan,
isLoading = true va data != null bir vaqtda bo'lib qolishi mumkin).
AsyncValue bu uch holatni bitta sealed-class kabi tipga birlashtiradi va
.when(data:, loading:, error:) orqali barcha holatlarni qayta ishlashni
majburlaydi — kompilyator hech qaysi holat unutilmaganini tekshiradi.
Savol: Nega ba'zi jamoalar GetX'dan voz kechib, Riverpod yoki
Bloc'ga o'tadi?
Javob: GetX'ning global, BuildContext'dan mustaqil kontrollerlari
tezkor yozish imkonini beradi, lekin loyiha kattalashganda ikkita muammo
chiqadi: (1) kontrollerlar orasidagi yashirin bog'liqliklarni kuzatish
qiyinlashadi, chunki ular istalgan joydan Get.find() bilan chaqirilishi
mumkin; (2) GetxController'larni widget daraxtisiz izolyatsiya qilib test
qilish, ayniqsa navigatsiya va DI bir xil paketda bo'lgani uchun, ko'proq
sozlash talab qiladi. Kichik-o'rta loyihalarda bu muammo emas, lekin ko'p
jamoa a'zosi ishlaydigan, uzoq umr ko'radigan kodbazada bashoratlanuvchan
arxitektura ustunlik qiladi.
Savol: State management kutubxonasini tanlashda qanday omillarga qaraysiz?
Javob: Loyihaning hajmi va kutilgan umri, jamoa hajmi va tajribasi, test qamrovi talabi, va domenning murakkabligi (masalan, fintech kabi audit talab qiladigan sohalar Bloc'ning qat'iy oqimidan foyda ko'radi). Bundan tashqari, mavjud kodbazada allaqachon o'rnatilgan konventsiya bor bo'lsa, "yaxshiroq" kutubxonaga o'tish narxi (migratsiya, jamoani qayta o'qitish) odatda foydadan katta bo'ladi — mavjud yondashuvni izchil davom ettirish ko'pincha to'g'ri qaror.
Savol: notifyListeners() juda tez-tez chaqirilsa (masalan, har bir
skroll pikselida) qanday performance muammosi kelib chiqishi mumkin va uni
qanday kamaytirasiz?
Javob: Har bir notifyListeners() chaqiruvi barcha obuna bo'lgan
widget'larni qayta quradi — juda tez-tez chaqirilsa, frame byudjetidan
(16ms) chiqib ketish va jank (sakrash) paydo bo'lishi mumkin. Buni
kamaytirish uchun: (1) Selector/Consumer'ni granular qilib, faqat
kerakli qismlarni kuzatish; (2) notifyListeners()'ni debounce/throttle
qilish (masalan, skroll holatini har freymda emas, oraliqda yangilash); (3)
haqiqatan real-vaqtda UI yangilanishi shart bo'lmagan holatlarda batching
qo'llash.
Savol: Bloc'da Cubit bilan to'liq Bloc orasida qachon
Cubit'dan Bloc'ga "o'tish" kerakligini qanday aniqlaysiz?
Javob: Agar bitta holat o'zgarishi bir nechta manbadan (masalan,
foydalanuvchi bosishi HAM tarmoq javobi) kelishi mumkin bo'lsa va ularni
farqlash muhim bo'lsa, yoki event'larni test qilishda "aynan qaysi
event qanday holatga olib keldi" deb aniq tekshirish kerak bo'lsa, to'liq
Bloc'ga o'tish oqlanadi. Oddiy, bitta manbali, chiziqli mantiq uchun
Cubit'ning qo'shimcha soddaligi ko'proq foyda beradi.
Bir xil savatni ikki xil yondashuvda qur
Amaliy topshiriq: "savatga mahsulot qo'shish, o'chirish, umumiy narxni
hisoblash" mantig'ini avval ChangeNotifier + Provider bilan, so'ng xuddi
shu mantiqni NotifierProvider bilan Riverpod'da qayta yozing. Ikkala holatda
ham widget daraxtisiz, faqat model/notifier darajasida bitta unit test
yozing va qaysi birini test qilish qulayroq bo'lganini o'zingiz uchun
xulosa qiling.
Maslahatni ko'rsatish
Avval oddiy Provider + ChangeNotifier bilan, keyin xuddi shu mantiqni Riverpod'ning NotifierProvider'i bilan qayta yoz. Ikkala versiyada ham bitta unit test yoz va farqini his qil.