Flutter TabBar + TabBarView 实现顶部导航栏,懒加载数据和状态保活

在公司开发的第一个 Flutter 项目告一段落,准备将一些值得分享的整理到博客,今天是 TabBarView。它相当一个单独的 View Page,可以和底部导航栏 BottomNavigationBar 结合使用,也可以和顶部导航栏 TabBar 关联使用,它们的默认用法虽然简单,但不好看,如果想满足 UI 要求,肯定要对各种属性了解。其次涉及到项目数据接口请求,数据初始化、懒加载等等,如何实现比较好,这是我今天要分享的。

TabBar

TabBar ` 就是顶部导航栏,包括导航栏标题和指示器,里面属性很多,这里列出我使用到的

TabBar(
  tabs: _myTabs, //标题,使用 Tab 构造
  isScrollable: false, //是否可以滑动,标题左右滑动
  padding: EdgeInsets.only(top: 32,left: 65,right: 65),
  indicatorWeight: 1, //指示器高度
  indicator: UnderlineTabIndicator(
      borderSide: BorderSide(width: 2,color: Colors.white),
      insets: EdgeInsets.only(left: 42,right: 42)
  ),
  labelColor: Colors.white, //标题选择时颜色
  unselectedLabelColor: Color(0xFFABABAB), //未被选择时颜色
  labelStyle: TextStyle(fontSize: 15), //被选择时label风格样式
  unselectedLabelStyle: //未被选择时风格,比如文本变大变小
),			

tabs 是标题,我这里只构造纯文本 Tab,它还能搭配图标。

final List<Tab> _myTabs = <Tab>[
    Tab(text: "动态",),
    Tab(text: "推荐",),
  ];

padding ,如果不加左右 Padding ,这俩标题显示如下。

指示器高度 indicatorWeight ,但使用自定义的指示器后,这个设置不会生效。与它对应的还有一个指示器长度,但只提供了两种固定好的 tab、label 常量。如果我们想设置具体长度就不行。

indicatorSize: TabBarIndicatorSize.tab,

所以我们最好要自定义指示器 indicator 的长度/宽度和高度,这就不能用默认指示器,使用 UnderlineTabIndicator

indicator: UnderlineTabIndicator(
    borderSide: BorderSide(width: 2,color: Colors.white), //高度和颜色
    insets: EdgeInsets.only(left: 42,right: 42) //左右间距反向设置长度
),

TabBarView

它是整一个页面 View,是一个 Page,可以放任何东西。TabBarView要注意的属性就是俩,childrencontroller。我的页面放了一张图做演示。

TabBarView(
  children: [MomentPage(),RecommendedPage()],
),

class MomentPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.only(top: 10),
      width: double.infinity,
      decoration: BoxDecoration(
          image: DecorationImage(
          alignment: Alignment.topCenter,
          image: AssetImage(HomePage.assetsImage1),
        ),
      ),
    );
  }
}

其实,TabBarViewTabBar 都有 controller 属性,需要填入 TabController ,它是关联 TabBarViewTabBar 的控制器。我们可以不实现 controller,只要最外层使用了 DefaultTabController

class HomePage extends StatelessWidget {
  final List<Tab> _myTabs = <Tab>[
    Tab(text: "动态",),
    Tab(text: "推荐",),
  ];

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 2,
      child: Scaffold(
        backgroundColor: Color(0xFF16181F),
        appBar: TabBar(
          tabs: _myTabs,
          isScrollable: false,
          padding: EdgeInsets.only(top: 32,left: 65,right: 65),
          indicatorWeight: 1,
          indicatorSize: TabBarIndicatorSize.label,
          indicator: UnderlineTabIndicator(
              borderSide: BorderSide(width: 2,color: Colors.white),
              insets: EdgeInsets.only(left: 42,right: 42)
          ),
          labelColor: Colors.white,
          unselectedLabelColor: Color(0xFFABABAB),
          labelStyle: TextStyle(fontSize: 15),
        ),
        body: TabBarView(
          children: [MomentPage(),RecommendedPage()],
        ),
      ),
    );
  }
}

懒加载数据

像这种 TabPage ,基本都会请求数据,但又分为很多种情况,我们不可能构造出 TabView 后,一次性将所有页面的接口都请求完成,这种体验就不好了。最好还是希望能做到,当点击 TabBar 时,再请求数据。并且只是第一次点击时请求数据,后面再次点击不再请求接口。

class _MomentPageState extends State<MomentPage> {
  @override
  void initState() {
    print('请求数据1');
    super.initState();
  }
}

直接在 initState 中使用应该会报错,而且每次切换都会重新请求,拉取数据,甚至会销毁一些状态。所以我们需要 TabBar 每次切换页面不重新加载。这里需要用到 AutomaticKeepAliveClientMixin

class _MomentPageState extends State<MomentPage> with AutomaticKeepAliveClientMixin{

  @override
  void initState() {
    print('请求数据1');
    super.initState();
  }

  @override
  bool get wantKeepAlive => true;
}

将你不需要重新加载的 TabBarView 页面实现 AutomaticKeepAliveClientMixin ,如果两个页面都配置,则每次切换打印如下

flutter: 请求数据1
flutter: 请求数据2

如果只配置第一个页面,第二个不配置,每次点击第二个 Tab 都会重新请求,加载状态

flutter: 请求数据1
flutter: 请求数据2
flutter: 请求数据2

但这样还不行,因为如果你是请求接口,获取数据,刷新界面的流程,可能会报错。我们可以使用 addPostFrameCallback ,它是指界面 build 完成后再执行回调。

WidgetsBinding.instance.addPostFrameCallback((_) {
  print('请求数据1');
});

TabController

上面的代码都没使用自定义 TabController,直接将默认的 DefaultTabController 放到最外层,包裹 TabBarTabBarView,也能实现它们的联动。但这还不够,如果想监听 TabBar 的滑动,监听 TabBarView 的变化。还是要用到自定义 TabController

这里涉及状态变化,将 HomePage 从 StatelessWidget 修改成 StatefulWidget。然后混入TickerProviderStateMixin

class _HomePageState extends State<HomePage> with TickerProviderStateMixin{}

@override
void initState() {
  ///初始化构造
  _tabController = TabController(vsync: this, length: 2,initialIndex: 0);
  ///添加监听
  ///_tabController(() => _handleTabChange());
  _tabController.addListener((){
    if(_tabController.indexIsChanging){
      _handleTabChange();
    }
  });
  super.initState();
}

void _handleTabChange() {
  if (_tabController.index == 0) {
    print('请求数据1');
  } else if (_tabController.index == 1) {
    print('请求数据2');
  }
}

这里貌似有一个 bug,如果直接使用 _tabController(() => _handleTabChange()); 添加监听,切换时会执行两次。如果你也碰到这种情况,可以用我上面提供的代码。

TabBarTabBarView 添加自定义的 TabController

appBar: TabBar(
  ...
  controller: _tabController,
),

body: TabBarView(
  ...
  controller: _tabController,
),

OK,以上就是要分享,希望能帮到各位。

本文由老郭种树原创,转载请注明:https://guozh.net/flutter-tabbarview/

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注