Flutter BottomNavigationBar底部导航栏

满昊然
2023-12-01

基本上每个app都含有一个底部导航栏,Flutter也提供了相关控件,本博主要从两个方面讲解底部导航栏BottomNavigationBar

  • BottomNavigationBar的使用方法
  • 解决BottomNavigationBar切换时状态重置问题

首先看看BottomNavigationBar的使用方法,先上代码

void main() => runApp(App());
class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Title',
      theme: ThemeData(
        primaryColor: Color(0xFF2B9242),
      ),
      home: MainPage()
    );
  }
}
class MainPage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
  int _selectedTab = 0;
  @override
  void initState() {
    super.initState();
  }
  @override
  void dispose() {
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
      body: _buildContent(),
      bottomNavigationBar:
          _BottomTabs(selectedTabIndex: _selectedTab, onTap: _tabSelected),
    );
  }
 _buildContent() {
    return Text('this is content');
 }
  _tabSelected(int tabIndex) {
    setState(() {
   	// update content
    });
  }
}

class _BottomTabs extends StatelessWidget {
  _BottomTabs({@required this.selectedTabIndex, @required this.onTap});

  final int selectedTabIndex;
  final ValueChanged<int> onTap;

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return BottomNavigationBar(
        onTap: this.onTap,
        type: BottomNavigationBarType.fixed,
        fixedColor: Colors.green.shade800,
        currentIndex: selectedTabIndex,
        iconSize: 28,
        items: <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            title: Text("首页"),
            icon: ImageIcon(AssetImage("image/image1.png")),
          ),
          BottomNavigationBarItem(
              title: Text("采购/销售"),
              icon: ImageIcon(AssetImage("image/image2.png"))),
          BottomNavigationBarItem(
            title: Text("出入库"),
            icon: ImageIcon(AssetImage("image/image3.png")),
          ),
          BottomNavigationBarItem(
            title: Text("基础资料"),
            icon: ImageIcon(AssetImage("image/image4.png")),
          ),
          BottomNavigationBarItem(
              title: Text("待办"),
              icon: ImageIcon(AssetImage("image/image5.png"))),
        ]);
  }
}

简单起见, 暂时还没处理根据tab切换内容, BottomNavigationBar的构建有一个type,我们一般使用fixed类型,使用fixed类型时,fixedColor就表示选中时的文字和图片颜色,切换时如果没设置activeIcon,会将图标使用fixedColor进行tint处理,选中时的tab文字会变大,这个我也没找到保持一样大的方法,还有一点值得注意的地方就是icon一般需要使用Icon或ImageIcon,否则会使BottomNavigationBar高度不正常,下面我们来处理页面切换,我们先用TabbarView来显示内容部分,替换_buildContent方法的内容

_buildContent() {
    return TabBarView(
      controller: _tabController,
      physics: const NeverScrollableScrollPhysics(),
      children: <Widget>[
        HomePage(),
        Center(
          child: Text(
            "Order",
            style: TextStyle(
              fontSize: 15,
            ),
          ),
        ),
        Center(
          child: Text("Warehouse"),
        ),
        Center(
          child: Text("Base Infra"),
        ),
        Center(
          child: Text("Pending"),
        ),
      ],
    );
  }

TabBarView构建需要用到TabController, 通过设置TabController的index来切换对应的页面
子控件相应的BottomNavigationBar切换时的页面, 先定义TabController

class _MainPageState extends State<MainPage>
    with SingleTickerProviderStateMixin {
  TabController _tabController;
  @override
  void initState() {
    super.initState();

    _tabController = TabController(length: 5, vsync: this);
  }

  @override
  void dispose() {
    super.dispose();
    _tabController.dispose();
  }
  // ignore othe non related code
}

下面我们来实现BottomNavigationBar切换时的回调方法

  _tabSelected(int tabIndex) {
    setState(() {
      _tabController.index = tabIndex;
      _selectedTab = tabIndex;
    });
  }

到此我们就实现了BottomNavigationBar页面的切换功能, 大家可能注意到我构建的第一个页面用的是HomePage控件,我这里还没定义,先可以使用Text代替,下面为了显示BottomNavigationBar + TabbarView页面状态重置的现象,我们来构建一个StatefullWidget的控件HomePage

class HomePage extends StatefulWidget {
  HomePage();

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

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: FlatButton(
          onPressed: () {
            Navigator.of(context).push(MaterialPageRoute(
                builder: (_) => Scaffold(
                      appBar: AppBar(elevation: 0,),
                      body: Center(
                        child: Text("Back"),
                      ),
                    )));
          },
          child: Text("Home")),
    );
  }

  @override
  void dispose() {
    super.dispose();
  }
}

内容很简单,我们在dispose方法加一个断点,会发现每次切换到其他页时都会调用到,说明状态重置了,如果我们显示静态页面倒也没我们问题,但涉及到网络请求数据时会发现都会重复请求一遍,那么问题来了,我们该怎么解决呢? 最简单的就是页面的State类 mixin AutomaticKeepAliveClientMixin

class _HomePageState extends State<HomePage> with AutomaticKeepAliveClientMixin {

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

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return Center(
      child: FlatButton(
          onPressed: () {
            Navigator.of(context).push(MaterialPageRoute(
                builder: (_) => Scaffold(
                      appBar: AppBar(elevation: 0,),
                      body: Center(
                        child: Text("Back"),
                      ),
                    )));
          },
          child: Text("Home")),
    );
  }

  @override
  void dispose() {
    super.dispose();
  }
}

再次运行切换到其他页面,dispose就不会调用到了,说明没有重置State,mixin AutomaticKeepAliveClientMixin 主要注意两点

  • wantKeepAlive 重载返回true
  • build方法先调用super.build(context);

第二种解决方法就是内容页不用TabbarView管理,改用IndexedStack,我们重新实现_buildContent()方法

  _buildContent() {
    return IndexedStack(
      index: _selectedTab,
      children: <Widget>[
        HomePage(),
        Center(
          child: Text(
            "Order",
            style: TextStyle(
              fontSize: 15,
            ),
          ),
        ),
        Center(
          child: Text("Warehouse"),
        ),
        Center(
          child: Text("Base Infra"),
        ),
        Center(
          child: Text("Pending"),
        ),
      ],
    );
  }

IndexedStack控件显示索引为index的child Widget,其他的不显示, Stack控件通过层叠的形式显示所有子控件,学过android的可能已经发现它和FrameLayout布局类似,其实我们也可以通过
Offstage + Stack实现类似的效果,具体怎么用大家自己试试,BottomNavigationBar就讲到这里,如果理解不对的地方欢迎大家指正,如需转载请注明出处

 类似资料: