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.
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.
So, I'm trying to adapt the login page from the flutter example "Shrine", however, when using the Login page as is, I get a weird null value error.
I have not the slightest hint what I'm doing wrong here, and what I should change.
Yes, I tried poking into it with the debugger, but I still have no clue.
The exact place the error seems to occur (I think) is here:
static GalleryOptions of(BuildContext context) {
final scope =
context.dependOnInheritedWidgetOfExactType<_ModelBindingScope>()!;
return scope.modelBindingState.currentModel;
}
For those interested, the code is up on my GitHub:
https://github.com/vguttmann/betterchips/tree/dev
So, what's going on here? How do I fix it? Why did it happen in the first place?
It's pretty clear from the code some of the widgets are trying to access GalleryOptions, which you have not provided, from their build contexts.
Look at the widget tree root of the original gallery app (main.dart), the entire app is wrapped in a ModelBinding widget with some pre-defined values:
#override
Widget build(BuildContext context) {
return ModelBinding(
initialModel: GalleryOptions(
themeMode: ThemeMode.system,
textScaleFactor: systemTextScaleFactorOption,
customTextDirection: CustomTextDirection.localeBased,
locale: null,
timeDilation: timeDilation,
platform: defaultTargetPlatform,
isTestMode: isTestMode,
),
child: Builder(
builder: (context) {
Some of the components you copied expect to have ModelBinding widget somewhere in their line of ancestors. Failing to find one, the function returns null, and since it's marked as non-nullable (bang operator following the expression), it immediately throws a runtime error.
If you're new to flutter I would advise you simply copy what they've done and wrap the entire app (MaterialApp) with a ModelBinding widget.
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
});
}
Before you vote down please read full problem.
I am getting this error I know the reason behind this error, the reason is userDetails Widget is removed from the tree when the login screen is selected. The question is how I re-insert that widget back to Tree. I have tried each and every solution available on StackOverflow and GitHub but no solution worked for me. I have tried using new keyword and global key in the scaffold.
Main.dart
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
SharedPreferences prefs = await SharedPreferences.getInstance();
var userPhoneNumber = prefs.getString('phoneNumber');
var userAlternateNumber = prefs.getString('AlternateNumber');
print(userPhoneNumber);
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp])
.then((_) {
runApp(
MaterialApp(
theme: ThemeData(
textSelectionHandleColor: kOrangeColor, cursorColor: kOrangeColor),
home: userPhoneNumber == null
? new LoginScreen()
: (userAlternateNumber == null
? new UserDetails()
: new HomeScreen()),
),
);
});
}
Function in LoginScreen.dart
void pushToUserDetailsScreen(BuildContext context) async {
Navigator.push(
context, MaterialPageRoute(builder: (context) => new UserDetails()));
}
I suggest you use a temporary selector page, meaning that you can create a new page, and in this page you don't have to show any UI (Just black screen).
What this would do is that it would decide what page you want to go next to, and this page would always be your homescreen, so when it's launched it will check if the phone number is null and would pushreplacement Login Screen , otherwise it would go to some other page.
Also don't worry about it being a "Black Screen" as this all happens too fast (That depends on the conditions that allow you to decide the starting screen) and it directly takes you to the screen you want depending on the conditions you have.
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).