flutter入门系列教程<一>:tab组件的灵活妙用

devtools/2025/1/23 5:42:57/

文章目录

  • 说明
  • 区分
  • TabBarView组件
    • TabBarView
    • TabBar
    • 实例
  • 需求升级
    • 写在中间的tabbar组件
      • 封装组件
      • 组件说明
      • 组件用法示例
  • 常规的tabbar封装
    • 常规用法

说明

前提:假设你已初步了解了flutter和dart语言,并且知道怎么创建一个简单的项目;
学习本文后,你将掌握:

  • tab组件的用法;
  • 组件的封装;

区分

tab页在上面的是TabBarView组件,形如下图:
在这里插入图片描述

tab页在底部的是BottomNavigationBar组件,形如下图:
在这里插入图片描述

TabBarView组件

TabBarView 是 Material 组件库中提供了 Tab 布局组件,通常和 TabBar 配合使用。

注:下面的示例源于:https://book.flutterchina.club/chapter6/tabview.html

TabBarView

TabBarView 封装了 PageView,它的构造方法很简单

 TabBarView({Key? key,required this.children, // tab 页this.controller, // TabControllerthis.physics,this.dragStartBehavior = DragStartBehavior.start,
}) 

TabController 用于监听和控制 TabBarView 的页面切换,通常和 TabBar 联动。如果没有指定,则会在组件树中向上查找并使用最近的一个 DefaultTabController 。

TabBar

TabBar 为 TabBarView 的导航标题,如图6-20所示:
图6-20
TabBar 有很多配置参数,通过这些参数我们可以定义 TabBar 的样式,很多属性都是在配置 indicator 和 label,拿上图来举例,Label 是每个Tab 的文本,indicator 指 “历史” 下面的白色下划线。

const TabBar({Key? key,required this.tabs, // 具体的 Tabs,需要我们创建this.controller,this.isScrollable = false, // 是否可以滑动this.padding,this.indicatorColor,// 指示器颜色,默认是高度为2的一条下划线this.automaticIndicatorColorAdjustment = true,this.indicatorWeight = 2.0,// 指示器高度this.indicatorPadding = EdgeInsets.zero, //指示器paddingthis.indicator, // 指示器this.indicatorSize, // 指示器长度,有两个可选值,一个tab的长度,一个是label长度this.labelColor, this.labelStyle,this.labelPadding,this.unselectedLabelColor,this.unselectedLabelStyle,this.mouseCursor,this.onTap,...
}) 

TabBar 通常位于 AppBar 的底部,它也可以接收一个 TabController ,如果需要和 TabBarView 联动, TabBar 和 TabBarView 使用同一个 TabController 即可,注意,联动时 TabBar 和 TabBarView 的孩子数量需要一致。如果没有指定 controller,则会在组件树中向上查找并使用最近的一个 DefaultTabController 。另外我们需要创建需要的 tab 并通过 tabs 传给 TabBar, tab 可以是任何 Widget,不过Material 组件库中已经实现了一个 Tab 组件,我们一般都会直接使用它:

const Tab({Key? key,this.text, //文本this.icon, // 图标this.iconMargin = const EdgeInsets.only(bottom: 10.0),this.height,this.child, // 自定义 widget
})

注意,text 和 child 是互斥的,不能同时制定。

实例

下面我们看一个例子:

class TabViewRoute1 extends StatefulWidget {_TabViewRoute1State createState() => _TabViewRoute1State();
}class _TabViewRoute1State extends State<TabViewRoute1>with SingleTickerProviderStateMixin {late TabController _tabController;List tabs = ["新闻", "历史", "图片"];void initState() {super.initState();_tabController = TabController(length: tabs.length, vsync: this);}Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text("App Name"),bottom: TabBar(controller: _tabController,tabs: tabs.map((e) => Tab(text: e)).toList(),),),body: TabBarView( //构建controller: _tabController,children: tabs.map((e) {return KeepAliveWrapper(child: Container(alignment: Alignment.center,child: Text(e, textScaleFactor: 5),),);}).toList(),),);}void dispose() {// 释放资源_tabController.dispose();super.dispose();}
}

运行后效果如图6-21所示:
在这里插入图片描述

滑动页面时顶部的 Tab 也会跟着动,点击顶部 Tab 时页面也会跟着切换。为了实现 TabBar 和 TabBarView 的联动,我们显式创建了一个 TabController,由于 TabController 又需要一个 TickerProvider (vsync 参数), 我们又混入了 SingleTickerProviderStateMixin;由于 TabController 中会执行动画,持有一些资源,所以我们在页面销毁时必须得释放资源(dispose)。综上,我们发现创建 TabController 的过程还是比较复杂,实战中,如果需要 TabBar 和 TabBarView 联动,通常会创建一个 DefaultTabController 作为它们共同的父级组件,这样它们在执行时就会从组件树向上查找,都会使用我们指定的这个 DefaultTabController。我们修改后的实现如下:

class TabViewRoute2 extends StatelessWidget {Widget build(BuildContext context) {List tabs = ["新闻", "历史", "图片"];return DefaultTabController(length: tabs.length,child: Scaffold(appBar: AppBar(title: Text("App Name"),bottom: TabBar(tabs: tabs.map((e) => Tab(text: e)).toList(),),),body: TabBarView( //构建children: tabs.map((e) {return KeepAliveWrapper(child: Container(alignment: Alignment.center,child: Text(e, textScaleFactor: 5),),);}).toList(),),),);}
}

可以看到我们无需去手动管理 Controller 的生命周期,也不需要提供 SingleTickerProviderStateMixin,同时也没有其他的状态需要管理,也就不需要用 StatefulWidget 了,这样简单很多。

需求升级

上述的代码示例,仅能把tabbar设置在顶部,而且是写在appBar中的,这就限制了它的写法及样式布局;如果我要是把tabbar写在中间位置呢,该怎么办?

写在中间的tabbar组件

其实,上文页提到了DefaultTabController的关键字,用该组件,可以灵活地设置tabbar的位置。
首先要明确tabbar组件的要素

  1. TabBarTabBarView组成;
  2. 明确指定tabs的个数,即length属性;
  3. 指定DefaultTabController或关联TabController

封装组件

我把封装的组件写在下面,以供参考:


import 'package:flutter/material.dart';// 自由布局的tabBar,上面或下面还有其他组件
class FreeTabBar extends StatefulWidget {final List tabs;final List<Widget> children;final Widget? topWidget;final Widget? bottomWidget;final double maxHeight;const FreeTabBar({super.key,this.topWidget, // 可选的顶部组件this.bottomWidget, // 可选的底部组件this.maxHeight = 300.0, // 可选的最大高度required this.tabs,required this.children,});State<FreeTabBar> createState() => _FreeTabBarState();
}class _FreeTabBarState extends State<FreeTabBar> {Widget build(BuildContext context) {return DefaultTabController(length: widget.tabs.length,child: Column(children: [if(widget.topWidget != null)widget.topWidget as Widget,TabBar(tabs: widget.tabs.map((e) => Tab(text: e)).toList(),),ConstrainedBox(constraints: BoxConstraints(maxHeight: widget.maxHeight), // 设置一个最大高度child: TabBarView(children: widget.children.map((cell) {return SingleChildScrollView(child: cell,);}).toList(),),),/// 以下为其他写法// Expanded// Expanded(//   child: TabBarView(//     children: widget.children.map((cell) => cell).toList(),//   ),// ),// SizedBox(//   height: 300,//   child: TabBarView(//     children: widget.children.map((cell) {//       return SingleChildScrollView(//         child: cell,//       );//     }).toList(),//   ),// ),// Flexible(//   child: TabBarView(//     children: widget.children.map((cell) => cell).toList(),//   ),// ),if(widget.bottomWidget != null)widget.bottomWidget as Widget,],),);}
}

组件说明

上面的组件用了好几种写法来包裹TabBarView,因为TabBarView组件需要有明确的高度;
上面的ExpandedConstrainedBoxSizedBoxFlexible这几种写法,都限定了其高度

  • 显式设置高度值height;
  • 尽可能大的占据垂直空间

友情提醒:不设置高度会报错

上述组件也示例了封装组件的参数、参数的类型、参数的默认值,大家封装组件是可参考该格式;

组件用法示例

import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import '/utils/platform_check.dart';
import '/components/custom/tabBarFree.dart';
import '/model/mine/poetry.dart';
import 'package:intl/intl.dart';// 我的 - 随机诗词 - iTab诗词源
class ITabPoetryPage extends StatefulWidget {const ITabPoetryPage({super.key});State<ITabPoetryPage> createState() => _ITabPoetryPageState();
}class _ITabPoetryPageState extends State<ITabPoetryPage> {PoetryResponse? myResult;// 如果是web端,则不设置请求头,否则设置final dio = Dio(BaseOptions(headers: {if(!PlatformCheck.isWeb)'Content-Type': 'application/json',if(!PlatformCheck.isWeb)'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36'}));void initState() {super.initState();debugPrint('init初始化,是否为web端:${PlatformCheck.isWeb}');getHttp();}Future<void> getHttp() async {try {// const apiUrl = 'https://v1.jinrishici.com/all.json';const apiUrl = 'https://api.codelife.cc/todayShici?lang=cn';final response = await dio.get(apiUrl);debugPrint('接口返回值: $response');setState(() {myResult = PoetryResponse.fromJson(response.data);});} catch (e) {debugPrint('报错啦啦啦啦:$e');}}Widget paddingWidget(Widget child) {return Padding(padding: const EdgeInsets.all(16.0),child: child,);}// 构建内容Widget buildText(String? data) {return data != null ? paddingWidget(Text(data)) : const Text('暂无数据');}// 刷新按钮Widget refreshBtn() {return OutlinedButton(onPressed: getHttp,child: const Text('今日诗词刷新'));}// 格式化时间String formatDateTime(String? dateTimeString) {if (dateTimeString == null) return '暂无时间数据';try {final dateTime = DateTime.parse(dateTimeString);final formatter = DateFormat('yyyy-MM-dd HH:mm:ss');return formatter.format(dateTime);} catch (e) {debugPrint('日期格式化错误: $e');return '日期格式化错误';}}Widget build(BuildContext context) {return Padding(padding: const EdgeInsets.all(8.0),child: FreeTabBar(topWidget: myResult != null? Column(children: [refreshBtn(),const SizedBox(height: 10,),// 标题Text(myResult!.data.title),// 朝代 - 作者Text('${myResult!.data.dynasty} · ${myResult!.data.author}',),// 内容Text(myResult!.data.content),],) : refreshBtn(),bottomWidget: Column(children: [// 创建时间Text(formatDateTime(myResult?.data.createTime)),],),tabs: const ['译文', '注释', '引言', '评语', '引言'],children: [// 译文buildText(myResult?.data.translate),// 注释buildText(myResult?.data.annotation),// 引言buildText(myResult?.data.preface),// 评语buildText(myResult?.data.reviews),// 引语buildText(myResult?.data.quotes),],),);}
}

上述代码可直接看FreeTabBar组件内部,其余可忽略;
由于组件外层还有一层其他组件,所以整体效果如下(红框内为主要内容)
在这里插入图片描述

常规的tabbar封装

import 'package:flutter/material.dart';
import '/utils/setting.dart';class CustomTabBar extends StatefulWidget {final List tabs;final List<Widget> children;const CustomTabBar({super.key,required this.tabs,required this.children,});State<CustomTabBar> createState() => _CustomTabBarState();
}class _CustomTabBarState extends State<CustomTabBar> with SingleTickerProviderStateMixin {late TabController _tabController;void initState() {super.initState();_tabController = TabController(length: widget.tabs.length, vsync: this);}void dispose() {_tabController.dispose();super.dispose();}Widget build(BuildContext context) {return Column(children: [TabBar(indicatorColor: DefaultColor.success,labelColor: DefaultColor.success,controller: _tabController,tabs: widget.tabs.map((e) => Tab(text: e)).toList()),Expanded(child: TabBarView(controller: _tabController,children: widget.children.map((cell) {return SingleChildScrollView(child: cell,);}).toList(),),),],);}
}

常规用法

可直接看 CustomTabBar组件内的代码

import 'package:flutter/material.dart';
import '/components/page/common.dart';
import '/components/custom/tabBar.dart';
import './api1.dart';
import './api2.dart';// 我的 - 随机诗词
class PoetryPage extends StatelessWidget {final String title;const PoetryPage({super.key,required this.title,});Widget build(BuildContext context) {return CommonPage(title: title,isShowLine: true,child: const CustomTabBar(tabs: ['iTab诗词', '今日诗词'],children: [ITabPoetryPage(),JRSCPage(),],),);}
}

效果图如下:
在这里插入图片描述


http://www.ppmy.cn/devtools/152798.html

相关文章

代码随想录算法【Day29】

Day29 134. 加油站 暴力法 遍历每一个加油站为起点的情况&#xff0c;进行模拟 class Solution { public:int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {for(int i 0; i < cost.size(); i){ //以谁为起点int rest gas[i] - cos…

HDFS HADOOP分布式文件系统

目录 一、 HDFS概述 1.1 HDFS简介 1.2 HDFS优缺点 1.2.1 优点 1.2.2 缺点 1.3 HDFS组成架构 1.4 HDFS文件块大小 二、HDFS的Shell操作&#xff08;开发重点&#xff09; 2.1 基本语法 2.2 命令大全 2.3 常用命令实操 2.3.1 上传 2.3.2 下载 2.3.3 HDFS直接操作 三、HDFS的API操…

C++|开源日志库log4cpp和glog

文章目录 log4cpp 和 glog对比1. **功能对比**2. **易用性和配置**3. **性能**4. **线程安全**5. **日志输出**6. **功能扩展**7. **适用场景**8. **总结** 其它开源C日志库1. **spdlog**2. **easylogging**3. **Boost.Log**4. **loguru**5. **Poco Logging**6. **Qt Logging (…

rocketmq-product-DefaultMQProducer启动流程

product线程分析 使用arthas如下 MQClientFactoryScheduledThread 定时器任务&#xff0c;详情查看这篇文章Rocketmq 探索MQClientFactoryScheduledThread线程工作PullMessageService 该线程专为消息consumer服务&#xff0c;product没用RebalanceService 该线程专为消息con…

MySQL 入门大全:运算符

&#x1f9d1; 博主简介&#xff1a;CSDN博客专家&#xff0c;历代文学网&#xff08;PC端可以访问&#xff1a;https://literature.sinhy.com/#/literature?__c1000&#xff0c;移动端可微信小程序搜索“历代文学”&#xff09;总架构师&#xff0c;15年工作经验&#xff0c;…

spring cloud之gateway和JWT回顾

最开始学习时&#xff0c;没怎么用&#xff0c;只知道它是网关&#xff0c;当时因为经常使用Nginx做网关&#xff0c;慢慢就淡忘了&#xff0c;最近为了代码整合性&#xff0c;就使用它&#xff0c;非常棒。关于JWT以前也使用&#xff0c;后面调用基本以第三方接口开发的比较多…

服务化架构 IM 系统之应用 MQ

在微服务化系统中&#xff0c;存在三个最核心的组件&#xff0c;分别是 RPC、注册中心和MQ。 在前面的两篇文章&#xff08;见《服务化架构 IM 系统之应用 RPC》和《服务化架构 IM 系统之应用注册中心》&#xff09;中&#xff0c;我们站在应用的视角分析了普适性的 RPC 和 注…

android studio本地打包后,无法热更,无法执行换包操作,plus.runtime.install没有弹窗

要解决这个问题我们首先要按顺序排查 1.检查安装代码是否正常&#xff0c;下面是一个热更安装进度页面的demo&#xff0c;可以参照一下 <template><view><view class"progress-box"><progress :percent"progress" show-info stroke…