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.
Related
I am new to developing in flutter, and I am trying to make an app that will redirect the user to a different screen depending on what role they are as a user. I technically need to implement this in two places, in my the build of my main, and when the log in button is pressed.
What I have been trying to do right now is in the main, check if the user is logged in, then get his user uid, then use that uid to query the database for his role. It seems like a very crude solution as the application feels very choppy + its crashing and it takes it a while at boot to redirect to the right page and I'm unsure if I should be using real time database instead of the normal one for this. Any pointers on how to optimize this would be greatly appreciated.
class MyApp extends StatelessWidget {
late String userRole;
late String path;
#override
Widget build(BuildContext context) {
String userUid = FirebaseAuth.instance.currentUser!.uid;
userRole = "uprav";
path = "";
if(FirebaseAuth.instance.currentUser!.uid.isNotEmpty)
{
Database.setUserUid(userUid);
Database.getRole().then((value) {
userRole = value;
});
switch(userRole) {
case "uprav":
path = "/repair-flow";
break;
case "majs":
path = "/majstor-flow";
break;
case "pred":
path = "/repair-flow";
break;
}
}
print(userRole + " this is .then");
return ScreenUtilInit(
designSize: Size(375, 812),
builder: () => MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Majstor',
theme: ThemeData(
primaryColor: Constants.primaryColor,
scaffoldBackgroundColor: Color.fromRGBO(255, 255, 255, 1),
visualDensity: VisualDensity.adaptivePlatformDensity,
textTheme: GoogleFonts.openSansTextTheme(),
),
initialRoute: FirebaseAuth.instance.currentUser == null ? "/" : path,
onGenerateRoute: _onGenerateRoute,
),
);
}
}
Route<dynamic> _onGenerateRoute(RouteSettings settings) {
switch (settings.name) {
case "/":
return MaterialPageRoute(builder: (BuildContext context) {
return Home();
});
case "/repair-flow":
return MaterialPageRoute(builder: (BuildContext context) {
return RequestServiceFlow();
});
case "/majstor-flow":
return MaterialPageRoute(builder: (BuildContext context){
return MajstorServiceFlow();
});
default:
return MaterialPageRoute(builder: (BuildContext context) {
return Home();
});
}
}
In flutter, we use need to manage the states of screens.
In your case,
State 1: Loading data - Show loading widget
State 2: Loaded data, you need to update layout - Show the screen based on the role.
There are many ways to do so:
Future builder (https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html)
BLOC(this is a bit hard for beginner but worth in the future development).
started to learn Flutter so I'm thinking if is it possible to create a function that returns a ThemeData object, but inside this function I wanna use MediaQuery.of(context). I mean, I know I can create such a function, but if I use MediaQuery.of(context) inside of it, I have an error
(MediaQuery.of() called with a context that does not contain a MediaQuery) complaining about the missing of MediaQueryProvider. I know I can use MediaQuery in child elements of MaterialApp, but I have a design problem now. Imagine this:
ThemeData getTheme(BuildContext context) {
// I wanna be able to call MediaQuery.of(contex) here
return ThemeData();
}
MaterialApp(
home: home,
// the getTheme() must be able to use MediaQuery. It takes a context and returns a ThemeData object
theme: theme.getTheme(context),
routes: routes,
)
Is there a way to do it? or event better, Should I do it?
Thank you for your help.
A workaround is not using the theme property of MaterialApp but wrapping the descendants with a Theme widget using the builder property. It will have the MaterialApp as an ancestor so you can use MediaQuery there.
MaterialApp(
builder: (context, child) {
return Theme(
data: getTheme(context), // you can use MediaQuery.of(context) in the called function
child: child,
);
},
home: HomeScreen(),
// etc, no theme here
)
Welcome to the brotherhood.
Actually, to call for MediaQuery you will need to wrap your main widget with a MaterialApp() which usually is done in your main.dart file.
Here is an example:
void main() => runApp(App());
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
Hope it helped!
You can try this.
Create a stateless/ stateful widget for the home of your MaterialApp, and inside that, create your getTheme method.
Example,
homePage extends StatelessWidget{
ThemeData getTheme(BuildContext context) {
// Call MediaQuery.of(contex) here
return ThemeData();
}
#override
//build method
}//homePage ends here.
Now, in your material app, simple call that method.
MaterialApp(
home: homePage(),
theme: homePage().getTheme(context),
routes: routes,
)
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.
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.
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.