My app's MyApp widget, which returns MaterialApp (as done in virtually every Flutter app) is rebuild whenever the build function on the home widget is called. I need to know why this happens, as it greatly reduces the performance of my app.
I use a StreamProvider (the Riverpod implementation of StreamBuilder) to either show my app's HomePage, LandingPage or loading screen (called PseudoSplashScreen for historical reasons), depending on whether a user is logged in or not, or whether the stream is waiting.
My main.dart contains, among other things:
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
FirebaseMessaging.instance.unsubscribeFromTopic('allUsers');
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
debugPrint("Returning MaterialApp");
return MaterialApp(
title: 'MyApp',
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
debugShowCheckedModeBanner: false,
theme: themeDataLight(),
darkTheme: themeDataDark(),
home: const ReDirector(),
);
}
}
class ReDirector extends ConsumerWidget {
const ReDirector({Key? key}) : super(key: key);
static const LandingPage landingPage = LandingPage();
static const PseudoSplashScreen pseudoSplashScreen = PseudoSplashScreen();
#override
Widget build(BuildContext context, WidgetRef ref) {
debugPrint("Building Redirector");
return ref.watch(authStreamProvider).when(
data: (data) {
debugPrint(data.toString());
if (data != null && data == AuthResultStatus.successful) {
debugPrint("Returning Homepage");
return Container(
width: double.infinity,
height: double.infinity,
color: Colors.blue,
);
} else {
debugPrint("AuthStreamProvider returned $data");
// When logging in, it is set to true. Hence, set it to false to prevent
// the isInAsync overlay from showing when logging out.
ref.read(landingPageProvider).isInAsync = false;
return landingPage;
}
},
error: (e, tb) {
debugPrint("Error in the AuthChecker");
debugPrint("$e\n$tb");
// When logging in, it is set to true. Hence, set it to false to prevent
// the isInAsync overlay from showing on error
ref.read(landingPageProvider).isInAsync = false;
return landingPage;
},
loading: () {
debugPrint("Returning PseudoSplashScreen");
return pseudoSplashScreen;
},
);
}
}
The Stream is derived from FirebaseAuth.instance.authStateChanges but is expanded to check some extra details on the user:
final authStreamProvider = StreamProvider.autoDispose<AuthResultStatus?>((ref) {
return FirebaseAuth.instance
.authStateChanges()
.asyncExpand((User? user) async* {
AuthResultStatus? result;
if (user != null) {
final IdTokenResult idTokenResult = await user.getIdTokenResult();
if (user.emailVerified && idTokenResult.claims!['approved'] == true) {
ref.read(userDataProvider).initialize(user, idTokenResult.claims!);
result = AuthResultStatus.successful;
} else {
result = AuthResultStatus.undefined;
}
}
debugPrint("AuthStreamProvider is yielding $result");
yield result;
});
});
Where AuthResultStatus is an enum. Now I would expect that while the stream is loading, the PseudoSplashScreen is shown, and when the Stream fires an AuthResultStatus.successful, the HomePage is shown. This is indeed what happens, but somehow my Redirector is rebuild about a second after the HomePage is shown. In fact, the build function of MyApp is called again! Regarding the debugPrints in the code, the console shows this:
I/flutter (22428): Returning MaterialApp
I/flutter (22428): Building Redirector
I/flutter (22428): Returning PseudoSplashScreen
I/flutter (22428): Creating new userdatamodel
I/flutter (22428): CURRENTUSER: Wessel van Dam
I/flutter (22428): AuthStreamProvider is yielding AuthResultStatus.successful
I/flutter (22428): Building Redirector
I/flutter (22428): AuthResultStatus.successful
I/flutter (22428): Returning Homepage
I/flutter (22428): Returning MaterialApp
I/flutter (22428): Building Redirector
I/flutter (22428): AuthResultStatus.successful
I/flutter (22428): Returning Homepage
Note that the rebuilding of the Redirector is not due to a new firing event of the Stream, because then you would expect another print of Returning successful ARS. However, this rebuild of the Redirector is pretty annoying as building the HomePage is a pretty intense process. This rebuild causes a the screen to flicker. Could anyone tell me why the Redirector's build function is called again in this sequence? If that can be prevented, the user experience for my app would be greatly improved.
Related
I'm trying to write a test for a row of buttons. The buttons should be enabled / disabled depending on a flag provided by a controller class.
class UserSettingsButtonRow extends ConsumerWidget {
const UserSettingsButtonRow({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
ref.watch(userSettingsControllerProvider);
UserSettingsController controller =
ref.read(userSettingsControllerProvider.notifier);
...
When I test this manually in the context of the whole program everything works fine. However I'd like to automate this test.
This is my test:
#GenerateNiceMocks([MockSpec<UserSettingsController>()])
void main() {
testWidgets(
'UserSettingsButtonRow - buttons enabled',
(tester) async {
// check if two buttons are enabled
UserSettingsController settingsController = MockUserSettingsController();
when(settingsController.hasValueChanged()).thenAnswer((_) => true);
await tester.runAsync(
() async {
await tester.pumpWidget(
ProviderScope(
overrides: [
userSettingsControllerProvider
.overrideWith((ref) => settingsController),
],
child: const MaterialApp(
home: Material(
child: UserSettingsButtonRow(),
),
),
),
);
},
);
await tester.pumpAndSettle();
expect(find.byType(ElevatedButton), findsNWidgets(2));
List<ElevatedButton> buttons = find
.byType(ElevatedButton)
.evaluate()
.map((e) => e.widget as ElevatedButton)
.toList();
for (ElevatedButton button in buttons) {
expect(button.enabled, isTrue);
}
},
);
<.......>
My problem is that a assertion in the build() method of my widget is not fulfilled.
When the automated test comes to
ref.watch(userSettingsControllerProvider);
I get the following exception:
Exception has occurred.
_AssertionError ('package:riverpod/src/framework/element.dart': Failed assertion: line 439 pos 9: 'getState() != null': Bad state, the provider did not initialize. Did "create" forget to set the state?)
I can't find many examples on testing / mocking with StateNotifierProviders. Can somebody help me out and tell me what's wrong here?
Thanks for your help!
I tried the solution from what is the correct approach to test riverpod with mockito but the atttribute state is not supported any more.
Riverpod Testing: How to mock state with StateNotifierProvider? is not a solution either for me as I want to override the hasValueChanged() method from the Controller.
I have a Flutter Application where an sqflite database stored the user preference of ThemeMode (viz Dark, Light and System). I have created a splash screen using flutter_native_splash which supports dark mode too.
The Problem is this that I want the splash screen to follow the users stored value for theme mode. Currently, the code I am using is as follows:
class MyRoot extends StatefulWidget {
// const MyRoot({Key? key}) : super(key: key);
static ValueNotifier<ThemeMode> themeNotifier = ValueNotifier(ThemeMode.system);
#override
State<MyRoot> createState() => _MyRootState();
}
class _MyRootState extends State<MyRoot> {
DatabaseHelper? databaseHelper = DatabaseHelper.dhInstance;
ThemeMode? tmSaved;
#override
void initState() {
Future.delayed(Duration.zero, () async => await loadData());
super.initState();
}
#override
Widget build(BuildContext context) {
//to prevent auto rotation of the app
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
return ValueListenableBuilder<ThemeMode>(
valueListenable: MyRoot.themeNotifier,
builder: (_, ThemeMode currentMode, __) {
return Sizer(
builder: (context, orientation, deviceType) {
return MaterialApp(
title: 'My Application',
theme: themeLight, //dart file for theme
darkTheme: themeDark, //dart file for theme
themeMode: tmSaved ?? currentMode,
initialRoute: // my initial root
routes: {
// my routes
.
.
.
// my routes
},
);
},
);
},
);
}
Future<void> loadData() async {
if (databaseHelper != null) {
ThemeMode? themeMode= await databaseHelper?.selectStoredTheme(); // function retrieving sqflite stored value and returning ThemeMode value
if (themeMode != null) {
MyRoot.themeNotifier.value = themeMode;
return;
}
}
MyRoot.themeNotifier.value = ThemeMode.system;
}
}
Currently, this shows a light theme splash screen loading, then converts it into dark with a visible flicker.
ValueListenableBuilder<ThemeMode>(... is to enable real time theme change from settings page in my app which working as intended (taken from A Goodman's article: "Flutter: 2 Ways to Make a Dark/Light Mode Toggle".
main.dart has the below code:
void main() {
runApp(MyRoot());
}
Have you tried loading the setting from sqflite in main() before runApp? If you can manage to do so, you should be able to pass the setting as argument to MyRoot and then the widgets would be loaded from the start with the correct theme. I'm speaking in theory, I can't test what I'm suggesting right now.
Something like:
void main() async {
ThemeMode? themeMode= await databaseHelper?.selectStoredTheme(); // function retrieving sqflite stored value and returning ThemeMode value
runApp(MyRoot(themeMode));
}
[...]
class MyRoot extends StatefulWidget {
ThemeMode? themeMode;
const MyRoot(this.themeMode, {Key? key}) : super(key: key);
static ValueNotifier<ThemeMode> themeNotifier = ValueNotifier(ThemeMode.system);
#override
State<MyRoot> createState() => _MyRootState();
}
EDIT
Regarding the nullable value you mentioned in comments, you can change the main like this:
void main() async {
ThemeMode? themeMode= await databaseHelper?.selectStoredTheme(); // function retrieving sqflite stored value and returning ThemeMode value
themeMode ??= ThemeMode.system;
runApp(MyRoot(themeMode!));
}
which makes themeMode non-nullable, and so you can change MyRoot in this way:
class MyRoot extends StatefulWidget {
ThemeMode themeMode;
const MyRoot(required this.themeMode, {Key? key}) : super(key: key);
[...]
}
Regarding the functionality of ValueNotifier, I simply thought of widget.themeMode as the initial value of your tmSaved property in your state, not as a value to be reused in the state logic. Something like this:
class _MyRootState extends State<MyRoot> {
DatabaseHelper? databaseHelper = DatabaseHelper.dhInstance;
late ThemeMode tmSaved;
#override
void initState() {
tmSaved = widget.themeMode;
super.initState();
}
[...]
}
so that your widgets would already have the saved value at the first build.
PS the code in this edit, as well as in the original part, isn't meant to be working by simply pasting it. Some things might need adjustments, like adding final to themeMode in MyRoot.
Make your splashscreen. A main widget which get data from sqlflite
And make splashscreen widget go to the your home widget with remove it using navigation pop-up
for example :
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'ToDo',
color: // color of background
theme: // theme light ,
darkTheme: // darktheme
themeMode: // choose default theme light - dark - system
home: Splashscreen(),// here create an your own widget of splash screen contains futurebuilder to fecth data and return the mainWidget ( home screen for example)
);
}
}
class Splashscreen extends StatelessWidget {
Future<bool> getData()async{
// get info
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: getData(),
builder: (context,snapshot){
// if you want test snapshot
//like this
if(snapshot.hasData) {
return Home();
} else {
return Container(color: /* background color as same as theme's color */);
}
}
);
}
}
What I am trying to do is that by starting the app, make a request to a server and all information is saved in the database, for that I use a FutureBuilder that does the whole process, once finished it starts the application as normal.
The problem is that the application executes my future synchronization more than twice, causing errors with the insert to database.
the following code is a basic example of what i am trying to do and the result i am getting.
main.dart
void main() => runApp(MyMaterialApp());
class MyMaterialApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
print('Running MyMaterialApp');
return MaterialApp(
home: SplashLoad(),
);
}
}
class SplashLoad extends StatefulWidget {
#override
_SplashLoadState createState() => _SplashLoadState();
}
class _SplashLoadState extends State<SplashLoad> {
final apiSimulation = new ApiSimulation();
#override
Widget build(BuildContext context) {
print('SplashScreen');
return Scaffold(
body: Container(
child: Center(
child: FutureBuilder(
future: apiSimulation.sincronizacion(),
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
if (snapshot.hasData) {
return Text('load finished data:${snapshot.data}');
}
return CircularProgressIndicator();
},
),
),
),
);
}
}
ApiSimulation
class ApiSimulation {
int number = 0;
Future<int> synchronization()async{
print('INIT SYNCHRONIZATION');
await Future.delayed(Duration(seconds: 2));
final data = await _getData();
return data;
}
Future<int> _getData() async{
number++;
print('running getData$number');
await Future.delayed(Duration(seconds: 2));
return number;
}
}
console result
I/flutter (32404): Running MyMaterialApp
I/flutter (32404): SplashScreen
I/flutter (32404): INIT SYNCHRONIZATION
I/flutter (32404): Running MyMaterialApp
I/flutter (32404): SplashScreen
I/flutter (32404): INIT SYNCHRONIZATION
I/flutter (32404): running getData1
I/flutter (32404): running getData2
Sometimes reaching 4 in the value of the number
This is what the document says
The future must have been obtained earlier, e.g. during
State.initState, State.didUpdateWidget, or
State.didChangeDependencies. It must not be created during the
State.build or StatelessWidget.build method call when constructing the
FutureBuilder. If the future is created at the same time as the
FutureBuilder, then every time the FutureBuilder's parent is rebuilt,
the asynchronous task will be restarted.
class _SplashLoadState extends State<SplashLoad> {
late final Future simFuture;
#override
void initState() {
super.initState();
simFuture= ApiSimulation().sincronizacion(); //initiate your future here
}
Within your future builder use
FutureBuilder(
future: simFuture,
builder: (ctx,snap){..},
)
I'm trying to use a ProviderListener from Riverpod to listen to my authProvider and control the page displayed if a user is authorized or not. I'm getting the error:
error: The argument type 'StateNotifierProvider<Auth, bool>' can't be assigned to the parameter type 'ProviderBase<Object, StateController>'.
The error shows up on the: provider: authProvider, inside the ProviderListener
I'm wondering if it's due to the update on StateNotifierProvider?
I would like to know how to use the ProviderListener better even if there's a better way to handle the authorization flow (I'm VERY open to feedback and criticism and greatly appreciate any time a person can take to help). I cut out non-relevant code
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class Auth extends StateNotifier<bool> {
Auth() : super(false);
void setAuth(bool auth) {
state = auth;
}
}
final authProvider = StateNotifierProvider<Auth, bool>((ref) => Auth());
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(
ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatefulHookWidget {
// const MyApp({Key key}) : super(key: key);
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final Future<FirebaseApp> _fbMyApp = Firebase.initializeApp();
Widget route = SplashScreen();
#override
Widget build(BuildContext context) {
return ProviderListener<StateController<bool>>(
provider: authProvider,
onChange: (context, auth) {
if (auth.state = true) {
route = HomeScreen();
} else {
route = SplashScreen();
}
},
child: MaterialApp(
home: route,
);
}
}
I managed to get it to sort of work by changing to:
return ProviderListener<StateNotifier<bool>>(
provider: authProvider.notifier,
it's giving me a non-breaking error of:
info: The member 'state' can only be used within instance members of subclasses of 'package:state_notifier/state_notifier.dart'. (invalid_use_of_protected_member)
and not working properly - the state isn't being updated when I'm using a context.read
context.read(authProvider.notifier).state = true;
So it's buggy but not fully broken. At least it's some progress. I would still love help and any feedback anyone wants to give!
Remove StateController from ProviderListener, leave only the type (bool in this case)
return ProviderListener<bool>(
provider: authProvider, //this will read the state of your provider (a bool state)
onChange: (context, auth) {
if (auth) { //remove setter auth = true, it doesn't make sense to set a value inside an if
route = HomeScreen();
} else {
route = SplashScreen();
}
},
child: MaterialApp(
home: route,
);
This way you're reading the state of your StateNotifier
I was trying to implement a simple login/logout functionality. My scenario is this:
I have 2 pages ( login page and home page), In the main.dart, I am using SharedPreferences to check if a user has already logged in or not if the user is logged in, I set a boolean value as true on click of a button.
The issue I am having is, I have a routeLogin function that I created to choose between Homepage and Landingpage.
And I get this error:
I/flutter ( 9026): ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
I/flutter ( 9026): The following assertion was thrown building MyApp(dirty):
I/flutter ( 9026): type 'Future<dynamic>' is not a subtype of type 'bool'
I/flutter ( 9026):
I/flutter ( 9026): Either the assertion indicates an error in the framework itself, or we should provide substantially
I/flutter ( 9026): more information in this error message to help you determine and fix the underlying cause.
I/flutter ( 9026): In either case, please report this assertion by filing a bug on GitHub:
I/flutter ( 9026): https://github.com/flutter/flutter/issues/new?template=BUG.md
This is my code :
import 'package:credit/src/pages/landing.dart';
import 'package:flutter/material.dart';
import 'package:credit/src/pages/credit/home.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
bool checkValue;
checkLoginValue () async{
SharedPreferences loginCheck = await SharedPreferences.getInstance();
checkValue = loginCheck.getBool("login");
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Test App',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: routeLogin());
//home: LandingPage());
}
routeLogin()
{
print("Check value");
if (checkValue == null){
return LandingPage();
}
else{
return HomePage();
}
}
}
Please let me know where did I went wrong, I am new to Flutter.
you can use future builder to obtain this behavior easily.
Future<bool> checkLoginValue() async {
SharedPreferences loginCheck = await SharedPreferences.getInstance();
return loginCheck.getBool("login");
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Test App',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: FutureBuilder<bool>(
future: checkLoginValue,
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
if (snapshot.data == false) {
return LandingPage();
} else {
return HomePage();
}
},
),
);
}
Assuming that your getBool function from loginCheck returns Future,
You are trying to put a Future into a bool.
Change that line to:
checkValue = await loginCheck.getBool("login");
checkValue has a value of Future not a bool.
Future checkValue;
So you could check whether it has returned a value or an error.
routeLogin() {
print("Check value");
checkValue.then((res) {
return LandingPage();
}).catchError(
(e) {
return HomePage();
},
);
}