Open indigo-dev opened 2 years ago
Hi @indigo-dev and thank you for this very good issue.
A first workaround is to use RM.inject
instead of RM.create
. Like this:
@override
Widget build(BuildContext context) {
return WhenRebuilder(
observe: () => RM.inject(() => WikiImageStore(
wikiRepository: Injector.get<WikiRepository>())), //_storeRM,
initState: (_, storeRM) => storeRM.setState(
(_) => wikiUrl != null ? storeRM.state.getWikiImageUrl(wikiUrl) : null),
onIdle: () => Container(),
onWaiting: () => _buildImageContainer(_buildImagePlaceHolder()),
onData: (store) => _buildImageContainer(store.imageUrl != null
? _buildImage(store.imageUrl)
: _buildImagePlaceHolder()),
onError: (_) => _buildImageContainer(_buildImagePlaceHolder()),
);
}
Try it and feed me back.
For the inherited state, it needs more investigation and I'ill be back.
@indigo-dev
Problem is: where would I tell the local RM / mini-store to download the image and call setState? a) In the statesOverride Parameter, I only have access to the local singleton but not not to the local RM which would allow me to call localRM.setState((s)=> s.getImage()).
Yes, it in the statesOverride
where to invoke getWikiImageUrl(wikiUrl)
method:
Try this (I assume it will work, but haven't tried it):
@override
Widget build(BuildContext context) {
return wikiImageStoreInjected.inherited(
stateOverride: () async {
final wikiImageStore = WikiImageStore(
wikiRepository: wikiRepositoryInjected.state,
);
await wikiImageStore.getWikiImageUrl(wikiUrl);
return wikiImageStore;
},
builder: (context) {
return wikiImageStoreInjected(context).onAll(
onWaiting: () => _buildImageContainer(_buildImagePlaceHolder()),
onData: (store) => _buildImageContainer(store.imageUrl != null
? _buildImage(store.imageUrl)
: _buildImagePlaceHolder()),
onError: (_) => _buildImageContainer(_buildImagePlaceHolder()),
);
},
);
}
Hi @indigo-dev any feedback?
Hi Mellati, big sorry for the delay, I had some confusing results and as soon as I find some more time to test I come back to you with the results.
Upfront I have a question: What's the difference between a) wikiImageStoreInjected(context).onAll(...) b) wikiImageStoreInjected(context).rebuild.onAll(...)
Additionally, in my setup I use a NestedScrollView with a TabBar and DefaultTabController, and the local state is being "inherited" on that "outer" level in the container page. Within the TabBarViews (i.e. pages within the tabs), I then need again access to that inherited state. How would I access it from within the TabViews? I guess it will be about reinherit or so?
Upfront I have a question: What's the difference between a) wikiImageStoreInjected(context).onAll(...) b) wikiImageStoreInjected(context).rebuild.onAll(...)
The latter will listen to the wikiImageStoreInjected
and rebuild accordingly.
The sooner only render the right widget depending on the state status of wikiImageStoreInjected
. It will not listen to the wikiImageStoreInjected
.
Additionally, in my setup I use a NestedScrollView with a TabBar and DefaultTabController, and the local state is being "inherited" on that "outer" level in the container page. Within the TabBarViews (i.e. pages within the tabs), I then need again access to that inherited state. How would I access it from within the TabViews? I guess it will be about reinherit or so?
This is a good case. A simple working example will very helpful if you have any.
reinherit
is used when changing the route where the new BuildContext
cannot access the old InheritedWidget
.
Whether this is similar to nested scroll view, I don't know. We have to check it.
I've setup a complete example including the main page (Tab container), the tab pages, and a store. From the perspective of the app, this page can exist multiple times in the navigation stack, each time with a different context/seed, and this is (at least my) justification why this page should only have a local store for each instance.
The code has two variants: a) the inherited case managing local state b) the normal case with global state
To my surprise, the following effects happen in either variant a) or b) and I have no explanation why:
Variant a) local state
Variant b) global state
I have to say I'm really used to using the "store approach" with a void getData() method much like reso coder showed in his video. It worked flawlessly for the last 2 years. But I somehow have the feeling I'm doing something wrong since things have changed in states rebuilder. With "store approach" I mean that I have a store/view model for each page (in most cases), and this store in being injected and carries the loading state". Those stores again have data repositories via dependency injection. But setState is only used in the stores. As one can see, my stores are mutable. So I don't know if mutability is a problem or maybe the void-methods for getData(). Compare the weather store at https://resocoder.com/2019/12/30/states-rebuilder-zero-boilerplate-flutter-state-management/
import 'package:flutter/material.dart';
import 'package:states_rebuilder/states_rebuilder.dart';
final Injected<TestStore?> testStoreInjected = RM.inject<TestStore?>(
// // variant A) inherited store
() => throw UnimplementedError(),
// // variant B) global store
// () => TestStore("global"),
debugPrintWhenNotifiedPreMessage: "TestStore",
);
class TestPage extends StatelessWidget {
final String seed;
const TestPage({required this.seed}) : super();
@override
Widget build(BuildContext context) {
List<Widget> tabs = [
Tab(text: "Tab One"),
Tab(text: "Tab Two"),
];
List<Widget> tabPages = [
MyTabPage(text: "TabPage One"),
MyTabPage(text: "TabPage Two"),
];
return DefaultTabController(
length: tabPages.length,
child: Scaffold(
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool boxIsScrolled) {
return <Widget>[
SliverAppBar(
title: Text(
"Outer Container($seed):\nCreate .inherited(...) Store & Init downloads",
maxLines: 2,
style: TextStyle(fontSize: 12),
),
bottom: TabBar(tabs: tabs),
),
];
},
// // variant A) inherited store
body: testStoreInjected.inherited(
stateOverride: () {
TestStore testStoreLocal = TestStore("local");
testStoreLocal.getData("inherited getData");
return testStoreLocal;
},
builder: (context) {
return testStoreInjected(context).rebuild.onAll(
onWaiting: () => Center(child: CircularProgressIndicator()),
onError: (_, __) =>
Center(child: Text("Error while loading.")),
onData: (_) => TabBarView(children: tabPages),
);
},
),
// // variant B) global store
// body: testStoreInjected.rebuild.onAll(
// initState: () => testStoreInjected
// .setState((s) => s!.getData("global getData")),
// onWaiting: () => Center(child: CircularProgressIndicator()),
// onError: (_, __) => Center(child: Text("Error while loading.")),
// onData: (_) => TabBarView(children: tabPages),
// ),
),
),
);
}
}
class MyTabPage extends StatelessWidget {
final String text;
const MyTabPage({required this.text}) : super();
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text(
text +
"\n\nWill be shown on .inherited(...) onData()\nAccesses data from .inherited(...) store of outer Container\n",
textAlign: TextAlign.center,
),
Text(
// // variant A) inherited store
"Result of .inherited() Store:\n" +
(testStoreInjected(context).state?.output ?? "isNull"),
// // variant B) global store
// "Result of global Store:\n" +
// (testStoreInjected.state?.output ?? "isNull"),
textAlign: TextAlign.center,
),
],
),
)
],
);
}
}
class TestStore {
final String seed;
TestStore(this.seed);
String? output;
void getData(String input) async {
debugPrint(runtimeType.toString() +
" -> getData($input) of seed $seed initiated. State change expected in 4sec!\nInput:$input | Result: tbd | output: $output");
String? result =
await Future<String>.delayed(Duration(seconds: 4), () => "Result Done");
output = input + " -> " + result;
debugPrint(runtimeType.toString() +
" -> getData($input) of seed $seed completed. State change now!\nInput:$input | Result: $result | output: $output");
}
}
Hi @indigo-dev and thank you for this very good issue.
A first workaround is to use
RM.inject
instead ofRM.create
. Like this:@override Widget build(BuildContext context) { return WhenRebuilder( observe: () => RM.inject(() => WikiImageStore( wikiRepository: Injector.get<WikiRepository>())), //_storeRM, initState: (_, storeRM) => storeRM.setState( (_) => wikiUrl != null ? storeRM.state.getWikiImageUrl(wikiUrl) : null), onIdle: () => Container(), onWaiting: () => _buildImageContainer(_buildImagePlaceHolder()), onData: (store) => _buildImageContainer(store.imageUrl != null ? _buildImage(store.imageUrl) : _buildImagePlaceHolder()), onError: (_) => _buildImageContainer(_buildImagePlaceHolder()), ); }
Try it and feed me back.
For the inherited state, it needs more investigation and I'ill be back.
As you asked for feedback on this particular setup: yes, it basically works, but of course only for global injection. To comply with your new syntax, I already went on replacing all WhenRebuilder occurances with something like testStoreInjected.rebuild.onAll(...). The paragraph "Old way" was only a showcase for the old fashioned way :)
Hi @indigo-dev and thank you for this very good issue. A first workaround is to use
RM.inject
instead ofRM.create
. Like this:@override Widget build(BuildContext context) { return WhenRebuilder( observe: () => RM.inject(() => WikiImageStore( wikiRepository: Injector.get<WikiRepository>())), //_storeRM, initState: (_, storeRM) => storeRM.setState( (_) => wikiUrl != null ? storeRM.state.getWikiImageUrl(wikiUrl) : null), onIdle: () => Container(), onWaiting: () => _buildImageContainer(_buildImagePlaceHolder()), onData: (store) => _buildImageContainer(store.imageUrl != null ? _buildImage(store.imageUrl) : _buildImagePlaceHolder()), onError: (_) => _buildImageContainer(_buildImagePlaceHolder()), ); }
Try it and feed me back. For the inherited state, it needs more investigation and I'ill be back.
As you asked for feedback on this particular setup: yes, it basically works, but of course only for global injection. To comply with your new syntax, I already went on replacing all WhenRebuilder occurances with something like testStoreInjected.rebuild.onAll(...). The paragraph "Old way" was only a showcase for the old fashioned way :)
You can also use OnBuilder.
syntax to replace the old WhenRebuilder
.
//Handle all possible state status
OnBuilder.all(
listenTo: myState,
onIdle: () => Text('onIdle'),
onWaiting: () => Text('onWaiting'),
onError: (err, errorRefresh) => Text('onError'),
onData: () => Text('{myState.state}'),
),
//Handle all possible state status with orElse fallback for the undefined status.
OnBuilder.orElse(
listenTo: myState,
onWaiting: () => Text('onWaiting'),
orElse: () => Text('{myState.state}'),
),
I've setup a complete example including the main page (Tab container), the tab pages, and a store. From the perspective of the app, this page can exist multiple times in the navigation stack, each time with a different context/seed, and this is (at least my) justification why this page should only have a local store for each instance.
The code has two variants: a) the inherited case managing local state b) the normal case with global state
To my surprise, the following effects happen in either variant a) or b) and I have no explanation why:
Variant a) local state
- the onWaiting state does not seem to work, no ProgressIndicator is shown
- after switching tabs manually, the inner pages fetch the new state
Variant b) global state
- progress indicator is shown correctly
- the onData method tells me data is null
- the inner pages read the old state
I have to say I'm really used to using the "store approach" with a void getData() method much like reso coder showed in his video. It worked flawlessly for the last 2 years. But I somehow have the feeling I'm doing something wrong since things have changed in states rebuilder. With "store approach" I mean that I have a store/view model for each page (in most cases), and this store in being injected and carries the loading state". Those stores again have data repositories via dependency injection. But setState is only used in the stores. As one can see, my stores are mutable. So I don't know if mutability is a problem or maybe the void-methods for getData(). Compare the weather store at https://resocoder.com/2019/12/30/states-rebuilder-zero-boilerplate-flutter-state-management/
import 'package:flutter/material.dart'; import 'package:states_rebuilder/states_rebuilder.dart'; final Injected<TestStore?> testStoreInjected = RM.inject<TestStore?>( // // variant A) inherited store () => throw UnimplementedError(), // // variant B) global store // () => TestStore("global"), debugPrintWhenNotifiedPreMessage: "TestStore", ); class TestPage extends StatelessWidget { final String seed; const TestPage({required this.seed}) : super(); @override Widget build(BuildContext context) { List<Widget> tabs = [ Tab(text: "Tab One"), Tab(text: "Tab Two"), ]; List<Widget> tabPages = [ MyTabPage(text: "TabPage One"), MyTabPage(text: "TabPage Two"), ]; return DefaultTabController( length: tabPages.length, child: Scaffold( body: NestedScrollView( headerSliverBuilder: (BuildContext context, bool boxIsScrolled) { return <Widget>[ SliverAppBar( title: Text( "Outer Container($seed):\nCreate .inherited(...) Store & Init downloads", maxLines: 2, style: TextStyle(fontSize: 12), ), bottom: TabBar(tabs: tabs), ), ]; }, // // variant A) inherited store body: testStoreInjected.inherited( stateOverride: () { TestStore testStoreLocal = TestStore("local"); testStoreLocal.getData("inherited getData"); return testStoreLocal; }, builder: (context) { return testStoreInjected(context).rebuild.onAll( onWaiting: () => Center(child: CircularProgressIndicator()), onError: (_, __) => Center(child: Text("Error while loading.")), onData: (_) => TabBarView(children: tabPages), ); }, ), // // variant B) global store // body: testStoreInjected.rebuild.onAll( // initState: () => testStoreInjected // .setState((s) => s!.getData("global getData")), // onWaiting: () => Center(child: CircularProgressIndicator()), // onError: (_, __) => Center(child: Text("Error while loading.")), // onData: (_) => TabBarView(children: tabPages), // ), ), ), ); } } class MyTabPage extends StatelessWidget { final String text; const MyTabPage({required this.text}) : super(); @override Widget build(BuildContext context) { return CustomScrollView( slivers: [ SliverToBoxAdapter( child: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ Text( text + "\n\nWill be shown on .inherited(...) onData()\nAccesses data from .inherited(...) store of outer Container\n", textAlign: TextAlign.center, ), Text( // // variant A) inherited store "Result of .inherited() Store:\n" + (testStoreInjected(context).state?.output ?? "isNull"), // // variant B) global store // "Result of global Store:\n" + // (testStoreInjected.state?.output ?? "isNull"), textAlign: TextAlign.center, ), ], ), ) ], ); } } class TestStore { final String seed; TestStore(this.seed); String? output; void getData(String input) async { debugPrint(runtimeType.toString() + " -> getData($input) of seed $seed initiated. State change expected in 4sec!\nInput:$input | Result: tbd | output: $output"); String? result = await Future<String>.delayed(Duration(seconds: 4), () => "Result Done"); output = input + " -> " + result; debugPrint(runtimeType.toString() + " -> getData($input) of seed $seed completed. State change now!\nInput:$input | Result: $result | output: $output"); } }
Thanks @amoslai5128 for the feedback! Besides the syntax, any thoughts an the quoted working example of local state and state update?
@indigo-dev your issue pushed me to think on a better manner to explain the concept of states_rebuilder. I come out with a set of simple and incremental examples to explain the core concepts of states_rebuilder. (I worked hard the last days and finely get it. It will be posted sooner).
Going back to your example:
I've setup a complete example including the main page (Tab container), the tab pages, and a store. From the perspective of the app, this page can exist multiple times in the navigation stack, each time with a different context/seed, and this is (at least my) justification why this page should only have a local store for each instance.
The code has two variants: a) the inherited case managing local state b) the normal case with global state
To my surprise, the following effects happen in either variant a) or b) and I have no explanation why:
Variant a) local state
the onWaiting state does not seem to work, no ProgressIndicator is shown after switching tabs manually, the inner pages fetch the new state Variant b) global state
progress indicator is shown correctly the onData method tells me data is null the inner pages read the old state
You have to override the state asynchronously. This will let you show CircularProgressIndicator:
// // variant A) inherited store
body: testStoreInjected.inherited(
stateOverride: () async {
TestStore testStoreLocal = TestStore("local");
await testStoreLocal.getData("inherited getData");
return testStoreLocal;
},
But sill, both tabs will use the same state of testStoreInjected
.
In my opinion, this is the right way to do it:
Just change the TestPage
.
class TestPage extends StatelessWidget {
final String seed;
const TestPage({required this.seed}) : super();
@override
Widget build(BuildContext context) {
List<Widget> tabs = [
Tab(text: "Tab One"),
Tab(text: "Tab Two"),
];
return DefaultTabController(
length: 2,
child: Scaffold(
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool boxIsScrolled) {
return <Widget>[
SliverAppBar(
title: Text(
"Outer Container($seed):\nCreate .inherited(...) Store & Init downloads",
maxLines: 2,
style: TextStyle(fontSize: 12),
),
bottom: TabBar(tabs: tabs),
),
];
},
body: TabBarView(
children: [
// This override state will be recreate after tab switching.
// We mast wrap it with AutomaticKeepAliveClientMixin widget
testStoreInjected.inherited(
stateOverride: () async {
TestStore testStoreLocal = TestStore("local one");
await testStoreLocal
.getData("inherited getData (TabPage One)");
return testStoreLocal;
},
builder: (context) {
return testStoreInjected(context).rebuild.onAll(
onWaiting: () =>
Center(child: CircularProgressIndicator()),
onError: (_, __) =>
Center(child: Text("Error while loading.")),
onData: (_) => MyTabPage(text: "TabPage One"),
);
},
),
// This state will not recreate after page switching because we
// used the predefined StateWithMixinBuilder.automaticKeepAlive.
StateWithMixinBuilder.automaticKeepAlive(
builder: (context, _) {
return testStoreInjected.inherited(
stateOverride: () async {
TestStore testStoreLocal = TestStore("local two");
await testStoreLocal
.getData("inherited getData (TabPage two)");
return testStoreLocal;
},
builder: (context) {
return testStoreInjected(context).rebuild.onAll(
onWaiting: () =>
Center(child: CircularProgressIndicator()),
onError: (_, __) =>
Center(child: Text("Error while loading.")),
onData: (_) => MyTabPage(text: "TabPage Two"),
);
},
);
},
),
],
),
),
),
);
}
}
@GIfatahTH Would be possible to have something like
InheritedBuilder
?
InheritedBuilder(
keepAlive: false,
state: testStoreInjected,
stateOverride: () async { ... }
builder: () { ... }
)
Also, it's worth taking a look at vue3:
@amoslai5128
InheritedBuilder( keepAlive: false, state: testStoreInjected, stateOverride: () async { ... } builder: () { ... } )
It looks interesting. Will see how to add it in the next 5.3 release.
Probably add a dedicated initState method which has a callback parameter for the injected inherited model? (like (storeRM) in the old days :)
InheritedBuilder(
keepAlive: false,
state: testStoreInjected,
stateOverride: () async { ... }
initstate: (injModel) { ... }
builder: () { ... }
)
Even after reading all resources covering local states I was still not able to figure out how to solve my current problem using v5.0.0.
My app makes use of some (highly) reusable widgets (image avatars) that have its own "mini-store / view model" which takes care of downloading an image resource. As this widgets occur multiple times on a single page, they all need their own reactive model with its own state.
I'm very used to the "old way" states rebuilder worked (using it for 2 years), but due to migration to null safety and so on it was time to upgrade the package.
In the old days, I used RM.create() to obtain a reactive model that was not injected globally. So each widget had its very own instance of the "mini-store / view model" and the corresponding reactive model to handle the local state. In combination with WhenRebuilder my setup worked as expected. In the observe parameter, I created the new local RM instance, and within the initState method I told the RM to start downloading the image and handle state, see Code below ("OLD WAY").
Now moving forward and using states rebuilder version ^5.0.0, I have some really hard time making this setup work by using .inherited as the docs tell me.
First, I globally inject the mini-store (aka wikiImageStoreInjected) with throw UnimplementedError(), as mentioned in the docs. Second, I use the global variable with .inherited() in the widget tree, to create a local instance at that place in the widget tree. The two important params here are statesOverride and builder.
As parameter for statesOverride, I return a fresh instance of WikiImageStore(...). Additionally, in the builder parameter, I create the avatar widget and try to access the downloaded image via wikiImageStoreInjected.of(context).imageUrl.
Problem is: where would I tell the local RM / mini-store to download the image and call setState? a) In the statesOverride Parameter, I only have access to the local singleton but not not to the local RM which would allow me to call localRM.setState((s)=> s.getImage()). b) in the builder method, by using wikiImageStore.of(context), it's the same problem. a + b) I tried calling wikiImageStore.of(context).getImage() wihtout the use of an RM, and the image will be downloaded, but the widgets won't display because of missing setState statement. (Using hot reload forces state update and then I can see the images) c) in the builder method, instead of using .of() but using wikiImageStore(context), I can access a RM, but calling wikiImageStore(context).setState(...) gives me errors telling me that the widget is already being built and I cannot mark it as needToRebuild again.
So to make it short: how to handle setState statements (e.g. for fetching data/use of futures) within .inherited structure?
OLD WAY
NEW WAY:
Error message displayed when calling wikiImageStoreInjected(context).setState():
Exception has occurred. FlutterError (setState() or markNeedsBuild() called during build. This StateBuilderBase<_OnWidget<Widget>> 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: StateBuilderBase<_OnWidget<Widget>> The widget which was currently being built when the offending call was made was: OnBuilder<DriverDetailStore>)