底部导航栏,每个选项卡均带有子导航器 [英] bottom navigation bar with sub-navigators for each tab

查看:120
本文介绍了底部导航栏,每个选项卡均带有子导航器的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想在我的应用程序中有一个底部导航栏,其行为类似于以下内容:

I want to have a bottom navigation bar in my app which behaves like followed:

  1. 每个选项卡都应具有自己的嵌套导航器,这样我就可以在保持bottomnavbar的同时切换到子路由
  2. 切换到另一个标签时,我希望能够使用后退"按钮返回到上一个标签
  3. 当我再次点击选项卡1时,我不想再次实例化该子路由,但是我想回到它的最后状态..例如,如果我在路由'/tab1/subRoute1'上,我想着陆再次在此视图上

我能够达到1& 2,但我停留在第3点. 这是我的构造:

I was able to achieve 1 & 2, but I am stuck on point 3. Here is my construct:

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Bottom NavBar Demo',
      home: BottomNavigationBarController(),
    );
  }
}

class BottomNavigationBarController extends StatefulWidget {
  BottomNavigationBarController({Key key}) : super(key: key);

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

class _BottomNavigationBarControllerState
    extends State<BottomNavigationBarController> {
  int _selectedIndex = 0;
  List<int> _history = [0];
  GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();

  final List<BottomNavigationBarRootItem> bottomNavigationBarRootItems = [
    BottomNavigationBarRootItem(
      routeName: '/',
      nestedNavigator: HomeNavigator(
        navigatorKey: GlobalKey<NavigatorState>(),
      ),
      bottomNavigationBarItem: BottomNavigationBarItem(
        icon: Icon(Icons.home),
        title: Text('Home'),
      ),
    ),
    BottomNavigationBarRootItem(
      routeName: '/settings',
      nestedNavigator: SettingsNavigator(
        navigatorKey: GlobalKey<NavigatorState>(),
      ),
      bottomNavigationBarItem: BottomNavigationBarItem(
        icon: Icon(Icons.home),
        title: Text('Settings'),
      ),
    ),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: WillPopScope(
        onWillPop: () async {
          final nestedNavigatorState =
              bottomNavigationBarRootItems[_selectedIndex]
                  .nestedNavigator
                  .navigatorKey
                  .currentState;

          if (nestedNavigatorState.canPop()) {
            nestedNavigatorState.pop();
            return false;
          } else if (_navigatorKey.currentState.canPop()) {
            _navigatorKey.currentState.pop();
            return false;
          }
          return true;
        },
        child: Navigator(
          key: _navigatorKey,
          initialRoute: bottomNavigationBarRootItems.first.routeName,
          onGenerateRoute: (RouteSettings settings) {
            WidgetBuilder builder;

            builder = (BuildContext context) {
              return bottomNavigationBarRootItems
                  .where((element) => element.routeName == settings.name)
                  .first
                  .nestedNavigator;
            };

            return MaterialPageRoute(
              builder: builder,
              settings: settings,
            );
          },
        ),
      ),
      bottomNavigationBar: BottomNavigationBar(
        items: bottomNavigationBarRootItems
            .map((e) => e.bottomNavigationBarItem)
            .toList(),
        currentIndex: _selectedIndex,
        selectedItemColor: Colors.amber[800],
        onTap: _onItemTapped,
      ),
    );
  }

  void _onItemTapped(int index) {
    if (index == _selectedIndex) return;
    setState(() {
      _selectedIndex = index;
      _history.add(index);
      _navigatorKey.currentState
          .pushNamed(bottomNavigationBarRootItems[_selectedIndex].routeName)
          .then((_) {
        _history.removeLast();
        setState(() => _selectedIndex = _history.last);
      });
    });
  }
}

class BottomNavigationBarRootItem {
  final String routeName;
  final NestedNavigator nestedNavigator;
  final BottomNavigationBarItem bottomNavigationBarItem;

  BottomNavigationBarRootItem({
    @required this.routeName,
    @required this.nestedNavigator,
    @required this.bottomNavigationBarItem,
  });
}

abstract class NestedNavigator extends StatelessWidget {
  final GlobalKey<NavigatorState> navigatorKey;

  NestedNavigator({Key key, @required this.navigatorKey}) : super(key: key);
}

class HomeNavigator extends NestedNavigator {
  HomeNavigator({Key key, @required GlobalKey<NavigatorState> navigatorKey})
      : super(
          key: key,
          navigatorKey: navigatorKey,
        );

  @override
  Widget build(BuildContext context) {
    return Navigator(
      key: navigatorKey,
      initialRoute: '/',
      onGenerateRoute: (RouteSettings settings) {
        WidgetBuilder builder;
        switch (settings.name) {
          case '/':
            builder = (BuildContext context) => HomePage();
            break;
          case '/home/1':
            builder = (BuildContext context) => HomeSubPage();
            break;
          default:
            throw Exception('Invalid route: ${settings.name}');
        }
        return MaterialPageRoute(
          builder: builder,
          settings: settings,
        );
      },
    );
  }
}

class SettingsNavigator extends NestedNavigator {
  SettingsNavigator({Key key, @required GlobalKey<NavigatorState> navigatorKey})
      : super(
          key: key,
          navigatorKey: GlobalKey<NavigatorState>(),
        );

  @override
  Widget build(BuildContext context) {
    return Navigator(
      key: navigatorKey,
      initialRoute: '/',
      onGenerateRoute: (RouteSettings settings) {
        WidgetBuilder builder;
        switch (settings.name) {
          case '/':
            builder = (BuildContext context) => SettingsPage();
            break;
          default:
            throw Exception('Invalid route: ${settings.name}');
        }
        return MaterialPageRoute(
          builder: builder,
          settings: settings,
        );
      },
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Home Page'),
      ),
      body: Center(
        child: RaisedButton(
          onPressed: () => Navigator.of(context).pushNamed('/home/1'),
          child: Text('Open Sub-Page'),
        ),
      ),
    );
  }
}

class HomeSubPage extends StatefulWidget {
  const HomeSubPage({Key key}) : super(key: key);

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

class _HomeSubPageState extends State<HomeSubPage> {
  String _text;

  @override
  void initState() {
    _text = 'Click me';
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Home Sub Page'),
      ),
      body: Center(
        child: RaisedButton(
          onPressed: () => setState(() => _text = 'Clicked'),
          child: Text(_text),
        ),
      ),
    );
  }
}

class SettingsPage extends StatelessWidget {
  const SettingsPage({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Settings Page'),
      ),
      body: Container(
        child: Center(
          child: Text('Settings Page'),
        ),
      ),
    );
  }
}

运行此代码时,然后单击打开子页面"->单击单击我",您应该在主页子页面"中看到已单击". 如果现在单击底部导航栏中的设置",然后使用android后退按钮,则返回主页"选项卡,显示与按钮说已单击"的页面完全相同. 如果您单击底部导航栏中的设置",然后单击主页",则您将再次位于按钮显示为已单击"的完全相同的页面上. 那正是我需要的行为 但是,当您执行后者时,您还会收到一条错误消息:在小部件树中检测到重复的GlobalKey".而且,如果您现在两次点击android后退按钮,您将进入一个空白页面(出于种种原因). 如何避免这种重复的全局密钥错误而又不会失去我想要的行为?

When you run this code, then tap "Open Sub-Page" -> tap "Click me" you should see "Clicked" in "Home Sub Page". if you now click on "Settings" in the bottom nav bar and then use the android back button you are back on "Home"-tab showing the exact same page where the button says "Clicked". if you click on "Settings" and then on "Home" in the bottom nav bar you are again on the exact same page where the button says "clicked". That is exactly the behaviour I need BUT when you do the latter you also get an error saying "Duplicate GlobalKey detected in widget tree.". And if you now tap the android back button twice you land on an empty page (out of obious reasons). How can I avoid this Duplicate Global Key Error without losing my desired behaviour?

我希望我的解释是有道理的.

I hope my explanation makes sense ..

可以完美实现此功能的示例应用程序是Instagram.

An example app where this is implemented perfectly is Instagram.

这与以下内容有关:具有命名路线的抖动持久导航栏?

推荐答案

您想要使导航选项卡像Twitter,Instagram,应用程序一样,因此每个选项卡都具有自己的导航历史记录和独家新闻
我想我了解您想要实现的目标,但是您做错了 您应该对'ScoopWillPop'中的标签内容使用'tabBarView',并让每个标签管理自己的导航历史记录,在对我的一个项目进行了如此多的努力之后,我发现了实现此想法的最佳方法
我对您的代码进行了许多更改,我希望您要弄清楚

you want make navigation tabs just like twitter , Instagram, apps so every tab has it own navigation history and scoop
i guess i understand what you want achieve , but you do it in a wrong way you should use 'tabBarView' for tabs content inside 'ScoopWillPop' and make every tab manage its own navigate history , after so many hard work on one of my projects i found the best way to implement this idea
i made many changes on your code , i hope to be clear

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Bottom NavBar Demo',
      home: BottomNavigationBarController(),
    );
  }
}

class BottomNavigationBarController extends StatefulWidget {
  BottomNavigationBarController({Key key}) : super(key: key);

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

class _BottomNavigationBarControllerState
    extends State<BottomNavigationBarController> with SingleTickerProviderStateMixin{
  int _selectedIndex = 0;
  List<int> _history = [0];
  GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
  TabController _tabController;
  List<Widget> mainTabs;
  List<BuildContext> navStack = [null, null]; // one buildContext for each tab to store history  of navigation

  @override
  void initState() {
    _tabController = TabController(vsync: this, length: 2);
    mainTabs = <Widget>[
      Navigator(
          onGenerateRoute: (RouteSettings settings){
            return PageRouteBuilder(pageBuilder: (context, animiX, animiY) { // use page PageRouteBuilder instead of 'PageRouteBuilder' to avoid material route animation
              navStack[0] = context;
              return HomePage();
            });
          }),
      Navigator(
          onGenerateRoute: (RouteSettings settings){
            return PageRouteBuilder(pageBuilder: (context, animiX, animiY) {  // use page PageRouteBuilder instead of 'PageRouteBuilder' to avoid material route animation
              navStack[1] = context;
              return SettingsPage();
            });
          }),
    ];
    super.initState();
  }

  final List<BottomNavigationBarRootItem> bottomNavigationBarRootItems = [
    BottomNavigationBarRootItem(
      bottomNavigationBarItem: BottomNavigationBarItem(
        icon: Icon(Icons.home),
        title: Text('Home'),
      ),
    ),
    BottomNavigationBarRootItem(
      bottomNavigationBarItem: BottomNavigationBarItem(
        icon: Icon(Icons.settings),
        title: Text('Settings'),
      ),
    ),
  ];


  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      child: Scaffold(
        body: TabBarView(
          controller: _tabController,
          physics: NeverScrollableScrollPhysics(),
          children: mainTabs,
        ),
        bottomNavigationBar: BottomNavigationBar(
          items: bottomNavigationBarRootItems.map((e) => e.bottomNavigationBarItem).toList(),
          currentIndex: _selectedIndex,
          selectedItemColor: Colors.amber[800],
          onTap: _onItemTapped,
        ),
      ),
      onWillPop: () async{
        if (Navigator.of(navStack[_tabController.index]).canPop()) {
          Navigator.of(navStack[_tabController.index]).pop();
          setState((){ _selectedIndex = _tabController.index; });
          return false;
        }else{
          if(_tabController.index == 0){
            setState((){ _selectedIndex = _tabController.index; });
            SystemChannels.platform.invokeMethod('SystemNavigator.pop'); // close the app
            return true;
          }else{
            _tabController.index = 0; // back to first tap if current tab history stack is empty
            setState((){ _selectedIndex = _tabController.index; });
            return false;
          }
        }
      },
    );
  }

  void _onItemTapped(int index) {
    _tabController.index = index;
    setState(() => _selectedIndex = index);
  }

}

class BottomNavigationBarRootItem {
  final String routeName;
  final NestedNavigator nestedNavigator;
  final BottomNavigationBarItem bottomNavigationBarItem;

  BottomNavigationBarRootItem({
    @required this.routeName,
    @required this.nestedNavigator,
    @required this.bottomNavigationBarItem,
  });
}

abstract class NestedNavigator extends StatelessWidget {
  final GlobalKey<NavigatorState> navigatorKey;

  NestedNavigator({Key key, @required this.navigatorKey}) : super(key: key);
}

class HomeNavigator extends NestedNavigator {
  HomeNavigator({Key key, @required GlobalKey<NavigatorState> navigatorKey})
      : super(
    key: key,
    navigatorKey: navigatorKey,
  );

  @override
  Widget build(BuildContext context) {
    return Navigator(
      key: navigatorKey,
      initialRoute: '/',
      onGenerateRoute: (RouteSettings settings) {
        WidgetBuilder builder;
        switch (settings.name) {
          case '/':
            builder = (BuildContext context) => HomePage();
            break;
          case '/home/1':
            builder = (BuildContext context) => HomeSubPage();
            break;
          default:
            throw Exception('Invalid route: ${settings.name}');
        }
        return MaterialPageRoute(
          builder: builder,
          settings: settings,
        );
      },
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Home Page'),
      ),
      body: Center(
        child: RaisedButton(
          onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => HomeSubPage())),
          child: Text('Open Sub-Page'),
        ),
      ),
    );
  }
}

class HomeSubPage extends StatefulWidget {
  const HomeSubPage({Key key}) : super(key: key);

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

class _HomeSubPageState extends State<HomeSubPage> with AutomaticKeepAliveClientMixin{
  @override
  // implement wantKeepAlive
  bool get wantKeepAlive => true;


  String _text;

  @override
  void initState() {
    _text = 'Click me';
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Home Sub Page'),
      ),
      body: Center(
        child: RaisedButton(
          onPressed: () => setState(() => _text = 'Clicked'),
          child: Text(_text),
        ),
      ),
    );
  }

}

/* convert it to statfull so i can use AutomaticKeepAliveClientMixin to avoid disposing tap */

class SettingsPage extends StatefulWidget {
  @override
  _SettingsPageState createState() => _SettingsPageState();
}

class _SettingsPageState extends State<SettingsPage> with AutomaticKeepAliveClientMixin{

  @override
  // implement wantKeepAlive
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Settings Page'),
      ),
      body: Container(
        child: Center(
          child: Text('Settings Page'),
        ),
      ),
    );
  }

}

这篇关于底部导航栏,每个选项卡均带有子导航器的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆