ikurek
ikurek

Reputation: 604

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

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.

Upvotes: 7

Views: 3511

Answers (1)

Cavitedev
Cavitedev

Reputation: 761

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.

Upvotes: 3

Related Questions