Flutter 更新 BottomNavigationBar

Flutter update BottomNavigationBar

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

我认为在你的情况下使用 PageViewTabBarView 更优雅。

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

当您在 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();
  }

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

    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
  }
}

还有我们别忘了主要的,应该是这样的:

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

void main() {
  runApp( MaterialApp(
        home: App(),
  )
    );
}

BottomNavigationBar的另一种视角

许多人甚至官方文档面对额外导航的方式与 Web 导航不同。他们没有呈现一个通用 and/or 部分通用模板,其中包含可用 link 以在单击后执行。相反,他们正在实施的是在额外的“tabs_screen”中预加载不同的屏幕(基本上只是一个额外的脚手架,当时包装和渲染一个屏幕)然后,根据 onTap 上的给定索引甚至BottomNavigationBar 的索引,您将使用该索引来确定哪个屏幕将在该脚手架内实际呈现。就这些了。

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

我像往常一样添加了 BottomNavigationBar,但是我没有 preloading/instantiating List _pages 上的屏幕,我只是为 FavoritesScreen 添加了另一个命名路由,

我设计了我的 bottomNavigationBar:

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(膳食)and/or 的 FoodCategoryShowScreen 进入特定的膳食,然后我们向后滑动 IOS 或在两个平台上单击返回,活动选项卡功能变得疯狂。它将不再正确显示当前活动标签。

为什么?

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

解决方法:

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

  1. 在你的主文件中,你将使用 RouteObserverProvider 包装你的整个应用程序,包含在提到的 mixin 中。 (我现在刚刚添加到上面的[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;

注意:在 FavoritesScreen 上默认为 1,而不是 0,当然,因为 Favorites 是第二个标签(索引 = 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 包将跟踪所有的推送和弹出,并根据用户每次向后滑动或向后点击 link 相应地执行正确的方法,因此相应地更新 _activeTab 属性 在每个状态屏幕上。

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

lib/screens/food_category_index_screen.飞镖

    .
    .
    .  
    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 的 re-rendering,这将允许始终相应地显示正确的选项卡索引BottomNavigationBar,基于我们的时间,始终将当前路由与活动选项卡匹配。在这种情况下,我们希望始终显示第一个选项卡处于活动状态,除非正在查看avoritesScreen.

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

FoodCategoriesIndexScreen:

FoodCategoryShowScreen:

FoodRecipeShowScreen:

收藏夹显示:

有关更多详细信息,您可以从 here.

在 GitHub 克隆我的应用程序的源代码

结束。