Flutter Navigator 2.0 - Router does not receive deep link when app is launched from URL?

1,091

This is happening because your routeInformationProvider is overriding that value with an empty route when the app has just been opened instead of receiving the proper route from the operating system.

Changing the emptyrouteInformationProvider to the one actually used by the Navigator by default.

  static PlatformRouteInformationProvider routeInformationProvider =
      PlatformRouteInformationProvider(
    initialRouteInformation: RouteInformation(
        location: PlatformDispatcher.instance.defaultRouteName),
  );

Keep in mind that you need to import dart:ui.

This will use the specific behavior for android and iOS for reading the deep link route when launching the application as it receives the route from the operating system.

This does not fix the iOS problem of the question though.

Share:
1,091
ikurek
Author by

ikurek

Updated on December 16, 2022

Comments

  • ikurek
    ikurek over 1 year

    I'm using Flutter Navigator 2.0 with Nested Routers - there's a main router created by calling MaterialApp.router() and child routers created as Router() widgets with proper RouterDelegates (child routers are used as pages for bottom navigation).

    In current use case, I want to use a deep link to open one of the pages inside a nested router, so I followed the instructions and configured:

    1. Android Manifest to receive custom schema and host URL's
    2. RouteInformationParser to parse it into my own model
    3. PlatformRouteInformationProvider to notify all nested routers about route change

    Everything works fine when application is in foreground (f.e. on splash screen) - I receive a pushRoute event and deep link is handled correctly by the root and nested Router widgets. However, when the application is not launched, I do not receive an initialRoute set to my deep link, and I get the default, empty RouteInformation instead. Why does that happen? Here are my code samples:

    Flutter MainActivity configuration in Android Manifest:

            <activity
                android:name=".MainActivity"
                android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
                android:exported="true"
                android:launchMode="singleTop"
                android:theme="@style/LaunchTheme"
                android:windowSoftInputMode="adjustResize">
    
                <meta-data
                    android:name="io.flutter.embedding.android.NormalTheme"
                    android:resource="@style/NormalTheme" />
    
                <meta-data
                    android:name="io.flutter.embedding.android.SplashScreenDrawable"
                    android:resource="@drawable/launch_background" />
    
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
    
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
    
                <!-- For deeplinking -->
                <meta-data
                    android:name="flutter_deeplinking_enabled"
                    android:value="true" />
    
                <!-- Accepts links in format myapp://*.myapp.com/ -->
                <intent-filter>
                    <action android:name="android.intent.action.VIEW" />
    
                    <category android:name="android.intent.category.DEFAULT" />
                    <category android:name="android.intent.category.BROWSABLE" />
    
                    <data
                        android:host="*.myapp.com"
                        android:scheme="myapp" />
                </intent-filter>
            </activity>
    

    Main Application class (used in RunApp):

    class MyApp extends StatelessWidget {
      static PlatformRouteInformationProvider routeInformationProvider =
          PlatformRouteInformationProvider(
        initialRouteInformation: const RouteInformation(),
      );
    
      const MyApp({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) => FutureBuilder(
            future: AppInit.initApp(),
            builder: (context, snapshot) {
              return _app();
            },
          );
    
      Widget _app() => MultiRepositoryProvider(
            providers: AppRepositoryProviders().list,
            child: MultiBlocProvider(
              providers: AppBlocProviders().list,
              child: MaterialApp.router(
                supportedLocales: LocalizationConfig.supportedLocales,
                localizationsDelegates: LocalizationConfig.localizationDelegates,
                theme: LightTheme().themeData,
                routerDelegate: UserSessionRouter(),
                routeInformationParser: AppRouteInformationParser(),
                routeInformationProvider: routeInformationProvider,
                backButtonDispatcher: RootBackButtonDispatcher(),
              ),
            ),
          );
        ...
    }
    

    RouteInformationParser implementation:

    class AppRouteInformationParser extends RouteInformationParser<DeepLinkRoute> {
      @override
      Future<DeepLinkRoute> parseRouteInformation(
          RouteInformation routeInformation) async {
        if (routeInformation.location.isNullOrEmpty) {
          return DeepLinkRoute.none();
        } else {
          return DeepLinkParser.parse(routeInformation.location!).fold(
            (data) => DeepLinkRoute(
              link: data,
              route: _getRouteFromDeeplink(data),
            ),
            (error) => DeepLinkRoute.none(),
          );
        }
      }
    
      RouteDefinition _getRouteFromDeeplink(DeepLink deepLink) {
        switch (deepLink.path) {
          case '/auth/signup':
            return AppRoutes.authSignup;
          default:
            return AppRoutes.none;
        }
      }
    
      @override
      RouteInformation restoreRouteInformation(DeepLinkRoute configuration) =>
          RouteInformation(
            location: configuration.link.path,
            state: configuration.link.queryParams,
          );
    }
    

    The screen with nested (child) router:

    class AuthScreen extends StatefulWidget {
      const AuthScreen({Key? key}) : super(key: key);
    
      @override
      _AuthScreenState createState() => _AuthScreenState();
    }
    
    class _AuthScreenState extends State<AuthScreen> {
      final AuthRouter _routerDelegate = AuthRouter();
      ChildBackButtonDispatcher? _backButtonDispatcher;
    
      @override
      void didChangeDependencies() {
        _initBackButtonDispatcher();
        super.didChangeDependencies();
      }
    
      void _initBackButtonDispatcher() {
        _backButtonDispatcher ??=
            ChildBackButtonDispatcher(context.router.backButtonDispatcher!);
        _backButtonDispatcher?.takePriority();
      }
    
      @override
      Widget build(BuildContext context) => MultiBlocProvider(
            providers: [
              BlocProvider(
                  create: (context) => SigninScreenCubit(
                        usersRepository: context.read<UsersRepository>(),
                      ))
            ],
            child: Router(
              routerDelegate: _routerDelegate,
              backButtonDispatcher: _backButtonDispatcher,
              routeInformationParser: AppRouteInformationParser(),
              routeInformationProvider:
                  MyApp.routeInformationProvider,
            ),
          );
    }
    

    And I test the deep linking with this command:

    adb shell am start -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "$1"
    

    With given classes, the deep link when launching the app is always empty. My best guess is the instance of RouteInformationProvider:

      static PlatformRouteInformationProvider routeInformationProvider =
          PlatformRouteInformationProvider(initialRouteInformation: const RouteInformation());
    

    Or some kind of configuration error, but I'm not able to identify it myself.

    UPDATE:

    I've tested this on iOS, and surprisingly, it works completely reverse than Android - when app is killed, it opens fine with a deep link, but when it's in foreground Router never gets update and RouteInformationProvider and RouteInformationParser are both never called. I've found some related issues on Flutter GitHub repository, and although they are closed, I don't think they solve my issue. This issue seems to be the almost same as my problem, but I've taken a look at the PR that's supposed to solve it and I can see other users also reporting problems with deep links on iOS.

  • ikurek
    ikurek over 2 years
    Thank you for the answer! I've tried setting the value of initial route to different things, but I didn't know that Platform can be used to retrieve initial route that way. I've noticed that using your solution with development channel of Flutter seems to fix the deep link handling when the application is in background. It doesn't however solve the iOS issue when the app is in foreground.
  • Cavitedev
    Cavitedev over 2 years
    I see. I have no hardware to test iOS devices. So I cannot help you there. I am glad this fixed the Android problem though.