aidoc.uz

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 setState butun 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 muammosiRiverpod yechimi
BuildContext orqali runtime qidiruv, topilmasa runtime xatoGlobal 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 kerakProviderContainer bilan widget'siz test qilish mumkin
Asinxron holatni qo'lda FutureBuilder bilan boshqarish kerakAsyncValue/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#

YondashuvEng yaxshi mos keladiAsosiy narx
setStateBitta widget ichidagi, boshqa hech kim bilmaydigan lokal holatUmumiy holat uchun umuman mos emas
ProviderO'rta hajmdagi loyihalar, Flutter jamoasi ham tavsiya qiladigan "rasmiy" yo'lRuntime bog'liqlik xatolari, katta loyihada boilerplate ko'payadi
RiverpodKatta, test qilinishi muhim, ko'p asinxron holatli loyihalarO'rganish egri chizig'i tikroq, ko'proq kontseptual investitsiya
Bloc/CubitKatta jamoalar, qat'iy me'yor va audit qilinadigan holat o'tishlari kerak bo'lgan (masalan, bank, fintech) loyihalarKo'p boilerplate, kichik ekranlar uchun ortiqcha bo'lishi mumkin
GetXKichik jamoa, MVP, tez yetkazib berish ustuvorTest 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 / savolJunior darajadagi javobSenior 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'ylaydiInheritedWidget faqat build() paytida bog'lanishi mumkinligini, didChangeDependencies to'g'ri joy ekanini tushuntiradi
Bloc'da nega emit faqat on<Event> handler ichida chaqiriladiE'tibor bermaydiBloc 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 etadiTest 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.

Amaliy topshiriq

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.