Irbisa · Шпаргалка

Базовое управление состоянием

setState, InheritedWidget, ValueNotifier

Junior-разработчик · 23 вопроса

  1. 01

    Зачем вообще нужен подход к управлению состоянием, если есть setState?

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

  2. 02

    Какие подходы к управлению состоянием во Flutter вы знаете и как выбираете подход для проекта?

    Встроенные: setState, InheritedWidget, ValueNotifier/ChangeNotifier; пакеты: Provider, Riverpod, BLoC/Cubit, реже MobX, Redux, GetX.

  3. 03

    Как работает setState(), что происходит при его вызове и почему его нельзя вызывать внутри build()?

    setState сразу выполняет ваше замыкание, затем помечает элемент этого State как «грязный» (dirty) и планирует кадр.

    class Counter extends StatefulWidget {  const Counter({super.key});   @override  State<Counter> createState() => _CounterState();}…
  4. 04

    Нажатие на кнопку вызывает setState, но счётчик всегда показывает 0. Почему, и из-за чего ещё setState может «ничего не делать»?

    class _CounterState extends State<Counter> {  @override  Widget build(BuildContext context) {    int count = 0;    return TextButton(      onPressed: () => setState(() => count++),      child: Text('$count'),    );  }}

    count является локальной переменной build.

  5. 05

    Что значит «поднять состояние» (lift state up) во Flutter и когда это нужно делать?

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

    class FilterBar extends StatefulWidget {  const FilterBar({super.key});  @override  State<FilterBar> createState() => _FilterBarState();}…
  6. 06

    Почему колбэк, который вы передаёте в setState, должен быть синхронным и что будет, если сделать его async?

    setState сразу выполняет замыкание и затем помечает элемент как dirty.

    // wrong: debug throws "setState() callback argument returned a Future"void _loadWrong() {  setState(() async {    _user = await api.fetchUser(); // assigned after the rebuild already ran  });}…
  7. 07

    Что нужно освободить в dispose() у State и что ломается, если про это забыть?

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

    class _EditorState extends State<Editor> with SingleTickerProviderStateMixin {  final _text = TextEditingController();  final _focus = FocusNode();  late final AnimationController _anim;  StreamSubscription<Draft>? _sub;  Timer? _autosave;…
  8. 08

    Что напечатает этот код после одного нажатия на кнопку (не считая начального build: 0) и сколько раз выполнится build?

    class _DemoState extends State<Demo> {  int _count = 0;   void _tap() {    setState(() => _count++);    setState(() => _count++);    setState(() => _count++);    print('after: $_count');  }   @override  Widget build(BuildContext context) {    print('build: $_count');    return TextButton(onPressed: _tap, child: Text('$_count'));  }}

    Сначала выведется after: 3, затем один раз build: 3.

  9. 09

    Пользователь дважды нажимает «Сохранить», и создаются два заказа. Как остановить второе нажатие локальным состоянием?

    Один bool _saving в State: выставьте его до await, сбросьте в finally, если экран остаётся открытым, и передавайте onPressed: null, пока он выставлен; тогда кнопка заметно неактивна, пока идёт работа.

    class _SaveButtonState extends State<SaveButton> {  bool _saving = false;   Future<void> _save() async {    if (_saving) return; // the second tap lands here    setState(() => _saving = true); // same synchronous slice: no race…
  10. 10

    Что будет, если вызвать setState после dispose(), и зачем проверять mounted после каждого await?

    Это приводит к исключению: в debug «setState() called after dispose()», в release ошибка null-check.

    class _ProfilePageState extends State<ProfilePage> {  Profile? _profile;   Future<void> _refresh() async {    final messenger = ScaffoldMessenger.of(context); // capture before the gap    try {…
  11. 11

    Чем initState отличается от didChangeDependencies?

    Оба вызываются до первого build, но initState выполняется ровно один раз, а didChangeDependencies вызывается сразу после него и затем каждый раз, когда меняется inherited-виджет, от которого зависит этот State.

    class _PostFeedState extends State<PostFeed> {  FeedRepository? _repo;  StreamSubscription<List<Post>>? _sub;  List<Post> _posts = const [];   @override…
  12. 12

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

    class _QuantityStepperState extends State<QuantityStepper> {  late int _qty;  @override  void initState() {    super.initState();    _qty = widget.quantity;  }   void _inc() {    setState(() => _qty++);    widget.onChanged(_qty);  }  @override  Widget build(BuildContext context) =>      TextButton(onPressed: _inc, child: Text('$_qty'));}

    _qty копируется из widget.quantity только в initState.

  13. 13

    Из-за чего возникает ошибка «setState() or markNeedsBuild() called during build» и как её исправить?

    Когда выполняется build, Flutter уже обходит дерево, и пометить в этот момент как dirty предка или уже построенный виджет значило бы оставить кадр недостроенным, поэтому фреймворк отказывается.

    // wrong: the parent's setState runs while the tree is being builtclass _ItemsPanelState extends State<ItemsPanel> {  @override  Widget build(BuildContext context) {    if (widget.items.isEmpty) widget.onEmpty(); // parent callback calls its setState    return ItemList(widget.items);…
  14. 14

    Чекбокс внутри диалога не обновляется при вызове setState. Почему и как это исправить?

    Диалог живёт в отдельном маршруте: setState страницы перестраивает страницу, а содержимое диалога построено builder, который вы передали в showDialog, и в поддерево страницы не входит.

    final accepted = await showDialog<bool>(  context: context,  builder: (dialogContext) {    var agreed = false; // lives as long as this builder's closure    return StatefulBuilder(      builder: (context, setLocalState) => AlertDialog(…
  15. 15

    Что такое ValueNotifier, чем он отличается от ChangeNotifier и когда он лучше setState?

    ValueNotifier<T> устроен как ChangeNotifier, который хранит одно value и уведомляет слушателей, когда это значение заменяют.

    class _FavoriteButtonState extends State<FavoriteButton> {  final _liked = ValueNotifier<bool>(false);   @override  void dispose() {    _liked.dispose();…
  16. 16

    Тяжёлый график внутри ValueListenableBuilder перестраивается на каждом тике. Зачем у билдера параметр child?

    child строится один раз, вне билдера, и передаётся в него без изменений.

    // wrong: a fresh ExpensiveChart instance on every tick, so it rebuildsValueListenableBuilder<int>(  valueListenable: _ticks,  builder: (context, ticks, _) => Column(children: [    Text('$ticks'),    ExpensiveChart(series: widget.series),…
  17. 17

    Форма перестраивает всю страницу на каждое нажатие клавиши, потому что onChanged вызывает setState. Где на самом деле должен жить этот текст?

    В TextEditingController.

    // wrong: every keystroke rebuilds the page, and _text duplicates the truthTextField(onChanged: (v) => setState(() => _text = v)); // right: the controller is the source of truth; only the button listensclass _ComposerState extends State<Composer> {  final _controller = TextEditingController();…
  18. 18

    Какую проблему решает InheritedWidget и как работает updateShouldNotify?

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

    class AppColors extends InheritedWidget {  const AppColors({super.key, required this.primary, required super.child});   final Color primary;   static AppColors of(BuildContext context) {…
  19. 19

    Чем InheritedWidget отличается от Provider?

    Provider построен на InheritedWidget.

  20. 20

    Какие главные претензии к GetX и почему многие команды его избегают?

    GetX объединяет состояние, навигацию, DI, переводы и HTTP в одном пакете, построенном на глобальных синглтонах и API без контекста.

  21. 21

    Найдите баг в том, как этот CartBadge подписывается на корзину, и исправьте его.

    class _CartBadgeState extends State<CartBadge> {  @override  void initState() {    super.initState();    widget.cart.addListener(() => setState(() {}));  }   @override  void dispose() {    widget.cart.removeListener(() => setState(() {}));    super.dispose();  }   @override  Widget build(BuildContext context) => Text('${widget.cart.count}');}

    removeListener получает новое замыкание, которое никогда не добавлялось, поэтому ничего не удаляет.

  22. 22

    Чем dependOnInheritedWidgetOfExactType отличается от getInheritedWidgetOfExactType и почему читатель иногда так и не перестраивается?

    Должны выполняться три условия: inherited-виджет должен быть новым экземпляром с новым значением, его updateShouldNotify должен вернуть true, а читатель должен реально зарегистрировать зависимость.

    class CartScope extends InheritedWidget {  const CartScope({super.key, required this.items, required super.child});  final List<Item> items;   // registers this context as a dependent  static CartScope of(BuildContext context) =>…
  23. 23

    Каждый виджет, читающий ваш InheritedWidget, перестраивается при изменении любого поля. Как сделать так, чтобы он зависел только от одного?

    Используйте InheritedModel: зависимый виджет при чтении называет аспект, и фреймворк для каждого зависимого спрашивает, изменились ли именно те аспекты, которые ему важны.

    enum PrefAspect { currency, locale } class Prefs extends InheritedModel<PrefAspect> {  const Prefs(      {super.key, required this.currency, required this.locale, required super.child});…