-
-
Notifications
You must be signed in to change notification settings - Fork 12
Description
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.