Flutter Provider: Provide 2 streams with one dependent on the other - flutter

I'm using the provider package. In the root of the widget tree I have a multiprovider:
Widget build(BuildContext context) {
return MultiProvider(
providers: [
StreamProvider<FirebaseUser>.value(
value: FirebaseConnection.getAuthenticationStream()),
StreamProvider<User>.value(
value: FirebaseConnection.getUserStream(uid: ???))
],
child: MaterialApp(
debugShowCheckedModeBanner: false,
initialRoute: LoginScreen.id,
onGenerateRoute: RouteGenerator.generateRoute,
),
);
}
The first StreamProvider provides the logged in user from firebase_auth.
The second StreamProvider is supposed to provide additional information to that user (stored in the users collection).
The problem is that to get the second stream I need the uid of the FirebaseUser provided with the first stream but I don't know how to access it.
Nesting the StreamProviders didn't work because in the second stream I can only access the (wrong) context of the build method (for Provider.of(context))

The solution is to save the second stream (loggedInUserStream) in the state and change it whenever the first stream (authenticationStream) emits a new value (by listening to it) like in the code below:
class _FloatState extends State<Float> {
StreamSubscription<FirebaseUser> authenticationStreamSubscription;
Stream<User> loggedInUserStream;
StreamSubscription<FirebaseUser> setLoggedInUserStream() {
authenticationStreamSubscription =
FirebaseConnection.getAuthenticationStream().listen((firebaseUser) {
loggedInUserStream =
FirebaseConnection.getUserStream(uid: firebaseUser?.uid);
});
}
#override
void initState() {
super.initState();
authenticationStreamSubscription = setLoggedInUserStream();
}
#override
void dispose() {
super.dispose();
authenticationStreamSubscription.cancel();
}
#override
Widget build(BuildContext context) {
return StreamProvider<User>.value(
value: loggedInUserStream,
child: MaterialApp(
debugShowCheckedModeBanner: false,
initialRoute: LoginScreen.id,
onGenerateRoute: RouteGenerator.generateRoute,
),
);
}
}
I only needed the first stream (authenticationStream) to get the second one (loggedInUserStream) so I didn't provide it to the widgets below.

Related

How Can I navigate to different screens on startup based on which type of user<Admin, Customer> is logged in by making the use of Streams

In my flutter app there are two kinds of users, Admin and Customer. I wish to implement a functionality which will navigate The Customer to CustomerHomePage() and Admin to AdminHomePage().
I have wrapped my home property of MaterialApp with StreamBuilder which should to listen to any added values to the currentUserStream and alter the UI accordingly :
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
// home: const MyHomePage(title: 'Flutter Demo Home Page'),
home: Scaffold(
body: StreamBuilder(
initialData: null,
stream: FirestoreServices.currentUserStream,
builder: (context, snapshot) {
Widget widget = LogInPage();
if (snapshot.data != null) {
// Go to AdminHomePage if the logged in User is a Admin
print("Logged in Usertype : ${snapshot.data!.userType.toString()}");
if (snapshot.data!.userType == UserType.admin) {
widget = AdminHomePage(caUser: snapshot.data!);
}
// Go to CustomerHomePage if the logged in User is a Customer
else if (snapshot.data?.userType == UserType.customer) {
widget = CustomerHomePage(
caUser: snapshot.data!,
);
}
} else {
widget = LogInPage();
}
return widget;
}),
));
}
}+
the Stream so used in this streamBuilder is is a static property of FirestoreServices Class which is made the following way :
static Stream<CAUser> get currentUserStream async*{
FirebaseAuth.instance.authStateChanges().map(
(event) async* {
yield await FirestoreServices().uidToCAUser(event!.uid);
});
}
According to me the problem that's occuring is the values are either not getting added to the stream or they aren't getting read by the StreamBuilder. The effect of this is that the screen isn't navigationg to any of the HomePages
I tried the code which I just posted above, and I expect there's something wrong with the getter function.
type here

Could not find the correct Provider<...> above this ContactsPage Widget

I'm experimenting with Flutter and I'm trying to build a simple app using the Providers pattern.
My Problem
I am trying to access a provider in one of the widgets and I'm getting this error once I get the required provider in the stateful widget class. I can't figure out what am I doing wrong here.
Error
Error: Could not find the correct Provider<ContactProvider> above this ContactsPage Widget
This happens because you used a `BuildContext` that does not include the provider
of your choice. There are a few common scenarios:
- You added a new provider in your `main.dart` and performed a hot-reload.
To fix, perform a hot-restart.
- The provider you are trying to read is in a different route.
Providers are "scoped". So if you insert of provider inside a route, then
other routes will not be able to access that provider.
- You used a `BuildContext` that is an ancestor of the provider you are trying to read.
Make sure that ContactsPage is under your MultiProvider/Provider<ContactProvider>.
This usually happens when you are creating a provider and trying to read it immediately.
...
The Code
main.dart
import 'package:ProvidersExample/provider_list.dart';
void main() async => runApp(Home());
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: providerList,
child: MaterialApp(
home: ContactsPage(),
)
);
}
}
providers_list.dart
List<SingleChildWidget> providerList = [
ChangeNotifierProvider(
create: (_) => ContactProvider(),
lazy: false,
)
];
The provider: contact_provider.dart
class ContactProvider extends ChangeNotifier{
List<Contact> _contactList = [];
// getter
List<Contact> get contacts {
return [..._contactList];
}
// setter
set contacts(List<Contact> newContacts) {
_contactList = newContacts;
notifyListeners();
}
...
The Widget contacts_page.dart
class ContactsPage extends StatefulWidget {
#override
_ContactsPageState createState() => _ContactsPageState();
}
class _ContactsPageState extends State<ContactsPage> {
#override
void initState(){
super.initState();
}
#override
Widget build(BuildContext context) {
// This line throws the error
ContactProvider _provider = Provider.of<ContactProvider>
(context, listen:false);
return Scaffold(
appBar: AppBar(
title: Text(
...
I think the reason is that you are referencing a list of providers which were created outside in provider_list.dart which does not have access to your context from your widget tree.
Try this instead:
List providerList(context) {
return [
ChangeNotifierProvider(
create: (context) => ContactProvider(),
lazy: false,
)
];
}
providerList() is now a method that takes in context, and uses that context to register your providers, and return it.
You should specify the type <T> when creating your provider so it knows which type you are looking for in the widget tree.
List<SingleChildWidget> providerList = [
ChangeNotifierProvider<ContactProvider>(
create: (_) => ContactProvider(),
lazy: false,
)
];

How to create a StreamProvider and subscribe to it later, Flutter

I have an issue where I want to create a MultiProvider with some different providers, the problem is that two of those are StreamProviders that require first to have a firebase auth login, and after that subscribe to a Stream in firebase based on the result of the user logged in.
So if I cannot launch the StreamProvider before the login at the top of my MaterialApp.
If I declare those providers after the login is complete I get an error that the provider is not on the correct route because I need the data in several routes around all my app.
Here is my code:
class Neybor extends StatelessWidget {
#override
Widget build(BuildContext context) {
final textTheme = GoogleFonts.nunito;
return MultiProvider(
providers: [
ChangeNotifierProvider<Data>(create: (context) => new Data()),
/// Settings Stream
StreamProvider<SettingsDataModel>.value(
value: Globals.firebaseCaller.settings(),
),
/// Plans Stream
StreamProvider<PlansDataModel>.value(
value: Globals.firebaseCaller.plans(),
),
],
child: MaterialApp(
...
}
For Globals.firebaseCaller.settings() and Globals.firebaseCaller.plans() I use the register user uid
Is there a way to declare a StreamProvider and subscribe to it later on my code?
Thanks in advance
Use create parameter in the StreamProvider to pass your stream and subscribe to it using Provider.of<T>(context)
class Neybor extends StatelessWidget {
#override
Widget build(BuildContext context) {
final textTheme = GoogleFonts.nunito;
return MultiProvider(
providers: [
/// Settings Stream
/// Globals.firebaseCaller.settings() should returns a Stream<SettingsDataModel>
StreamProvider<SettingsDataModel>(create: (context) =>
Globals.firebaseCaller.settings(),
),
],
child: HomeView()
..
then in the HomeView()
import 'package:provider/provider.dart';
class HomeView extends StatelessWidget {
#override
Widget build(BuildContext context) {
SettingsDataModel settings = Provider.of<SettingsDataModel>(context);
if (settings == null) {
return Align(child: new CircularProgressIndicator());
} else {
// your code
...
}
...

StreamProvider doesn't update list (Firestore)

I've created collection in Firestore called users and added several documents in it.
In Flutter's main, I've initialised StreamProvider
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
StreamProvider(create: (_) => Firestore.instance.collection('users').snapshots()),
ChangeNotifierProvider<UserStore>(create: (_) => UserStore()),
],
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MainPage(),
),
);
}
and in my Widget, I want to see how many documents (users) i've in my collection
StreamProvider<List<User>>.value(
value: streamUsers(), child: Text("${Provider.of<List<User>>(context).length}")),
My streamUsers method is as follows (mapping documents to list of documents)
Stream<List<User>> streamUsers() {
var ref = Firestore.instance.collection('users');
return ref.snapshots().map((list) => list.documents.map((doc) => User.fromFirestore(doc)).toList());
}
There is an obvious issue, that Provider.of<List<User>>.. cannot be used like this. Also in the StreamProviders initialisation, I believe I miss my model type, but I couldn't understand how can I put there List<User>because it required to be type of QuerySnapshot
StreamProvider<List<User>>(create: (_) => Firestore.instance.collection('users').snapshots())
What do I miss here?
..aand I found the solution.
First, for creating a StreamProvider we do like this
StreamProvider<List<User>>(create: (_) => streamUsers()),
streamUsers method as follows
Stream<List<User>> streamUsers() {
var ref = Firestore.instance.collection('users');
return ref.snapshots().map((list) => list.documents.map((doc) => User.fromFirestore(doc)).toList());
}
Then in our UI widget we simply call Provider to get any value from our data set (List<User>). In this case - only length.
Text("${Provider.of<List<User>>(context).length}")
Now everything updates automatically once any data changes are made.

Opening keyboard causes stateful widgets to be re-initialized

I am using Flutter 1.2.1 in the Stable branch. To illustrate my problem imagine I have pages A and B. A navigates to B using Navigator.push and B navigates back to A using Navigator.pop. Both are stateful widgets.
When I navigate from A to B and then pop back to A everything is fine and A keeps its state. However, if I navigate from A to B, tap a textfield in B opening the keyboard, then close the keyboard and pop back to A, A's entire state is refreshed and the initState() method for A is called again. I verified this by using print statements.
This only happens when I open the keyboard before popping back to A. If I navigate to B, then immediately navigate back to A without interacting with anything then A keeps its state and is not re-initialized.
From my understanding the build method is called all the time but initState() should not get called like this. Does anyone know what is going on?
After much trial and error I determined the problem. I forgot that I had setup a FutureBuilder for the / route in my MaterialApp widget. I was passing a function call that returns a future to the future parameter of the FutureBuilder constructor rather than a variable pointing to a future.
So every time the routes got updated a brand new future was being created. Doing the function call outside of the MaterialApp constructor and storing the resulting future in a variable, then passing that to the FutureBuilder did the trick.
It doesn't seem like this would be connected to the weird behavior I was getting when a keyboard opened, but it was definitely the cause. See below for what I mean.
Code with a bug:
return MaterialApp(
title: appTitle,
theme: ThemeData(
primarySwatch: Colors.teal,
accentColor: Colors.tealAccent,
buttonColor: Colors.lightBlue,
),
routes: {
'/': (context) => FutureBuilder<void>(
future: futureFun(), //Bug! I'm passing a function that returns a future when called. So a new future is returned each time
builder: (context, snapshot) {
...
}
...
}
...
}
Fixed Code:
final futureVar = futureFun(); //calling the function here instead and storing its future in a variable
return MaterialApp(
title: appTitle,
theme: ThemeData(
primarySwatch: Colors.teal,
accentColor: Colors.tealAccent,
buttonColor: Colors.lightBlue,
),
routes: {
'/': (context) => FutureBuilder<void>(
future: futureVar, //Fixed! Passing the reference to the future rather than the function call
builder: (context, snapshot) {
...
}
...
}
...
}
did you use AutomaticKeepAliveClientMixin in "A" widget ?
if you don't , see this https://stackoverflow.com/a/51738269/3542938
if you already use it , please give us a code that we can test it directly into "main.dart" to help you
Yup, happened to me, perhaps it's much better to wrap the FutureBuilder itu a PageWidget, and make it singleton
return MaterialApp(
title: appTitle,
theme: ThemeData(
primarySwatch: Colors.teal,
accentColor: Colors.tealAccent,
buttonColor: Colors.lightBlue,
),
routes: {
'/': (context) => PageWidget() // wrap it by PageWidget
...
}
...
}
class PageWidget extends StatelessWidget {
static final _instance = PageWidget._internal(); // hold instance
PageWidget._internal(); // internal consturctor
factory PageWidget() {
return _instance; // make it singleton
}
#override
Widget build(BuildContext context) {
return FutureBuilder<void>( ... );
}
}
I got a solution, I was initialising variables in the constructor of the superclass. I removed it and worked!
I just removed the FutureBuilder from the home of MaterialApp and changed the MyApp into a Stateful widget and fetched the requisite info in the initState and called setState in the .then(); of the future and instead of passing multiple conditions in the home of MaterialApp, I moved those conditions to a separate Stateful widget and the issue got resolved.
initState:
#override
void initState() {
// TODO: implement initState
// isSignedIn = SharedPrefHelper.getIsSignedIn();
getIsSignedInFromSharedPreference().then((value) {
setState(() {
isSignedInFromSharedPref = value ?? false;
if (isSignedInFromSharedPref) {
merchantKey = LocalDatabase.getMerchantKeyWithoutAsync();
}
isLoadingSharedPrefValue = false;
});
});
super.initState();
}
Future<bool?> getIsSignedInFromSharedPreference() async {
return SharedPrefHelper.getIsSignedIn();
}
MaterialApp (now):
MaterialApp(
title: 'Loveeatry POS',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Home(
isLoadingSharedPrefValue: isLoadingSharedPrefValue,
isSignedInFromSharedPref: isSignedInFromSharedPref,
merchantKey: merchantKey,
),
),
Home:
class Home extends StatelessWidget {
final bool isLoadingSharedPrefValue;
final bool isSignedInFromSharedPref;
final String merchantKey;
const Home({
Key? key,
required this.isLoadingSharedPrefValue,
required this.isSignedInFromSharedPref,
required this.merchantKey,
}) : super(key: key);
#override
Widget build(BuildContext context) {
if (!isLoadingSharedPrefValue) {
if (isSignedInFromSharedPref) {
return const Homepage(
shouldLoadEverything: true,
);
} else if (merchantKey.isNotEmpty) {
return LoginPage(merchantKey: merchantKey);
} else {
return const AddMerchantKeyPage();
}
} else {
return loading(context);
}
}
}
P.S.: If you need any more info, please leave a comment.