null safety ON in Flutter ModalRoute breaks the args passed to RouteSettings arguments

4.5k Views Asked by At

It drives me nuts that Flutter example on official documentation page for passing arguments to a named route fails with null-safety turned ON.

Here's the example I am trying to debug. There's a Consumer building a list view with ListView.builder which returns the following:

return ListTile(
  title: Text(data.items[index].title),
  onTap: () {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => DetailsScreen(),
        settings: RouteSettings(
          // arguments: data.items[index] // <- this does not work
          arguments: {
            'title': data.items[index].title,  // <- read below
          }
        ),
      ),
    );
  },
);

inside the DetailsScreen I am trying to get the arguments as following:

final ScreenArguments args = ModalRoute.of(context)!.settings.arguments!;

just like as it is described in the official documentation here. But debugger throws me an error:

A value of type 'Object' can't be assigned to a variable of type 'ScreenArguments'.

though I am just walking through the example and can't find the right way. The print(args.runtimeType); shows that suddenly my arguments are being transformed into unknown yet _InternalLinkedHashMap<String, String> and thus can't be casted as ScreenArguments in the example above. The only way I can get the args by not setting the type like this:

final args = ModalRoute.of(context)!.settings.arguments!;

but again it's not possible to get anything from this Object since it does not implement getters of my custom object obviously.

I also tried to convert the passed argument as a String from json-like object, but the arguments are still converted into _InternalLinkedHashMap. Don't understand where to go further from here...


UPDATE

So I do not know what happened exactly, but from what I see in the performance of Android Studio, it heavily depends on available CPU power as well as memory. I did not clean up the cache, but restarted the MacOS, accordingly all system garbage collectors should have cleaned up some system cache, then restarted the Android Studio and then tried to cast the

arguments: {
  'title': menusData.menus[index].title.toString(),
}

as Map like this:

final args = ModalRoute.of(context)!.settings.arguments! as Map;

and suddenly printing the result in the child Widget like this

print(args);
print(args.runtimeType);
print(args['title']);

yeilds the following result:

I/flutter ( 8653): _InternalLinkedHashMap<String, String>
I/flutter ( 8653): my custom string

which is not perfect, because I stull have to work with the `` class, yet I am able to get the passed String argument and use it in the child Widget. IMO this is still not how it is supposed to work...

1

There are 1 best solutions below

1
On

If you previously had RouteSettings set to

arguments: {
  'title': data.items[index].title,
}

and only did hot reloads, the state saved into the tree for your MaterialPageRouts->Settings->Arguments would remain that map. Have you done a hot restart or restarted the app since you started seeing the problem? That's the only way I could see the wrong object type having been passed through.

I've pasted in the entire code example, modified to work with null-safety below. Also see it on dartpad here. If it doesn't work for you, try rebuilding the app from scratch.

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        // Provide a function to handle named routes. Use this function to
        // identify the named route being pushed, and create the correct
        // Screen.
        onGenerateRoute: (settings) {
          // If you push the PassArguments route
          if (settings.name == PassArgumentsScreen.routeName) {
            // Cast the arguments to the correct type: ScreenArguments.
            final ScreenArguments args = settings.arguments! as ScreenArguments;

            // Then, extract the required data from the arguments and
            // pass the data to the correct screen.
            return MaterialPageRoute(
              builder: (context) {
                return PassArgumentsScreen(
                  title: args.title,
                  message: args.message,
                );
              },
            );
          }
          // The code only supports PassArgumentsScreen.routeName right now.
          // Other values need to be implemented if we add them. The assertion
          // here will help remind us of that higher up in the call stack, since
          // this assertion would otherwise fire somewhere in the framework.
          assert(false, 'Need to implement ${settings.name}');
          return null;
        },
        title: 'Navigation with Arguments',
        home: HomeScreen(),
        routes: {
          ExtractArgumentsScreen.routeName: (context) =>
              ExtractArgumentsScreen(),
        });
  }
}

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Home Screen'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            // A button that navigates to a named route that. The named route
            // extracts the arguments by itself.
            ElevatedButton(
              child: Text("Navigate to screen that extracts arguments"),
              onPressed: () {
                // When the user taps the button, navigate to a named route
                // and provide the arguments as an optional parameter.
                Navigator.pushNamed(
                  context,
                  ExtractArgumentsScreen.routeName,
                  arguments: ScreenArguments(
                    'Extract Arguments Screen',
                    'This message is extracted in the build method.',
                  ),
                );
              },
            ),
            // A button that navigates to a named route. For this route, extract
            // the arguments in the onGenerateRoute function and pass them
            // to the screen.
            ElevatedButton(
              child: Text("Navigate to a named that accepts arguments"),
              onPressed: () {
                // When the user taps the button, navigate to a named route
                // and provide the arguments as an optional parameter.
                Navigator.pushNamed(
                  context,
                  PassArgumentsScreen.routeName,
                  arguments: ScreenArguments(
                    'Accept Arguments Screen',
                    'This message is extracted in the onGenerateRoute function.',
                  ),
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}

// A Widget that extracts the necessary arguments from the ModalRoute.
class ExtractArgumentsScreen extends StatelessWidget {
  static const routeName = '/extractArguments';

  @override
  Widget build(BuildContext context) {
    // Extract the arguments from the current ModalRoute settings and cast
    // them as ScreenArguments.
    final ScreenArguments args = ModalRoute.of(context)!.settings.arguments as ScreenArguments;

    return Scaffold(
      appBar: AppBar(
        title: Text(args.title),
      ),
      body: Center(
        child: Text(args.message),
      ),
    );
  }
}

// A Widget that accepts the necessary arguments via the constructor.
class PassArgumentsScreen extends StatelessWidget {
  static const routeName = '/passArguments';

  final String title;
  final String message;

  // This Widget accepts the arguments as constructor parameters. It does not
  // extract the arguments from the ModalRoute.
  //
  // The arguments are extracted by the onGenerateRoute function provided to the
  // MaterialApp widget.
  const PassArgumentsScreen({
    Key? key,
    required this.title,
    required this.message,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Text(message),
      ),
    );
  }
}

// You can pass any object to the arguments parameter. In this example,
// create a class that contains both a customizable title and message.
class ScreenArguments {
  final String title;
  final String message;

  ScreenArguments(this.title, this.message);
}

https://dartpad.dev/?null_safety=true&id=7fc7f48276f696508799c960bf983400&run=true