deep_link_navigation 1.3.1 copy "deep_link_navigation: ^1.3.1" to clipboard
deep_link_navigation: ^1.3.1 copied to clipboard

Deep link navigation for Flutter apps with an elegant configuation internally orchestrating a native Flutter navigator.

Flutter Deep Link Navigation #

pub.dev package Github stars Open source license Awesome Flutter

Provides an elegant abstraction for complete deep linking navigation in Flutter.

This package only provides deep linking for internal navigation. Any external platform-level deep linking solution can optionally be used in conjuction with this package.

The target audience of the documentation is experienced Flutter developers.

Motivation #

There's nothing wrong with not using deep links for internal navigation.

Partially implementing deep links would either have limited benefits or be extremely complicated (not to mention confusing).

Hence why if you decide to use deep links, it makes sense to exclusively use deep links.

If you try to implement complete deep linking yourself here's some of the issues you'll run into:

  • Widgets become coupled with their place in the navigation hierarchy
    • Becomes an issue if you want to reuse a page
    • No amount of fancy iteration or recursion will help
  • It's difficult to 'pass down' deep link values used in higher levels
    • Given the hierarchy is Artist --> Song
    • Being able to do (artist) => ArtistPage(artist) and later (song) => SongPage(artist, song)
    • I actually published a rest-like router package while trying to solve this issue
  • How do you represent deep links internally?
    • How to keep configuration as terse as possible?
    • How to represent current route in a string-friendly format (eg. for analytics, debugging)
    • How to serialize route to be used with a platform-level deep linking solution
    • How to handle native navigator pops (eg. back button)?
    • How to handle a route that doesn't exist (or any other exception that occurs during dispatch)?
  • How do you integrate custom logic for validating deep link navigation?
    • Ideally provide context to be able to access state management
    • eg. certain subtrees of the navigation hierarchy are available only for subscribed or authenticated users

TL;DR

I separated the navigation system from Diet Driven (shameless plug, please hire me) into its own package and published it.

This package provides a solution for all the aforementioned difficulties.

Examples #

This example demonstrates:

  • Dispatchers with path-only deep links
  • Dispatchers with value deep links (ArtistDL, SongDL)
  • Exception handling (RouteNotFoundDL)
  • Cross-branch navigation (from favorite's song page to artist page) Navigation diagram for multiple base routes example

This example demonstrates:

  • Everything from single base route example
  • Bottom navigation (library, favorites, user pages) persists across navigation
  • Login and error pages are full screen (hide bottom navigation)
  • Using the asynchronous result of push (AuthenticationDL from UserDL)
  • Custom Authenticated mixin ensures user is authenticated (LibraryDL, FavoritesDL, UserDL) Navigation diagram for multiple base routes example

Configuration #

DeepLink is base unit of routes, a deep link is mapped to a Widget by a Dispatcher.

Deep links may be reused in different levels of the navigation hierarchy.

A route is the full location of a page, represented by List<DeepLink>.

path is the string representation of the route aka route.join("/").

Path

class LibraryDL extends DeepLink {
    LibraryDL() : super("library");
}
copied to clipboard

Value

Deep links can also store data, value dispatchers are strongly typed.

class SongDL extends ValueDeepLink<Song> {
    SongDL(Song song) : super("song", song);
}

class SongDL extends ValueDeepLink<Song> {
    // Override toString
    SongDL(Song song) : super("song", song, toString: (song) => song.id);
}
copied to clipboard

Mixin for sake of inheritence

This could also be achieved by implementing an abstract class.

See use in Child builder section.

mixin FullScreen on DeepLink {}

class LoginDL extends DeepLink with FullScreen {
    LoginDL() : super("login");
}
copied to clipboard

Mixin with logic

mixin Authenticated on DeepLink {
    @override
    void onDispatch(BuildContext context) {
        // Get state from context or global/static variable
        final isAuthenticated = Provider.of<AuthenticationService>(context, listen: false).authenticated;

        // Throw custom exception
        if (!isAuthenticated) {
            throw Unauthenticated();
        }
    }
}

// ...

navigation: (context) => Dispatcher()
    // Unauthenticated login page
    ..exception<Unauthenticated>((exception, route) => [LoginDL()])
    ..path<LoginDL>((route) => LoginPage()),
copied to clipboard

Application

Use DeepLinkMaterialApp instead of using Flutter's MaterialApp.

This replaces native navigation options with their deep link counterparts.

At most one deep link of a type can exist on a dispatcher.

Deep link material app

DeepLinkMaterialApp(
    navigation: (context) => Dispatcher() // see next section ...
    defaultRoute: [LibraryDL()], // if ommited, the splash screen is shown until explicit navigation
    splashScreen: SplashPage(),
    childBuilder: // see child builder section ...

    // Non-navigation related fields are still available
    themeMode: ThemeMode.light,
    // ...
);
copied to clipboard

Path dispatcher

..path<LoginDL>((route) => LoginPage()),
copied to clipboard

Value dispatcher

..value<Song, SongDL>((song, route) => SongPage(song: song)),
copied to clipboard

Sub navigation

..path<LibraryDL>(
    (route) => LibraryPage(),
    subNavigation: Dispatcher() // ...
),

..value<Song, SongDL>(
    (song, route) => SongPage(song: song),
    subNavigation: (song) => Dispatcher() // song may be used from this point onward
),
copied to clipboard

Exception mapping

Exceptions that are thrown while running through the navigation hierarchy are mapped to routes.

..exception<RouteNotFound> MUST be defined on the base-level dispatcher.

If multiple mappings of the same type are found thoughout the hierarchy, the deep-most mapping is used.

..exception<RouteNotFound>((exception, route) => [ErrorDL<RouteNotFound>(exception)])
copied to clipboard

Child builder

The widget specified in childBuilder is rebuilt when the route in deepLinkNavigator changes.

/// [DeepLink]s associated with the bottom navigation.
final bottomNavigationDeepLinks = [LibraryDL(), FavoritesDL(), UserDL()];

/// Current index of bottom navigation based on [currentRoute].
int currentIndex(List<DeepLink> currentRoute) {
    final index = bottomNavigationDeepLinks.indexOf(currentRoute?.first);
    return index != -1 ? index : 0;
}

/// ...

childBuilder: (BuildContext context, DeepLinkNavigator deepLinkNavigator, Widget child) => Scaffold(
    body: child,
    // Don't show bottom navigation while [currentRoute] is null, or any deep list is [FullScreen]
    bottomNavigationBar: deepLinkNavigator.currentRoute?.any((dl) => dl is FullScreen) ?? true ? null : BottomNavigationBar(
        currentIndex: currentIndex(deepLinkNavigator.currentRoute),
        onTap: (int index) => deepLinkNavigator.navigateTo([bottomNavigationDeepLinks[index]]),
        items: [
            BottomNavigationBarItem(title: Text("Library"), icon: Icon(Icons.queue_music)),
            BottomNavigationBarItem(title: Text("Favorites"), icon: Icon(Icons.favorite)),
            BottomNavigationBarItem(title: Text("User"), icon: Icon(Icons.person)),
        ],
    ),
)
copied to clipboard

In-app navigation

DeepLinkNavigator mirrors Navigator's interface as much as possible (including push and pop futures).

All methods internally orchestrate a native flutter navigator.

Push a deep link

await DeepLinkNavigator.of(context).push(ArtistDL(...));
copied to clipboard

Pop a value

DeepLinkNavigator.of(context).pop(...);

// or

Navigator.of(context).pop(...);
copied to clipboard

Navigate to specific route

DeepLinkNavigator.of(context).navigateTo([
    LibraryDL(),
    ArtistDL(...),
]);
copied to clipboard

Return to default route (if any)

DeepLinkNavigator.of(context).replaceWithDefault();
copied to clipboard

TODO: Throw exception to be caught by mapper

// TODO DeepLinkNavigator.of(context).throw(Exception(...));
copied to clipboard

TODO: Access deep link navigator from anywhere

// TODO DeepLinkNavigator()...
copied to clipboard

TODO: Page transitions

// await DeepLinkNavigator.of(context).push(
//   ArtistDL(...),
//   transition: ...,
//   duration: ...,
// );

// Possibly:
// await DeepLinkNavigator.of(context).fadeIn(...);
copied to clipboard

// TODO: serialize List<DeepLink>

Limitations #

  • Must FULLY specify ALL generic types since this is how deep links are matched internally
    • Please look very carefully when debugging
    • Fails by throwing RouteNotFound if the route doesn't exist
    • Fails by entering infinite recursion if RouteNotFound maps to a route that doesn't exist
  • Can't currently define arbitrarily deep navigation hierarchies (think Spotify)
  • Can't store separate persisted navigation states for a multi-base route application (think Instagram)

What's left to do #

[ ] Custom/predefined page transitions

[ ] Access deep link navigator from anywhere using static method and factory pattern

[ ] Assert RouteNotFound dispatcher exists by running through navigation tree

[ ] Unit test deep link navigator logic

[ ] Cupertino and Widget apps

[ ] Explicit exception throwing

[ ] Platform deep links example + documentation

[ ] Route changed callback for analytics, etc

26
likes
40
points
62
downloads

Publisher

unverified uploader

Weekly Downloads

2024.09.14 - 2025.03.29

Deep link navigation for Flutter apps with an elegant configuation internally orchestrating a native Flutter navigator.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

after_layout, flutter, merge_map, provider

More

Packages that depend on deep_link_navigation