Where is the best place to put your ChangeNotifierProvider? - flutter

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.

Related

How can I access the values in a set from any screen in an app?

When my app starts, it iterates through a list of words and creates a particular set. This set is created right when the app starts. Once it is created, it will no longer change.
How can I access this set from any screen?
Do I have to use provider? Should I pass this set from screen to screen down the widget tree?
Or as it is in a way a "constant set" (it will not change, once it is created), there is another way to make it accessible from everywhere in the app?
It depends. If your App only has 2 or 3 screens, you can pass the data from screen to screen. If you have more screens, I would recommend InheritedWidget:
https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html
It is not the only way to do it, but it is what I generally use.
Your InheritedWidget should look like this (this code was not tested)
class MyInheritedWidget extends InheritedWidget
{
final allValues;
MyInheritedWidget(
{
Key? key,
required this.allValues,
required Widget child
}): super(key: key, child: child);
static MyInheritedWidget? of(BuildContext context)
{
return context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>();
}
}
You can then you it in your main():
void main() {
///You can generate your values here
final theValues = generateYourValues();
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
routes: {
'/': (context) => MyInheritedWidget
(
allValues: theValues,
child: AppStarter()
),
'/message': (context) => AnotherPage(),
},
),
);
}

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

How to connect multiple models in connector (ScopedModel)

I am trying to add fruit item to the cart, but nothing happens
Once I pressed on the 'add fruit' button nothing happens. It supposes to add fruit items in the cart list.
I get an error once trying to access the cart screen by pressing on the cart icon in the app bar after the 'add fruit' button was pressed.
In this way doesn't work properly:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new ScopedModel(
model: ListModel(),
child: ScopedModel(
model: CartModel(),
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'State Management Scoped Model',
theme: myAppTheme,
initialRoute: '/',
routes: {
'/': (context) => MyList(),
'/cart': (context) => MyCart(),
},
),
),
);
}
}
Another way as well:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new ScopedModel<ListModel>(
model: ListModel(),
child: ScopedModelDescendant<ListModel>(
builder: (context, child, model) => ScopedModel<CartModel>(
model: CartModel(),
),
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'State Management Scoped Model',
theme: myAppTheme,
initialRoute: '/',
routes: {
'/': (context) => MyList(),
'/cart': (context) => MyCart(),
},
),
),
);
}
}
With Provider package everything works fine :)
Cannot comment yet.
Try adding the MaterialApp as the direct descendant child of ScopedModel and the use of the ScopedModelDescendant when the changes to the Model actually affect the UI.
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new ScopedModel<ListModel>(
model: ListModel(),
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'State Management Scoped Model',
theme: myAppTheme,
initialRoute: '/',
routes: {
'/': (context) => MyList(),
'/cart': (context) => MyCart(),
},
),
);
}
}
and when the property changes lets say in MyCart widget when you list card items
... widget tree ...
child: ScopedModelDescendant<ListModel>(
builder: (context, child, ScopedModel<CartModel> model){
List cartItems = model.cartItems;
List<Widget> cartItemWidgets=[];
cartItems.forEach((cartItemData){
cartItemWidgets.add(new CartItemWidget(cartItemData));
});
return Column(
children:cartItemWidgets,
);
}
),
...widget tree...
Hope this helps.
Note, ScopedModelDescendant changes every time notifyLsteners() is called.
Doing so on the entire app would be quite expensive I'd think.
Edit:
forgot to add the rebuildOnChange: true property. And also made a mistake.
... widget tree ...
//__YOUR__MODEL__: model that changes
child: ScopedModelDescendant<__YOUR__MODEL__>(
rebuildOnChange: true, // now the widget rebuilds when notifyListeners(); is called inside the __YOUR__MODEL__
builder: (context, child,__YOUR__MODEL__ model){
List cartItems = model.cartItems;
List<Widget> cartItemWidgets=[];
cartItems.forEach((cartItemData){
cartItemWidgets.add(new CartItemWidget(cartItemData));
});
return Column(
children:cartItemWidgets,
);
}
),
...widget tree...
EDIT 2:
From going through the git repository I was able to build an example of what you wanted to do. Here is the link to the GitHub repository. Note: I've initiated an ownership transfer to you. And I'll update the link if you choose to accept. But to also describe the implementation.
You wanted to have two models a ListModel and a CartModel, the list model currently serves as a placeholder for an actual list.
In the CartModel you attached the ListModel.
Whenever you needed the list you got it through the CartModel.
How I changed things?
Split class Fruit into a separate file. // objects.dart
Turned ListModel into abstract ShoppingModel that extends Model
Created a singleton SuperModel that extends Model with ShoppingModel and CartModel. Gave it a private constructor and an instance attribute. There will ever only be one instance of this object.
SuperModel is a mixing it is able to access all the public properties of ShoppingModel and CartModel.
To avoid editor displayed errors a file was added//analysis_options.yaml that suppresses the error. Note this doesn't affect the program, you can find more information about it on the web.
CartModel extends ShoppingModel, now CartModel has access to all the methods of the ShoppingModel it does not have to store the ListModel.
Use ShoppingModel to add attributes that you might have to use across multiple models.
Wrap the app in a ScopeModel, the model attribute is the SuperModel. Since SuperModel is a singleton I used SuperModel.instance, it never has to be instantiated.
Added ScopedModelDescendant everywhere where changes might occur, don't forget the rebuildOnChange: true, property.
I've also given you the owner of the repository.
In general, when using multiple models, use inheritance and mixins if they need to exchange attributes. I always used a top abstract Model descendant class that holds all the data that all of my other Models use. Other models extend this class so they are able to get access to these attributes.
But since we need access to their properties across the app and the abstract Model descendant doesn't know their children, we can create a mixin of all the Model in this case SuperModel.
And because we will ever need a single instance make it a Singleton
class SuperModel extend Model with Model1, Model2{
SuperModel._privateConstructor();
static final SuperModel _instance = SuperModel._privateConstructor();
static SuperModel get instance {
return _instance;
}
}
now we can pass the SuperModel to the ScopedModel.
To enable error free mixins add:
analyzer:
strong-mode: true
language:
enableSuperMixins: true
to the end of the pubspec.yaml file
and a new root file analysis_options.yaml:
analyzer:
errors:
mixin_inherits_from_not_object: ignore
Now this applies to Visual studio code, I don't know if this is handled any differently for Android Studio

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.