StreamProvider returns no data when used with Navigator - flutter

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

Related

How do I access the url path params from GoRouter when using a MultiBlocProvider?

Currently we're building an app to learn Flutter and Bloc pattern at my company. We use a MultiRepositoryProvider as the main widget and GoRouter for routing. My route looks like this:
GoRoute(
path: '/game/:id',
builder: (context, state) => GameDetailScreen(),
),
In the MultiRepositoryProvider the child is a MultiBlocProvider and the provider for this screen is:
BlocProvider(
create: (BuildContext context) {
return GameDetailBloc(context.read<FirestoreRepo>());
},
),
The BlocProvider's create function returns the BuildContext but it's not clear to me how I get the GoRoute state to pass the url param id to the GameDetailBloc.
We managed to get this to work by setting the game's id in GoRoute's build function when creating the GameDetailScreen. Then we removed that BlocProvider in the MultiBlocProvider and then accessed the bloc from the BuildContext when building the widget but it doesn't seem correct and we're trying to find the "correct solution" to this problem. Any help is greatly appreciated. Thanks!
go_router has it's params in its state.
Hence pass the state to the page
Router
GoRoute(
name: "test",
path: "/test/:id",
builder: (context, state) {
return SampleWidget(
goRouterState: state, 👈 Pass state here
);
},
),
Usage
context.goNamed("test", params: {"id": "123"}),
Accesing in the page
class SampleWidget extends StatelessWidget {
GoRouterState? goRouterState;
SampleWidget({super.key, this.goRouterState});
#override
Widget build(BuildContext context) {
print(goRouterState?.params.toString()); 👈 access anywhere like so
return const Scaffold(
body: ...
);
}
}
I couln't completely understand the question. I have attempted to answer based on my interpretation. Let me know the specifics if you have any other requrirements.
Uri.base.toString().replaceAll(Uri.base.origin, '')

Provider in different route Flutter

I wanted to upload a picture inside Firebase Storage when I saw this error :
The provider you are trying to read is in a different route.
So I started study what is this Provider and now I understood that I need to include the page that I'm using to upload the picture inside the same route of the ancestor Provider of type Database.
So I had the issue inside "Registration_form" that is created by "LogInPage" through a Navigator like this :
void navigateToRegistrationForm(context) {
Navigator.push(context,
MaterialPageRoute<void>(builder: (BuildContext context) {
return Reg_Form();
}));
return;
}
Now I'm changing the code that is something like :
void navigateToRegistrationForm(context) {
Navigator.push(
context,
MaterialPageRoute<void>(
builder: (BuildContext context) => Provider<Database>(
create: (_) => FirestoreDatabase(),
builder: (context, child) => Reg_Form(),
)));
return;
}
But I don't understand what I should pass to the create method...I just want to create Reg_Formin the right route to call Firebase functions to put the file inside firebase storage so I think that FirebaseDatabase isn't what suits my needs.
If I understand your issue correctly, you can either move your ChangeNotifierProvider for your Database class higher up in the widget tree by wrapping your MaterialApp like so:
ChangeNotifierProvider<Database> (
builder: (context) => Database(),
child: MaterialApp(...
Or just change the create in what you provided to
create: (_) => Database(),
Either way should give you access to the correct Provider/BuildContext in your Registration form with standard context.read<Database>()...

How to combine multiple providers in Flutter

I am new to Flutter development.
I am building application where once users login they are shown list of posts.
If the user is not login still they are shown some random post.
I got parts of the application various posts in the internet.
This is what I did
class TestApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
Provider<AuthBase>(
create: (context) => Auth(),
),
StreamProvider<User>.value(
value: Auth().onAuthStateChanged,
),
ProxyProvider<User, Database>(
update: (BuildContext context, User user, Database db) {
return user == null
? FirestoreDatabase(uid: null)
: FirestoreDatabase(uid: user.uid);
}),
ChangeNotifierProxyProvider<Database, PostProvider>(
create: (context) => PostProvider(),
update: (ctx, database, previousProvider) {
print("at ChangeNotifierProxyProvider $previousProvider");
return previousProvider.update(database);
},
),
],
child: MaterialApp(
title: 'Test Project',
home: LandingPage(),
),
);
}
My idea is;
if the user is logged in StreamProvider<User>.value will give that user to
ProxyProvider<User, Database> and it will create database with that user.
And that database in turn will be used by ChangeNotifierProxyProvider<Database, PostProvider> to create the provider that actually get posts. It uses the database to get the posts.
I noticed even if the user is logged in at the start I get a null value for the user then immediately i get the actual user.
In the landing page, I only have the following line and it generate an error
final provider = Provider.of<PostProvider>(context);
I noticed the cause for the error was this line;
previousProvider.update(database);
First time time Postprovider constructor and update methods get called without any problem. (here we get null user)
When the FirestoreDatabase get created with the actual user "previousProvider" is null.
This is the reason for the error.
Not sure why this is happening or how to fix it.
Is there a better way of doing this?
Initially getting a null value even when there is a logged in user may be the reason. How to prevent it?
I will try to explain your code a bit so we are on the same page
Provider<AuthBase>(
create: (context) => Auth(), //this subscribe an Auth Class to the provider
),
StreamProvider<User>.value(
value: Auth().onAuthStateChanged,
/// This subscribe a stream from another Auth Class instance
/// Unless Auth its a singleton class, this is not related to the previous Provider
/// and changes to Provider<AuthBase> won't affect this Stream
),
I noticed even if the user is logged in at the start I get a null
value for the user then immediately i get the actual user.
That happens when using Stream, while the first value is captured (sometimes is delayed a tick) its null, you can use initialData parameter if you know the first value while the stream subscription is done
Finally
ChangeNotifierProxyProvider<Database, PostProvider>(
create: (context) => PostProvider(),
update: (ctx, database, previousProvider) {
print("at ChangeNotifierProxyProvider $previousProvider");
return previousProvider.update(database);
},
),
update expects to return a value of type PostProvider, but you're returning whatever the method previousProvider.update(database) returns (which I think it just updates some inner variable or something, I believe it's some void method maybe?)
Change the update like this
update: (ctx, database, previousProvider) {
print("at ChangeNotifierProxyProvider $previousProvider");
return previousProvider..update(database);
/// It's the same as writing this
/// previousProvider..update(database);
/// return previousProvider;
},

Implementation of Firebase Auth with Flutter and 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.

BlocProvider.of() called with a context that does not contain a Bloc of type MainBloc

I have a MainBloc that resides inside a main route, this route has a bottom app bar with multiple sub-routes, I want the same BLoC to run on all five sub-routes so that when one of them changes the state of the block the others will see the effect.
I tried this SO question but its really far from what I'm looking for, also I tried following what the error advised me to, but didn't work, here is the message that I got:
This can happen if:
1. The context you used comes from a widget above the BlocProvider.
2. You used MultiBlocProvider and didn't explicity provide the BlocProvider types.
Good: BlocProvider<MainBloc>(builder: (context) => MainBloc())
Bad: BlocProvider(builder: (context) => MainBloc()).
Main route:
#override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<MainBloc>(
builder: (BuildContext context) => MainBloc(),
),
BlocProvider<OtherBloc>(
builder: (BuildContext context) => OtherBloc(),
),
],
child: /..., //here I have the bottom app bar with 5 buttons to navigate between sub-routes
);
one of the sub-routes:
#override
Widget build(BuildContext context) {
final MainBloc bloc = BlocProvider.of<MainBloc>(context);
return /...; //here I have the context of this sub-route.
}
from what I've seen from tutorials and articles this code should work, but I can't seem to find why not.
The problem is you cannot access InheritedWidgets across routes unless you provide the InheritedWidget above MaterialApp. I would recommend wrapping your new route in BlocProvider.value to provide the existing bloc to the new route like:
Navigator.of(context).push(
MaterialPageRoute<MyPage>(
builder: (_) {
return BlocProvider.value(
value: BlocProvider.of<MyBloc>(context),
child: MyPage(),
);
},
),
);
You can find more detailed information about this in the bloc documentation
As this child has the bottom app bar:
child: /..., //here I have the bottom app bar
then I assume that the MultiBlocProvider(..) is not wrapping the whole part of app which is using this Bloc, my suggestion here is to wrap the "MaterialApp" with "MultiBlocProvider".
return MultiBlocProvider(
providers: [..],
child: MaterialApp(..) // Set MaterialApp as the child of the MultiBlocProvider
//..
)