How to handle deeplinking in Flutter using routes

Viewed 4244

I'm attempting to build deeplink functionality and so far the initial start of the app and retrieving parameters from the deeplink is going fine.

However I am having issues navigating to a screen after I deeplink into the app. How should I do this?

My code looks like this:

void main() { 
    runApp(MyApp()); 
}

class MyApp extends StatefulWidget {   
    @override   
    _MyAppState createState() => _MyAppState(); 
}

class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {   
   Uri _latestUri;  
   Object _err;

  StreamSubscription _sub;

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

  @override void dispose() {
    _sub?.cancel();
    super.dispose();   
  }

  void _handleIncomingLinks() {
    _sub = uriLinkStream.listen((Uri uri) {
      if (!mounted) return;
      print('got uri: $uri'); // printed: got uri: myapp://?key1=test
      setState(() {
        _latestUri = uri;
        _err = null;

        Navigator.pushNamed(context, 'login'); // This doesn't work because the context does not include navigator
      });
    }, onError: (Object err) {
      if (!mounted) return;
      print('got err: $err');
      setState(() {
        _latestUri = null;
        if (err is FormatException) {
          _err = err;
        } else {
          _err = null;
        }
      });
    });   
  }

  @override Widget build(BuildContext context) {
    return MaterialApp(
          initialRoute: 'splash-screen',
          onGenerateRoute: (settings) {
            switch (settings.name) {
              case 'splash-screen':
                return
                  PageTransition(
                        child: BlocProvider(
                          create: (context) => SplashScreenCubit(APIRepository(
                              apiClient: APIClient(httpClient: http.Client()))),
                          child: SplashScreen(),
                        ),
                        type: PageTransitionType.rightToLeft,
                        settings: settings);
                break;
              case 'create-account':
                return PageTransition(
                    child: BlocProvider(
                      create: (context) => CreateAccountScreenCubit(
                          APIRepository(
                              apiClient: APIClient(httpClient: http.Client()))),
                      child: CreateAccountScreen(),
                    ),
                    type: PageTransitionType.rightToLeft,
                    settings: settings);
                break;
              case 'login':
                return PageTransition(
                        child: BlocProvider(
                          create: (context) => LoginScreenCubit(APIRepository(
                              apiClient: APIClient(httpClient: http.Client()))),
                          child: LoginScreen(),
                        ),
                        type: PageTransitionType.rightToLeft,
                        settings: settings);
                break;
              default:
                 return null;
           },
        );
     }
  }
2 Answers

If what you needed is to be able to navigate without getting the context from Navigtor.of as you want to handling deeplink, you need to use navigatorKey property, you can read the details here.

then your code will be look like this. [EDITED, I add where to add the navigator key on the material app]

void main() { ... }

class MyApp extends StatefulWidget { ... }

class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {   
   Uri _latestUri;  
   Object _err;
   GlobalKey<NavigatorState> navigatorKey = GlobalKey();

  StreamSubscription _sub;

  @override   void initState() { ... }

  @override void dispose() { ... }

  void _handleIncomingLinks() {
     _sub = uriLinkStream.listen((Uri uri) {
      if (!mounted) return;
      print('got uri: $uri'); // printed: got uri: myapp://?key1=test
      setState(() {
        _latestUri = uri;
        _err = null;
      });

      // use the navigatorkey currentstate to navigate to the page you are intended to visit
      navigatorKey.currentState.pushNamedAndRemoveUntil('login', (route) => false);
    }, onError: (Object err) { ... });

  @override Widget build(BuildContext context) { 
      return MaterialApp(
          ...
          navigatorKey: navigatorKey,
          ...
      );
  }

}

Your deep link stream can be triggered before the build method, but you are not allowed to call Navigator at the time. So, you can fix it using addPostFrameCallback provided by SchedulerBinding:

addPostFrameCallback

Schedule a callback for the end of this frame.

Does not request a new frame.

This callback is run during a frame, just after the persistent frame callbacks (which is when the main rendering pipeline has been flushed). If a frame is in progress and post-frame callbacks haven't been executed yet, then the registered callback is still executed during the frame. Otherwise, the registered callback is executed during the next frame.

The callbacks are executed in the order in which they have been added.

Post-frame callbacks cannot be unregistered. They are called exactly once.

...

void _handleIncomingLinks() {
  _sub = uriLinkStream.listen((Uri uri) {
    if (!mounted) return;
    print('got uri: $uri'); // printed: got uri: myapp://?key1=test
    setState(() {
      _latestUri = uri;
      _err = null;

      // Call your navigator inside addPostFrameCallback
      WidgetsBinding.instance?.addPostFrameCallback((_) {
        Navigator.pushNamed(context, 'login');
      });
    });
  }, onError: (Object err) {
    if (!mounted) return;
    print('got err: $err');
    setState(() {
      _latestUri = null;
      if (err is FormatException) {
        _err = err;
      } else {
        _err = null;
      }
    });
  });
}

...
Related