167

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

wawa
  • 4,816
  • 3
  • 29
  • 52

7 Answers7

197

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.

AnasSafi
  • 5,353
  • 1
  • 35
  • 38
rmtmckenzie
  • 37,718
  • 9
  • 112
  • 99
  • The Page in question is the first page and this called through `MaterialApp`'s `home` property. So I don't really do a push there. Could you give me an Example how to do it in the `build` function? Currently it just returns a new `Scaffold` with an `appBar`, `drawer`, `body` and `floatingActionButton` – wawa Mar 23 '18 at 21:11
  • One option that comes to my mind would be to outsource all the widgets used in the modal in a function and in the homepages `Scaffold` display them (as none modal), if the check fails and otherwise just display the home page. This however feels kind of hacky to me. – wawa Mar 23 '18 at 21:22
  • 5
    this is very bad. the first place you can access the context is the `didChangeDependencies` method – Razvan Cristian Lung Mar 23 '18 at 23:15
  • 1
    @wawa - I fixed up the example a bit. I actually forgot that `context` is actually a member variable of the state =D. So you don't need the boolean, you can just directly use the `Future.delayed` in your initstate. That is still required though - without it you'll get assertion errors around trying to push while in a push. – rmtmckenzie Mar 25 '18 at 16:00
  • I also added another suggestion at the end, which I believe would be a better solution. I tend to avoid popup dialogs as much as possible on mobile - it's generally better to show a full page. – rmtmckenzie Mar 25 '18 at 16:01
  • 2
    in my case it says 'Undefined name context' in initState – temirbek Mar 17 '19 at 03:23
  • 2
    @rmtmckenzie is it always the case that the function Future.delayed(Duration.zero,() {} runs just after build() ? What if you add non-Future or another Future method in the initState() ? Are you aware of any gotchas? I implemented your example and it works fine so far. – Pitos Jul 17 '19 at 12:04
  • SchedulerBinding.instance.addPostFrameCallback((_) { ... showDialog(context, ....) }); this method works perfectly. I never knew that context can be take later after build. wow – petras J May 23 '22 at 06:39
65

Wrapping with Future

  @override
  void initState() {
    super.initState();
    _store = Store();
    new Future.delayed(Duration.zero,() {
      _store.fetchContent(context);
    });
  }
Pablo Cegarra
  • 20,955
  • 12
  • 92
  • 110
26

====== 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):
Bayu
  • 2,340
  • 1
  • 26
  • 31
  • 1
    You have that error because you are not using the "listen: false" paramenter. Provider detects that is not being called from the widget tree (inside a "build" method). – Lucas Rueda Dec 29 '20 at 03:00
  • 1
    Thanks for pointing it out @LucasRueda, looks like I did "listen: false" or `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. – Bayu Dec 29 '20 at 16:56
4

Simple use Timer.run()

@override
void initState() {
  super.initState();
  Timer.run(() {
    // you have a valid context here
  });
}
CopsOnRoad
  • 237,138
  • 77
  • 654
  • 440
  • @Kamlesh This question doesn't have to do anything with your other question. I personally think your question isn't reproducible on my end. – CopsOnRoad Jun 18 '21 at 10:41
  • I thought you have any previous experience of my raised query that's why I asked you. Thanks dear. – Kamlesh Jun 18 '21 at 11:37
  • @Kamlesh I understand but your wrote "your solution works" and then shared a link of a question; I thought this post is somewhat related to your new post but it wasn't. Anyway, I am not able to produce your new post. It would be better if you can share a minimal reproducible code. Thanks – CopsOnRoad Jun 18 '21 at 13:45
3

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.

Pedro Molina
  • 1,101
  • 12
  • 12
2

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),
        ));
      }


}
Avijit Nagare
  • 8,482
  • 7
  • 39
  • 68
1

As init methods is called once when widget is rendering. There is a method just to be called once after the whole widget is finished rendering itself and then you can get your context also.

WidgetsBinding.instance.addPostFrameCallback((_) {
  /// Here you can have your context and do what ever you want
});

this callback must be used inside initState and after super.initState().

Muddassar
  • 349
  • 2
  • 13