底部导航栏,每个选项卡均带有子导航器

时间:2020-03-03 21:07:58

标签: flutter

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

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

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

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后退按钮,您将进入一个空白页面(出于种种原因)。 如何避免这种重复的全局密钥错误而又不会丢失我想要的行为?

我希望我的解释是合理的..

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

这与:Flutter persistent navigation bar with named routes?

3 个答案:

答案 0 :(得分:5)

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

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'),
        ),
      ),
    );
  }

}

答案 1 :(得分:0)

您只需要使用nestedNavigator

    child: bottomNavigationBarRootItems[_selectedIndex].nestedNavigator,

删除父导航器

//  child: Navigator(
//           key: _navigatorKey,
//           initialRoute: bottomNavigationBarRootItems.first.routeName,
//           onGenerateRoute: (RouteSettings settings) {
//             WidgetBuilder builder;
//             builder = (BuildContext context) {
//               return bottomNavigationBarRootItems
//                   .where((element) => element.routeName ==settings.name)
//                   .last
//                   .nestedNavigator;
//             };
//              builder = (BuildContext context) => HomePage();
//             return MaterialPageRoute(
//               builder: builder,
//               settings: settings,
//             );
//           },
//         ),
  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);
//       });
    });
  }

答案 2 :(得分:0)

通常,每当您切换到另一个选项卡时,选项卡的状态都会丢失,我之前使用过Notes应用程序,它的底部导航栏可以在两者之间进行切换,我设法保存每个选项卡的状态甚至更新数据其他选项卡,然后再仅使用数据库切换到其他选项卡(如果这样做不能帮助尝试使用共享首选项)。希望对您有所帮助! 祝你好运!