【问题标题】:Flutter update BottomNavigationBarFlutter 更新 BottomNavigationBar
【发布时间】:2018-04-11 17:31:03
【问题描述】:

我将 BottomNavigationBar 与 TabController 一起使用。 通过单击 BottomNavigationBar 的不同选项卡,TabView 正在更改内容。 但是,如果我在 TabView 上滑动以切换到另一个视图/选项卡,BottomNavigationBar 不会更新到我滑动到的选项卡。 我已经向 TabController 添加了一个侦听器来检测更改。 但是如何以编程方式更新 BottomNavigationBar 以反映更改?

【问题讨论】:

    标签: dart flutter bottomnavigationview


    【解决方案1】:

    我认为在你的情况下使用PageView 而不是TabBarView 更优雅。

    class BottomBarExample extends StatefulWidget {
      @override
      _BottomBarExampleState createState() => new _BottomBarExampleState();
    }
    
    class _BottomBarExampleState extends State<BottomBarExample> {
      int _page = 0;
      PageController _c;
      @override
      void initState(){
        _c =  new PageController(
          initialPage: _page,
        );
        super.initState();
      }
      @override
      Widget build(BuildContext context) {
        return new Scaffold(
          bottomNavigationBar: new BottomNavigationBar(
            currentIndex: _page,
            onTap: (index){
              this._c.animateToPage(index,duration: const Duration(milliseconds: 500),curve: Curves.easeInOut);
            },
            items: <BottomNavigationBarItem>[
            new BottomNavigationBarItem(icon: new Icon(Icons.supervised_user_circle), title: new Text("Users")),
            new BottomNavigationBarItem(icon: new Icon(Icons.notifications), title: new Text("Alerts")),
            new BottomNavigationBarItem(icon: new Icon(Icons.email), title: new Text("Inbox")),
    
          ],
    
          ),
          body: new PageView(
            controller: _c,
            onPageChanged: (newPage){
              setState((){
                this._page=newPage;
              });
            },
            children: <Widget>[
              new Center(
                child: new Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    new Icon(Icons.supervised_user_circle),
                    new Text("Users")
                  ],
                ),
              ),
              new Center(
                child: new Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    new Icon(Icons.notifications),
                    new Text("Alerts")
                  ],
                ),
              ),
              new Center(
                child: new Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    new Icon(Icons.mail),
                    new Text("Inbox")
                  ],
                ),
              ),
            ],
          ),
        );
      }
    }
    

    【讨论】:

    • 很好的解决方案!
    【解决方案2】:

    完整答案在这里:https://stackoverflow.com/a/63258130/10563627

    先做一个类MyBottomBarDemo

    class MyBottomBarDemo extends StatefulWidget {
      @override
      _MyBottomBarDemoState createState() => new _MyBottomBarDemoState();
    }
    
    class _MyBottomBarDemoState extends State<MyBottomBarDemo> {
      int _pageIndex = 0;
      PageController _pageController;
    
      List<Widget> tabPages = [
        Screen1(),
        Screen2(),
        Screen3(),
      ];
    
      @override
      void initState(){
        super.initState();
        _pageController = PageController(initialPage: _pageIndex);
      }
    
      @override
      void dispose() {
        _pageController.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text("BottomNavigationBar", style: TextStyle(color: Colors.white)),
            backgroundColor: Colors.deepPurple,
          ),
          bottomNavigationBar: BottomNavigationBar(
            currentIndex: _pageIndex,
            onTap: onTabTapped,
            backgroundColor: Colors.white,
            items: <BottomNavigationBarItem>[
              BottomNavigationBarItem( icon: Icon(Icons.home), title: Text("Home")),
              BottomNavigationBarItem(icon: Icon(Icons.mail), title: Text("Messages")),
              BottomNavigationBarItem(icon: Icon(Icons.person), title: Text("Profile")),
            ],
    
          ),
          body: PageView(
            children: tabPages,
            onPageChanged: onPageChanged,
            controller: _pageController,
          ),
        );
      }
      void onPageChanged(int page) {
        setState(() {
          this._pageIndex = page;
        });
      }
    
      void onTabTapped(int index) {
        this._pageController.animateToPage(index,duration: const Duration(milliseconds: 500),curve: Curves.easeInOut);
      }
    }
    

    然后创建一个你的屏幕

    class Screen1 extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Container(
            color: Colors.green,
                child: Center(child: Text("Screen 1")),
        );
      }
    }
    
     screen 2... Screen 3...
    

    【讨论】:

      【解决方案3】:

      当你在 TabView 上滑动时,你应该更新 bottomNavigationBar 的当前索引以匹配 tabview 的新索引

      class _TabBarWithBottomNavBarState extends State<TabBarWithBottomNavBar> with SingleTickerProviderStateMixin {
        int _bottomNavBarIndex = 0;
        TabController _tabController;
        void _tabControllerListener(){
          setState(() {
            _bottomNavBarIndex = _tabController.index;
          });
        }
      
      
       @override
        void initState() {
          _tabController = TabController(length: 3, vsync: this);
          _tabController.addListener(_tabControllerListener);
          super.initState();
        }
      
        @override
        void dispose() {
          _tabController.dispose();
          _tabController.removeListener(_tabControllerListener);
          super.dispose();
        }
      

      【讨论】:

        【解决方案4】:

        BottomNavigationBar 的另一个视角

        许多人甚至官方文档面对额外导航的方式与 Web Navigation 不同。它们不会呈现具有可用链接的通用和/或部分通用模板,以在单击后执行。相反,他们正在实现的是在额外的“tabs_screen”中预加载不同的屏幕(基本上只是一个额外的 Scaffold,一次包装和呈现一个屏幕),然后,根据 onTap 上的给定索引,甚至是 BottomNavigationBar,您将使用该索引来确定哪个屏幕将在该脚手架内实际呈现。就是这样。

        相反,我建议以这种方式处理额外导航:

        我像往常一样添加了 BottomNavigationBar,但我没有在 List _pages 上预加载/实例化屏幕,我所做的只是为 FavoritesScreen 添加另一个命名路由,

        我设计了我的底部导航栏:

        bottomNavigationBar: BottomNavigationBar(
          onTap: (index) => onTapSelectNavigation(index, context),
          items: [
            BottomNavigationBarItem(
              icon: Icon(
                Icons.category,
              ),
              label: '',
              activeIcon: Icon(
                Icons.category,
                color: Colors.red,
              ),
              tooltip: 'Categories',
            ),
            BottomNavigationBarItem(
              icon: Icon(
                Icons.star,
              ),
              label: '',
              activeIcon: Icon(
                Icons.star,
                color: Colors.red,
              ),
              tooltip: 'Favorites',
            ),
          ],
          currentIndex: _activeIndex,
        ),
        

        然后在 onTap 上,我什至调用了 onTapSelectNavigation 方法。

        在该方法中,根据给定的点击标签索引,我将决定调用哪条路由。这个 BottomNavigationBar 可用于整个应用程序。为什么?因为我在我的 FeeddyScaffold 上实现了这一点,这对于所有屏幕都是通用的,因为 FeeddyScaffold 将所有内部小部件包装在所有这些屏幕上。在每个屏幕上,我都会实例化 FeeddyScaffold,将 Widget 列表作为命名参数传递。这样,我保证脚手架对于每个屏幕都是通用的,如果我实现了该通用脚手架的额外导航,那么它将可用于屏幕。这是我的 FeeddyScaffold 组件:

        // Packages:
        import 'package:feeddy_flutter/_inner_packages.dart';
        import 'package:feeddy_flutter/_external_packages.dart';
        
        // Screens:
        import 'package:feeddy_flutter/screens/_screens.dart';
        
        // Models:
        import 'package:feeddy_flutter/models/_models.dart';
        
        // Components:
        import 'package:feeddy_flutter/components/_components.dart';
        
        // Helpers:
        import 'package:feeddy_flutter/helpers/_helpers.dart';
        
        // Utilities:
        import 'package:feeddy_flutter/utilities/_utilities.dart';
        
        class FeeddyScaffold extends StatefulWidget {
          // Properties:
          final bool _showPortraitOnly = false;
          final String appTitle;
          final Function onPressedAdd;
          final String objectName;
          final int objectsLength;
          final List<Widget> innerWidgets;
          final int activeIndex;
        
          // Constructor:
          const FeeddyScaffold({
            Key key,
            this.appTitle,
            this.onPressedAdd,
            this.objectName,
            this.objectsLength,
            this.innerWidgets,
            this.activeIndex,
          }) : super(key: key);
        
          @override
          _FeeddyScaffoldState createState() => _FeeddyScaffoldState();
        }
        
        class _FeeddyScaffoldState extends State<FeeddyScaffold> {
          final bool _showPortraitOnly = false;
          int _activeIndex;
        
          @override
          void initState() {
            // TODO: implement initState
            super.initState();
            _activeIndex = widget.activeIndex;
          }
        
          void onTapSelectNavigation(int index, BuildContext context) {
            switch (index) {
              case 0:
                Navigator.pushNamed(context, FoodCategoryIndexScreen.screenId);
                break;
              case 1:
                Navigator.pushNamed(context, FavoritesScreen.screenId);
                break;
            }
          }
        
          @override
          Widget build(BuildContext context) {
            AppData appData = Provider.of<AppData>(context, listen: true);
            Function closeAllThePanels = appData.closeAllThePanels; // Drawer related:
            bool deviceIsIOS = DeviceHelper.deviceIsIOS(context);
        
            // WidgetsFlutterBinding.ensureInitialized(); // Without this it might not work in some devices:
            SystemChrome.setPreferredOrientations([
              DeviceOrientation.portraitUp,
              // DeviceOrientation.portraitDown,
              if (!_showPortraitOnly) ...[
                DeviceOrientation.landscapeLeft,
                DeviceOrientation.landscapeRight,
              ],
            ]);
        
            FeeddyAppBar appBar = FeeddyAppBar(
              appTitle: widget.appTitle,
              onPressedAdd: widget.onPressedAdd,
              objectName: widget.objectName,
            );
        
            return Scaffold(
              appBar: appBar,
              onDrawerChanged: (isOpened) {
                if (!isOpened) {
                  closeAllThePanels();
                }
              },
        
              drawer: FeeddyDrawer(),
        
              body: NativeDeviceOrientationReader(
                builder: (context) {
                  final orientation = NativeDeviceOrientationReader.orientation(context);
                  bool safeAreaLeft = DeviceHelper.isLandscapeLeft(orientation);
                  bool safeAreaRight = DeviceHelper.isLandscapeRight(orientation);
                  bool isLandscape = DeviceHelper.isLandscape(orientation);
        
                  return SafeArea(
                    left: safeAreaLeft,
                    right: safeAreaRight,
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.spaceAround,
                      crossAxisAlignment: CrossAxisAlignment.stretch,
                      children: widget.innerWidgets,
                    ),
                  );
                },
              ),
        
              bottomNavigationBar: BottomNavigationBar(
                onTap: (index) => onTapSelectNavigation(index, context),
                items: [
                  BottomNavigationBarItem(
                    icon: Icon(
                      Icons.category,
                    ),
                    label: '',
                    activeIcon: Icon(
                      Icons.category,
                      color: Colors.red,
                    ),
                    tooltip: 'Categories',
                  ),
                  BottomNavigationBarItem(
                    icon: Icon(
                      Icons.star,
                    ),
                    label: '',
                    activeIcon: Icon(
                      Icons.star,
                      color: Colors.red,
                    ),
                    tooltip: 'Favorites',
                  ),
                ],
                currentIndex: _activeIndex,
              ),
        
              // FAB
              floatingActionButton: deviceIsIOS
                  ? null
                  : FloatingActionButton(
                      tooltip: 'Add ${widget.objectName.inCaps}',
                      child: Icon(Icons.add),
                      onPressed: () => widget.onPressedAdd,
                    ),
              // floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
              floatingActionButtonLocation: deviceIsIOS ? null : FloatingActionButtonLocation.endDocked,
            );
          }
        }
        

        因此,在 onTapSelectNavigation 方法上,根据给定的选项卡索引,并通过一个简单的 Switch case 语句,我决定调用哪个命名路由。就这么简单。

        void onTapSelectNavigation(int index, BuildContext context) {
          switch (index) {
            case 0:
              Navigator.pushNamed(context, FoodCategoryIndexScreen.screenId);
              break;
            case 1:
              Navigator.pushNamed(context, FavoritesScreen.screenId);
              break;
          }
        }
        

        现在,这还不够。为什么?因为当我为整个应用程序提供额外导航时,会发生一些奇怪的事情。

        如果我们只使用一个简单的 setState 在本地状态中设置和存储这个通用脚手架上的 activeTab,那么当我们在两个选项卡之间移动时,它将起到一个魅力。但是,当我们从 FoodCategoryIndexScreen 点击进入特定的 FoodCategory 时,访问包含 FoodRecipesList(膳食)和/或进入特定膳食的 FoodCategoryShowScreen,然后我们在 IOS 上滑动或在两个平台上点击返回,活动选项卡功能将获得疯狂的。它将不再正确显示当前的活动标签。

        为什么?

        因为您是通过弹出路线到达那里的,而不是通过单击然后执行 onTab 事件,因此不会执行 setState 函数,因此不会更新 activeTab。

        解决办法:

        使用 RouteObserver 功能。这可以手动完成,但最简单的方法就是安装 route_observer_mixin 包。这就是我们接下来要做的:

        1. 在您的主文件中,您将使用上述 mixin 中包含的 RouteObserverProvider 包装整个应用程序。 (我现在刚刚添加到上面的[RouteObserverProvider],其余的我之前已经添加了):
          void main() {
            runApp(MultiProvider(
              providers: [
                RouteObserverProvider(),    
        
                  // Config about the app:
                  ChangeNotifierProvider<AppData>(
                    create: (context) => AppData(),
                  ),
            
                  // Data related to the FoodCategoriesData objects: (sqlite)
                  ChangeNotifierProvider<FoodCategoriesData>(
                    create: (context) => FoodCategoriesData(),
                  ),
            
                  // Data related to the FoodCategoriesFoodRecipesData objects: (sqlite)
                  ChangeNotifierProvider<FoodCategoriesFoodRecipesData>(
                    create: (context) => FoodCategoriesFoodRecipesData(),
                  ),
            
                  // Data related to the FoodRecipesData objects: (sqlite)
                  ChangeNotifierProvider<FoodRecipesData>(
                    create: (context) => FoodRecipesData(),
                  ),
            
                  // Data related to the FoodIngredientsData objects: (sqlite)
                  ChangeNotifierProvider<FoodIngredientsData>(
                    create: (context) => FoodIngredientsData(),
                  ),
            
                  // Data related to the RecipeStepsData objects: (sqlite)
                  ChangeNotifierProvider<RecipeStepsData>(
                    create: (context) => RecipeStepsData(),
                  ),
                ],
                // child: MyApp(),
                child: InitialSplashScreen(),
              ));
            }
        
        1. 在包含额外导航的每个屏幕上(显示 BottomNavigationBar),您将使用 RouteAware 和 RouteObserverMixin 到您的州。

        例子:

        .
        .
        .
        class _FoodCategoryIndexScreenState extends State<FoodCategoryIndexScreen> with RouteAware, RouteObserverMixin {
        .
        .
        .
        
        1. 在每个有状态的屏幕上(FavoritesScreen 除外),我们都会将此属性添加到本地状态:

          int _activeTab = 0;

        注意:在收藏夹屏幕上,默认值为 1,而不是 0,因为收藏夹是第二个标签(索引 = 1)。所以在FavoritesScreen 中是这样的:

        int _activeTab = 1;
        
        1. 然后您将在每个有状态屏幕上覆盖 RouteAware 方法(我们只需要 didPopNext 和 didPush):
            /// Called when the top route has been popped off, and the current route
            /// shows up.
            @override
            void didPopNext() {
              print('didPopNext => Emerges: $_screenId');
              setState(() {
                _activeTab = 0;
              });
            }
            
            /// Called when the current route has been pushed.
            @override
            void didPush() {
              print('didPush => Arriving to: $_screenId');
              setState(() {
                _activeTab = 0;
              });
            }
            
            /// Called when the current route has been popped off.
            @override
            void didPop() {
              print('didPop => Popping of: $_screenId');
            }
            
            /// Called when a new route has been pushed, and the current route is no
            /// longer visible.
            @override
            void didPushNext() {
              print('didPushNext => Covering: $_screenId');
            }
        

        当然,在FavoritesScreen中是这样的:

            /// Called when the top route has been popped off, and the current route
            /// shows up.
            @override
            void didPopNext() {
              print('didPopNext => Emerges: $_screenId');
              setState(() {
                _activeTab = 1;
              });
            }
            
            /// Called when the current route has been pushed.
            @override
            void didPush() {
              print('didPush => Arriving to: $_screenId');
              setState(() {
                _activeTab = 1;
              });
            }
            
            /// Called when the current route has been popped off.
            @override
            void didPop() {
              print('didPop => Popping of: $_screenId');
            }
            
            /// Called when a new route has been pushed, and the current route is no
            /// longer visible.
            @override
            void didPushNext() {
              print('didPushNext => Covering: $_screenId');
            }
        

        RouterObserver 包将跟踪所有的推送和弹出,并根据用户单击的每次向后滑动或返回链接相应地执行正确的方法,从而相应地更新每个有状态屏幕上的 _activeTab 属性。

        1. 然后我们将简单地将 int _activeTab 属性作为命名参数传递给每个有状态屏幕内的每个 FeeddyScaffold。像这样:

        lib/screens/food_category_index_screen.dart

            .
            .
            .  
            return FeeddyScaffold(
                activeIndex: _activeTab,
                appTitle: widget.appTitle,
                innerWidgets: [
                  // Food Categories Grid:
                  Expanded(
                    flex: 5,
                    child: FoodCategoriesGrid(),
                  ),
                ],
                objectsLength: amountTotalFoodCategories,
                objectName: 'category',
                onPressedAdd: () => _showModalNewFoodCategory(context),
              );
            }
            .
            .
            .
        

        基于 setState 执行的 _activeTab 属性的每次更新,总是会引发 UI 的重新渲染,这将允许始终在 BottomNavigationBar 上相应地显示正确的选项卡索引,根据我们的时间,始终匹配当前路线与活动选项卡。在这种情况下,我们希望始终显示第一个选项卡,除非正在查看收藏夹屏幕。

        因此,无论我们是推送还是弹出路由,它都会以非常一致的方式看起来总是这样:

        FoodCategoriesIndexScreen:

        FoodCategoryShowScreen:

        FoodRecipeShowScreen:

        FavoritesShow:

        更多详情,您可以在 GitHub 上从here 克隆我的应用程序的源代码。

        结束。

        【讨论】:

          【解决方案5】:

          对于正在寻找简短解决方案的人来说,这是我的,也许它对某人有用:

              class App extends StatefulWidget {
            @override
            State<StatefulWidget> createState() => AppState();
          }
          
          class AppState extends State<App> {
          
            int currentTab = 0;
          
            void _selectTab(int index) {
              debugPrint (" index = $index ");
              setState(() {
                currentTab = index;
              });
          
              switch (currentTab) {
              case 0:
          
                debugPrint (" my index 0 ");
                break;
          
                case 1:
                 debugPrint (" my index 1 ");
                break;
          
                case 2:
                 debugPrint (" my index 2 ");
                break;
          
            }
          
            }
          
            @override
            Widget build(BuildContext context) {
              return Scaffold(
                body: _buildBody(),
                bottomNavigationBar: BottomNavigationBar(
                 currentIndex: currentTab,
                 onTap: _selectTab,
          
                items: [
                  BottomNavigationBarItem(
                    icon: Icon(Icons.home), title: Text("Home"),
                  ),
          
                  BottomNavigationBarItem(
                    icon: Icon(Icons.message), title: Text("Message"),
                  ),
          
                  BottomNavigationBarItem(
                    icon: Icon(Icons.settings), title: Text("Settings"),
                  ),
          
                ],
          
                ),
              );
            }
          
            Widget _buildBody() {
              // return a widget representing a page
            }
          }
          

          而且我们不要忘记main,应该是这样的:

          import 'package:flutter/material.dart';
          import 'App.dart';
          
          void main() {
            runApp( MaterialApp(
                  home: App(),
            )
              );
          }
          

          【讨论】:

            猜你喜欢
            • 2019-02-02
            • 2020-07-28
            • 2020-07-29
            • 1970-01-01
            • 2021-06-29
            • 1970-01-01
            • 2020-09-11
            • 2021-06-18
            • 1970-01-01
            相关资源
            最近更新 更多