74

I have a StatefulWidget which I want to use in named route. I have to pass some arguments which I am doing as suggested in https://flutter.dev/docs/cookbook/navigation/navigate-with-arguments i.e.

Navigator.pushNamed(
      context,
      routeName,
      arguments: <args>,
    );

Now, I need to access these argument's in the state's initState method as the arguments are needed to subscribe to some external events. If I put the args = ModalRoute.of(context).settings.arguments; call in initState, I get a runtime exception.

20:49:44.129 4 info flutter.tools I/flutter ( 2680): ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
20:49:44.129 5 info flutter.tools I/flutter ( 2680): The following assertion was thrown building Builder:
20:49:44.129 6 info flutter.tools I/flutter ( 2680): inheritFromWidgetOfExactType(_ModalScopeStatus) or inheritFromElement() was called before
20:49:44.130 7 info flutter.tools I/flutter ( 2680): _CourseCohortScreenState.initState() completed.
20:49:44.130 8 info flutter.tools I/flutter ( 2680): When an inherited widget changes, for example if the value of Theme.of() changes, its dependent
20:49:44.131 9 info flutter.tools I/flutter ( 2680): widgets are rebuilt. If the dependent widget's reference to the inherited widget is in a constructor
20:49:44.131 10 info flutter.tools I/flutter ( 2680): or an initState() method, then the rebuilt dependent widget will not reflect the changes in the
20:49:44.131 11 info flutter.tools I/flutter ( 2680): inherited widget.
20:49:44.138 12 info flutter.tools I/flutter ( 2680): Typically references to inherited widgets should occur in widget build() methods. Alternatively,
20:49:44.138 13 info flutter.tools I/flutter ( 2680): initialization based on inherited widgets can be placed in the didChangeDependencies method, which
20:49:44.138 14 info flutter.tools I/flutter ( 2680): is called after initState and whenever the dependencies change thereafter.
20:49:44.138 15 info flutter.tools I/flutter ( 2680): 
20:49:44.138 16 info flutter.tools I/flutter ( 2680): When the exception was thrown, this was the stack:
20:49:44.147 17 info flutter.tools I/flutter ( 2680): #0      StatefulElement.inheritFromElement.<anonymous closure> (package:flutter/src/widgets/framework.dart:3936:9)
20:49:44.147 18 info flutter.tools I/flutter ( 2680): #1      StatefulElement.inheritFromElement (package:flutter/src/widgets/framework.dart:3969:6)
20:49:44.147 19 info flutter.tools I/flutter ( 2680): #2      Element.inheritFromWidgetOfExactType (package:flutter/src/widgets/framework.dart:3285:14)
20:49:44.147 20 info flutter.tools I/flutter ( 2680): #3      ModalRoute.of (package:flutter/src/widgets/routes.dart:698:46)
20:49:44.147 21 info flutter.tools I/flutter ( 2680): #4      _CourseCohortScreenState.initState.<anonymous closure> (package:esk2/cohort_screen.dart:57:23)

I do not want to put that logic in build method as build could be called multiple times and the initialization needs to happen only once. I could put the entire logic in a block with a boolean isInitialized flag, but that does not seem like the right way of doing this. Is this requirement/case not supported in flutter as of now?

adarsh
  • 1,070
  • 1
  • 7
  • 17
  • 2
    use https://flutter.dev/docs/cookbook/navigation/navigate-with-arguments#alternatively-extract-the-arguments-using-ongenerateroute – pskink May 23 '19 at 07:45
  • @pskink onGenerateRoute seems apt here. I had not considered it. You may put this as an answer so that I may accept it. Overall onGenerateRoute seems more convenient. It can also be used with initialRoute which was not clear from the example given on the page. – adarsh May 23 '19 at 16:05

9 Answers9

56

use MaterialApp.onGenerateRoute property like this:

onGenerateRoute: (RouteSettings settings) {
  print('build route for ${settings.name}');
  var routes = <String, WidgetBuilder>{
    "hello": (ctx) => Hello(settings.arguments),
    "other": (ctx) => SomeWidget(),
  };
  WidgetBuilder builder = routes[settings.name];
  return MaterialPageRoute(builder: (ctx) => builder(ctx));
},

now you can simply use NavigatorState.pushNamed:

Navigator.of(context).pushNamed("hello", arguments: "world");

here you have some test Hello widget:

class Hello extends StatelessWidget {
  final String greet;

  Hello(this.greet);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: Text(
          'hello $greet',
          textScaleFactor: 5.0,
        ),
      ),
    );
  }
}
pskink
  • 23,874
  • 6
  • 66
  • 77
  • 3
    In below line if you dont pass `settings`, routing URL will not appear in the web: return MaterialPageRoute(builder: (ctx) => builder(ctx), settings: settings); – MohammadReza Eram Nov 25 '20 at 22:05
  • 1
    what about `didChangeDependencies`. Any reason not to use that one as it is called right after the init function when the context has changed? – Ørjan Mar 15 '21 at 08:18
  • Agreed. I think `didChangeDependencies` should be the answer here. – lenz Jul 07 '21 at 18:02
  • 2
    Something to note is that `didChangeDependencies` get called after every build as opposed to `initState` – lenz Aug 27 '21 at 17:23
  • i have a question. what a function of SomeWidget? is something like name page route? – Michael Fernando Mar 17 '22 at 04:05
  • This answer lead me in the right direction... but this article here was the solution for me, since it shows it with a stateful widget: https://dev.to/geekpius/how-to-use-on-generate-route-in-flutter-4kml – Gerros Oct 08 '22 at 20:36
26

IMHO The accepted should be didChangeDependencies.

late Object args; 

@override
void didChangeDependencies() {
  args = ModalRoute.of(context).settings.argument
  super.didChangeDependencies();
}

@override
Widget build(BuildContext context) {
   /// use args here 
}

It's mentioned in the docs

This method is also called immediately after initState. It is safe to call BuildContext.dependOnInheritedWidgetOfExactType from this method.

It's also mentioned in your error code

initialization based on inherited widgets can be placed in the didChangeDependencies is called after initState and whenever the dependencies change thereafter.

lenz
  • 2,193
  • 17
  • 31
  • 1
    But the question is specifically about accessing args *during* `initState`, not after. So this does not actually answer the question. – James Allen Jun 29 '22 at 14:32
23

I just had the same problem as you and put a solution together. Instead of using onGenerateRoute, you can still use pushNamed Navigator to pass arguments and you can still access the ModalRoute arguments in initState - and here's how:

1) Use a future in initState to gain access to the context.

  • You can do this with Future.delayed(Duration.zero, () {} )
  • This gives you access to context and you can also do things like showDialog in initState using this because you can access the context here outside of the build method.

2) Extract the arguments using ModalRoute.of(context).settings.arguments

  • Inside the future, extract the arguments and store them in a declared, but un-initialised variable that you made before initState but obviously still in the State object.
  • Once you have the arguments you can do whatever you want with them, like passing the variable into a function perhaps.
  • Important note: you have to use the variable inside of the future function body, otherwise Flutter will skip over the future (as its programmed to do) and complete whatever is outside first, so you var will still return null because the future hasn't resolved to give the var a value yet.

All together it would look like this:

var = args;
_yourFunction(args) async {
// whatever you want to do
}

@override
  void initState() {
    super.initState();
    // future that allows us to access context. function is called inside the future
    // otherwise it would be skipped and args would return null
    Future.delayed(Duration.zero, () {
      setState(() {
        args = ModalRoute.of(context).settings.arguments;
      });
      print(args['id']);
      _yourFunction(args);
    });
  }

halfer
  • 19,824
  • 17
  • 99
  • 186
Marvioso
  • 359
  • 6
  • 15
  • 12
    The Flutter Cookbook recommends using onGenerateRoute. onGenerateRoute exists for this purpose. Better to use it than this over complicated state hackery. – devdanke Jun 21 '20 at 06:50
16

Instead of sending arguments through pushNamed, you could call push with a new PageRoute.

Suppose your argument type is called Argument. Here is what your stateful widget and its state classes look like:

class YourStatefulWidget extends StatefulWidget {
    final Argument argument;

    YourStatefulWidget({
        @required this.argument,
    });

    @override
    State<StatefulWidget> createState() {
        return YourStatefulWidgetState();
    }
}

class YourStatefulWidgetState extends State<YourStatefulWidget> {

    @override
    initState() {
        super.initState();

        // Refer to your argument here by "widget.argument"

    }
}

Here is how you call push with a PageRoute:

Navigator.of(context).push(MaterialPageRoute(builder: (context) => YourStatefulWidget(argument: Argument())));
Chunlong Li
  • 161
  • 1
  • 4
8

You can use named routes passing de argument in constructor.

   routes: {
    '/hello': (context) => Hello(
          argument: ModalRoute.of(context).settings.arguments,
        ),
  },

Then in your widget.

 class Hello extends StatefulWidget {
  final argument;

  Hello({this.argument});

  @override
  _HelloState createState() => _HelloState();
}
Community
  • 1
  • 1
5

I do it with WidgetsBinding. It can be called inside initState, and will be called only once after Build widgets done with rendering.

@override
void initState() {
   super.initState();

   final widgetsBinding = WidgetsBinding.instance;
   widgetsBinding.addPostFrameCallback((callback) {
  if (ModalRoute.of(context).settings.arguments != null) {
    _currentIndex = ModalRoute.of(context).settings.arguments;
  }
 });
}
Salmon
  • 51
  • 1
  • 2
0

If the routeName doesnt import that much this is a great way to me

Navigator.push(
    context,
    MaterialPageRoute(
        builder: (_) => MySecondaryPage(requiredAttrib: myValue),
    ),
);

Then in your Widget, clearly this is just a dumb use case with the TextEditingController on the initState:

class MySecondaryPage extends StatefulWidget {
  final requiredAttrib;

  MySecondaryPage({this.requiredAttrib});

  @override
  _MySecondaryPageState createState() => _MySecondaryPageState();
}

class _MySecondaryPageState extends State<MySecondaryPage> {

    TextEditingController _myController = TextEditingController();

    @override
    void initState() { 
      super.initState();
      _myController.text = widget.requiredAttrib;
      ...
    }

  @override
  Widget build(BuildContext context) {
    return TextField(
        controller: _myController,
        ...
    );
  }
}
0

Use this in initState. This will load the arguments which you get from the previous page in the initState of your current page.

   Future.delayed(Duration.zero, () {
  resetState(); //it is just a function where i am pasisng my obtained route data
});
0

As is often the case in Flutter, the simple answer is to break your widget into multiple smaller widgets.

Instead of having a single widget like this:

class MyWidgetSettings {}

class MyWidget extends StatefulWidget {

  const MyWidget();

  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {

  @override
  void initState() {
    // CAUSES AN EXCEPTION:
    final args = ModalRoute.of(context)!.settings.arguments as MyWidgetSettings;
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

Break it into an outer widget which will access the settings passed to the route during build, which builds an inner widget, passing in the settings as it's configuration:

class MyOuterWidget extends StatelessWidget {

  const MyOuterWidget();

  @override
  Widget build(BuildContext context) {
    final settings = ModalRoute.of(context)!.settings.arguments as MyWidgetSettings;
    return MyInnerWidget(settings);
  }
}


class MyInnerWidget extends StatefulWidget {

  final MyWidgetSettings settings;

  const MyInnerWidget(this.settings);

  @override
  State<MyInnerWidget> createState() => _MyInnerWidgetState();
}

class _MyInnerWidgetState extends State<MyInnerWidget> {

  @override
  void initState() {
    // Access settings from the parent widget:
    widget.settings.whatever
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

No need for Future, WidgetBindings, no need to change your routing - just plain flutter goodness

James Allen
  • 6,406
  • 8
  • 50
  • 83
  • But your accomplishing the same thing because the first frame of the parent would need to be rendered to build your next widget. – martinseal1987 Jul 20 '22 at 12:15