欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Flutter TabBar+TabBarView实战DEMO

程序员文章站 2022-06-01 18:34:59
...

理论上适用于大部分标签页+列表切换效果,唯一的区别就是实体类接口这块的逻辑,按照自己的项目改一改,然后把listview的item布局按照自己需求改一改,改吧改吧自己就能用了
效果图如下:
Flutter TabBar+TabBarView实战DEMO

底下的每个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