I'm performing some widget tests on my Flutter app with flutter_test.
It basically works fine except when my testing widget is a Consumer.
Here, I basically have a DeviceProvider objects that wraps an object into a ChangeNotifier so that updates are correctly propagated to consumers.
To be able to use it with a WidgetTester, I put my Consumer widget inside a ChangeNotifierProvider that intantiates a DeviceProvider.
_pumpTestableWidgetWithProvider(WidgetTester tester, Widget widget) async {
DeviceProvider device = DeviceProvider(Device());
await tester.pumpWidget(ChangeNotifierProvider(
create: (context) => device,
child: MaterialApp(home: widget)));
return device;
}
Then, I use the WidgetTester I have just pumped and check whether some text fields are in them.
testWidgets("Infos - default device infos", (WidgetTester tester) async {
DeviceProvider device = await _pumpTestableWidgetWithProvider(
tester, DeviceInfos());
expect(find.text("Battery state: "), findsOneWidget);
});
It returns with an error telling me that 'A Timer is still pending even after the widget tree was disposed'. Looking at the log, this timer corresponds to a _FakeTimer created with the DeviceProvider. I thus tried to manually dispose the provider by using
device.dispose();
But now, I have an error telling me that 'A DeviceProvider was used after being disposed.'
Does anyone have a solution for me ?
I was able to solve it using a different constructor for the changeNotifierProvider on the tests:
Widget buildWidget() {
return ChangeNotifierProvider.value(
value: locator<TaNaLeiProvider>(),
child: MaterialApp(
home: Scaffold(
body: SizedBox(
width: 640,
child: DrawerTaNaLei(),
),
),
),
);
}
Note that I'm using ChangeNotifierProvider.value instead of create.
I'm also using getIt to create singletons on my app.
locator.registerLazySingleton<TaNaLeiProvider>(() => TaNaLeiProvider());
Hope that helps!
Finally, I found the problem, I had an actual timer running inside my DeviceProvider and this timer was not canceled.
Thanks for your support !
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 using the auto_route package in my flutter project and wrapped my App in MaterialApp.router.
Now I want to test a widget which calls AutoRouter.of(context).pop(); in one place.
My test looks like this:
testWidgets('my widget test',
(WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: MyWidget(),
),
);
});
Pumping the widget understandably throws the following error message:
AutoRouter operation requested with a context that does not include an AutoRouter. The context used to retrieve the Router must be that of a widget that is a descendant of an AutoRouter widget.
So I somehow need to provide an AutoRouter instance above my widgetin my test.
My widget also uses MediaQuery and I needed to wrap my tested widget inside MaterialApp to avoid an error which was thrown due to MediaQuery.of(context) lookup.
I guess I need to do something similar with AutoRouter but couldn't figure it out yet. Is there something like a MockAutoRouter?
Happy for any help.
Okay I got it working.
AutoRouter.of(context) looks up an instance of StackRouter which is provided through StackRouterScope. So I mocked StackRouter and provided it via StackRouterScope:
#GenerateMocks([StackRouter])
...
await tester.pumpWidget(
StackRouterScope(
controller: MockStackRouter(),
stateHash: 0,
child: MyWidget(),
),
);
Now AutoRouter.of(context).pop() does not throw anymore.
It should also be possible now to stub the MockStackRouter and verify if e.g. pop is called.
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
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
});
}
In production mode, is there a way to force a full restart of the application (I am not talking about a hot reload at development time!).
Practical use cases:
At initialization process the application detects that there is no network connection. The lack of network connectivity might have prevented a correct start up (e.g. loading of external resource such as JSON files...).
During the initial handshaking, new versions of some important resources need to be downloaded (kind of update).
In both use cases, I would like the application to proceed with a full restart, rather than having to build a complex logic at the ApplicationState level.
You could wrap your whole app into a statefulwidget. And when you want to restart you app, rebuild that statefulwidget with a child that possess a different Key.
This would make you loose the whole state of your app.
import 'package:flutter/material.dart';
void main() {
runApp(
RestartWidget(
child: MaterialApp(),
),
);
}
class RestartWidget extends StatefulWidget {
RestartWidget({this.child});
final Widget child;
static void restartApp(BuildContext context) {
context.findAncestorStateOfType<_RestartWidgetState>().restartApp();
}
#override
_RestartWidgetState createState() => _RestartWidgetState();
}
class _RestartWidgetState extends State<RestartWidget> {
Key key = UniqueKey();
void restartApp() {
setState(() {
key = UniqueKey();
});
}
#override
Widget build(BuildContext context) {
return KeyedSubtree(
key: key,
child: widget.child,
);
}
}
In this example you can reset your app from everywhere using RestartWidget.restartApp(context).
The flutter_phoenix package is based on Rémi Rousselet's answer, making it even simpler.
void main() {
runApp(
Phoenix(
child: App(),
),
);
}
Then when you need to restart the app, just call:
Phoenix.rebirth(context);
I developed the restart_app plugin to restart the whole app natively.
Update:
For anyone who get this exception:
MissingPluginException(No implementation found for method restartApp on channel restart)
Just stop and rebuild the app.
You can also use the runApp(new MyWidget) function to do something similar
This is what this function does:
Inflate the given widget and attach it to the screen.
The widget is given constraints during layout that force it to fill the entire screen. If you wish to align your widget to one side of the screen (e.g., the top), consider using the Align widget. If you wish to center your widget, you can also use the Center widget
Calling runApp again will detach the previous root widget from the screen and attach the given widget in its place. The new widget tree is compared against the previous widget tree and any differences are applied to the underlying render tree, similar to what happens when a StatefulWidget rebuilds after calling State.setState.
https://docs.flutter.io/flutter/widgets/runApp.html
So simple package: flutter_restart
dependencies:
flutter_restart: ^0.0.3
to use:
void _restartApp() async {
FlutterRestart.restartApp();
}
I just want to add Regarding I have Tried #Remi answer which works great on most of the cases to restart the app. The only problem with the answer is that some things if you are doing Navigation route extensively you probably go to a state which It gives you an error like,
The method 'restartApp' was called on null.
To resolve this error you have to know the Context and use Navigator.of(context).pop(); multiples times back. For me, the solution is that just go to the initial route. It will inject all the states from a new. Where you want to restart just add this Line.
Navigator.pushNamedAndRemoveUntil(context,'/',(_) => false);
If you want to only restart a specific widget then the Remi solution is awesome. Thanks for the solution Remi though. It help me understand states in flutter.
I have found Hossein's restart_app package also pretty useful for native restarts (not only on Flutter level).
To everyone having the MissingPluginException error, just reinstall the app again on the device, means that hot reload won't work. The app has native methods which need to compiled in the Android/iOS App.
I wanted to restart my app after logout.
so I used https://pub.dev/packages/flutter_phoenix (flutter phoenix).
It worked for me.
Install flutter_phoenix by running this command on your terminal inside your flutter app directory.
$ flutter pub add flutter_phoenix
Import it inside your "main.dart".
Wrap your root widget inside Phoenix.
runApp(
Phoenix(
child: MyApp()
));
Now you can call this wherever you want to restart your app :-
Phoenix.rebirth(context)
Note: flutter_phoenix does not restart the app on OS level, it only restarts the app on app level.
Thecnically this is not a restart but it will work for most of the scenarios:
// Remove any route in the stack
Navigator.of(context).popUntil((route) => false);
// Add the first route. Note MyApp() would be your first widget to the app.
Navigator.push(
context,
CupertinoPageRoute(builder: (context) => const MyApp()),
);
Follow the steps-
Go to your terminal and type in the following:
flutter pub add flutter_restart
This will update some dependencies in pubspec.yaml file.
Import the following package in whichever file you want to implement the restart code-
import 'package:flutter_restart/flutter_restart.dart';
Create a void function
void _restartApp() async {
await FlutterRestart.restartApp();
}
Write this wherever you want to start the app-
_restartApp();
I tried the above suggested methods and none of them worked and i was using getx.
so i ended up modified the accepted answer with a delay as a workaround and it works now.
class RestartAppWidget extends StatefulWidget {
RestartAppWidget({this.child});
final Widget child;
static void restartApp(BuildContext context) {
context.findAncestorStateOfType<_RestartAppWidgetState>().restartApp();
}
#override
_RestartAppWidgetState createState() => _RestartAppWidgetState();
}
class _RestartAppWidgetState extends State<RestartAppWidget> {
bool restarting = false;
void restartApp() async {
restarting = true; // restart variable is set to true
setState(() {});
Future.delayed(Duration(milliseconds: 300)).then((value) {
setState(() {
restarting = false; //restart variable is set to false
});
});
// setState(() {
// key = UniqueKey();
// });
}
#override
Widget build(BuildContext context) {
if (restarting) {
return SizedBox(); //an empty Sizedbox is displayed for 300 milliseconds you can add a loader if you want
}
return SizedBox(
child: widget.child,
);
}
}`
wrap the root widget with RestartAppWidget
runApp(RestartAppWidget(
child: MyApp(),
))
you can use this code to restart the app at flutter level
RestartAppWidget.restartApp(Get.context);