I am trying to "read a provider immediately when creating it" in context.watch<FooModel>().foo, which would trigger an error.
How to obtain a new BuildContext and generate a value from it?
Consumer<FooModel>(
key: ValueKey( context.watch<FooModel>().foo ),
builder: (context, value, child) {
return fooWidget;
},
)
I simply wrap the child part of the provider into another class, to prevent reading a provider immediately. It worked.
Related
I'm having an issue of the widget subtree constantly rebuilding from inside the ValueListenableBuilder. It is supposed to run a rebuild on change, and in this case it is listening to a table on a Flutter Hive Database.
Things I've tired:
I had all of my Hive Boxes open in the main method, so that I have access to each box from anywhere in the app. I tired only opening the Hive box when something is changed, then promptly closing this box. Didn't work
Things I think it could be, but not sure:
Mixing ChangeNotifierProvider with the ValueListenableBuilder - Because some of the subtree also utilizes changenotifier, but with ValueListenableBuilder constantly rebuilding the subtree, any changes I pass into the provider get wiped out.
Is there anyway of only rebuilding on a change only?
#override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable:
Hive.box<Manifest>(HiveTables.manifestBox).listenable(),
child: assignmentWidgets,
builder: (context, Box<Manifest> manifestBox, child) {
if (manifestBox.isNotEmpty)
return child!;
},
);
}
the Hive provides listening to a whole Box, so every time something happens inside that Box, the builder will be called :
ValueListenableBuilder<Box>(
valueListenable: Hive.box('settings').listenable(),
builder: (context, box, widget) {
// build widget
},
),
but you can also specify a List<String> of keys that you want only them to be listenable by the ValueListenableBuilder with the keys property like this:
ValueListenableBuilder<Box>(
valueListenable: Hive.box('settings').listenable(keys: ['firstKey', 'secondKey']),
builder: (context, box, widget) {
// build widget
},
),
now for keys other than firstKey and secondKey, every operation that's executed over them will not update the ValueListenableWidget, but operation on firstKey and secondKey will update it only.
The issue is that I don't get any values out of my StreamProviders (which are defined on a global level) within my Authenticated route:
runApp(MultiProvider(
providers: [
Provider.value(value: userService),
StreamProvider.value(value: authService.userStream, initialData: null),
StreamProvider.value(value: userService.userDataStream),
StreamProvider.value(value: userService.characterStream),
],
child: MyApp(),
));
}
I noticed that it's to do with the logic that I have for my Navigator (if I remove it the provider values are passed down the widget tree as expected). The Navigator I'm using is based around the idea that the app has 3 states: Not Authenticated, Authenticated and Authenticated-First-Time. I get the value whether I'm authenticated from the loginStream (so far everything works):
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: loginStream,
builder: (ctx, snapshot) {
if (!snapshot.hasData) return Loading();
LoginState state = snapshot.data;
if (state == LoginState.LOGGED_OUT) return LoginScreen();
if (state == LoginState.FIRST_TIME) return CharacterCreationScreen();
return Navigator(
key: navigatorKey,
initialRoute: "/home",
onGenerateRoute: (settings) => PageRouteBuilder(
pageBuilder: (ctx, _, __) => routes(settings)(ctx),
transitionsBuilder: pageTransition,
),
);
},
);
The thing is that if I'm Authenticated and say in the HomeScreen, then both userDataStream and characterStream return null even if there's actual data available. If I remove the StreamBuilder + LoginLogic itself and just have the Navigator widget returned above, then HomeScreen gets the correct values.
UPDATE:
I noticed that it's not even the StreamBuilder. If I remove the 3 if's within the builder, then the stream values are propagated correctly. Not sure why that happens.
I´m not quite sure if this helps since I´m lacking details but here is what I noticed so far:
If you create the objects in the multiprovider for the first time you should not use .value - check if this applies.
Try cleaning up the if statements in the function body of your StreamBuilder (use if, else if and else keywords.
Also, following your description, it sounds like whenever an if statement is true, returns and thus cancels the build´s function body, the stream somehow resets and defaults to null. Maybe look into that & update your question.
Change this
Provider.value(value: userService),
StreamProvider.value(value: authService.userStream, initialData: null),
To this
Provider(create: (context) => userService)
StreamProvider(create:(context) => authService.userStream, initialData: null),
Do the same for all the providers that u are registering
To expose a newly created object, use the default constructor of a provider. Do not use the .value constructor if you want to create an object, or you may otherwise have undesired side effects.
https://pub.dev/packages/provider
I am a bit confused about the implementation of Provider with Firebase Auth. Every tutorial seems to use different methods. I have a service named AuthService which contains a variable final FirebaseAuth auth = FirebaseAuth.instance. So, I want to use the following stream: AuthService.auth.onAuthStateChanged in a widged. The question is should I use something like:
StreamProvider<FirebaseUser>.value(
value: AuthService().auth.onAuthStateChanged,
child: ...
or create a constructor like this:
Provider<AuthService>(
create: (_) => AuthService(),
child: ...
)
and access it somewhere in the widget:
final authService = Provider.of<AuthService>(context);<br>
final user = authService.auth.onAuthStateChanged;
You can use similar to this thing, If user is logged in he will lead to MainScreen() otherwise it will lead you to LoginPage(), you don't need to use
provider
Widget _handleAuth() {
return StreamBuilder<FirebaseUser>(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (BuildContext context, snapshot) {
return (!snapshot.hasData)
? LoginPage()
: MainScreen();
},
);
The provider package exposes different types of providers, you can check all types in their page here (At the bottom of their page).
Using a provider instead of another it depends on your needs, as far I'm concerned the Provider(the first one in the list) is fine.
I used two pages. and I added StreamBuilder in my first page and I passed snapshot.data to next Page. but when value change in 2nd-page value is not changing. How to fix it? I can't call streamBuilder in both pages because it's meaning two read in firebase. Is there any way to create singleton for services and access anywhere?
StreamBuilder(
stream: db.getData(),
builder: (context,snapshot){
return Column(
children: <Widget>[
InkWell(
onTap: ()=> Navigator.pushNamed(context, '/nextPage',arguments: Arguments(
data: snapshot.data
)),
)
],
);
},
)
InkWell(
onTap: ()=> Navigator.pushNamed(context, '/nextPage',arguments: Arguments(
data: snapshot.data
),
),
When using the above code, a data snapshot is only sent when you Tap on the InkWell. Meaning unless tapped on the inkwell it will not provide new data to nextPage.
To resolve this issue, I would suggest the following:
In First page
Create ValueNotifier instance to observe changes in the common reference:
// change Cast to type that you receive from firebase, or you can omit the cast
final ValueNotifier<snapshot_data_type> firebaseDataNotifier = ValueNotifier<snapshot_data_type>();
Update the value of firebaseDataNotifier when you receive data from firebase:
StreamBuilder(
stream: db.getData(),
builder: (context,snapshot){
firebaseDataNotifier.value = snapshot.data;
...
Pass the firebaseDataNotifier as data for the nextPage
In the next Page
Wrap the Next page widgets with ValueListenable
ValueListenableBuilder(
valueListenable: valueNotifier,
builder: (context, firebaseData, child) {
// return your next page widget here and use firebaseData instead of snapshot.data
)
Note: Make sure you keep the ValueNotifier instance outside of both widgets so that they can access it without any dependency.
Imagine in the TodoScreen of the application, I have a TodoObjectList (list of todos obtained from some API) and I want to show them inside a list. So I created a list of TodoWidgets (StatelessWidget), each one having its own TodoObject as its property. Now I want TodoWidgets to be bound with its TodoObject, so I used the Provider package. The code is something like below (inside TodoScreen):
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ChangeNotifierProvider<TodoObject>(
builder: (_) => TodoObjectList[index],
child: Consumer<TodoObject>(
builder: (context, TodoObject, child) => TodoWidget(todo: TodoObject)
),
);
},
childCount: TodoObjectList.length,
),
)
This code works fine for the first time. But when I go back and navigate to TodoScreen for the second time (I won’t call the API again, TodoObjectList is already cached), Provider throws an error:
“A TodoObject was used after being disposed.”
I Know why this error is being thrown, but my question is how should I bind TodoWidget with TodoObject using provider, without facing this error when I have the API data stored somewhere.
You should use ChangeNotifierProvider.value instead. This link would probably help you if you had any follow up questions.