Irbisa · Шпаргалка

Состояние на Provider

Пакет Provider, ChangeNotifier, Consumer, context.watch/read/select

Middle-разработчик · 21 вопрос

  1. 01

    Что такое ChangeNotifier и ChangeNotifierProvider? Напишите на них счётчик.

    ChangeNotifier представляет собой класс, который хранит состояние и после каждого изменения вызывает notifyListeners(); ChangeNotifierProvider создаёт его, кладёт в дерево виджетов и вызывает dispose, когда уходит из дерева.

    class Counter extends ChangeNotifier {  int _value = 0;  int get value => _value;   void increment() {    _value++;…
  2. 02

    Нажатие на +1 не меняет число на экране, а в debug нажатие падает. Что не так с этим виджетом?

    class CounterView extends StatelessWidget {  const CounterView({super.key});   @override  Widget build(BuildContext context) {    final counter = context.read<Counter>();    return Column(children: [      Text('${counter.value}'),      ElevatedButton(        onPressed: () => context.watch<Counter>().increment(),        child: const Text('+1'),      ),    ]);  }}

    Вызовы перепутаны местами: read в build берёт значение один раз без подписки, а watch в onPressed подписывается вне фазы build, поэтому срабатывает ассерт, и increment() даже не вызывается.

  3. 03

    Что такое MultiProvider и за что отвечает флаг lazy у провайдера?

    MultiProvider даёт плоскую запись вложенных провайдеров (то же дерево, только читаемое).

    void main() {  runApp(    MultiProvider(      providers: [        Provider<ApiClient>(create: (_) => ApiClient()),        ChangeNotifierProvider(…
  4. 04

    Переключатель ниже не двигается при нажатии, хотя darkMode действительно меняется. Почему?

    class Settings {  bool darkMode = false;}// above the appProvider<Settings>(create: (_) => Settings(), child: const App());// in the settings screen@overrideWidget build(BuildContext context) {  final settings = context.watch<Settings>();  return SwitchListTile(    title: const Text('Dark mode'),    value: settings.darkMode,    onChanged: (v) => settings.darkMode = v,  );}

    Provider<T> только хранит значение и не подписывается на него.

  5. 05

    Чем отличаются context.watch, context.read, context.select и Consumer?

    watch возвращает значение и перестраивает весь виджет на каждое уведомление, read возвращает его один раз без подписки, select перестраивает только когда меняется одно производное значение, а Consumer перестраивает только свой builder.

    class CartBadge extends StatelessWidget {  const CartBadge({super.key});   @override  Widget build(BuildContext context) {    // rebuilds only when count changes, not when prices change…
  6. 06

    Чем отличаются Provider, ChangeNotifierProvider, FutureProvider и StreamProvider?

    Они отличаются тем, что заставляет зависимые виджеты перестраиваться: Provider отдаёт значение и не подписывается на него, ChangeNotifierProvider перестраивает по notifyListeners(), а FutureProvider и StreamProvider отдают последний результат Future или Stream.

    MultiProvider(  providers: [    Provider<ApiClient>(create: (_) => ApiClient()),    ChangeNotifierProvider(create: (_) => CartNotifier()),    FutureProvider<RemoteConfig?>(      create: (context) => context.read<ApiClient>().fetchConfig(),…
  7. 07

    Чем отличается ChangeNotifierProvider(create: ...) от ChangeNotifierProvider.value и кто вызывает dispose у объекта?

    create создаёт объект и делает провайдер его владельцем: объект строится один раз и уничтожается, когда провайдер уходит из дерева.

    // right: the provider owns it, creates once, disposes on unmountChangeNotifierProvider(  create: (_) => CartNotifier(),  child: const ShopApp(),)…
  8. 08

    Что делают Selector и context.select и как они сокращают перестройки по сравнению с Consumer?

    Оба подписываются на одно значение, вычисленное из провайдера, и перестраивают только когда это значение меняется, а Consumer и watch перестраивают на каждый notifyListeners(), что бы ни изменилось.

    // rebuilds only when the name changesfinal name = context.select<UserNotifier, String>((u) => u.name); // several fields: records compare by valuefinal (count, total) = context.select<CartNotifier, (int, double)>(  (c) => (c.count, c.total),…
  9. 09

    Найдите баг: после add() новый товар так и не появляется в списке корзины, хотя нотифаер меняется.

    class CartNotifier extends ChangeNotifier {  final List<String> _items = [];  List<String> get items => _items;   void add(String item) {    _items.add(item);    notifyListeners();  }} // in CartList.buildWidget build(BuildContext context) {  final items = context.select<CartNotifier, List<String>>((c) => c.items);  return Column(children: [for (final name in items) Text(name)]);}

    add() меняет тот же List на месте, а select сравнивает список, который вернул в прошлый раз, с тем, что возвращает сейчас.

  10. 10

    Что такое ProxyProvider и что сломается, если ChangeNotifierProxyProvider.update каждый раз возвращает новый нотифаер?

    ProxyProvider строит значение из других провайдеров и снова вызывает update, когда меняется любой из них.

    MultiProvider(  providers: [    ChangeNotifierProvider(create: (_) => AuthNotifier()),     // immutable service: a new instance per token is fine    ProxyProvider<AuthNotifier, ApiService>(…
  11. 11

    Из-за чего возникает «A ChangeNotifier was used after being disposed» и как это предотвратить?

    Кто-то вызвал notifyListeners() у нотифаера, у которого уже отработал dispose(): почти всегда это асинхронный колбэк, завершившийся после того, как экран закрыли.

    class FeedNotifier extends ChangeNotifier {  FeedNotifier(this._repo) {    _sub = _repo.updates().listen((_) => notifyListeners());  }   final FeedRepository _repo;…
  12. 12

    Эта страница падает с «setState() or markNeedsBuild() called during build». Почему и где вместо этого вызывать load()?

    class FeedNotifier extends ChangeNotifier {  FeedNotifier(this._repo);  final FeedRepository _repo;  List<Post> posts = const [];  Future<void> load() async {    notifyListeners(); // e.g. show a spinner    posts = await _repo.fetch();    notifyListeners();  }}// FeedList watches FeedNotifierWidget build(BuildContext context) {  context.read<FeedNotifier>().load();  return const FeedList();}

    load() вызывает notifyListeners() синхронно, ещё до первого await.

  13. 13

    Диалог, открытый через showDialog, выбрасывает ProviderNotFoundException. Почему и как это исправить?

    Диалог представляет собой новый маршрут, который строит оверлей Navigator, а не виджет, вызвавший showDialog, поэтому его контекст не находится под провайдером, который вы разместили внутри страницы.

    // screen-scoped state: capture it, then re-expose inside the routeonPressed: () {  final cart = context.read<CartNotifier>(); // still inside the page  showDialog(    context: context,    builder: (_) => ChangeNotifierProvider<CartNotifier>.value(…
  14. 14

    Где показывать snackbar или делать навигацию при изменении состояния нотифаера, если в Provider нет BlocListener?

    Вне build: подпишитесь через addListener в initState и реагируйте на переходы между состояниями или представьте событие как данные, которые виджет обработает один раз.

    class _CheckoutPageState extends State<CheckoutPage> {  late final CheckoutNotifier _notifier;  late CheckoutStatus _last;  @override  void initState() {    super.initState();…
  15. 15

    Найдите баг: эта страница падает каждый раз, когда пользователь с неё уходит.

    class _OrdersPageState extends State<OrdersPage> {  @override  void initState() {    super.initState();    context.read<OrdersNotifier>().addListener(_onChanged);  }  void _onChanged() {    if (!context.read<OrdersNotifier>().failed) return;    ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Failed')));  }  @override  void dispose() {    context.read<OrdersNotifier>().removeListener(_onChanged);    super.dispose();  }}

    dispose() ищет нотифаер через context, но к этому моменту элемент уже деактивирован и не может искать предков.

  16. 16

    Как тестировать экран, который зависит от Provider?

    На двух уровнях: нотифаер отдельно, как обычный Dart-код с моками зависимостей, и экран в виджет-тесте с нотифаером, подставленным через ChangeNotifierProvider.value.

    class MockCartApi extends Mock implements CartApi {} void main() {  late MockCartApi api;  setUp(() => api = MockCartApi());…
  17. 17

    Чем Riverpod отличается от Provider, какие проблемы Provider он решает и когда всё же стоит выбрать Provider?

    Автор тот же: Riverpod представляет собой переработку, в которой поиск зависимостей больше не идёт через дерево виджетов.

  18. 18

    Расскажите, что на самом деле происходит, когда виджет вызывает context.watch<T>().

    watch<T>() регистрирует ваш элемент как зависимого от inherited-элемента провайдера, а тот сам подписан на значение.

  19. 19

    После выхода одного пользователя и входа другого приложение всё ещё показывает данные первого. Как сбросить провайдеры, которые живут выше MaterialApp?

    Уничтожьте поддерево, которое ими владеет, вместо того чтобы сбрасывать каждый нотифаер вручную: положите провайдеры пользователя под виджет с ключом из id пользователя, и новый id размонтирует старые, вызовет у них dispose и заново запустит create.

    class SignedInScope extends StatelessWidget {  const SignedInScope({super.key, required this.child});  final Widget child;   @override  Widget build(BuildContext context) {…
  20. 20

    Метод меняет десять полей и вызывает notifyListeners() после каждого. Сколько перестроек виджетов это вызовет и во что это обходится на самом деле?

    Обычно одну перестройку на каждого зависимого: уведомление только помечает элементы грязными, а грязный элемент перестраивается один раз в следующем кадре.

  21. 21

    Нотифаер над MaterialApp тикает раз в секунду, на него подписаны несколько экранов, и чем глубже пользователь уходит в навигацию, тем сильнее тормозит приложение. Почему важна глубина стека?

    Открытие маршрута не размонтирует экраны под ним.