I have used a Future.delayed to show a FAB in 1 second using this code:
Future.delayed(const Duration(seconds: 1), () {
setState(() {
_showFab = true;
});
});
Now the most basic smoke test has stopped working:
void main() {
testWidgets('smoke test', (WidgetTester tester) async {
await tester.pumpWidget(MyApp());
expect(find.byType(MyHomePage), findsOneWidget);
});
}
This is the error message:
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following assertion was thrown running a test:
A Timer is still pending even after the widget tree was disposed.
'package:flutter_test/src/binding.dart': Failed assertion: line 933 pos 7:
'_currentFakeAsync.nonPeriodicTimerCount == 0' import 'dart:async';
Here is all the code used:
import 'package:flutter/material.dart';
import 'dart:async';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool _showFab = false;
#override
Widget build(BuildContext context) {
Future.delayed(const Duration(seconds: 1), () {
setState(() {
_showFab = true;
});
});
return Scaffold(
floatingActionButton: AnimatedOpacity(
opacity: _showFab ? 1.0 : 0.0,
duration: Duration(milliseconds: 1400),
child: FloatingActionButton(
onPressed: null,
child: Icon(Icons.add),
),
),
);
}
}
How can I change the unit test to make the test pass?
The test framework runs with FakeAsync by default, that has some limitations and might cause the error you get.
You can explicitly run with async proper like:
void main() {
testWidgets('smoke test', (WidgetTester tester) async {
await tester.runAsync(() async {
await tester.pumpWidget(MyApp());
expect(find.byType(MyHomePage), findsOneWidget);
});
});
}
You might still need suggestions made in other answers.
You need to give the tester enough time to process all the async calls you have scheduled, try change your smoke test to something like this:
void main() {
testWidgets('smoke test', (WidgetTester tester) async {
await tester.pumpWidget(MyApp());
// Since you wait 1 second to start the animation and another 1.4 to complete it
await tester.pump(Duration(seconds: 3));
expect(find.byType(MyHomePage), findsOneWidget);
});
}
You will also need to move the Future.delayed out of the build() method, because this is causing a cyclic behaviour, every time you call setState() the build() is called again, change your state like that:
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool _showFab = false;
#override
void initState() {
Future.delayed(const Duration(seconds: 1), () {
setState(() {
_showFab = true;
});
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: AnimatedOpacity(
opacity: _showFab ? 1.0 : 0.0,
duration: Duration(milliseconds: 1400),
child: FloatingActionButton(
onPressed: null,
child: Icon(Icons.add),
),
),
);
}
}
You can wrap your Widget with TickerMode.
As you can see below:
void main() {
testWidgets('smoke test', (WidgetTester tester) async {
await tester.pumpWidget(TickerMode(child: MyApp(), enabled: false));
await tester.pumpAndSettle();
expect(find.byType(MyHomePage), findsOneWidget);
});
}
Try calling pumpAndSettle after you've pumped the widget. Like this:
void main() {
testWidgets('smoke test', (WidgetTester tester) async {
await tester.pumpWidget(MyApp());
await tester.pumpAndSettle();
expect(find.byType(MyHomePage), findsOneWidget);
});
}
From the docs
This essentially waits for all animations to have completed.
I tried using pump and pumpAndSettle but it didn't work. But add Duration in pumpAndSettle (not work for pump) makes it work again
await tester.pumpAndSettle(Duration(seconds: 1));
Related
Not sure where to add the: debugShowCheckedModeBanner: false, I'm trying to build for ios with xcode.
Here is the current main.dart:
Not sure what I need to change in order to get this to build. I know it has something to do with MaterialApp but I can't figure out the placement.
`import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:kittscoaching/src/app.dart';
import 'package:kittscoaching/src/resources/theme.dart';
void main() {
runApp(App());
}
class App extends StatefulWidget {
_AppState createState() => _AppState();
}
class _AppState extends State<App> {
// Set default `_initialized` and `_error` state to false
bool _initialized = false;
bool _error = false;
// Define an async function to initialize FlutterFire
void initializeFlutterFire() async {
try {
// Wait for Firebase to initialize and set `_initialized` state to true
await Firebase.initializeApp();
setState(() {
_initialized = true;
});
} catch(e) {
// Set `_error` state to true if Firebase initialization fails
setState(() {
_error = true;
});
}
}
#override
void initState() {
initializeFlutterFire();
super.initState();
}
#override
Widget build(BuildContext context) {
// Show error message if initialization failed
if(_error) {
//TODO:
//return SomethingWentWrong();
}
// Show a loader until FlutterFire is initialized
if (!_initialized) {
// TODO:
return Container(
decoration: BoxDecoration(color: KittsTheme.primary),
child: Center(
child: Directionality(
textDirection: TextDirection.ltr,
child: Text('Loading...')
)
)
);
}
return MyApp();
}
}`
There isn't a MaterialApp in the code that you're showing. Find the MaterialApp, if there is one, and apply the property there.
Or run your app in release mode: flutter run --release
Or open dev tools and click off the debug banner from there.
Hope this will help you
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:kittscoaching/src/app.dart';
import 'package:kittscoaching/src/resources/theme.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: App(),
debugShowCheckedModeBanner: false,
);
}
}
class App extends StatefulWidget {
_AppState createState() => _AppState();
}
class _AppState extends State<App> {
// Set default `_initialized` and `_error` state to false
bool _initialized = false;
bool _error = false;
// Define an async function to initialize FlutterFire
void initializeFlutterFire() async {
try {
await Firebase.initializeApp();
setState(() {
_initialized = true;
});
} catch(e) {
// Set `_error` state to true if Firebase initialization fails
setState(() {
_error = true;
});
}
}
#override
void initState() {
initializeFlutterFire();
super.initState();
}
#override
Widget build(BuildContext context) {
// Show error message if initialization failed
if(_error) {
//TODO:
//return SomethingWentWrong();
}
// Show a loader until FlutterFire is initialized
if (!_initialized) {
// TODO:
return Container(
decoration: BoxDecoration(color: KittsTheme.primary),
child: Center(
child: Directionality(
textDirection: TextDirection.ltr,
child: Text('Loading...')
)
)
);
}
return MyApp();
}
}
Simply you have to specify it in MaterialApp widget. Find the sample code below.
return MaterialApp(
debugShowCheckedModeBanner: false,
home: SafeArea(
),
),
I want my flutter app run only if internet connection is available.
If the internet is not present show a dialog(internet is not present)
I'm using conectivity plugin but still not satisfied.
Here is my main function
Future main() async {
try {
final result = await InternetAddress.lookup('google.com');
if (result.isNotEmpty && result[0].rawAddress.isNotEmpty) {
print('connected');
}
} on SocketException catch (_) {
print('not connected');
}
runApp(MyApp());}
You can't use dialog in main() method directly because there is no valid context available yet.
Here is the basic code of what you are looking for.
void main() => runApp(MaterialApp(home: MyApp()));
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
void initState() {
super.initState();
Timer.run(() {
try {
InternetAddress.lookup('google.com').then((result) {
if (result.isNotEmpty && result[0].rawAddress.isNotEmpty) {
print('connected');
} else {
_showDialog(); // show dialog
}
}).catchError((error) {
_showDialog(); // show dialog
});
} on SocketException catch (_) {
_showDialog();
print('not connected'); // show dialog
}
});
}
void _showDialog() {
// dialog implementation
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text("Internet needed!"),
content: Text("You may want to exit the app here"),
actions: <Widget>[FlatButton(child: Text("EXIT"), onPressed: () {})],
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Internet")),
body: Center(
child: Text("Working ..."),
),
);
}
}
I have two screens in my app.
Screen A runs a computationally expensive operation while opened, and properly disposes by cancelling animations/subscriptions to the database when dispose() is called to prevent memory leaks.
From Screen A, you can open another screen (Screen B).
When I use Navigator.pushNamed, Screen A remains in memory, and dispose() is not called, even though Screen B is now shown.
Is there a way to force disposal of Screen A when it is not in view?
Example code where first route is never disposed:
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
title: 'Navigation Basics',
home: FirstRoute(),
));
}
class FirstRoute extends StatefulWidget {
#override
_FirstRouteState createState() => _FirstRouteState();
}
class _FirstRouteState extends State<FirstRoute> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('First Route'),
),
body: RaisedButton(
child: Text('Open route'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
},
),
);
}
#override
void dispose() {
// Never called
print("Disposing first route");
super.dispose();
}
}
class SecondRoute extends StatefulWidget {
#override
_SecondRouteState createState() => _SecondRouteState();
}
class _SecondRouteState extends State<SecondRoute> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second Route"),
),
body: RaisedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Go back!'),
),
);
}
#override
void dispose() {
print("Disposing second route");
super.dispose();
}
}
I know it's a bit late but I think you should override the deactivate method. Since we are changing the page we are not actually destroying it, that's why the dispose isn't being called.
If you'd like more information this page lists the lifecycle of the stateful widgets.
From the link:
'deactivate()' is called when State is removed from the tree, but it might be
reinserted before the current frame change is finished. This method exists basically
because State objects can be moved from one point in a tree to another.
call Navigator.pushReplacement when routing between first and second screen.
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
title: 'Navigation Basics',
home: FirstRoute(),
));
}
class FirstRoute extends StatefulWidget {
#override
_FirstRouteState createState() => _FirstRouteState();
}
class _FirstRouteState extends State<FirstRoute> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('First Route'),
),
body: RaisedButton(
child: Text('Open route'),
onPressed: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
},
),
);
}
#override
void dispose() {
// Never called
print("Disposing first route");
super.dispose();
}
}
class SecondRoute extends StatefulWidget {
#override
_SecondRouteState createState() => _SecondRouteState();
}
class _SecondRouteState extends State<SecondRoute> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second Route"),
),
body: RaisedButton(
onPressed: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => FirstRoute()),
);
},
child: Text('Go back!'),
),
);
}
#override
void dispose() {
print("Disposing second route");
super.dispose();
}
}
Try this
In flutter new versions deactivate won't be called when you push a new widget on top of another widget. Also there is an open issue related to this topic on flutter github: https://github.com/flutter/flutter/issues/50147
The best way to handle this issue is to add RouteObserver<PageRoute> to your material app and override didPushNext and didPushNext functions.
There is a very helpful medium article related to this topic which you can find here: https://medium.com/koahealth/how-to-track-screen-transitions-in-flutter-with-routeobserver-733984a90dea
As Article said create your own RouteAwareWidget, you can add these two call backs to the fields of the widget:
didPopNext
didPushNext
class RouteAwareWidget extends StatefulWidget {
final Widget child;
final VoidCallback? didPopNext;
final VoidCallback? didPushNext;
const RouteAwareWidget({
Key? key,
required this.child,
this.didPopNext,
this.didPushNext,
}) : super(key: key);
#override
State<RouteAwareWidget> createState() => RouteAwareWidgetState();
}
class RouteAwareWidgetState extends State<RouteAwareWidget> with RouteAware {
#override
void didChangeDependencies() {
super.didChangeDependencies();
routeObserver.subscribe(this, ModalRoute.of(context) as PageRoute);
}
#override
void dispose() {
routeObserver.unsubscribe(this);
super.dispose();
}
#override
void didPush() {}
#override
void didPopNext() {
dPrint('didPopNext');
widget.didPopNext == null ? null : widget.didPopNext!();
super.didPopNext();
}
#override
void didPushNext() {
dPrint('didPushNext');
widget.didPushNext == null ? null : widget.didPushNext!();
super.didPushNext();
}
#override
Widget build(BuildContext context) => widget.child;
}
Create a global RouteObserver<PageRoute> and add it to your material app:
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
MaterialApp(
navigatorObservers: [routeObserver],
debugShowCheckedModeBanner: false,
routes: _routes,
)
then in your routs you should wrap your routes with RouteAwareWidget and add custom function you want:
final _routes = {
HomePage.routeName: (context) => RouteAwareWidget(
child: const HomePage(),
didPushNext: () => sl<CameraBloc>().add(Dispose()),
didPopNext: () => sl<CameraBloc>().add(Init()),
),
MyQuestions.routeName: (context) => const RouteAwareWidget(
child: MyQuestions(),
),
};
didPushNext will be called when you push a widget on top of HomePage and didPopNext will be called when you pop the last widget above HomePage.
With Navigator.pushReplacement(), if using MaterialPageRoute, then setting
maintainState:false
will ensure that dispose() is called.
A light weight solution for a single route case is using a callback function triggered from the SecondRoute.
Trigger the callback from the WidgetsBinding.instance.addPostFrameCallback() within the initState() on the SecondRoute
More information on WidgetsBinding and when they run can be found here: Flutter: SchedulerBinding vs WidgetsBinding.
WidgetsBinding & SchedulerBinding will be printed only once as we called it initState(), but it will be called when build method finished it’s rendering.
import 'package:flutter/material.dart';
class FirstRoute extends StatefulWidget {
const FirstRoute({super.key});
#override
State<FirstRoute> createState() => _FirstRouteState();
}
class _FirstRouteState extends State<FirstRoute> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Screen A')),
body: Center(
child: TextButton(
child: const Text('Go to Screen B'),
onPressed: () async {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => SecondRoute(_callbackFn),
),
);
_secondRouteDone();
},
),
),
);
}
_callbackFn() {
print("Widget B Loaded, Free up memory, dispose things, etc.");
}
_secondRouteDone() {
print("SecondRoute Popped, Reinstate controllers, etc.");
}
}
class SecondRoute extends StatefulWidget {
final Function() notifyIsMountedFn;
const SecondRoute(this.notifyIsMountedFn, {super.key});
#override
State<SecondRoute> createState() => _SecondRouteState();
}
class _SecondRouteState extends State<SecondRoute> {
#override
void initState() {
super.initState();
// Notify FirstRoute after paint
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.notifyIsMountedFn();
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Screen B')),
);
}
}
Not only to call 'deactivate()' but also to use 'Navigator.pushReplacement()' for page moving is necessary. Not working if you are using 'Navigator.push()'.
Let's say, I have a test for a screen in Flutter using WidgetTester. There is a button, which executes a navigation via Navigator. I would like to test behavior of that button.
Widget/Screen
class MyScreen extends StatefulWidget {
MyScreen({Key key}) : super(key: key);
#override
_MyScreenState createState() => _MyScreenScreenState();
}
class _MyScreenState extends State<MyScreen> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton(
onPressed: () {
Navigator.of(context).pushNamed("/nextscreen");
},
child: Text(Strings.traktTvUrl)
)
)
);
}
}
Test
void main() {
testWidgets('Button is present and triggers navigation after tapped',
(WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(home: MyScreen()));
expect(find.byType(RaisedButton), findsOneWidget);
await tester.tap(find.byType(RaisedButton));
//how to test navigator?
});
}
I there a proper way how to check, that Navigator was called? Or is there a way to mock and replace navigator?
Pleas note, that code above will actually fail with an exception, because there is no named route '/nextscreen' declared in application. That's simple to solve and you don't need to point it out.
My main concern is how to correctly approach this test scenario in Flutter.
While what Danny said is correct and works, you can also create a mocked NavigatorObserver to avoid any extra boilerplate:
import 'package:mockito/mockito.dart';
class MockNavigatorObserver extends Mock implements NavigatorObserver {}
That would translate to your test case as follows:
void main() {
testWidgets('Button is present and triggers navigation after tapped',
(WidgetTester tester) async {
final mockObserver = MockNavigatorObserver();
await tester.pumpWidget(
MaterialApp(
home: MyScreen(),
navigatorObservers: [mockObserver],
),
);
expect(find.byType(RaisedButton), findsOneWidget);
await tester.tap(find.byType(RaisedButton));
await tester.pumpAndSettle();
/// Verify that a push event happened
verify(mockObserver.didPush(any, any));
/// You'd also want to be sure that your page is now
/// present in the screen.
expect(find.byType(DetailsPage), findsOneWidget);
});
}
I wrote an in-depth article about this on my blog, which you can find here.
In the navigator tests in the flutter repo they use the NavigatorObserver class to observe navigations:
class TestObserver extends NavigatorObserver {
OnObservation onPushed;
OnObservation onPopped;
OnObservation onRemoved;
OnObservation onReplaced;
#override
void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
if (onPushed != null) {
onPushed(route, previousRoute);
}
}
#override
void didPop(Route<dynamic> route, Route<dynamic> previousRoute) {
if (onPopped != null) {
onPopped(route, previousRoute);
}
}
#override
void didRemove(Route<dynamic> route, Route<dynamic> previousRoute) {
if (onRemoved != null)
onRemoved(route, previousRoute);
}
#override
void didReplace({ Route<dynamic> oldRoute, Route<dynamic> newRoute }) {
if (onReplaced != null)
onReplaced(newRoute, oldRoute);
}
}
This looks like it should do what you want, however it may only work form the top level (MaterialApp), I'm not sure if you can provide it to just a widget.
Inspired by the other posts, this is my 2022 null-safe Mockito-based approach. Imagine I have this helper method I want to unit test:
navigateToNumber(int number, BuildContext context) {
Navigator.of(context).pushNamed(
number.isEven ? '/even' : '/odd'
);
}
It can be tested this way:
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
import 'package:my_app/number_route_helper.dart';
import 'number_route_helper_test.mocks.dart';
#GenerateMocks([],
customMocks: [
MockSpec<NavigatorObserver>(returnNullOnMissingStub: true)
])
void main() {
group('NumberRouteHelper', () {
testWidgets('navigateToNumber', (WidgetTester tester) async {
final mockObserver = MockNavigatorObserver();
// "Fake" routes used to verify the right route was pushed
final evenRoute = MaterialPageRoute(builder: (_) => Container());
final oddRoute = MaterialPageRoute(builder: (_) => Container());
await tester.pumpWidget(
MaterialApp(
home: Container(),
navigatorObservers: [mockObserver],
onGenerateRoute: (RouteSettings settings) {
switch (settings.name) {
case '/even':
return evenRoute;
case '/odd':
return oddRoute;
}
}
),
);
final BuildContext context = tester.element(find.byType(Container));
/// Verify that a push to evenRoute happened
navigateToNumber(2, context);
await tester.pumpAndSettle();
verify(mockObserver.didPush(evenRoute, any));
/// Verify that a push to oddRoute happened
navigateToNumber(3, context);
await tester.pumpAndSettle();
verify(mockObserver.didPush(oddRoute, any));
});
});
}
Just remember you need to have Mockito installed, as described here: https://pub.dev/packages/mockito
This is modified version of the other answer to show how to do it with mocktail instead of mockito:
import 'package:mocktail/mocktail.dart';
class MockNavigatorObserver extends Mock implements NavigatorObserver {}
class FakeRoute extends Fake implements Route {}
void main() {
setUpAll(() {
registerFallbackValue(FakeRoute());
});
testWidgets('Button is present and triggers navigation after tapped',
(WidgetTester tester) async {
final mockObserver = MockNavigatorObserver();
await tester.pumpWidget(
MaterialApp(
home: MyScreen(),
navigatorObservers: [mockObserver],
),
);
expect(find.byType(RaisedButton), findsOneWidget);
await tester.tap(find.byType(RaisedButton));
await tester.pumpAndSettle();
verify(mockObserver.didPush(any(), any()));
expect(find.byType(DetailsPage), findsOneWidget);
});
}
Following solution is, let's say, a general approach and it's not specific to Flutter.
Navigation could be abstracted away from a screen or a widget. Test can mock and inject this abstraction. This approach should be sufficient for testing such behavior.
There are several ways how to achieve that. I will show one of those, for purpose of this response. Perhaps it's possible to simplify it a bit or to make it more "Darty".
Abstraction for navigation
class AppNavigatorFactory {
AppNavigator get(BuildContext context) =>
AppNavigator._forNavigator(Navigator.of(context));
}
class TestAppNavigatorFactory extends AppNavigatorFactory {
final AppNavigator mockAppNavigator;
TestAppNavigatorFactory(this.mockAppNavigator);
#override
AppNavigator get(BuildContext context) => mockAppNavigator;
}
class AppNavigator {
NavigatorState _flutterNavigator;
AppNavigator._forNavigator(this._flutterNavigator);
void showNextscreen() {
_flutterNavigator.pushNamed('/nextscreen');
}
}
Injection into a widget
class MyScreen extends StatefulWidget {
final _appNavigatorFactory;
MyScreen(this._appNavigatorFactory, {Key key}) : super(key: key);
#override
_MyScreenState createState() => _MyScreenState(_appNavigatorFactory);
}
class _MyScreenState extends State<MyScreen> {
final _appNavigatorFactory;
_MyScreenState(this._appNavigatorFactory);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton(
onPressed: () {
_appNavigatorFactory.get(context).showNextscreen();
},
child: Text(Strings.traktTvUrl)
)
)
);
}
}
Example of a test (Uses Mockito for Dart)
class MockAppNavigator extends Mock implements AppNavigator {}
void main() {
final appNavigator = MockAppNavigator();
setUp(() {
reset(appNavigator);
});
testWidgets('Button is present and triggers navigation after tapped',
(WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(home: MyScreen(TestAppNavigatorFactory())));
expect(find.byType(RaisedButton), findsOneWidget);
await tester.tap(find.byType(RaisedButton));
verify(appNavigator.showNextscreen());
});
}
I would like to be able to run functions once a Widget has finished building/loading but I am unsure how.
My current use case is to check if a user is authenticated and if not, redirect to a login view. I do not want to check before and push either the login view or the main view, it needs to happen after the main view has loaded.
Is there anything I can use to do this?
You could use
https://github.com/slightfoot/flutter_after_layout
which executes a function only one time after the layout is completed.
Or just look at its implementation and add it to your code :-)
Which is basically
void initState() {
super.initState();
WidgetsBinding.instance
.addPostFrameCallback((_) => yourFunction(context));
}
UPDATE: Flutter v1.8.4
Both mentioned codes are working now:
Working:
WidgetsBinding.instance
.addPostFrameCallback((_) => yourFunction(context));
Working
import 'package:flutter/scheduler.dart';
SchedulerBinding.instance.addPostFrameCallback((_) => yourFunction(context));
Best ways of doing this,
1. WidgetsBinding
WidgetsBinding.instance.addPostFrameCallback((_) {
print("WidgetsBinding");
});
2. SchedulerBinding
SchedulerBinding.instance.addPostFrameCallback((_) {
print("SchedulerBinding");
});
It can be called inside initState, both will be called only once after Build widgets done with rendering.
#override
void initState() {
// TODO: implement initState
super.initState();
print("initState");
WidgetsBinding.instance.addPostFrameCallback((_) {
print("WidgetsBinding");
});
SchedulerBinding.instance.addPostFrameCallback((_) {
print("SchedulerBinding");
});
}
both above codes will work the same as both use the similar binding framework.
For the difference find the below link.
https://medium.com/flutterworld/flutter-schedulerbinding-vs-widgetsbinding-149c71cb607f
There are 3 possible ways:
1) WidgetsBinding.instance.addPostFrameCallback((_) => yourFunc(context));
2) Future.delayed(Duration.zero, () => yourFunc(context));
3) Timer.run(() => yourFunc(context));
As for context, I needed it for use in Scaffold.of(context) after all my widgets were rendered.
But in my humble opinion, the best way to do it is this:
void main() async {
WidgetsFlutterBinding.ensureInitialized(); //all widgets are rendered here
await yourFunc();
runApp( MyApp() );
}
Flutter 1.2 - dart 2.2
According with the official guidelines and sources if you want to be certain that also the last frame of your layout was drawned you can write for example:
import 'package:flutter/scheduler.dart';
void initState() {
super.initState();
if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
SchedulerBinding.instance.addPostFrameCallback((_) => yourFunction(context));
}
}
If you are looking for ReactNative's componentDidMount equivalent, Flutter has it. It's not that simple but it's working just the same way. In Flutter, Widgets do not handle their events directly. Instead they use their State object to do that.
class MyWidget extends StatefulWidget{
#override
State<StatefulWidget> createState() => MyState(this);
Widget build(BuildContext context){...} //build layout here
void onLoad(BuildContext context){...} //callback when layout build done
}
class MyState extends State<MyWidget>{
MyWidget widget;
MyState(this.widget);
#override
Widget build(BuildContext context) => widget.build(context);
#override
void initState() => widget.onLoad(context);
}
State.initState immediately will be called once upon screen has finishes rendering the layout. And will never again be called even on hot reload if you're in debug mode, until explicitly reaches time to do so.
In flutter version 1.14.6, Dart version 28.
Below is what worked for me, You simply just need to bundle everything you want to happen after the build method into a separate method or function.
#override
void initState() {
super.initState();
print('hello girl');
WidgetsBinding.instance
.addPostFrameCallback((_) => afterLayoutWidgetBuild());
}
The PostFrameCallback fires before the screen has fully painted. Therefore Devv's answer above was helpful with the added delay to allow the screen to paint.
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
Future.delayed(Duration(seconds: 3), () => yourFunction());
});
}
Try SchedulerBinding,
SchedulerBinding.instance
.addPostFrameCallback((_) => setState(() {
isDataFetched = true;
}));
if you having issue with new SDK and old answer you can try my solution.I have tested it on v3.0.4
WidgetsBinding.instance.endOfFrame.then(
(_) {
if (mounted) {
// do some suff
// you can get width height of specific widget based on GlobalKey
};
},
);
If you don't want to use WidgetsBinding or SchedulerBinding:
Use Future or Timer (easy-peasy)
Future<void> _runsAfterBuild() async {
// This code runs after build ...
}
#override
Widget build(BuildContext context) {
Future(_runsAfterBuild); // <-- Use Future or Timer
return Container();
}
Await a dummy Future
Future<void> _runsAfterBuild() async {
await Future((){}); // <-- Dummy await
// This code runs after build ...
}
#override
Widget build(BuildContext context) {
_runsAfterBuild();
return Container();
}
my english is poor
forgive me
import 'package:flutter/material.dart';
class TestBox extends StatefulWidget {
final Color color;
final Duration delay;
const TestBox({
Key? key,
this.color = Colors.red,
this.delay = const Duration(seconds: 5),
}) : super(key: key);
#override
_TestBoxState createState() => _TestBoxState();
}
class _TestBoxState extends State<TestBox> {
String? label;
#override
void initState() {
initialMembers();
super.initState();
}
void initialMembers() async {
label = await fetchLabel();
if (mounted) setState(() {});
/// don't worry
/// if `(!mounted)`, means wen `build` calld
/// the label already has the newest value
}
Future<String> fetchLabel() async {
await Future.delayed(widget.delay);
print('fetchLabel call');
return 'from fetchLabel()';
}
#override
Widget build(BuildContext context) {
return AnimatedContainer(
margin: EdgeInsets.symmetric(vertical: 12),
duration: Duration(milliseconds: 500),
width: 220,
height: 120,
color: label == null ? Colors.white : widget.color,
child: Center(
child: Text(label ?? 'fetching...'),
),
);
}
}
Column(
children: [
TestBox(
delay: Duration(seconds: 1),
color: Colors.green,
),
TestBox(
delay: Duration(seconds: 3),
color: Colors.yellow,
),
TestBox(
delay: Duration(seconds: 5),
color: Colors.red,
),
],
),
I have a Stateful widget where I use html_editor_enhanced plugin widget. This is the only way to set initial message in it.
class _SendChatMessageState extends State<SendChatMessage> {
final _htmlController = HtmlEditorController();
#override
void initState() {
super.initState();
Future.delayed(const Duration(seconds: 3), () {
_htmlController.setText(widget.chatMessage.message ?? '');
});
}
I tried addPostFrameCallback but it didn't work because a JavaScript generates exception "HTML editor is still loading, please wait before evaluating this JS ..."
another solution that worked pretty well for me is wrapping the function you want to call by Future.delayed() as showen below:
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
Future.delayed(Duration(seconds: 3), () => yourFunction());
});
}