Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BottomTabNavigation + deep link into nested route inside tab] Q: How can I ensure state of other tabs is preserved? #596

Open
annawidera opened this issue Feb 13, 2023 · 3 comments
Labels
question A question about usage

Comments

@annawidera
Copy link

Hey @slovnicki ! Thank you for the fantastic navigation package! I've tried a couple of them already 🙈 and Beamer appears to have a well-thought-through structure. It also has the balance between automating things and not being a pure 🔮 black box.
I also appreciate an extensive catalogue of examples. I found a few solutions for the navigation I need: the bottom tab bar.

Background: requirements

preserving tabs state

An essential requirement, in my case, is to preserve the state of the tabs (their nested navigation) while switching between them. So I based my experiments on bottom_navigation_multiple_beamers example.

deep linking to nested pages

Another feature that we need is deep linking that navigates to the nested pages in the tabs (like for example books/2 or articles/3).

Changes to the example

I added deep link support by adding the following:

            <!-- Deep linking -->
            <meta-data android:name="flutter_deeplinking_enabled" android:value="true" />
            <intent-filter android:autoVerify="true">
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="bmr" android:host="beamer.dev" />
            </intent-filter>
        </activity>

to the examples/bottom_navigation_multiple_beamers/android/app/src/main/AndroidManifest.xml.

Full AndroidManifest.xml content
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.bottom_navigation_multiple_beamers">
   <application
        android:label="bottom_navigation_multiple_beamers"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTask"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <!-- Specifies an Android theme to apply to this Activity as soon as
                 the Android process has started. This theme is visible to the user
                 while the Flutter UI initializes. After that, this theme continues
                 to determine the Window background behind the Flutter UI. -->
            <meta-data
              android:name="io.flutter.embedding.android.NormalTheme"
              android:resource="@style/NormalTheme"
              />
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>

            <!-- Deep linking -->
            <meta-data android:name="flutter_deeplinking_enabled" android:value="true" />
            <intent-filter android:autoVerify="true">
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="bmr" android:host="beamer.dev" />
            </intent-filter>
        </activity>

        <!-- Don't delete the meta-data below.
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
    </application>
</manifest>

Current behaviour

On Android, when sending an intent from the terminal:

adb shell 'am start -W -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "bmr://beamer.dev/books/1"'

Makes the first tab (Books) present the Details page with the 1st book. 👍🏻
Unfortunately, the state of the other tab (Articles) is lost. 👎🏻

What I have already found

So far, I discovered that it happens becase the intent /books/1 is sent to the main Beamer, and it passes it (?) down to both BeamerDelegates (that are part of the AppScreenState):

  final routerDelegates = [
    BeamerDelegate(
      initialPath: '/books',
      locationBuilder: (routeInformation, _) {
        if (routeInformation.location!.contains('books')) {
          return BooksLocation(routeInformation);
        }
        print("Books, not found");
        return NotFound(path: routeInformation.location!);
      },
    ),
    BeamerDelegate(
      initialPath: '/articles',
      locationBuilder: (routeInformation, _) {
        if (routeInformation.location!.contains('articles')) {
          return ArticlesLocation(routeInformation);
        }
        print("Articles, not found");
        return NotFound(path: routeInformation.location!);
      },
    ),
  ];

Both locationBuilders are called. For /books/1, the first BeamerDelegate shows the detail page, but the second one has to return NotFound(path: routeInformation.location!); and its state is lost.

Expected behaviour + question

How can I ensure that only relevant BeamerDelegate's locationBuilder is called when handling a deep link intent?
In the case of my example, this would be BeamerDelegate for /books.

Shall I try another configuration, and specify the tabs' routes in the top-level routerDelegate?

late final routerDelegate = BeamerDelegate(
    initialPath: initialDeepLink ?? '/books',
    locationBuilder: RoutesLocationBuilder(
      routes: {
        '*': (context, state, data) => AppScreen(),
       // 'books` => ???
       // 'articles' => ???
      },
    ),
  );

Please let me know if I can provide any further details!
Thanks for any advice!

@annawidera annawidera changed the title [BottomTabNavigation + deep link into nested route in one tab] Q: How can I ensure state in the other tab is preserved? [BottomTabNavigation + deep link into nested route inside tab] Q: How can I ensure state of other tabs is preserved? Feb 13, 2023
@annawidera
Copy link
Author

Update: I discovered that using uni_link for reading in the deep link intent + opting out from flutter_deeplinking_enabled made it possible to orchestrate the traffic between tabs.

<meta-data android:name="flutter_deeplinking_enabled" android:value="false" />
class _AppScreenState extends State<AppScreen> {
  late int currentIndex;
  StreamSubscription? _sub;

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

    // listening to links stream from uni_links was added
    _sub = uriLinkStream.listen(
      (Uri? uri) {
        if (uri != null) {
          // Handcrafted uri parsing, and handling forwarded to the **right** delegate 
          if (uri.path.contains('books')) {
            routerDelegates[0].beamToNamed(uri.path);
            setState(() => currentIndex = 0);
          } else if (uri.path.contains('articles')) {
            routerDelegates[1].beamToNamed(uri.path);
            setState(() => currentIndex = 1);
          }

        }
      },
      onError: (err) {
        print("uriLinkStream error! $err");
        // Handle exception by warning the user their action did not succeed
      },
    );
  }

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

  // exactly as they were in example: 'bottom_navigation_multiple_beamers'
  final routerDelegates = [
    BeamerDelegate(
      initialPath: '/books',
      locationBuilder: (routeInformation, _) {
        if (routeInformation.location!.contains('books')) {
          return BooksLocation(routeInformation);
        }
        return NotFound(path: routeInformation.location!);
      },
    ),
    BeamerDelegate(
      initialPath: '/articles',
      locationBuilder: (routeInformation, _) {
        if (routeInformation.location!.contains('articles')) {
          return ArticlesLocation(routeInformation);
        }
        return NotFound(path: routeInformation.location!);
      },
    ),
  ];

  @override
  void didChangeDependencies() {
    // exactly as in example: 'bottom_navigation_multiple_beamers'
  }

  @override
  Widget build(BuildContext context) {
    // exactly as in example: 'bottom_navigation_multiple_beamers'
  }
}

Is this the way to do deep linking with multiple Beamers on Android?
On iOS, I didn't have to opt-out from FlutterDeepLinkingEnabled to make this work even without uni_links and manual URI parsing and picking the right routerDelegate for the deep link to handle.

Does anybody know where the difference is between delivering the deep links when FlutterDeepLinkingEnabled is enabled on iOS and Android? How may this confuse Beamer?

@dleurs
Copy link

dleurs commented Mar 25, 2023

Thank you that solved my problem !
Only difference is that I am using Cubit instead of Stateful widget to update index

@slovnicki
Copy link
Owner

Hey @annawidera 👋
Thanks for creating a wonderfully elaborated issue and sorry for my absence lately.
Also, thanks for the kind words 🙂

This is a good solution.
Nevertheless, working with tabs often causes confusion because they are tricky from routing perspective. That's why I'm planing to add additional support for tab control - #618

Another thing I can additionally suggest for preserving the state of tabs while the app is running and receives the link is the following:

Instead of

 routes: {
        '*': (context, state, data) => AppScreen(),
      },

we can do

 routes: {
        '*': (context, state, data) => BeamPage(
          key: ValueKey('app'),
          child: AppScreen(),
        ),
      },

which will prevent the rebuild of the entire app and therefore losing what the state of tabs (scroll, navigation, ...).
This works because Navigator will only rebuild the page if the key has changed. If we keep it constant, then we will never rebuild AppScreen, but will react to routes because we have listeners on root Beamer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question A question about usage
Projects
None yet
Development

No branches or pull requests

3 participants