I'm not sure if the initState
is the right function for this. What I'm trying to achieve is to check when the page is rendered to perform some checks and based on them opening a AlertDialog
to make some settings if needed.
I've got a Page which has a state. It's initState
function looks like this:
@override
void initState() {
super.initState();
if (!_checkConfiguration()) {
_showConfiguration(context);
}
}
The _showConfiguration
like this:
void _showConfiguration(BuildContext context) {
AlertDialog dialog = new AlertDialog(
content: new Column(
children: <Widget>[
new Text('@todo')
],
),
actions: <Widget>[
new FlatButton(onPressed: (){
Navigator.pop(context);
}, child: new Text('OK')),
],
);
showDialog(context: context, child: dialog);
}
If there's a better way to make this checks and if needed call the modal, please point me in the proper direction, I was looking for a onState
or onRender
function, or a callback I could assign to the build
function to be called on render but wasn't able to find one.
Edit: It seams over here they had a similar problem: Flutter Redirect to a page on initState
The member variable context can be accessed during initState
but can't be used for everything. This is from the flutter for initState
documentation:
You cannot use [BuildContext.inheritFromWidgetOfExactType] from this method. However, [didChangeDependencies] will be called immediately following this method, and [BuildContext.inheritFromWidgetOfExactType] can be used there.
You could move your initialization logic to didChangeDependencies
, however that might not be exactly what you want as didChangeDependencies
can be called multiple times in the lifecycle of the widget.
If you instead make an asynchronous call which delegates your call until after the widget has been initialized, you can then use context as you intend.
A simple way to do that is to use a future.
Future.delayed(Duration.zero,() {
... showDialog(context, ....)
}
Another way, which may be more 'correct', is to use flutter's scheduler to add a post-frame callback:
SchedulerBinding.instance.addPostFrameCallback((_) {
... showDialog(context, ....)
});
And finally, here's a little trick I like to do to use asynchronous calls in the initState function:
() async {
await Future.delayed(Duration.zero);
... showDialog(context, ...)
}();
Here's a fully fleshed out example using the simple Future.delayed:
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
bool _checkConfiguration() => true;
void initState() {
super.initState();
if (_checkConfiguration()) {
Future.delayed(Duration.zero,() {
showDialog(context: context, builder: (context) => AlertDialog(
content: Column(
children: <Widget>[
Text('@todo')
],
),
actions: <Widget>[
FlatButton(onPressed: (){
Navigator.pop(context);
}, child: Text('OK')),
],
));
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
);
}
}
With more context from the OP provided in comments, I can give a slightly better solution to their specific problem. Depending on the app, you may actually want to make a decision based on which page to show depending on whether it's the first time the app is opened i.e. set home
to something different. And dialogs aren't necessarily the best UI element on mobile; it may be better to show a full page with the settings they need to add and a next button.
Wrapping with Future
@override
void initState() {
super.initState();
_store = Store();
new Future.delayed(Duration.zero,() {
_store.fetchContent(context);
});
}
====== UPDATED ======
Just like pointed by Lucas Rueda ( thanks to him :), When we need to get context
inside initState()
in order to work with "Provider", we should set the parameter listen
to be = false
. It makes sense because we should not listen to the initState()
phase. So, for example, it should be:
final settingData = Provider.of<SettingProvider>(context, listen: false);
=========== OLD ANSWER =======
Most examples of initState()
in this thread may be works for "UI" things such as "Dialog" which is the case in the root question of this thread.
But unfortunately, it doesn't work for me when applying it to get context
for "Provider".
Hence, I pick didChangeDependencies()
approach. As mentioned in the accepted answer, it has a caveat which is, it can be called multiple times in the lifecycle of the widget. However, it quite easy to handle it. Just use a single helper variable which is bool
to prevent multiple calls inside didChangeDependencies()
. Here is the example usage of _BookListState
class with variable _isInitialized
as the main "stopper" of "multiple calls":
class _BookListState extends State<BookList> {
List<BookListModel> _bookList;
String _apiHost;
bool _isInitialized; //This is the key
bool _isFetching;
@override
void didChangeDependencies() {
final settingData = Provider.of<SettingProvider>(context);
this._apiHost = settingData.setting.apiHost;
final bookListData = Provider.of<BookListProvider>(context);
this._bookList = bookListData.list;
this._isFetching = bookListData.isFetching;
if (this._isInitialized == null || !this._isInitialized) {// Only execute once
bookListData.fetchList(context);
this._isInitialized = true; // Set this to true to prevent next execution using "if()" at this root block
}
super.didChangeDependencies();
}
...
}
Here is error logs when I am trying to do initState()
approach:
E/flutter ( 3556): [ERROR:flutter/lib/ui/ui_dart_state.cc(177)] Unhandled Exception: 'package:provider/src/provider.dart': Failed assertion: line 242 pos 7: 'context.owner.debugBuilding ||
E/flutter ( 3556): listen == false ||
E/flutter ( 3556): debugIsInInheritedProviderUpdate': Tried to listen to a value exposed with provider, from outside of the widget tree.
E/flutter ( 3556):
E/flutter ( 3556): This is likely caused by an event handler (like a button's onPressed) that called
E/flutter ( 3556): Provider.of without passing `listen: false`.
E/flutter ( 3556):
E/flutter ( 3556): To fix, write:
E/flutter ( 3556): Provider.of<SettingProvider>(context, listen: false);
E/flutter ( 3556):
E/flutter ( 3556): It is unsupported because may pointlessly rebuild the widget associated to the
E/flutter ( 3556): event handler, when the widget tree doesn't care about the value.
E/flutter ( 3556):
E/flutter ( 3556): The context used was: BookList(dependencies: [_InheritedProviderScope<BookListProvider>], state: _BookListState#1008f)
E/flutter ( 3556):
E/flutter ( 3556): #0 _AssertionError._doThrowNew (dart:core-patch/errors_patch.dart:46:39)
E/flutter ( 3556): #1 _AssertionError._throwNew (dart:core-patch/errors_patch.dart:36:5)
E/flutter ( 3556): #2 Provider.of
package:provider/src/provider.dart:242
E/flutter ( 3556): #3 _BookListState.initState.<anonymous closure>
package:perpus/…/home/book-list.dart:24
E/flutter ( 3556): #4 new Future.delayed.<anonymous closure> (dart:async/future.dart:326:39)
E/flutter ( 3556): #5 _rootRun (dart:async/zone.dart:1182:47)
E/flutter ( 3556): #6 _CustomZone.run (dart:async/zone.dart:1093:19)
E/flutter ( 3556): #7 _CustomZone.runGuarded (dart:async/zone.dart:997:7)
E/flutter ( 3556): #8 _CustomZone.bindCallbackGuarded.<anonymous closure> (dart:async/zone.dart:1037:23)
E/flutter ( 3556): #9 _rootRun (dart:async/zone.dart:1190:13)
E/flutter ( 3556): #10 _CustomZone.run (dart:async/zone.dart:1093:19)
E/flutter ( 3556): #11 _CustomZone.bindCallback.<anonymous closure> (dart:async/zone.dart:1021:23)
E/flutter ( 3556): #12 Timer._createTimer.<anonymous closure> (dart:async-patch/timer_patch.dart:18:15)
E/flutter ( 3556): #13 _Timer._runTimers (dart:isolate-patch/timer_impl.dart:397:19)
E/flutter ( 3556): #14 _Timer._handleMessage (dart:isolate-patch/timer_impl.dart:428:5)
E/flutter ( 3556): #15 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:168:12)
E/flutter ( 3556):
context.read()
, but was doing "hot reloat" instead "restart" on my VSCode. After got your message, I really try "restart" after applying "listen: false" to my provider. I confirm that it was really caused by "listen: true" or context.watch()
. Will update my answer soon.
Simple use Timer.run()
@override
void initState() {
super.initState();
Timer.run(() {
// you have a valid context here
});
}
We can use Global key as:
class _ContactUsScreenState extends State<ContactUsScreen> {
//Declare Global Key
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
//key
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
title: Text('Contact Us'),
),
body:
}
//use
Future<void> send() async {
final Email email = Email(
body: _bodyController.text,
subject: _subjectController.text,
recipients: [_recipientController.text],
attachmentPaths: attachments,
isHTML: isHTML,
);
String platformResponse;
try {
await FlutterEmailSender.send(email);
platformResponse = 'success';
} catch (error) {
platformResponse = error.toString();
}
if (!mounted) return;
_scaffoldKey.currentState.showSnackBar(SnackBar(
content: Text(platformResponse),
));
}
}
This work using a key in your method build widget.
First create the key:
final GlobalKey<NavigatorState> key =
new GlobalKey<NavigatorState>();
After we bind with our widget:
@override
Widget build(BuildContext context) {
return Scaffold(key:key);
}
Finally we use the key calling .currentContext parameter.
@override
void initState() {
super.initState();
SchedulerBinding.instance.addPostFrameCallback((_) {
// your method where use the context
// Example navigate:
Navigator.push(key.currentContext,"SiestaPage");
});
}
Happy coding.
Success story sharing
MaterialApp
'shome
property. So I don't really do a push there. Could you give me an Example how to do it in thebuild
function? Currently it just returns a newScaffold
with anappBar
,drawer
,body
andfloatingActionButton
didChangeDependencies
methodcontext
is actually a member variable of the state =D. So you don't need the boolean, you can just directly use theFuture.delayed
in your initstate. That is still required though - without it you'll get assertion errors around trying to push while in a push.