StreamProvider doesn't update list (Firestore) - flutter

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.

Related

Is there any way to insert a query parameters to a named route in flutter web?

I want to insert query parameters to a named route.
I have this code on my MaterialApp
Widget build(BuildContext context) {
return MaterialApp(
title: 'Web',
theme: ThemeData(
primarySwatch: Colors.amber,
),
// Start the app with the "/" named route. In this case, the app starts
// on the FirstScreen widget.
initialRoute: '/login',
routes: {
'/login': (context) => LoginPage(),
'/mainmenu': (context) => MainMenu(),
},
);
}
Now I want to insert query parameters (for example id) to '/mainmenu' so when I want to navigate to the main menu page, the URL becomes for example: http://localhost:57430/#/mainmenu/?id=1234. Is there any way to do that? Thanks
You can pass Data through Navigator in Flutter by,
Navigator.pushReplacementNamed(context, '/home', arguments: {
'id': 1234
});
In the above code you will be pass data as a map to the next screen using arguments.
You can decode the map by these steps:
Declaring a Map variable in the next screen:
Map data = {}
Then decoding it by:
data = ModalRoute.of(context).settings.arguments;
print(data);
It's recommended to create a class to specify the arguments that need to be passed to the route, for example:
class MainMenuArguments {
final int id;
MainMenuArguments(this.id);
}
That can be passed to a Navigator:
Navigator.pushNamed(context, MainMenuScreen.routeName, arguments: MainMenuArguments(1234)); // id = 1234
And can be then accessed from the MainMenu Widget:
class MainMenuScreen extends StatelessWidget {
static const routeName = '/mainMenu';
#override
Widget build(BuildContext context) {
final MainMenuArguments args = ModalRoute.of(context).settings.arguments;
return Scaffold(
body: Center(
child: Text(args.id.toString()), // displays 1234
),
);
}
}
In order to do so, you'd need to register the route inside the MaterialApp constructor:
MaterialApp(
routes: {
MainMenuArgumentsScreen.routeName: (context) => MainMenuArgumentsScreen(),
},
);
Flutter has a cookbook specially for this situation. Link here

Where is the best place to put your ChangeNotifierProvider?

I'm learning the state management approache called Provider & Scope Model.
I made an example and its working fine.
In my example I have a list of entries and a button "+" to add a new entry.
Both views have their own routes, as shown bellow:
static Widget _buildRoute({
#required BuildContext context,
#required String routeName,
Object arguments,
}) {
switch (routeName) {
case Login:
return LoginScreen();
case OccurrenceentriesRoute:
return OccurrenceEntries();
case OccurrenceFormRoute:
Occurrence occurrence = arguments as Occurrence;
return OccurrenceForm(occurrence: occurrence);
default:
throw 'Route $routeName is not defined';
}
}
}
class MyApp extends StatelessWidget {
const MyApp({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => OccurrenceProvider()..loadOccurrences(),
child: MaterialApp(
title: 'Mapify',
theme: ThemeData(
primarySwatch: Colors.blue,
),
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute(
builder: (BuildContext context) => Routes.makeRoute(
context: context,
routeName: settings.name,
arguments: settings.arguments,
),
maintainState: true,
fullscreenDialog: false,
);
},
),
);
}
}
After reading the flutter documentation about the Provider approach I thought that would be a better idea to place the ChangeNotifierProvider as down as possible in the widget tree, as the documentation says:
You don’t want to place ChangeNotifierProvider higher than necessary (because you don’t want to pollute the scope)
My first attempt was to use the ChangeNotifierProviders in my buildRoute, adding only the providers that i needed in each route. I did this, but the notifications made on one route don't affect the others... So, I'm really confuse, where should I place this ChangeNotifierProviders other than in the top of the widget tree?
agree you really shouldn't place your change notifier at the top because that will rebuild the whole app instead
use Multiprovider and wrap it to the very top of you widget tree
like... MaterialApp(
child:MultiProvider(
providers:[],
child:yourWidget()
));
then you can access it by final var _sampleProvider = Provider.of<SomeModel>(context);
I suggest reading more into this if this explanation isn't still clear.

How to generate map of routes from a list of routes classes

I want to automate the routes assignment in my app, by creating a list of routes.
Then this list will be mapped.My aim is to improve my existing code.
I know this is confusing you can better understand it by the code given below:
MaterialApp(
navigatorObservers: <NavigatorObserver>[observer],
home: getWidgetByRouteName(hasRoute), //GameDetails(),
routes: <String, WidgetBuilder>{
SplashPage.routeName: (BuildContext context) => SplashPage(),
Phone.routeName: (BuildContext context) => Phone(),
OTP.routeName: (BuildContext context) => OTP(),
Team.routeName: (BuildContext context) => Team(),
},
),
I want to convert the above code into this type:
List route = [SplashPage, Phone, OTP];
Map<String, WidgetBuilder> namedRoutes = {};
route.forEach((element) {
namedRoutes[element.routeName] = (BuildContext context) => element();
});
MaterialApp(
navigatorObservers: <NavigatorObserver>[observer],
home: getWidgetByRouteName(hasRoute), //GameDetails(),
routes: namedRoutes
),
and in case you are confused what is routeName :
class SplashPage extends StatelessWidget {
static const routeName = '/splash';
..
..
..
}
I am getting this error:
The following NoSuchMethodError was thrown building MainApp(dirty):
Class '_Type' has no instance getter 'routeName'.
Receiver: SplashPage
Tried calling: routeName
You cannot create an instance of a class from a Type object representing the class, at least not without mirrors (and neither the Flutter nor the web platform supports mirrors).

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

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.

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.