Milad-Akarie / auto_route_library

Flutter route generator
MIT License
1.58k stars 400 forks source link

Question: Push nested route from nested route? Keep the navigation state? #136

Closed JRamos29 closed 4 years ago

JRamos29 commented 4 years ago

Hi Milad, regarding the #66 question, I'd like to implement a BottomNavigationBar using AutoRoute, and I like to know what's would be a good approach, considering that i have some cases in which from a nested page from a bar item, i need to navigate to another nested page from other bar item. Can I navigate directly from a nested route to another, or I need to get the reference from a specific root navigator from each bar item?

I believe that feature its related to the #112 with deep link navigation, by example I'd like to navigate from /page1/page_list to /page2/page_detail/:id, or using a root router like /rootRouter/page1/page_list call /rootRouter/page2/page_detail/:id.

And it's possible to keep the nested navigation state? I mean, for a specific bottomNavigationBar page, when i go to a second page, click another bottomBar page, and then back to the previous item, i got the initial page and not the second page. There's some way to keep the page that i was before?

Milad-Akarie commented 4 years ago

Hey @JRamos29, sorry for the delay, I've been under the weather lately. I don't believe your goal is doable using deep linking since pushing a deep link means pushing all the segments leading to your final destination ("/", "/page2","page_detail/:id") -> You're gonna need to pop all three pages to go back to /page1/page_list which can be confusing to users in my opinion.

You could try to create global keys for all of your nested navigators above your TabView widget
I'm thinking something like this

class HomeScreenState extends State<HomeScreen> {
  final navigatorKeys = <String, GlobalKey>{
    "tab1": GlobalKey<ExtendedNavigatorState>(),
    "tab2": GlobalKey<ExtendedNavigatorState>(),
    "tab3": GlobalKey<ExtendedNavigatorState>(),
  };

  // use this to access the navigators inside tabs not outside
  static ExtendedNavigatorState navigatorOf(BuildContext context, String tabKey) {
    return context.findAncestorStateOfType<HomeScreenState>()?.navigatorKeys[tabKey]?.currentState;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: TabBarView(
        children: <Widget>[
          ExtendedNavigator(
            router: NestedRouter1(),
            key: navigatorKeys['tab1'],
          ),
          ExtendedNavigator(
            router: NestedRouter2(),
            key: navigatorKeys['tab2'],
          ),
          ExtendedNavigator(
            router: NestedRouter3(),
            key: navigatorKeys['tab3'],
          )
        ],
      ),
    );
  }
}
JRamos29 commented 4 years ago

Hi Milad. Thanks for the answer and this great package.

I implemented my tabs as you said, and it's working. But when i try to push some route of another tab, i'm getting an error message saying that the navigator is null:

flutter: ══╡ EXCEPTION CAUGHT BY GESTURE ╞═══════════════════════════════════════════════════════════════════
flutter: The following NoSuchMethodError was thrown while handling a gesture:
flutter: The method 'pushNamed' was called on null.
flutter: Receiver: null
flutter: Tried calling: pushNamed<Object>("/post-detail")

Here is how i'm trying to push the route. I made just a little change in the map of keys using a enum instead of a string for the tabs:

import 'package:auto_route/auto_route.dart';
import 'package:auto_route_example/screens/home/bottom_bar_item.dart';
import 'package:auto_route_example/screens/home/home_page.dart';
import 'package:auto_route_example/screens/post/post_nested_router.gr.dart'
    as postNestedRouter;
import 'package:auto_route_example/screens/post/post_nested_router.gr.dart';
import 'package:auto_route_example/screens/profile/profile_nested_router.gr.dart';
import 'profile_nested_router.gr.dart' as profileNestedRouter;

import 'package:flutter/material.dart';

class ProfileView extends StatelessWidget {
  final String profileId;

  ProfileView({this.profileId = 'default'});

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        FlatButton(
          child: Text("Profile View"),
          onPressed: () {
            HomePageState.navigatorOf(context, BottomBarItemType.POST)
                .pushNamed(postNestedRouter.PostNestedRoutes.postDetail);
          },
        ),
      ],
    );
  }
}
Milad-Akarie commented 4 years ago

make sure you're using the passed context not the HomePage's context..rename it to ctx if you have to

  static ExtendedNavigatorState navigatorOf(BuildContext ctx, String tabKey) {
    return ctx.findAncestorStateOfType<HomeScreenState>()?.navigatorKeys[tabKey]?.currentState;
  }

also make sure you're passing the keys to the navigators

      ExtendedNavigator(
            router: NestedRouter1(),
            key: navigatorKeys['tab1'],
          ),

maybe you're calling the keys before they're initialized?

JRamos29 commented 4 years ago

I'm initializing the keys and the nestedNavigator in the initState of my HomePageState. In the code above i'm trying to call a route from another tab. By example, Profile is a Tab and Post is another tab. I can do this?

Here is my HomePage:

import 'package:auto_route/auto_route.dart';
import 'package:auto_route_example/screens/activity/activity_nested_router.gr.dart';
import 'package:auto_route_example/screens/post/post_nested_router.gr.dart';
import 'package:auto_route_example/screens/profile/profile_nested_router.gr.dart';
import 'package:auto_route_example/screens/settings/settings_nested_router.gr.dart';
import 'package:flutter/material.dart';

import 'bottom_bar_item.dart';

class HomePage extends StatefulWidget {
  final String title;
  const HomePage({Key key, this.title = "Main Page"}) : super(key: key);

  @override
  HomePageState createState() => HomePageState();
}

class HomePageState extends State<HomePage> {
  Map<BottomBarItemType, GlobalKey<NavigatorState>> _navigatorKeys;
  // Map<BottomBarItemType, WidgetModule> _navigatorModules;
  List _nestedNavigators;
  BottomBarItemType _currentTab = BottomBarItemType.ACTIVITY;
  int controllerIndex = 0;

  initState() {
    _navigatorKeys = {
      BottomBarItemType.ACTIVITY: GlobalKey<ExtendedNavigatorState>(),
      BottomBarItemType.POST: GlobalKey<ExtendedNavigatorState>(),
      BottomBarItemType.PROFILE: GlobalKey<ExtendedNavigatorState>(),
      BottomBarItemType.SETTINGS: GlobalKey<ExtendedNavigatorState>(),
    };

    _nestedNavigators = [
      ExtendedNavigator(
        router: ActivityNestedRouter(),
        key: _navigatorKeys[BottomBarItemType.ACTIVITY],
      ),

      ExtendedNavigator(
        router: PostNestedRouter(),
        key: _navigatorKeys[BottomBarItemType.POST],
      ),

      ExtendedNavigator(
        router: ProfileNestedRouter(),
        key: _navigatorKeys[BottomBarItemType.PROFILE],
      ),
      ExtendedNavigator(
        router: SettingsNestedRouter(),
        key: _navigatorKeys[BottomBarItemType.SETTINGS],
      ),

      // HomeContentPage(),
    ];
    super.initState();
  }

  // use this to access the navigators inside tabs not outside
  static ExtendedNavigatorState navigatorOf(
      BuildContext ctx, BottomBarItemType tabKey) {
    return ctx
        .findAncestorStateOfType<HomePageState>()
        ?._navigatorKeys[tabKey]
        ?.currentState;
  }

  void _selectTab(BottomBarItemType tabItem) {
    setState(() => _currentTab = tabItem);
  }

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () async {
        final isFirstRouteInCurrentTab =
            !await _navigatorKeys[_currentTab].currentState.maybePop();
        if (isFirstRouteInCurrentTab) {
          // if not on the 'main' tab
          if (_currentTab != BottomBarItemType.ACTIVITY) {
            // select 'main' tab
            _selectTab(BottomBarItemType.ACTIVITY);
            // back button handled by app
            return false;
          }
        }
        // let system handle back button if we're on the first route
        return isFirstRouteInCurrentTab;
      },
      child: Scaffold(
        body: _nestedNavigators.elementAt(controllerIndex),
        bottomNavigationBar: bottomNavigationBar(),
      ),
    );
  }

  Widget bottomNavigationBar() {
    return BottomNavigationBar(
        backgroundColor: Colors.white,
        showSelectedLabels: true,
        type: BottomNavigationBarType.fixed,
        items: [
          _buildItem(item: BottomBarItem.fromType(BottomBarItemType.ACTIVITY)),
          _buildItem(item: BottomBarItem.fromType(BottomBarItemType.POST)),
          _buildItem(item: BottomBarItem.fromType(BottomBarItemType.PROFILE)),
          _buildItem(item: BottomBarItem.fromType(BottomBarItemType.SETTINGS)),
        ],
        onTap: (index) {
          _selectTab(
            BottomBarItemType.values[index],
          );
          controllerIndex = index;
        });
  }

  BottomNavigationBarItem _buildItem({BottomBarItem item}) {
    return BottomNavigationBarItem(
      icon: Icon(
        item.icon,
        color: _colorTabMatching(item: item),
      ),
      title: Text(
        item.tabName,
        style: TextStyle(
          color: _colorTabMatching(item: item),
        ),
      ),
    );
  }

  Color _colorTabMatching({BottomBarItem item}) {
    return _currentTab == item.itemType ? item.tabColor : Colors.grey;
  }
}
Milad-Akarie commented 4 years ago

Everything looks fine actually can you debug this method and see if it's HomePageState that's null or the keys?

 static ExtendedNavigatorState navigatorOf(
      BuildContext ctx, BottomBarItemType tabKey) {
var homeState = ctx
        .findAncestorStateOfType<HomePageState>()
print(homeState);
    return homeState.
        ?._navigatorKeys[tabKey]
        ?.currentState;
  }
JRamos29 commented 4 years ago

The homeState and the Keys are ok. Just the currentState for the given key is returning null. It could be because i'm getting just one NestedRouter each time in the Scaffold's body like you can see in the code bellow?

When i press the bottom navigation bar, it get a specific router, which can let the others with state equals null, even if they are instantiated in the map on the initState?

Scaffold(
        body: _nestedNavigators.elementAt(controllerIndex),
        bottomNavigationBar: bottomNavigationBar(),
)
Milad-Akarie commented 4 years ago

Hey @JRamos29 I've been working on publishing the new auto_route 0.6.0 lately so forgive my delay. I've created a demo app to demonstrate how you can implement parallel navigation using auto_route Parallel Navigation Demo hope this helps!

JRamos29 commented 4 years ago

Hi @Milad-Akarie, Yes, that is a great example, mainly because the navigation from the Post tab to Profile tab, and without loss the bottom navigation bar. I really appreciated you help and attention. I believe you could add this example in the auto-route examples also, because there's many questions on stack overflow about how to do navigation in this way. I noticed that are a lot of new features in the 0.6.0 version. Congratulations for the great work. Tnks very much.

II11II commented 2 years ago

Hi @Milad-Akarie, ExtendedNavigator is not available in the new version of the package. Is there replacement for this widget?