Flutter TabBar+TabBarView实战DEMO
程序员文章站
2022-06-01 18:34:59
...
理论上适用于大部分标签页+列表切换效果,唯一的区别就是实体类接口这块的逻辑,按照自己的项目改一改,然后把listview的item布局按照自己需求改一改,改吧改吧自己就能用了
效果图如下:
底下的每个TabBarView都做了缓存,切换的时候第一次需要加载,之后再切换回来很流畅。现在粘贴代码:
main.dart:
import 'package:dio/dio.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app01/mainpage.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'TypeModel.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
// This makes the visual density adapt to the platform that you run
// the app on. For desktop platforms, the controls will be smaller and
// closer together (more dense) than on mobile platforms.
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
final String title;
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
MyHomePage({Key key, this.title}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin{
TypeModel typeModel;
RefreshController _refreshController =
RefreshController(initialRefresh: false);
TabController _tabController;
List<Data> data;
@override
void initState() {
super.initState();
_tabController = TabController(vsync: this, length: data?.length ?? 0);
this.getHttp();
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child:Column(
children: <Widget>[
TabBar(
onTap: (tab) {
print(tab);
},
labelStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
unselectedLabelStyle: TextStyle(fontSize: 16),
isScrollable: true,
controller: _tabController,
labelColor: Colors.blue,
indicatorWeight: 3,
indicatorPadding: EdgeInsets.symmetric(horizontal: 10),
unselectedLabelColor: Colors.grey,
indicatorColor: Colors.orangeAccent,
tabs: data?.map((e) => Tab(text: e.title))?.toList()??[],
),
Expanded(child: Container(
width: MediaQuery.of(context).size.width,
child: buildTabBarView()) )
],
) ,
),
);
}
Widget buildTabBarView()=>TabBarView(
controller: _tabController,
children: data?.map((e) => Center(
child:A(e.id)
))?.toList()??[],
);
void getHttp() async {
try {
Response response = await Dio().get("https://api.sunofbeach.net/shop/discovery/categories");
typeModel= TypeModel.fromJson(response.data);
data=typeModel.data;
setState(() {
_tabController = TabController(vsync: this, length: data?.length);
_tabController.addListener(() {
});
});
} catch (e) {
print(e);
}
}
}
mainpage.dart,这个是底下列表的界面,类似android的fragment:
import 'package:dio/dio.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app01/web_detail.dart';
import 'package:modal_progress_hud/modal_progress_hud.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'TypeListModel.dart';
class A extends StatefulWidget {
int id;
A(this.id);
@override
_AState createState() => _AState(id);
}
class _AState extends State<A> with AutomaticKeepAliveClientMixin {
RefreshController _refreshController =
RefreshController(initialRefresh: false);
TypeListModel typeListModel;
List<DataListBean> listData;
int id;
bool isLoading=true;
int page=1;
_AState(this.id);
@override
void initState() {
super.initState();
getListData(id.toString(),true);
}
@override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
body: Center(
child: ModalProgressHUD(child: buildRefreshContainer(),inAsyncCall:isLoading),
),
);
}
void _onRefresh() async{
page=1;
getListData(id.toString(),true);
}
void _onLoading() async{
page++;
getListData(id.toString(),false);
}
Widget buildRefreshContainer()=>SmartRefresher(
enablePullDown: true,
enablePullUp: true,
header: WaterDropHeader(),
footer: CustomFooter(
builder: (BuildContext context,LoadStatus mode){
Widget body ;
if(mode==LoadStatus.idle){
body = Text("pull up load");
}
else if(mode==LoadStatus.loading){
body = CupertinoActivityIndicator();
}
else if(mode == LoadStatus.failed){
body = Text("Load Failed!Click retry!");
}
else if(mode == LoadStatus.canLoading){
body = Text("release to load more");
}
else{
body = Text("No more Data");
}
return Container(
height: 55.0,
child: Center(child:body),
);
},
),
controller: _refreshController,
onRefresh: _onRefresh,
onLoading: _onLoading,
child: buildListView(),
);
Widget buildListView()=>ListView.separated(
itemCount: listData?.length??0,
separatorBuilder: (context,index){
return Divider(
height: 0.5,
indent: 10,
endIndent: 10,
color: Color(0xFFDDDDDD),
);
},
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
child:new Container(
height: 80,
padding:
EdgeInsets.only(left: 10, right: 10, top: 5, bottom: 5),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Expanded(
child: Column(
children: <Widget>[
Text(
listData[index].title,
style: TextStyle(
fontSize: 16,
color: Colors.black,
fontWeight: FontWeight.bold,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
Expanded(child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Text(
this.getStartTime(listData[index].couponStartTime).toString(),
style: TextStyle(fontSize: 14, color: Colors.grey),
)
],
)
),
],
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
)
),
Image.network(
"http:"+listData[index].pictUrl ,
height: 70,
width: 90,
fit: BoxFit.cover,
)
],
),
) ,
onTap: (){
String url=this.listData[index].clickUrl;
Navigator.push(context, MaterialPageRoute(builder: (_) {
return new WebDetail(url);
}));
},
) ;
});
@override
bool get wantKeepAlive => true;
DateTime getStartTime(String startTime){
var date1= DateTime.fromMillisecondsSinceEpoch(int.parse(startTime??"0"));
return date1;
}
void getListData(String materialId,bool isRefresh) async{
try {
Response response = await Dio().get("https://api.sunofbeach.net/shop/discovery/"+materialId+"/"+page.toString());
typeListModel= TypeListModel.fromJson(response.data);
if(isRefresh){
_refreshController.refreshCompleted();
listData=typeListModel.data;
}else{
_refreshController.loadComplete();
listData..addAll(typeListModel.data);
}
setState(() {
isLoading=false;
});
print(response);
} catch (e) {
print(e);
}
}
}
下面是两个model代码,一个是,上面分类标签的实体类,一个是下面列表的实体类
TypeModel.dart:
class TypeModel {
bool success;
int code;
String message;
List<Data> data;
TypeModel({this.success, this.code, this.message, this.data});
TypeModel.fromJson(Map<String, dynamic> json) {
success = json['success'];
code = json['code'];
message = json['message'];
if (json['data'] != null) {
data = new List<Data>();
json['data'].forEach((v) {
data.add(new Data.fromJson(v));
});
}
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['success'] = this.success;
data['code'] = this.code;
data['message'] = this.message;
if (this.data != null) {
data['data'] = this.data.map((v) => v.toJson()).toList();
}
return data;
}
}
class Data {
int id;
String title;
Data({this.id, this.title});
Data.fromJson(Map<String, dynamic> json) {
id = json['id'];
title = json['title'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['id'] = this.id;
data['title'] = this.title;
return data;
}
}
TypeListModel.dart:
class TypeListModel {
String message;
bool success;
int code;
List<DataListBean> data;
TypeListModel({this.message, this.success, this.code, this.data});
TypeListModel.fromJson(Map<String, dynamic> json) {
this.message = json['message'];
this.success = json['success'];
this.code = json['code'];
this.data = (json['data'] as List)!=null?(json['data'] as List).map((i) => DataListBean.fromJson(i)).toList():null;
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['message'] = this.message;
data['success'] = this.success;
data['code'] = this.code;
data['data'] = this.data != null?this.data.map((i) => i.toJson()).toList():null;
return data;
}
}
class DataListBean {
String categoryName;
String clickUrl;
String commissionRate;
String couponClickUrl;
String couponEndTime;
String couponInfo;
String couponShareUrl;
String couponStartFee;
String couponStartTime;
String itemDescription;
String levelOneCategoryName;
String nick;
String pictUrl;
String shopTitle;
String title;
String zkFinalPrice;
int categoryId;
int couponAmount;
int couponRemainCount;
int couponTotalCount;
int levelOneCategoryId;
int sellerId;
int userType;
int volume;
num itemId;
SmallImagesBean smallImages;
DataListBean({this.categoryName, this.clickUrl, this.commissionRate, this.couponClickUrl, this.couponEndTime, this.couponInfo, this.couponShareUrl, this.couponStartFee, this.couponStartTime, this.itemDescription, this.levelOneCategoryName, this.nick, this.pictUrl, this.shopTitle, this.title, this.zkFinalPrice, this.categoryId, this.couponAmount, this.couponRemainCount, this.couponTotalCount, this.levelOneCategoryId, this.sellerId, this.userType, this.volume, this.itemId, this.smallImages});
DataListBean.fromJson(Map<String, dynamic> json) {
this.categoryName = json['category_name'];
this.clickUrl = json['click_url'];
this.commissionRate = json['commission_rate'];
this.couponClickUrl = json['coupon_click_url'];
this.couponEndTime = json['coupon_end_time'];
this.couponInfo = json['coupon_info'];
this.couponShareUrl = json['coupon_share_url'];
this.couponStartFee = json['coupon_start_fee'];
this.couponStartTime = json['coupon_start_time'];
this.itemDescription = json['item_description'];
this.levelOneCategoryName = json['level_one_category_name'];
this.nick = json['nick'];
this.pictUrl = json['pict_url'];
this.shopTitle = json['shop_title'];
this.title = json['title'];
this.zkFinalPrice = json['zk_final_price'];
this.categoryId = json['category_id'];
this.couponAmount = json['coupon_amount'];
this.couponRemainCount = json['coupon_remain_count'];
this.couponTotalCount = json['coupon_total_count'];
this.levelOneCategoryId = json['level_one_category_id'];
this.sellerId = json['seller_id'];
this.userType = json['user_type'];
this.volume = json['volume'];
this.itemId = json['item_id'];
this.smallImages = json['small_images'] != null ? SmallImagesBean.fromJson(json['small_images']) : null;
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['category_name'] = this.categoryName;
data['click_url'] = this.clickUrl;
data['commission_rate'] = this.commissionRate;
data['coupon_click_url'] = this.couponClickUrl;
data['coupon_end_time'] = this.couponEndTime;
data['coupon_info'] = this.couponInfo;
data['coupon_share_url'] = this.couponShareUrl;
data['coupon_start_fee'] = this.couponStartFee;
data['coupon_start_time'] = this.couponStartTime;
data['item_description'] = this.itemDescription;
data['level_one_category_name'] = this.levelOneCategoryName;
data['nick'] = this.nick;
data['pict_url'] = this.pictUrl;
data['shop_title'] = this.shopTitle;
data['title'] = this.title;
data['zk_final_price'] = this.zkFinalPrice;
data['category_id'] = this.categoryId;
data['coupon_amount'] = this.couponAmount;
data['coupon_remain_count'] = this.couponRemainCount;
data['coupon_total_count'] = this.couponTotalCount;
data['level_one_category_id'] = this.levelOneCategoryId;
data['seller_id'] = this.sellerId;
data['user_type'] = this.userType;
data['volume'] = this.volume;
data['item_id'] = this.itemId;
if (this.smallImages != null) {
data['small_images'] = this.smallImages.toJson();
}
return data;
}
}
class SmallImagesBean {
List<String> string;
SmallImagesBean({this.string});
SmallImagesBean.fromJson(Map<String, dynamic> json) {
List<dynamic> stringList = json['string'];
this.string = new List();
this.string.addAll(stringList.map((o) => o.toString()));
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['string'] = this.string;
return data;
}
}
附上这个DEMO在pubspec.yaml需要依赖的库:
pull_to_refresh: ^1.6.3
dio: 3.0.10
json_serializable: 3.5.0
json_annotation: 3.1.1
modal_progress_hud: 0.1.3
推荐阅读
-
Flutter实战之自定义日志打印组件详解
-
Flutter定时器、倒计时的快速上手及实战讲解
-
Flutter 即学即用系列博客——02 一个纯 Flutter Demo 说明
-
spring-cloud-kubernetes官方demo运行实战
-
务必收藏备用:.net core中通过Json或直接获取图形验证码(数字验证码、字母验证码、混合验证码),有源代码全实战demo(开源代码.net core3.0)
-
TX-LCN分布式事务Demo实战
-
Flutter实战教程之酷炫的开关动画效果
-
REST风格框架实战:从MVC到前后端分离(附完整Demo)
-
apollo入门demo实战(二)
-
springboot spring cloud gateway demo实战项目