I would like to show a widget on the screen as quickly as possible when my app launches. After the widget has been shown to the user, I'd like to do some app initialization (e.g. set up a database) before leaving the launch screen by pushing another screen. I cannot figure out where to put my app initialization code. Something analogous to viewDidAppear on iOS.
This is what I tried based on initState.
class Launch extends StatefulWidget {
#override
_LaunchState createState() {
return _LaunchState();
}
}
class _LaunchState extends State<Launch> {
#override
Widget build(final BuildContext context) {
print('LaunchState build start');
final Widget w = Center(
child: Text('Launching...'),
);
print('LaunchState build end');
return w;
}
#override
void initState() {
print('LaunchState initState start');
super.initState();
print('LaunchState initState middle');
_appInitialization();
print('LaunchState initState end');
}
void _appInitialization() {
print('LaunchState _appInitialization');
}
}
The output was
flutter: LaunchState initState start
flutter: LaunchState initState middle
flutter: LaunchState _appInitialization
flutter: LaunchState initState end
flutter: LaunchState build start
flutter: LaunchState build end
The output I think I want to see is
flutter: LaunchState build start
flutter: LaunchState build end
flutter: LaunchState initState start
flutter: LaunchState initState middle
flutter: LaunchState _appInitialization
flutter: LaunchState initState end
Maybe I'm going about this in a very un-flutter way. If there is a completely different approach, I'm all ears.
SOLUTION
Thanks to Mangaldeep Pannu's answer, it appears I'm missing async.
class Launch extends StatefulWidget {
#override
_LaunchState createState() {
return _LaunchState();
}
}
class _LaunchState extends State<Launch> {
#override
Widget build(final BuildContext context) {
print('LaunchState build start');
final Widget w = Center(
child: Text('Launching...'),
);
print('LaunchState build end');
return w;
}
#override
void initState() {
print('LaunchState initState start');
super.initState();
print('LaunchState initState middle');
_appInitialization(); // no await
print('LaunchState initState end');
}
void _appInitialization() async {
print('LaunchState _appInitialization begin');
// simulate some time consuming initialization task
await Future.delayed(Duration(seconds: 5));
print('LaunchState _appInitialization middle');
Navigator.push(...);
print('LaunchState _appInitialization end');
}
}
The results are
flutter: LaunchState initState start
flutter: LaunchState initState middle
flutter: LaunchState _appInitialization begin
flutter: LaunchState initState end
flutter: LaunchState build start
flutter: LaunchState build end
[5 second pause]
flutter: LaunchState _appInitialization middle
flutter: LaunchState _appInitialization end
In your first widget that opens when the app starts
#override
void initState() {
onStart();
}
void onStart() async {
await loadData(); //load your data here
Navigator.push(); //push to next screen
}
onStart will load the data asynchronously.
And when the data gets loaded push to next screen.
Well we can handle this with BLoC.
create a app_bloc.dart file and have AppBloc class as shown bellow in it:
import 'dart:async';
final appBloc = AppBloc();
enum AppEvent{
onStart, onAppInitialized, onStop
}
class AppBloc {
final _appEventController = StreamController<AppEvent>.broadcast();
Stream<AppEvent> get appEventsStream => _appEventController.stream;
dispatch(AppEvent event) {
switch(event) {
case AppEvent.onStart:
_initializeApp();
_sinkEvent(AppEvent.onStart);
break;
case AppEvent.onStop:
_dispose();
_sinkEvent(AppEvent.onStop);
break;
case AppEvent.onAppInitialized:
_sinkEvent(AppEvent.onAppInitialized);
break;
}
}
void _sinkEvent(AppEvent appEvent) => _appEventController.sink.add(appEvent);
_dispose() {
_appEventController.close();
}
void _initializeApp() async {
await DBProvider.db.initDB();
dispatch(AppEvent.onAppInitialized); // will execute when all initializations are complete,
}
}
create a database.dart file and have your DBProvider class in it:
class DBProvider {
static final DBProvider _instance = new DBProvider._internal();
static final db = DBProvider();
factory DBProvider() {
return _instance;
}
DBProvider._internal();
initDB() async {
// todo initialize your database here.
}
}
You should have your main file something like this:
import 'package:flutter/material.dart';
import 'package:st_overflow/app_bloc.dart';
void main() => runApp(App());
class App extends StatefulWidget {
App() {
appBloc.dispatch(AppEvent.onStart);
}
#override
_AppState createState() => _AppState();
}
class _AppState extends State<App> {
#override
Widget build(BuildContext context) {
return Launch();
}
#override
void dispose() {
appBloc.dispatch(AppEvent.onStop);
super.dispose();
}
}
and in your Launch Widget consume the onAppInitialize event using StreamBuilder like this:
StreamBuilder(
stream: appBloc.appEventsStream,
builder: (context, snapshot){
switch (snapshot.data) {
case AppEvent.onAppInitialized:
// remove your Launch widget and show Screen 1 of your app (may be dashboard or something).
break;
}
},
);
Related
I am trying to use the bloc pattern to manage data from an API and show them in my widget. I am able to fetch data from API and process it and show it, but I am using a bottom navigation bar and when I change tab and go to my previous tab, it returns this error:
Unhandled Exception: Bad state: Cannot add new events after calling
close.
I know it is because I am closing the stream and then trying to add to it, but I do not know how to fix it because not disposing the publishsubject will result in memory leak.
here is my Ui code:
class CategoryPage extends StatefulWidget {
#override
_CategoryPageState createState() => _CategoryPageState();
}
class _CategoryPageState extends State<CategoryPage> {
#override
void initState() {
serviceBloc.getAllServices();
super.initState();
}
#override
void dispose() {
serviceBloc.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: serviceBloc.allServices,
builder: (context, AsyncSnapshot<ServiceModel> snapshot) {
if (snapshot.hasData) {
return _homeBody(context, snapshot);
}
if (snapshot.hasError) {
return Center(
child: Text('Failed to load data'),
);
}
return CircularProgressIndicator();
},
);
}
}
_homeBody(BuildContext context, AsyncSnapshot<ServiceModel> snapshot) {
return Stack(
Padding(
padding: EdgeInsets.only(top: screenAwareSize(400, context)),
child: _buildCategories(context, snapshot))
],
);
}
_buildCategories(BuildContext context, AsyncSnapshot<ServiceModel> snapshot) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 20),
child: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, crossAxisSpacing: 3.0),
itemCount: snapshot.data.result.length,
itemBuilder: (BuildContext context, int index) {
return InkWell(
child: CategoryWidget(
title: snapshot.data.result[index].name,
icon: Icons.phone_iphone,
),
onTap: () {},
);
},
),
);
}
here is my bloc code:
class ServiceBloc extends MainBloc {
final _repo = new Repo();
final PublishSubject<ServiceModel> _serviceController =
new PublishSubject<ServiceModel>();
Observable<ServiceModel> get allServices => _serviceController.stream;
getAllServices() async {
appIsLoading();
ServiceModel movieItem = await _repo.getAllServices();
_serviceController.sink.add(movieItem);
appIsNotLoading();
}
void dispose() {
_serviceController.close();
}
}
ServiceBloc serviceBloc = new ServiceBloc();
I did not include the repo and API code because it is not in the subject of this error.
Use StreamController.isClosed to check if the controller is closed or not, if not closed add data to it.
if (!_controller.isClosed)
_controller.sink.add(...); // safe to add data as _controller isn't closed yet
From Docs:
Whether the stream controller is closed for adding more events.
The controller becomes closed by calling the close method. New events cannot be added, by calling add or addError, to a closed controller.
If the controller is closed, the "done" event might not have been delivered yet, but it has been scheduled, and it is too late to add more events.
If the error is actually caused by the code you posted, I'd just add a check to ensure no new events are added after dispose() was called.
class ServiceBloc extends MainBloc {
final _repo = new Repo();
final PublishSubject<ServiceModel> _serviceController =
new PublishSubject<ServiceModel>();
Observable<ServiceModel> get allServices => _serviceController.stream;
getAllServices() async {
// do nothing if already disposed
if(_isDisposed) {
return;
}
appIsLoading();
ServiceModel movieItem = await _repo.getAllServices();
_serviceController.sink.add(movieItem);
appIsNotLoading();
}
bool _isDisposed = false;
void dispose() {
_serviceController.close();
_isDisposed = true;
}
}
ServiceBloc serviceBloc = new ServiceBloc();
I run into same error and noticed that if you check isClosed, the screen is not updated. In your code you have to remove the last line from Bloc file:
ServiceBloc serviceBloc = new ServiceBloc();
and put this line in CategoryPage just before the initState(). This way your widget is creating and disposing the bloc. Before, the widget only disposes the bloc but it is never re-created when the widget is re-created.
besides the provided solution I think you should also drain the stream allServices used in your ServiceBloc with:
#override
void dispose() {
...
allServices?.drain();
}
#cwhisperer is absolutely right. Initialize and dispose your block inside widget just like bellow.
final ServiceBloc serviceBloc = new ServiceBloc();
#override
void initState() {
serviceBloc.getAllServices();
super.initState();
}
#override
void dispose() {
serviceBloc.dispose();
super.dispose();
}
and delete ServiceBloc serviceBloc = new ServiceBloc(); from your class ServiceBloc
You should not worry about memory leak while using flutter_bloc as
When using bloc you do not need to close the bloc manually, if you have used a bloc provider to inject the bloc. Bloc Providers handle that for you out of the box as mentioned in the flutter_bloc docs.
BlocProvider is responsible for creating the bloc, it will automatically handle closing the bloc
You can test this in your application. Try printing on the close() override of bloc.
If the Screen at which the bloc was provided is removed from navigation stack then the close() method for that given bloc is called out of the box.
ServiceBloc serviceBloc = new ServiceBloc();
// remove this code
// don't init class in the same page that will cause of bad state.
I also faced this issue in production, and I realized that we should either dispose BehaviorSubject (or any other StreamController) when the Widget is disposed or Check to see if Stream is closed before adding new value.
Here is a nice extension to do all the job:
extension BehaviorSubjectExtensions <T> on BehaviorSubject<T> {
set safeValue(T newValue) => isClosed == false ? add(newValue) : () {};
}
You can use it like so:
class MyBloc {
final _data = BehaviorSubject<String>();
void fetchData() {
// get your data from wherever it is located
_data.safeValue = 'Safe to add data';
}
void dispose() {
_data.close();
}
}
How to dispose in Widget:
class CategoryPage extends StatefulWidget {
#override
_CategoryPageState createState() => _CategoryPageState();
}
class _CategoryPageState extends State<CategoryPage> {
late MyBloc bloc;
#override
void initState() {
bloc = MyBloc();
bloc.fetchData();
super.initState();
}
#override
void dispose() {
bloc.dispose();
super.dispose();
}
// Other part of your Widget
}
even better, if you aren't sure you won't reuse the stream after disposing:
call the drain() function on the stream before closing the stream.
dispose() async{
await _coinDataFetcher.drain();
_coinDataFetcher.close();
_isdisposed = true;
}
Check if the bloc/cubit is closed by isClosed variable. Wrap this if conditions to those states which are throwing exception.
Example code
class LandingCubit extends Cubit<LandingState> {
LandingCubit(this.repository) : super(LandingInitial());
final CoreRepository repository;
// Fetches image urls that needs to shown in landing page
void getLandingImages() async {
emit(LandingImagesLoading());
try {
List<File> landingImages = await repository.landingImages();
if (!isClosed) {
emit(LandingImagesSuccess(landingImages));
}
} catch (e) {
if (!isClosed) {
emit(LandingImagesFetchError(e.toString()));
}
}
}
}
I want to show a confirmation alert dialog right before opening the app, can someone please tell me how can I achieve that in flutter?
The showDialog() method needs a context, hence I should put it somewhere with buildContext, I assume in app's build method, but how can I trigger the dialog before the actual layout will be built on screen?
In your initState you can add your callback which will show your dialog with WidgetsBinding.instance.addPostFrameCallback which will be displayed immediately after layout. You can update your layout state according to your dialog result.
class HomePageState extends State<HomePage> {
#override
void initState() {
super.initState();
WidgetsBinding.instance
.addPostFrameCallback((_) => showDialog(...));
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('HomePage'),
),
body: Container(),
);
}
Code below works, I guess this is the answer
#override
Widget build(BuildContext context) {
Future.delayed(Duration.zero, () => showAlert(context));
return new WhateverLayoutYouWantToBeDisplayed();
}
void showAlert(BuildContext context) {
showDialog(
child: new WhateverCustomDialogYouHave(),
context: context);
}
Best ways of doing this,
1. WidgetsBinding
WidgetsBinding.instance.addPostFrameCallback((_) {
showDialog();
});
2. SchedulerBinding
SchedulerBinding.instance.addPostFrameCallback((_) {
showDialog();
});
WidgetsBinding & SchedulerBinding will be called only once as we called it in initState(), but remember it will be called when the build method finished its rendering.
void initState() {
// TODO: implement initState
super.initState();
print("initState");
WidgetsBinding.instance.addPostFrameCallback((_) {
print("WidgetsBinding");
});
SchedulerBinding.instance.addPostFrameCallback((_) {
print("SchedulerBinding");
});
}
Detail Description: https://medium.com/flutterworld/flutter-schedulerbinding-vs-widgetsbinding-149c71cb607f
Logging into our Flutter app opens to dashboard that has a Scaffold with a Drawer full of menu items.
I'd like to perform some A/B testing with having the Drawer open on page load or at least animating the Drawer being opened immediately on load.
I'm aware of Scaffold.of(context).openDrawer() but I'm not sure where to place this code so that it will run immediately after the build() method. I'm also not aware of any fields on either Drawer or Scaffold which would load with the Drawer open.
Thanks for your time and help.
You need to wait after the first frame is loaded.
_onLayoutDone(_) {
//your logic here
}
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback(_onLayoutDone);
super.initState();
}
I wrote a post about this, you can take a look if you want : https://medium.com/#diegoveloper/flutter-widget-size-and-position-b0a9ffed9407
Override initState.
#override
void initState() {
super.initState();
// use this
Timer.run(() => Scaffold.of(context).openDrawer());
}
Store a state variable to hide and show drawer - isDrawerBeingShown.
Based on the state variable toggle the state of drawer. It is set to false by default so it will be displayed for the first time.
void _showDrawer(BuildContext context) async it must be marked as async so that it runs after build method.
Create showDrawerUtility method to show drawer on demand when ever required.
Edit:
Use GlobalKey
GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey();
class MainScreen extends StatefulWidget {
MainScreen({Key key }) : super(key: key);
#override
State<MainScreen> createState() => new MainScreenState();
}
class MainScreenState extends State<MainScreen> {
bool isDrawerBeingShown;
#override
void initState() {
super.initState();
isDrawerBeingShown = false;
_showDrawer(context);
}
void _showDrawer(BuildContext context) async {
if(!isDrawerBeingShown) {
_scaffoldKey.currentState.openDrawer();
setState(() => isDrawerBeingShown = true);
}
}
#override
Widget build(BuildContext context) { // build method goes here}
}
follow my code
import 'package:easy_debounce/easy_debounce.dart';
import 'package:flutter/material.dart';
GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey();
class openDrawerOnLoadPage extends StatefulWidget {
openDrawerOnLoadPage({Key? key}) : super(key: key);
#override
_openDrawerOnLoadPageState createState() => _openDrawerOnLoadPageState();
}
class _openDrawerOnLoadPageState extends State<openDrawerOnLoadPage> {
late bool isDrawerBeingShown;
#override
void initState() {
super.initState();
isDrawerBeingShown = false;
_showDrawer(context);
}
void _showDrawer(BuildContext context) async {
if (!isDrawerBeingShown) {
EasyDebounce.debounce('openDrawer', Duration(milliseconds: 100),
() async {
_scaffoldKey.currentState!.openDrawer();
setState(() => isDrawerBeingShown = true);
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
);
}
}
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());
});
}