my goal is to provide value of user location with a Provider in the whole app. It's important to me that is't on top of the app as I want to use the value in the routes also.
However, in my app user first needs to login. Only afterwards he gets to the map. Here is the thing. According to bussiness requirements I can't call for permissions before the user gets to the map widget.
So the provider needs to be on top o the app but the future function has to be called once the user logs in.
How can I achieve that?
Here is a sample of my MyApp widget.
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
StreamProvider<UserModel?>.value(
initialData: null,
value: AuthService().user,
),
FutureProvider<LatLng?>.value(
value: GeolocationService().getUserLocation(),
initialData: null,
)
],
child: MaterialApp(
title: '',
theme: themeData(),
onGenerateRoute: onGenerateRoute(),
builder: EasyLoading.init(),
home: AuthWrapper(),
),
);
}
}
SchedulerBinding.instances from scheduler. You can use it inside the initState of StatefulWidget
{
//inside initState method
SchedulerBinding.instances?.addPostframecallback((_){
//anything run within this function will be called just after the very first build method
// how it works?
// before the build method ran, initState will be called first synchronously
// after that, build method will be called
// then right after that build method finished the first render task,
// the post frame callback will be called, in this place we can use context
// since the UI has been built
});
}
Related
I just arrived on a flutter project for a web app, and all developers have a problem using flutter provider for state management.
What is the problem
When you arrive on a screen, the variables of the corresponding provider are initialised by calling a function of the provider. This function calls an api, and sets the variables in the provider.
Problem : This function is called in the build section of the widget. Each time the window is resized, the widget is rebuilt, and the function is called again.
What we want
We want to call an api when the page is first displayed, set variables with the result, and not call the api again when the widget is rebuilt.
What solution ?
We use a push from the first screen to go to the second one. We can call the function of the provider at this moment, to initialise the provider just before the second screen.
→ But a refresh on the second page will clear the provider variables, and the function to initialise them will not be called again.
We call the function to initialise the provider in the constructor of the second screen. Is it a good pattern ?
Thank you for your help in my new experience with flutter :)
I think you're mixing a couple different issues here:
How do you correctly initialize a provider
How do you call a method on initialization (only once)
For the first question:
In your main.dart file you want to do something like this:
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => SomeProvider()),
ChangeNotifierProvider(create: (context) => AnotherProvider()),
],
child: YourRootWidget();
);
}
Then in a widget (that probably represents a "screen" in your app), you need to do something like this to consume state changes from that provider:
#override
Widget build(BuildContext context) {
return Container(
child: Consumer<SomeProvider>(
builder: (context, provider, child) {
return Text(provider.someState);
}
),
)
}
And you need to do something like this to get access to the provider to mutate state:
#override
Widget build(BuildContext context) {
SomeProvider someProvider = Provider.of<SomeProvider>(context, listen: false);
return Container(
child: TextButton(
child: Text('Tap me'),
onPressed: () async {
await someProvider.mutateSomeState();
}
),
)
}
Regarding the second question... You can (I think) just use the initState() method on a widget to make the call only 1 time. So...
#override
void initState() {
super.initState();
AnotherProvider anotherProvider = Provider.of<AnotherProvider>(context, listen: false);
Future.microtask(() {
anotherProvider.doSomethingElse();
});
}
If I'm off on any of that, I'm sorry. That mirrors my implementation and works fine/well.
A caveat here is that I think RiverPod is likely the place you really want to go (it's maybe easier to work with and has additional features that are helpful, etc.) but I've not migrated to RiverPod yet and do not have that figured out all the way.
Anyway... Good luck!
As far as I understood, you can wrap your application with MultiProvider and call the API before going to the second screen.
I'm building an application that uses lazy internationalization, this way there will be no translation files in the application and all translations will be fetched from the internet when a new page is opened. For that I am using a localization cubit.
Each screen of my application is divided into a "view" that receives the translated messages as a parameter, a "cubit" that contains the cubit screen and its states, and a "container" that contains the BlocProvider for the cubit and the screen.
For now my app starts in the presentation screen, after that it goes to the login screen and finally goes to the home screen.
So in the main file, instead of using the presentation screen directly, I use the localization container and the presentation container comes as its child:
return MaterialApp(
title: 'My App',
theme: myTheme(context),
debugShowCheckedModeBanner: false,
home: LocalizationContainer(
child: PresentationContainer(),
),
);
The PresentationContainer is composed this way:
class PresentationContainer extends BlocContainer {
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => PresentationCubit(),
child: I18NLoadingContainer(
language: BlocProvider.of<CurrentLocaleCubit>(context).state,
viewKey : "Presentation",
creator: (messages) => PresentationView(PresentationViewLazyI18N(messages)),
),
);
}
}
So in the container I have a BlocProvider with PresentationCubit and I18NLoadingContainer as a child.
I18NLoadingContainer just obtains the transalted messages according to the language provided and the screen name, that is "Presentation" in this case. The translated messages are returned in the variable messages, so this messages are passed as parameter to the screen.
If I use this only for my presentation screen everything works fine, but the issue comes when I need to open a new page.
After the presentation screen I need to open the login screen. So in the PresentationView I have the following function when the user clicks the button to open the login screen:
void _goToLogin(BuildContext blocContext) {
Navigator.of(blocContext).pushReplacement(
MaterialPageRoute(
builder: (context) => BlocProvider.value(
value: BlocProvider.of<CurrentLocaleCubit>(blocContext),
child: LoginContainer(),
),
),
);
}
And the LoginContainer works exaclty as the PresentationContainer:
class LoginContainer extends BlocContainer {
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => LoginCubit(),
child: I18NLoadingContainer(
language: BlocProvider.of<CurrentLocaleCubit>(context).state,
viewKey : "Login",
creator: (messages) => LoginView(LoginViewLazyI18N(messages)),
),
);
}
}
If I keep in the presentation screen and use the hot reload everything works fine, but if I open a new screen using this method, I got the following error when try to use hot reload:
The following _CastError was thrown building Builder(dirty): Null
check operator used on a null value
I'm not sure your LoginContainer is still wrapped by the LocalizationContainer when you change the route. I would suggest you to provide a CurrentLocaleCubit above the MaterialApp widget and check whether it's working or not. I think you're loosing a CurrentLocaleCubit instance
I need to pass an argument for my initialRoute. I found this issue and tried it like this:
initialRoute: AuthService.isLoggedIn() ? Views.home : Views.firstStart,
onGenerateInitialRoutes: (String initialRouteName) {
return [
AppRouter.generateRoute(
RouteSettings(
name: AuthService.isLoggedIn() ? Views.home : Views.firstStart,
arguments: notificationPayloadThatLaunchedApp,
),
),
];
},
onGenerateRoute: AppRouter.generateRoute,
This almost works. The problem I have is that somehow after calling this...
Navigator.pushReplacementNamed(
context,
Views.loading,
);
... my Multiprovider which is a parent of my GetMaterialApp is being called again which crashed my app because I call a function when initializing my providers:
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) {
var dataProvider = DataProvider();
dataProvider.init( // <- this is called again which should not happen
context,
);
return dataProvider;
},
),
],
child: GetMaterialApp(
title: 'Flutter Boilerplate',
navigatorKey: Get.key,
initialRoute: AuthService.isLoggedIn() ? Views.home : Views.firstStart,
onGenerateInitialRoutes: (String initialRouteName) {
return [
AppRouter.generateRoute(
RouteSettings(
name: AuthService.isLoggedIn() ? Views.home : Views.firstStart,
arguments: notificationPayloadThatLaunchedApp,
),
),
];
},
onGenerateRoute: AppRouter.generateRoute,
),
);
}
I feel like the way how I pass the initial argument is wrong. Is there any other/better way to get this done? Let me know if you need any more info!
I think you misunderstand the usage of new API onGenerateInitialRoutes cause by the name it should load without the calling of
Navigator.pushReplacementNamed(
context,
Views.loading,
);
API at all. if you call this route from another widget, it means this is already the second route already. it should be the default route for your application. so it makes no sense to give a parameter to an initial route at all.
since you have used Provider Package, it is better to just get whatever argument(parameter) that you want to send via Provider API itself.
if you want to give hardcoded data, then just treat it as a normal Widget class.
home: MyHomePage(name:"parameter name",data:DataHome()), :
GetMaterialApp(
title: 'Flutter Boilerplate',
navigatorKey: Get.key,
home: MyHomePage(name:"parameter name",data:DataHome()),
onGenerateRoute: AppRouter.generateRoute,
);
take note that if you calling Navigator.pushReplacementNamed API . your provider will be gone cause of the Flutter Widget Tree most Navigator API will create a new Widget tree level from the root. since the provider only provides for all its child only. so you cant use any provider data since you have a different ancestor.
so if you only have one page to go I suggest you use the home attribute in MaterialApp API cause it will treat your initial page as a child and Provider API can receive the data cause up in the Widget tree, it has an Ancestor Provider API:
MultiProvider(
providers: [
ChangeNotifierProvider(
if you want to move between pages via navigator after the main page.
consider using NestedNavigator so flutter will not create a new route from the root of the widget tree. so you still can access Provider API's data.
guessing from your variable name. I assume that you want to handle a notification
routing, check this deeplink
Need to query server for an object before pushing the ProfileWidget route.
Not sure if error message has anything to do with the Future call inside the MaterialPageRoute's builder (see routes function)?
ERROR
flutter: Another exception was thrown: The builder for route "null" returned null.
..
#override
Widget build(BuildContext context) {
return new MaterialApp(
onGenerateRoute: routes,
debugShowCheckedModeBanner: false,
theme: new ThemeData(
primarySwatch: Colors.blueGrey,
textTheme: TextTheme(
)
),
home: ...,
)
;
}
Route routes(RouteSettings settings) {
if (settings.name.startsWith("/profile")) {
String username = settings.name.replaceFirst("/profile/", '');
return MaterialPageRoute(
builder: (context) {
api.fetchProfile(username)
.then((profile) {
return ProfileWidget(profile);
});
}
);
}
}
Yes, that's exactly where the issue is. Async methods don't work properly within a build function as the build function is expecting a widget to be returned right away - by using a future you're actually just returning null.
Another issue is that you're calling your api in the build function - think about it this way... the build function is called any time anything is changed in your widget. Do you really want to fetch the profile every time that happens?
Instead, I'd recommend using a FutureBuilder and starting the future in initState, or starting the future in initState and using setState(() ...) after completion. Either way, if you do it this way you'll have to deal with the case where there's a brief period of time before the profile is created, unless you think about doing something like moving the profile loading out of the widget into the main function (I don't know if that's really recommended but it seems to work for me).
You might even think about putting the profile into an InheritedWidget or ScopedModel so that if you change the logged-in user it propagates automatically.
This question already has answers here:
How do I pass non-string data to a named route in Flutter?
(11 answers)
Closed 4 years ago.
I have MyApp class which is responsible for loading application. I'm loading a Class HomeWidget as home in MyApp.
void main() => runApp(new myApp());
class myApp extends StatelessWidget{
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'My App',
color: Colors.grey,
home: new HomeWidget(),
theme: new ThemeData(
primarySwatch: Colors.lightBlue,
),
routes: <String, WidgetBuilder>{
"/ItemDetailWidget": (BuildContext context) => new MovieDetailsPage(),
},
);}}
HomeWidget contains - Header, ListView, BottomNavigation.
When user taps particular item from ListView, I wanted to show new Widget/Page(ItemDetailWidget) which has all information about that particular item.
So i created ItemDetailWidget which is stateful and it accepts one parameter which is of type Object MyModel suppose. I made it stateful for purpose.
How should i add ItemDetailWidget into Routes as i'm passing
parameter to it?
I tried using
"/ItemDetailWidget": (BuildContext context) => new ItemDetailWidget(),
However, It throwing error as "The Constructor return type dynamic that isn't of expected type widget"
& Also how can i pass MyModel object to ItemDetailWidget using Navigator syntax? I'm having onTap() function in ListView.
Navigator.of(context).pushNamed('/widget1');
This depends on the data you're sending; it sounds like in your case you have a bunch of movie details in a DB (or something), and you want to be able to show details for that movie. What you can do is use a unique identifier for each movie, and put that in the request; this is described more or less in the potential duplicate mentioned in the comments. The flutter stocks example also explains this.
To summarize:
When you push, do a pushNamed("moviedetails/${movieUniqueIdentifier}").
In your MaterialApp, you can set
routes:
routes: <String, WidgetBuilder>{
'/': (BuildContext context) => new Movie(movies, _configuration),
'/settings': (BuildContext context) => new MovieSettings(_configuration)
},
and:
onGenerateRoute: (routeSettings) {
if (routeSettings.name.startsWith("movie:") {
// parse out movie, get data, etc
}
}
However, this isn't always the easiest way of doing things - say for example your database takes a while to respond and so you want to do a query before and then pass in the result (caching would be a good answer to this but let's ignore that for now =D). I'd still recommend the first method, but there are cases where it doesn't work.
To instead pass an object directly in (as the question actually asks) you can use:
Navigator.of(context).push(new PageRouteBuilder(pageBuilder:
(context, animation, secondaryAnimation) {
// directly construct the your widget here with whatever object you want
// to pass in.
})
Note that this will get quite messy if you have these Navigator.of(context).push blocks all over your code; I got away from this by using static convenience methods to it such as MyNavigator.pushMovie(context, movieObject) which would call Navigator.of(context).... under the hood. (I also subclass Navigator and so do MyNavigator.of(context) instead, but my setup is complicated as it does a bunch of additional custom navigation stuff).