Skip to content

Calling "Logic.write()" during "initState()" leads to error #3

@Delgan

Description

@Delgan

Hi again!

This is a continuation of the discussion that took place in #2.

I put back here the offending code:

final fetchedCountRef = StateRef(0);

final dataFetcherRef = LogicRef((scope) => DataFetcher(scope));

class DataFetcher with Logic {
  const DataFetcher(this.scope);

  @override
  final Scope scope;

  int fetch(int num) {
    update<int>(fetchedCountRef, (count) => count + 1);
    return num * 2;
  }
}

class MyData extends StatefulWidget {
  final int num;

  const MyData(this.num);

  @override
  _MyDataState createState() => _MyDataState();
}

class _MyDataState extends State<MyData> {
  int data;

  @override
  void initState() {
    super.initState();
    data = context.use(dataFetcherRef).fetch(widget.num);
  }

  @override
  Widget build(BuildContext context) => Text("Fetched data: $data");
}

Updating fetchedCountRef forces a rebuild, but this is not possible during initState() and raises an exception.

════════ Exception caught by widgets library ════════
The following assertion was thrown building _BodyBuilder:
setState() or markNeedsBuild() called during build.

This BinderScope 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.

I have (yet another) question: the documentation about context.use() states that it "cannot be called while building a widget". It's a little bit ambiguous for me: does it includes the initState() phase too?

From what I see in the source examples, you are systematically calling addPostFrameCallback() while using a Logic from initState(). Also, not using addPostFrameCallback() can cause subtle errors as reported by this ticket. Should context.use() be forbidden inside initState()?

Also, in #2 I said that as a workaround I would probably call Future.microtask(() => update(...)); in the Logic but finally I'm discarding this solution. 😄
It can lead to subtle bugs if another logic doesn't expect the change to be asynchronous. In addition, I prefer for the the logic to be decoupled from the view as much as possible.

So, I think I'm back to the FutureBuilder() solution that I initially used, but storing the Future as you suggested.

class _MyDataState extends State<MyData> {
  Future<int> data;

  @override
  void initState() {
    super.initState();
    data = Future.microtask(() => context.use(dataFetcherRef).fetch(widget.num));
  }

  @override
  Widget build(BuildContext context) => FutureBuilder(
        future: data,
        builder: (context, snapshot) => Text("Fetched data: ${snapshot.data ?? '?'}"),
      );
}

It's a bit inconvenient as it requires handling invalid data and forces the widget to be immediately rebuilt. However that's the solution I prefer. Additionally, I will refrain myself from directly using context.use() in any initState() method as I'm afraid of inadvertently breaking a widget while modifying a Logic method.

I don't know if other ideas will come to your mind. Sadly, it seems not possible to "fix" this internally without changing the binder api. It is understandable that you prefer not to extend the api for this particular use case. I have been thinking to a possible workaround, I wonder if it can be implemented as an extension. 🤔

data = context.borrow(dataFetcherRef, (ref) => ref.fetch(widget.num));

The idea would be to allow borrow() only inside initState(). The callback would be executed immediately, but first the scope would be somehow "marked" so that each call to setState() would be automatically postponed using addPostFrameCallback(). Then the scope would return to normal state.

In any case, this is as far as my knowledge goes. Thank you again for having looked into my problem! Hopefully, an elegant solution will eventually be found.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions