Flutter之TabBarView組件項目實戰(zhàn)示例
TabBarView
TabBarView 是 Material 組件庫中提供了 Tab 布局組件,通常和 TabBar 配合使用。
TabBarView 封裝了 PageView,它的構(gòu)造方法:
TabBarView({
Key? key,
required this.children, // tab 頁
this.controller, // TabController
this.physics,
this.dragStartBehavior = DragStartBehavior.start,
})
TabController 用于監(jiān)聽和控制 TabBarView 的頁面切換,通常和 TabBar 聯(lián)動。如果沒有指定,則會在組件樹中向上查找并使用最近的一個 DefaultTabController 。
TabBar
TabBar 為 TabBarView 的導(dǎo)航標(biāo)題,如下圖所示

TabBar 有很多配置參數(shù),通過這些參數(shù)我們可以定義 TabBar 的樣式,很多屬性都是在配置 indicator 和 label,拿上圖來舉例,Label 是每個Tab 的文本,indicator 指 “新聞” 下面的白色下劃線。
const TabBar({
Key? key,
required this.tabs, // 具體的 Tabs,需要我們創(chuàng)建
this.controller,
this.isScrollable = false, // 是否可以滑動
this.padding,
this.indicatorColor,// 指示器顏色,默認(rèn)是高度為2的一條下劃線
this.automaticIndicatorColorAdjustment = true,
this.indicatorWeight = 2.0,// 指示器高度
this.indicatorPadding = EdgeInsets.zero, //指示器padding
this.indicator, // 指示器
this.indicatorSize, // 指示器長度,有兩個可選值,一個tab的長度,一個是label長度
this.labelColor,
this.labelStyle,
this.labelPadding,
this.unselectedLabelColor,
this.unselectedLabelStyle,
this.mouseCursor,
this.onTap,
...
})
TabBar 通常位于 AppBar 的底部,它也可以接收一個 TabController ,如果需要和 TabBarView 聯(lián)動, TabBar 和 TabBarView 使用同一個 TabController 即可,注意,聯(lián)動時 TabBar 和 TabBarView 的孩子數(shù)量需要一致。如果沒有指定 controller,則會在組件樹中向上查找并使用最近的一個 DefaultTabController 。另外我們需要創(chuàng)建需要的 tab 并通過 tabs 傳給 TabBar, tab 可以是任何 Widget,不過Material 組件庫中已經(jīng)實現(xiàn)了一個 Tab 組件,我們一般都會直接使用它:
const Tab({
Key? key,
this.text, //文本
this.icon, // 圖標(biāo)
this.iconMargin = const EdgeInsets.only(bottom: 10.0),
this.height,
this.child, // 自定義 widget
})
注意,text 和 child 是互斥的,不能同時制定。
全部代碼:
import 'package:flutter/material.dart';
/// @Author wywinstonwy
/// @Date 2022/1/18 9:09 上午
/// @Description:
class MyTabbarView1 extends StatefulWidget {
const MyTabbarView1({Key? key}) : super(key: key);
@override
_MyTabbarView1State createState() => _MyTabbarView1State();
}
class _MyTabbarView1State extends State<MyTabbarView1>with SingleTickerProviderStateMixin {
List<String> tabs =['頭條','新車','導(dǎo)購','小視頻','改裝賽事'];
late TabController tabController;
@override
void initState() {
// TODO: implement initState
super.initState();
tabController = TabController(length: tabs.length, vsync: this);
}
@override
void dispose() {
tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('TabbarView',textAlign: TextAlign.center,),
bottom:TabBar(
unselectedLabelColor: Colors.white.withOpacity(0.5),
labelColor: Colors.white,
// indicatorSize:TabBarIndicatorSize.label,
indicator:const UnderlineTabIndicator(),
controller: tabController,
tabs: tabs.map((e){
return Tab(text: e,);
}).toList()) ,
),
body: Column(
children: [
Expanded(
flex: 1,
child: TabBarView(
controller: tabController,
children: tabs.map((e){
return Center(child: Text(e,style: TextStyle(fontSize: 50),),);
}).toList()),)
],),
);
}
}
運(yùn)行效果:

滑動頁面時頂部的 Tab 也會跟著動,點(diǎn)擊頂部 Tab 時頁面也會跟著切換。為了實現(xiàn) TabBar 和 TabBarView 的聯(lián)動,我們顯式創(chuàng)建了一個 TabController,由于 TabController 又需要一個 TickerProvider (vsync 參數(shù)), 我們又混入了 SingleTickerProviderStateMixin;
由于 TabController 中會執(zhí)行動畫,持有一些資源,所以我們在頁面銷毀時必須得釋放資源(dispose)。綜上,我們發(fā)現(xiàn)創(chuàng)建 TabController 的過程還是比較復(fù)雜,實戰(zhàn)中,如果需要 TabBar 和 TabBarView 聯(lián)動,通常會創(chuàng)建一個 DefaultTabController 作為它們共同的父級組件,這樣它們在執(zhí)行時就會從組件樹向上查找,都會使用我們指定的這個 DefaultTabController。
我們修改后的實現(xiàn)如下:
class TabViewRoute2 extends StatelessWidget {
@override
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( //構(gòu)建
children: tabs.map((e) {
return KeepAliveWrapper(
child: Container(
alignment: Alignment.center,
child: Text(e, textScaleFactor: 5),
),
);
}).toList(),
),
),
);
}
}
可以看到我們無需去手動管理 Controller 的生命周期,也不需要提供 SingleTickerProviderStateMixin,同時也沒有其它的狀態(tài)需要管理,也就不需要用 StatefulWidget 了,這樣簡單很多。
TabBarView+項目實戰(zhàn)
實現(xiàn)導(dǎo)航信息流切換效果并緩存前面數(shù)據(jù):

1 構(gòu)建導(dǎo)航頭部搜索框

import 'package:flutter/material.dart';
import 'package:qctt_flutter/constant/colors_definition.dart';
enum SearchBarType { home, normal, homeLight }
class SearchBar extends StatefulWidget {
final SearchBarType searchBarType;
final String hint;
final String defaultText;
final void Function()? inputBoxClick;
final void Function()? cancelClick;
final ValueChanged<String>? onChanged;
SearchBar(
{this.searchBarType = SearchBarType.normal,
this.hint = '搜一搜你感興趣的內(nèi)容',
this.defaultText = '',
this.inputBoxClick,
this.cancelClick,
this.onChanged});
@override
_SearchBarState createState() => _SearchBarState();
}
class _SearchBarState extends State<SearchBar> {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
height: 74,
child: searchBarView,
);
}
Widget get searchBarView {
if (widget.searchBarType == SearchBarType.normal) {
return _genNormalSearch;
}
return _homeSearchBar;
}
Widget get _genNormalSearch {
return Container(
color: Colors.white,
padding: EdgeInsets.only(top: 40, left: 20, right: 60, bottom: 5),
child: Container(
height: 30,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
color: Colors.grey.withOpacity(0.5)),
padding: EdgeInsets.only(left: 5, right: 5),
child: Row(
children: [
const Icon(
Icons.search,
color: Colors.grey,
size: 24,
),
Container(child: _inputBox),
const Icon(
Icons.clear,
color: Colors.grey,
size: 24,
)
],
),
),);
}
//可編輯輸入框
Widget get _homeSearchBar{
return Container(
padding: EdgeInsets.only(top: 40, left: 20, right: 40, bottom: 5),
decoration: BoxDecoration(gradient: LinearGradient(
colors: [mainColor,mainColor.withOpacity(0.2)],
begin:Alignment.topCenter,
end: Alignment.bottomCenter
)),
child: Container(
height: 30,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
color: Colors.grey.withOpacity(0.5)),
padding: EdgeInsets.only(left: 5, right: 5),
child: Row(
children: [
const Icon(
Icons.search,
color: Colors.grey,
size: 24,
),
Container(child: _inputBox),
],
),
),);
}
//構(gòu)建文本輸入框
Widget get _inputBox {
return Expanded(
child: TextField(
style: const TextStyle(
fontSize: 18.0, color: Colors.black, fontWeight: FontWeight.w300),
decoration: InputDecoration(
// contentPadding: EdgeInsets.fromLTRB(1, 3, 1, 3),
// contentPadding: EdgeInsets.only(bottom: 0),
contentPadding:
const EdgeInsets.symmetric(vertical: 0, horizontal: 12),
border: InputBorder.none,
hintText: widget.hint,
hintStyle: TextStyle(fontSize: 15),
enabledBorder: const OutlineInputBorder(
// borderSide: BorderSide(color: Color(0xFFDCDFE6)),
borderSide: BorderSide(color: Colors.transparent),
borderRadius: BorderRadius.all(Radius.circular(4.0)),
),
focusedBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
borderSide: BorderSide(color: Colors.transparent))),
),
);
;
}
}
通常一個應(yīng)該會出現(xiàn)多出輸入框,但是每個地方的輸入框樣式和按鈕功能類型會有一定的區(qū)別,可以通過初始化傳參的方式進(jìn)行區(qū)分。如上面事例中enum SearchBarType { home, normal, homeLight }枚舉每個功能頁面出現(xiàn)SearchBar的樣式和響應(yīng)事件。
2 構(gòu)建導(dǎo)航頭部TabBar

//導(dǎo)航tabar 關(guān)注 頭條 新車 ,,。
_buildTabBar() {
return TabBar(
controller: _controller,
isScrollable: true,//是否可滾動
labelColor: Colors.black,//文字顏色
labelPadding: const EdgeInsets.fromLTRB(20, 0, 10, 5),
//下劃線樣式設(shè)置
indicator: const UnderlineTabIndicator(
borderSide: BorderSide(color: Color(0xff2fcfbb), width: 3),
insets: EdgeInsets.fromLTRB(0, 0, 0, 10),
),
tabs: tabs.map<Tab>((HomeChannelModel model) {
return Tab(
text: model.name,
);
}).toList());
}
因為Tabbar需要和TabBarView進(jìn)行聯(lián)動,需要定義一個TabController進(jìn)行綁定

3 構(gòu)建導(dǎo)航底部TabBarView容器

//TabBarView容器 信息流列表
_buildTabBarPageView() {
return KeepAliveWrapper(child:Expanded(
flex: 1,
child: Container(
color: Colors.grey.withOpacity(0.3),
child: TabBarView(
controller: _controller,
children: _buildItems(),
),
)));
}
4 構(gòu)建導(dǎo)航底部結(jié)構(gòu)填充
底部內(nèi)容結(jié)構(gòu)包含輪播圖左右切換,信息流上下滾動,下拉刷新,上拉加載更多、刷新組件用到SmartRefresher,輪播圖和信息流需要拼接,需要用CustomScrollView。
代碼如下:
_buildRefreshView() {
//刷新組件
return SmartRefresher(
controller: _refreshController,
enablePullDown: true,
enablePullUp: true,
onLoading: () async {
page++;
print('onLoading $page');
//加載頻道數(shù)據(jù)
widget.homeChannelModel.termId == 0 ? _getTTHomeNews() : _getHomeNews();
},
onRefresh: () async {
page = 1;
print('onRefresh $page');
//加載頻道數(shù)據(jù)
widget.homeChannelModel.termId == 0 ? _getTTHomeNews() : _getHomeNews();
},
//下拉頭部UI樣式
header: const WaterDropHeader(
idleIcon: Icon(
Icons.car_repair,
color: Colors.blue,
size: 30,
),
),
//上拉底部UI樣式
footer: CustomFooter(
builder: (BuildContext context, LoadStatus? mode) {
Widget body;
if (mode == LoadStatus.idle) {
body = const Text("pull up load");
} else if (mode == LoadStatus.loading) {
body = const CupertinoActivityIndicator();
} else if (mode == LoadStatus.failed) {
body = const Text("Load Failed!Click retry!");
} else if (mode == LoadStatus.canLoading) {
body = const Text("release to load more");
} else {
body = const Text("No more Data");
}
return Container(
height: 55.0,
child: Center(child: body),
);
},
),
//customScrollview拼接輪播圖和信息流。
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: _buildFutureScroll()
),
SliverList(
delegate: SliverChildBuilderDelegate((content, index) {
NewsModel newsModel = newsList[index];
return _buildChannelItems(newsModel);
}, childCount: newsList.length),
)
],
),
);
}
5 構(gòu)建導(dǎo)航底部結(jié)構(gòu)輪播圖
輪播圖單獨(dú)封裝SwiperView小組件
//首頁焦點(diǎn)輪播圖數(shù)據(jù)獲取
_buildFutureScroll(){
return FutureBuilder(
future: _getHomeFocus(),
builder: (BuildContext context, AsyncSnapshot<FocusDataModel> snapshot){
print('輪播圖數(shù)據(jù)加載 ${snapshot.connectionState} 對應(yīng)數(shù)據(jù):${snapshot.data}');
Container widget;
switch(snapshot.connectionState){
case ConnectionState.done:
if(snapshot.data != null){
widget = snapshot.data!.focusList!.isNotEmpty?Container(
height: 200,
width: MediaQuery.of(context).size.width,
child: SwiperView(snapshot.data!.focusList!,
MediaQuery.of(context).size.width),
):Container();
}else{
widget = Container();
}
break;
case ConnectionState.waiting:
widget = Container();
break;
case ConnectionState.none:
widget = Container();
break;
default :
widget = Container();
break;
}
return widget;
});
}
輪播圖組件封裝,整體基于第三方flutter_swiper_tv
import "package:flutter/material.dart";
import 'package:flutter_swiper_tv/flutter_swiper.dart';
import 'package:qctt_flutter/http/api.dart';
import 'package:qctt_flutter/models/home_channel.dart';
import 'package:qctt_flutter/models/home_focus_model.dart';
class SwiperView extends StatelessWidget {
// const SwiperView({Key? key}) : super(key: key);
final double width;
final List<FocusItemModel> items;
const SwiperView(this.items,this.width,{Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Swiper(
itemCount: items.length,
itemWidth: width,
containerWidth: width,
itemBuilder: (BuildContext context,int index){
FocusItemModel focusItemModel = items[index];
return Stack(children: [
Container(child:Image.network(focusItemModel.picUrlList![0],fit: BoxFit.fitWidth,width: width,))
],
);
},
pagination: const SwiperPagination(),
// control: const SwiperControl(),
);
}
}
6 構(gòu)建導(dǎo)航底部結(jié)構(gòu)信息流
信息流比較多,每條信息流樣式各一,具體要根據(jù)服務(wù)端返回的數(shù)據(jù)進(jìn)行判定。如本項目不至于22種樣式,
_buildChannelItems(NewsModel model) {
//0,無圖,1單張小圖 3、三張小圖 4.大圖推廣 5.小圖推廣 6.專題(統(tǒng)一大圖)
// 8.視頻小圖,9.視頻大圖 ,,11.banner廣告,12.車展,
// 14、視頻直播 15、直播回放 16、微頭條無圖 17、微頭條一圖
// 18、微頭條二圖以上 19分組小視頻 20單個小視頻 22 文章折疊卡片(關(guān)注頻道)
switch (model.style) {
case '1':
return GestureDetector(
child: OnePicArticleView(model),
onTap: ()=>_jumpToPage(model),
);
case '3':
return GestureDetector(
child: ThreePicArticleView(model),
onTap: ()=>_jumpToPage(model),
);
case '4':
return GestureDetector(
child: AdBigPicView(newsModel: model,),
onTap: ()=>_jumpToPage(model),) ;
case '9':
return GestureDetector(
child: Container(
padding: const EdgeInsets.only(left: 10, right: 10),
child: VideoBigPicView(model),
),
onTap: ()=>_jumpToPage(model),
);
case '15':
return GestureDetector(
child: Container(
width: double.infinity,
padding: const EdgeInsets.only(left: 10, right: 10),
child: LiveItemView(model),
),
onTap: ()=>_jumpToPage(model),
);
case '16'://16、微頭條無圖
return GestureDetector(
child: Container(
width: double.infinity,
padding: const EdgeInsets.only(left: 10, right: 10),
child: WTTImageView(model),
),
onTap: ()=>_jumpToPage(model),
);
case '17'://17、微頭條一圖
return GestureDetector(
child: Container(
width: double.infinity,
padding: const EdgeInsets.only(left: 10, right: 10),
child: WTTImageView(model),
),
onTap:()=> _jumpToPage(model),
);
case '18'://18、微頭條二圖以上
//18、微頭條二圖以上
return GestureDetector(
child: Container(
width: double.infinity,
padding: const EdgeInsets.only(left: 10, right: 10),
child: WTTImageView(model),
),
onTap: ()=>_jumpToPage(model),
);
case '19': //19分組小視頻
return Container(
width: double.infinity,
padding: const EdgeInsets.only(left: 10, right: 10),
child: SmallVideoGroupView(model.videoList),
);
case '20':
//20小視頻 左上方帶有藍(lán)色小視頻標(biāo)記
return Container(
padding: const EdgeInsets.only(left: 10, right: 10),
child: VideoBigPicView(model),
);
default:
return Container(
height: 20,
color: Colors.blue,
);
}
}
每種樣式需要單獨(dú)封裝Cell組件視圖。

通過_buildChannelItems(NewsModel model)方法返回的是單獨(dú)的Cell視圖,需要提交給對應(yīng)的list進(jìn)行組裝:
SliverList(
delegate: SliverChildBuilderDelegate((content, index) {
NewsModel newsModel = newsList[index];
return _buildChannelItems(newsModel);
}, childCount: newsList.length),
)
這樣整個App首頁的大體結(jié)構(gòu)就完成了,包含App頂部搜索,基于Tabbar的頭部頻道導(dǎo)航。TabbarView頭部導(dǎo)航聯(lián)動。CustomScrollView對輪播圖信息流進(jìn)行拼接,等。網(wǎng)絡(luò)數(shù)據(jù)是基于Dio進(jìn)行了簡單封裝,具體不在這里細(xì)說。具體接口涉及隱私,不展示。

至于底部BottomNavigationBar會在后續(xù)組件介紹的時候詳細(xì)介紹到。
總結(jié)
本章主要介紹了TabBarView的基本用法以及實際復(fù)雜項目中TabBarView的組合使用場景,更多關(guān)于Flutter TabBarView組件的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
iOS10適配之權(quán)限Crash問題的完美解決方案
這篇文章主要為大家詳細(xì)介紹了iOS10適配之權(quán)限Crash問題的完美解決方案,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-09-09
iOS開發(fā)之UIKeyboardTypeNumberPad數(shù)字鍵盤自定義按鍵
這篇文章主要介紹了iOS開發(fā)之UIKeyboardTypeNumberPad數(shù)字鍵盤自定義按鍵 的相關(guān)資料,需要的朋友可以參考下2016-08-08
解決iOS11圖片下拉放大出現(xiàn)信號欄白條的bug問題
這篇文章主要介紹了iOS11圖片下拉放大出現(xiàn)信號欄白條的bug問題,需要的朋友參考下吧2017-09-09

