MobX и setState() или markNeedsBuild() вызываются во время исключения сборки

#flutter #dart #mobx

Вопрос:

В нескольких местах нашего приложения есть исключения, подобные этому:

 EXCEPTION CAUGHT BY FLUTTER_MOBX 
The following MobXCaughtException was thrown:
setState() or markNeedsBuild() called during build.
This Observer widget cannot be marked as needing to build because the framework is already in the
process of building widgets.  A widget can be marked as needing to be built during the build phase
only if one of its ancestors is currently building. This exception is allowed because the framework
builds parent widgets before children, which means a dirty descendant will always be built.
Otherwise, the framework might not visit this widget during this build phase.
The widget on which setState() or markNeedsBuild() was called was:
  Observer
The widget which was currently being built when the offending call was made was:
  Builder

When the exception was thrown, this was the stack:
#0      Element.markNeedsBuild.<anonymous closure> (package:flutter/src/widgets/framework.dart:4292:11)
#1      Element.markNeedsBuild (package:flutter/src/widgets/framework.dart:4307:6)
#2      ObserverElementMixin.invalidate (package:flutter_mobx/src/observer_widget_mixin.dart:70:24)
#3      ReactionImpl._run (package:mobx/src/core/reaction.dart:119:22)
#4      ReactiveContext._runReactionsInternal (package:mobx/src/core/context.dart:345:18)
#5      ReactiveContext.runReactions (package:mobx/src/core/context.dart:319:5)
#6      ReactiveContext.endBatch (package:mobx/src/core/context.dart:149:7)
#7      ActionController.endAction (package:mobx/src/core/action.dart:107:9)
#8      _$CatalogState.changeCatalogIndex (package:my_app/catalog/state/catalog_state.g.dart:37:43)
#9      _CatalogViewState.initState (package:my_app/catalog/catalog_view.dart:277:19)
#10     StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:4765:58)
#11     ComponentElement.mount (package:flutter/src/widgets/framework.dart:4601:5)
...     Normal element mounting (112 frames)
#123    Element.inflateWidget (package:flutter/src/widgets/framework.dart:3569:14)
#124    Element.updateChild (package:flutter/src/widgets/framework.dart:3327:18)
#125    RenderObjectElement.updateChildren (package:flutter/src/widgets/framework.dart:5705:32)
#126    MultiChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:6246:17)
#127    Element.updateChild (package:flutter/src/widgets/framework.dart:3314:15)
#128    ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4652:16)
#129    StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:4800:11)
#130    Element.rebuild (package:flutter/src/widgets/framework.dart:4343:5)
#131    StatefulElement.update (package:flutter/src/widgets/framework.dart:4832:5)
#132    Element.updateChild (package:flutter/src/widgets/framework.dart:3314:15)
#133    ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4652:16)
#134    Element.rebuild (package:flutter/src/widgets/framework.dart:4343:5)
#135    ProxyElement.update (package:flutter/src/widgets/framework.dart:4987:5)
#136    _InheritedNotifierElement.update (package:flutter/src/widgets/inherited_notifier.dart:183:11)
#137    Element.updateChild (package:flutter/src/widgets/framework.dart:3314:15)
#138    SingleChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:6125:14)
#139    Element.updateChild (package:flutter/src/widgets/framework.dart:3314:15)
#140    ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4652:16)
#141    StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:4800:11)
#142    Element.rebuild (package:flutter/src/widgets/framework.dart:4343:5)
#143    StatefulElement.update (package:flutter/src/widgets/framework.dart:4832:5)
#144    Element.updateChild (package:flutter/src/widgets/framework.dart:3314:15)
#145    SingleChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:6125:14)
#146    Element.updateChild (package:flutter/src/widgets/framework.dart:3314:15)
#147    SingleChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:6125:14)
#148    Element.updateChild (package:flutter/src/widgets/framework.dart:3314:15)
#149    ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4652:16)
#150    Element.rebuild (package:flutter/src/widgets/framework.dart:4343:5)
#151    StatelessElement.update (package:flutter/src/widgets/framework.dart:4708:5)
#152    Element.updateChild (package:flutter/src/widgets/framework.dart:3314:15)
#153    ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4652:16)
#154    Element.rebuild (package:flutter/src/widgets/framework.dart:4343:5)
#155    ProxyElement.update (package:flutter/src/widgets/framework.dart:4987:5)
#156    Element.updateChild (package:flutter/src/widgets/framework.dart:3314:15)
#157    ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4652:16)
#158    StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:4800:11)
#159    Element.rebuild (package:flutter/src/widgets/framework.dart:4343:5)
#160    BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2730:33)
#161    WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:913:20)
#162    RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:302:5)
#163    SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1117:15)
#164    SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1055:9)
#165    SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:971:5)
#169    _invoke (dart:ui/hooks.dart:251:10)
#170    _drawFrame (dart:ui/hooks.dart:209:3)
(elided 3 frames from dart:async)
 

Иногда можно избежать такой ошибки, отложив изменение состояния с SchedulerBinding.instance.addPostFrameCallback помощью . И мне не нравится такое решение, но, по крайней мере, без ошибок. Но в коде есть места, где этот хак не работает.
Я действительно хочу понять, как с этим справиться без взломов. Проблема в том, что я не смог предоставить воспроизводимый код, потому что я не могу воспроизвести это поведение в простом демонстрационном приложении. И я понятия не имею, почему.
_CatalogViewState выглядит так:

 class _CatalogViewState extends State<CatalogView> {
  PageController _pageController;
  TabController _tabController;

  ProductsState _productsState;
  FarmersState _farmersState;
  ToursState _toursState;
  DishesState _dishesState;
  CatalogState _catalogState;
  CategoriesState _categoriesState;
  FiltersState _filtersState;
  GeoState _geoState;

  int get _index() => _catalogState.catalogIndex;

  // ...
 
  @override
  void initState() {
    super.initState();
    _productsState = Provider.of<ProductsState>(context, listen: false);
    _farmersState = Provider.of<FarmersState>(context, listen: false);
    _toursState = Provider.of<ToursState>(context, listen: false);
    _dishesState = Provider.of<DishesState>(context, listen: false);
    _catalogState = Provider.of<CatalogState>(context, listen: false);
    _categoriesState = Provider.of<CategoriesState>(context, listen: false);
    _filtersState = Provider.of<FiltersState>(context, listen: false);
    _geoState = Provider.of<GeoState>(context, listen: false);
    if (widget.defaultCategorySlug != null) {
      SchedulerBinding.instance.addPostFrameCallback((_) => _initDefaultSelectedCategory());
    }
    _clearEmptyDishesCategories(); 
    _catalogState.changeCatalogIndex(widget.defaultIndex);
    _pageController = PageController(initialPage: _index);
    _pageController.addListener(_switchTabColor);
  }

  // ...
}
 

and CatalogState like that:

 class CatalogState = _CatalogStateBase with _$CatalogState;

abstract class _CatalogStateBase with Store {
  @observable
  int catalogIndex = 0;

  @action
  void changeCatalogIndex(int index) {
    catalogIndex = index;
  }
}
 

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

Где-то обнаружил, что я должен менять магазин только во время реакции. Но в этом случае какую реакцию мне следует выбрать? Кроме того, мне нужно запустить этот код только один раз. Я пробовал реакцию автозапуска, но она просто нарушает код, и ничто не работает так, как должно.

Если я не могу мутировать в хранилище initState (несмотря на то, что в большинстве случаев это хорошо работает), то где для этого альтернативное место?

Кроме того, я пробовал это:

 autorun((_) => _catalogState.catalogIndex, (int index){
   _catalogState.changeCatalogIndex(widget.defaultIndex);
});
 

и это:

 untracked(() {
  _catalogState.changeCatalogIndex(widget.defaultIndex);
});
 

но не повезло(. Странно, что untracked это не удается, потому что неотслеженные изменения хранилища не должны запускать процесс признания недействительными.

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

Любая помощь или идея о том, как ее отладить и исправить, приветствуется.

Комментарии:

1. эта ошибка возникла при настройке состояния приложения во время сборки виджета,

2. Проблема не initState в вас, а в вашем build методе. Вам нужно проверить, вызываете ли вы setState или поставщиков обновлений/состояние mobx при создании представления

3. Я ничего не вижу в методе сборки. Кроме того, если бы что-то было, это вообще не должно было бы работать, но в большинстве случаев это работает. Также, если я прокомментирую changeCatalogIndex , то ошибок не будет.

Ответ №1:

Эта ошибка возникает, когда вы пытаетесь запустить перестройку во время initState работы . Если вы хотите запустить перестройку по логике инициализации, вы можете обернуть этот триггер перестройки внутри postFrameCallback . В данном случае, я полагаю, проблема была в том catalogState .

 @override
  void initState() {
    super.initState();
    _productsState = Provider.of<ProductsState>(context, listen: false);
    _farmersState = Provider.of<FarmersState>(context, listen: false);
    _toursState = Provider.of<ToursState>(context, listen: false);
    _dishesState = Provider.of<DishesState>(context, listen: false);
    _catalogState = Provider.of<CatalogState>(context, listen: false);
    _categoriesState = Provider.of<CategoriesState>(context, listen: false);
    _filtersState = Provider.of<FiltersState>(context, listen: false);
    _geoState = Provider.of<GeoState>(context, listen: false);
    if (widget.defaultCategorySlug != null) {
      SchedulerBinding.instance.addPostFrameCallback((_) => _initDefaultSelectedCategory());
    }
    _clearEmptyDishesCategories(); 
    // this triggers a rebuild 
    // _catalogState.changeCatalogIndex(widget.defaultIndex);
    // you can do this in the first frame after initial build
    WidgetsBinding.instance.addPostFrameCallback((_) => _catalogState.changeCatalogIndex(widget.defaultIndex));
    _pageController = PageController(initialPage: _index);
    _pageController.addListener(_switchTabColor);
  }
 

Комментарии:

1. Я уже упоминал, что здесь это addPostFrameCallback не работает. Кроме того, я хочу избавиться от addPostFrameCallback всей кодовой базы. Мне нужно чистое и идиоматичное решение.

2. @ChessMax ты нашел какое-нибудь решение?

3. @ShashankSrivastava Я не нашел хорошего решения. Теперь используется addPostFrameCallback в качестве обходного пути.