Flutter:仿京东项目实战(2)-分类和商品列表页面功能实现

在我个人认为学习一门新的语言(快速高效学习) 一定是通过实践,最好的就是做项目,这里我会简单写一个京东的Demo。

在上篇文章里面创建了BottomNavigationBar,里面包含了4个主界面,今天完成第二个主界面,分类页面的功能和商品列表功能。

用到的知识点

1. 命名路由传参

  • 路由表 routes里面增加
'/product_list': (context, {arguments}) => ProductListPage(arguments: arguments),
复制代码
  • 需要跳转页面的地方
Navigator.pushNamed(context, '/product_list', arguments: {'cid': _rightCateList[index].sId!});
复制代码
  • 跳转到的页面
class ProductListPage extends StatefulWidget {
  
  Map arguments;

  ProductListPage({Key? key, required this.arguments}) : super(key: key);

  @override
  _ProductListPageState createState() => _ProductListPageState();
}
复制代码

2. 配置抓包

  • 引入这两个dio 的头文件
import 'package:dio/adapter.dart';
import 'package:dio/dio.dart';
复制代码
  • 配置抓包代码
//设置只在debug模式下抓包
final kReleaseMode = false;
final Dio dio = Dio();
if (!kReleaseMode){
  //设置代理 抓包用
  (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (HttpClient client) {
    client.findProxy = (uri) {
      return "PROXY localhost:8888";
    };
  };
}
复制代码
  • 配置好后的抓包的效果:

截屏2021-12-23 上午9.05.59.png

3. 上拉加载下拉刷新通过 flutter_easyrefresh 实现

  • 这里是 flutter_easyrefresh 官网列出的几种实现方式:
import 'package:flutter_easyrefresh/easy_refresh.dart';
....
  // 方式一
  EasyRefresh(
    child: ScrollView(),
    onRefresh: () async{
      ....
    },
    onLoad: () async {
      ....
    },
  )
  // 方式二
  EasyRefresh.custom(
    slivers: <Widget>[],
    onRefresh: () async{
      ....
    },
    onLoad: () async {
      ....
    },
  )
  // 方式三
  EasyRefresh.builder(
    builder: (context, physics, header, footer) {
      return CustomScrollView(
        physics: physics,
        slivers: <Widget>[
          ...
          header,
          ...
          footer,
        ],
      );
    }
    onRefresh: () async{
      ....
    },
    onLoad: () async {
      ....
    },
  )
复制代码
  • 在商品列表中的使用
EasyRefresh(
  child: ListView.builder(
      itemCount: productList.length,
      itemBuilder: (context, index) {
        //创建列表内容
        return createContent(index);
      }
      ),
  //下拉刷新
  onRefresh: () async{
    _page = 1;
    _getProductListData(false);
  },
  //上拉加载
  onLoad: () async {
    _page += 1;
    if(!_hasMore){
      return;
    }
    _getProductListData(true);
  },
)
复制代码

4. 保持页面状态 AutomaticKeepAliveClientMixin

Flutter切换tabar后不会保留tabbar状态 ,为了节约内存不会保存widget的状态,widget都是临时变量。当我们使用TabBar,切换tabar,initState又会被调用一次。可以使用 AutomaticKeepAliveClientMixin 解决这个问题

  • 当前类要继承 AutomaticKeepAliveClientMixin
class _CategoryPageState extends State<CategoryPage> with AutomaticKeepAliveClientMixin
复制代码
  • 实现这个方法
bool get wantKeepAlive =>true;
复制代码
  • 添加 super.build(context)
  @override
  Widget build(BuildContext context) {
    super.build(context);
    return Container();
  }
复制代码

5. 数据和模型的转换

这里只是简单的数据模型转换,我采用手动的方式实现了

class ProductItemModel {
  String? sId;
  String? title;

  ProductItemModel({this.sId, this.title,});

  ProductItemModel.fromJson(Map<String, dynamic> json) {
    sId = json['_id'];
    title = json['title'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['_id'] = this.sId;
    data['title'] = this.title;
    return data;
  }
}
复制代码

6.ListView 的使用

截屏2021-12-20 下午9.33.30.png

7. GridView 网格布局的实现

截屏2021-12-22 下午9.35.07.png

8. Image 常用方法

加入图片的几种方式:
Image.asset:加载本地资源图片
Image.network:加载网络资源图片
Image.file:加载本地文件中的图片
复制代码

截屏2021-12-23 上午11.16.21.png 截屏2021-12-23 上午11.16.30.png

9. 本地项目国际化

flutter_localizations:
    sdk: flutter
复制代码
import 'package:flutter_localizations/flutter_localizations.dart';

new MaterialApp(
 localizationsDelegates: [
   // ... app-specific localization delegate[s] here
   GlobalMaterialLocalizations.delegate,
   GlobalWidgetsLocalizations.delegate,
 ],
 supportedLocales: [
    const Locale('en', 'US'), // English
    const Locale('he', 'IL'), // Hebrew
    // ... other locales the app supports
  ],
  // ...
)
复制代码

具体支持国际化更多的方案可以参考:zhuanlan.zhihu.com/p/145992691

具体功能实现

实现的效果

截屏2021-12-22 下午9.51.30.png

全局配置信息类config.dart

例如可以在里面存放域名

class Config{
  static String domain="https://jdmall.itying.com/";
}
复制代码

分类页面的实现

整体页面左边通过ListView、右边通过GridView实现,然后通过点击左边列表实现右边列表的数据刷新。

定义数据模型

class CateModel {
  List<CateItemModel> result = [];

  CateModel({required this.result});

  CateModel.fromJson(Map<String, dynamic> json) {
    if (json['result'] != null) {
      json['result'].forEach((v) {
        result.add(new CateItemModel.fromJson(v));
      });
    }
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    if (this.result.length > 0) {
      data['result'] = this.result.map((v) => v.toJson()).toList();
    }
    return data;
  }
}

class CateItemModel {
  String? sId; //String? 表示可空类型
  String? title;
  Object? status;
  String? pic;
  String? pid;
  String? sort;

  CateItemModel(
      {this.sId, this.title, this.status, this.pic, this.pid, this.sort});

  CateItemModel.fromJson(Map<String, dynamic> json) {
    sId = json['_id'];
    title = json['title'];
    status = json['status'];
    pic = json['pic'];
    pid = json['pid'];
    sort = json['sort'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['_id'] = this.sId;
    data['title'] = this.title;
    data['status'] = this.status;
    data['pic'] = this.pic;
    data['pid'] = this.pid;
    data['sort'] = this.sort;
    return data;
  }
}
复制代码

实现代码

class CategoryPage extends StatefulWidget {
  CategoryPage({Key? key}) : super(key: key);

  _CategoryPageState createState() => _CategoryPageState();
}

class _CategoryPageState extends State<CategoryPage> with AutomaticKeepAliveClientMixin{
  
  //当前选中
  int _selectIndex=0;
  //左侧列表数据
  List _leftCateList=[];
  //右侧列表数据
  List _rightCateList=[];

   @override
  // TODO: implement wantKeepAlive 缓存当前页面
  bool get wantKeepAlive =>true;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    
    _getLeftCateData();
  }

  //左侧分类的数据
  _getLeftCateData() async{
       var api = '${Config.domain}api/pcate';
      var result = await Dio().get(api);
      var leftCateList = new CateModel.fromJson(result.data);
      setState(() {
        this._leftCateList = leftCateList.result;
      });
      _getRightCateData(leftCateList.result[0].sId);
  }

 //右侧分类数据
 _getRightCateData(pid) async{
      var api = '${Config.domain}api/pcate?pid=${pid}';
      var result = await Dio().get(api);
      var rightCateList = new CateModel.fromJson(result.data);
      setState(() {
        this._rightCateList = rightCateList.result;
      });
  }

  //左侧列表布局
  Widget _leftCateWidget(leftWidth){
    if(_leftCateList.length>0){
      return Container(         
            width: leftWidth,
            height: double.infinity,
            // color: Colors.red,
            child: ListView.builder(
                itemCount: _leftCateList.length,
                itemBuilder: (context,index){
                  return Column(
                    children: <Widget>[
                      InkWell(                      
                        onTap: (){
                            setState(() {
                              //刷新右侧列表的数据
                              _selectIndex= index;
                              _getRightCateData(_leftCateList[index].sId);
                            });
                        },
                        child: Container(                        
                            width: double.infinity,
                            height: ScreenAdapter.height(84),
                            padding: EdgeInsets.only(top:ScreenAdapter.height(24)),
                            child: Text("${_leftCateList[index].title}",textAlign: TextAlign.center),
                            color: _selectIndex==index? Color.fromRGBO(240, 246, 246, 0.9):Colors.white,
                        ),
                      ),
                      Divider(height: 1),
                    ],
                  );
                },

            ),
          );
      } else {
         return Container(         
            width: leftWidth,
            height: double.infinity
         );
      }
  }

  //创建右侧列表
  Widget _rightCateWidget(rightItemWidth,rightItemHeight){
    if(_rightCateList.length>0){
      return Expanded(
            flex: 1,
            child: Container(
                padding: EdgeInsets.all(10),
                height: double.infinity,
                color: Color.fromRGBO(240, 246, 246, 0.9),
                child: GridView.builder(
                  gridDelegate:SliverGridDelegateWithFixedCrossAxisCount(
                    crossAxisCount:3,
                    childAspectRatio: rightItemWidth/rightItemHeight,
                    crossAxisSpacing: 10,
                    mainAxisSpacing: 10
                  ),
                  itemCount: _rightCateList.length,
                  itemBuilder: (context,index){
                      //处理图片
                      String pic = _rightCateList[index].pic;
                      pic = Config.domain+pic.replaceAll('\', '/');

                      return InkWell(
                        onTap: (){
                          Navigator.pushNamed(context, '/product_list', arguments: {'cid': _rightCateList[index].sId!});
                        },
                        child: Container(
                          // padding: EdgeInsets.all(10),
                          child: Column(
                            children: <Widget>[
                              AspectRatio(
                                aspectRatio: 1/1,
                                child: Image.network("${pic}",fit: BoxFit.cover),
                              ),
                              Container(
                                height: ScreenAdapter.height(28),
                                child: Text("${_rightCateList[index].title}"),
                              )
                            ],
                          ),
                        ),
                      );
                  },
                )
            ),
        );
    } else {
        return Expanded(
            flex: 1,
            child: Container(
                padding: EdgeInsets.all(10),
                height: double.infinity,
                color: Color.fromRGBO(240, 246, 246, 0.9),
                child: Text("加载中..."),
            )
        );
    }
  }

  @override
  Widget build(BuildContext context) {
    //左侧宽度
    var leftWidth=ScreenAdapter.getScreenWidth()/4;
    //右侧每一项宽度=(总宽度-左侧宽度-GridView外侧元素左右的Padding值-GridView中间的间距/3
    var rightItemWidth=(ScreenAdapter.getScreenWidth()-leftWidth-20-20)/3;
    //获取计算后的宽度
    rightItemWidth=ScreenAdapter.width(rightItemWidth);
    //获取计算后的高度
    var rightItemHeight=rightItemWidth+ScreenAdapter.height(28);
    return Scaffold(
      appBar: AppBar(
        title: Text('分类页面'),
      ),
      body: Row(
        children: <Widget>[
          _leftCateWidget(leftWidth),
          _rightCateWidget(rightItemWidth,rightItemHeight)
        ],
      ),
    );
  }
}
复制代码

实现效果

Simulator Screen Shot - iPhone 12 Pro - 2021-12-22 at 21.50.45.png

商品列表页面

创建商品列表Model

class ProductModel {
  List<ProductItemModel> result=[];

  ProductModel({required this.result});

  ProductModel.fromJson(Map<String, dynamic> json) {
    if (json['result'] != null) {
      json['result'].forEach((v) {
        result.add(new ProductItemModel.fromJson(v));
      });
    }
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    if (this.result != null) {
      data['result'] = this.result.map((v) => v.toJson()).toList();
    }
    return data;
  }
}

class ProductItemModel {
  String? sId;   //String? 表示可空类型
  String? title;
  String? cid;
  Object? price;   //所有的类型都继承 Object
  String? oldPrice;
  String? pic;
  String? sPic;

  ProductItemModel(
      {this.sId,
        this.title,
        this.cid,
        this.price,
        this.oldPrice,
        this.pic,
        this.sPic});

  ProductItemModel.fromJson(Map<String, dynamic> json) {
    sId = json['_id'];
    title = json['title'];
    cid = json['cid'];
    price = json['price'];
    oldPrice = json['old_price'];
    pic = json['pic'];
    sPic = json['s_pic'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['_id'] = this.sId;
    data['title'] = this.title;
    data['cid'] = this.cid;
    data['price'] = this.price;
    data['old_price'] = this.oldPrice;
    data['pic'] = this.pic;
    data['s_pic'] = this.sPic;
    return data;
  }
}
复制代码

实现代码

class ProductListPage extends StatefulWidget {

  Map arguments;

  ProductListPage({Key? key, required this.arguments}) : super(key: key);

  @override
  _ProductListPageState createState() => _ProductListPageState();
}

class _ProductListPageState extends State<ProductListPage> {

  final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();

  //当前页码
  int _page = 1;
  //每次请求返回多少条数据
  int _pageSize = 10;
  //排序
  String _sort = '';
  //是否还有更多
  bool _hasMore = true;
  //每组ID
  int _selectHeaderId = 1;
  //页面列表数据
  List productList = [];
  //搜索关键字
  String _keyWords = '';
  //文本输入框的控制器
  var _initKeywordsController = TextEditingController();

  /*二级导航数据*/
  List _subHeaderList = [
    {"id": 1, "title": "综合", "fileds": "all", "sort": -1,},
    //排序     升序:price_1     {price:1}        降序:price_-1   {price:-1}
    {"id": 2, "title": "销量", "fileds": 'salecount', "sort": -1},
    {"id": 3, "title": "价格", "fileds": 'price', "sort": -1},
    {"id": 4, "title": "筛选"}
  ];

  @override
  void initState() {
    // TODO: implement initState
    super.initState();

    _getProductListData(false);
  }

  //请求列表数据,异步请求
  _getProductListData(bool isMore) async {

    var api;
    if(_keyWords.isEmpty){
      api = '${Config.domain}api/plist?cid=${widget.arguments["cid"]}&page=${_page}&sort=${_sort}&pageSize=${_pageSize}';
    } else {
      api = '${Config.domain}api/plist?cid=${widget.arguments["cid"]}&page=${_page}&sort=${_sort}&pageSize=${_pageSize}&search=${_keyWords}';
    }

    //设置只在debug模式下抓包
    final kReleaseMode = false;
    final Dio dio = Dio();
    if (!kReleaseMode){
      //设置代理 抓包用
      (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (HttpClient client) {
        client.findProxy = (uri) {
          return "PROXY localhost:8888";
        };
      };
    }

    var result = await dio.get(api);

    //解析json数据,目前我都还是采用手动解析
    var dataList = ProductModel.fromJson(result.data).result;
    if(dataList.length > 10){
      _hasMore = true;
    }

    setState(() {
      if(isMore){
        productList.addAll(dataList);
      } else {
        productList = dataList;
      }
    });
  }

  //改变分组的处理
  _subHeaderChange(id){
    if(id==4){
      setState(() {
        _selectHeaderId = id;
        _scaffoldKey.currentState!.openEndDrawer();
      });
    } else {
      setState(() {
        _selectHeaderId = id;
        _sort =
        "${_subHeaderList[id - 1]["fileds"]}_${_subHeaderList[id - 1]["sort"]}";
        _page = 1;
        productList = [];
        //改变sort排序
        _subHeaderList[id - 1]['sort'] = _subHeaderList[id - 1]['sort'] * -1;
        _hasMore = true;
        _getProductListData(false);
      });
    }
  }

  //列表的内容
  Widget createContent(index) {
    ProductItemModel itemModel = productList[index];
    String pic = '';
    if(itemModel.pic != null){
      //Config存放全局配置的类
      //由于这个图片链接有问题才这样处理
      pic = Config.domain + itemModel.pic!.replaceAll('\', '/');
    }
    return Column(
      children: [
        InkWell(
          onTap: (){
            //push到下个页面并传参
            Navigator.pushNamed(context, '/product_content', arguments: {'id' : itemModel.sId});
          },
          child: Row(
            children: [
              Container(
                margin: EdgeInsets.only(left: 10),
                width: ScreenAdapter.width(180),
                height: ScreenAdapter.height(180),
                child: Image.network(
                  pic,
                  fit: BoxFit.cover,
                ),
              ),
              Expanded(
                flex: 1,
                child: Container(
                  margin: EdgeInsets.all(10),
                  height: ScreenAdapter.height(180),
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        itemModel.title!,
                        maxLines: 2,
                        overflow: TextOverflow.ellipsis,
                      ),
                      Row(
                        children: [
                          Container(
                            alignment: Alignment.center,
                            height: ScreenAdapter.height(36),
                            margin: EdgeInsets.only(right: 10),
                            padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
                            decoration: BoxDecoration(
                              borderRadius: BorderRadius.circular(10),
                              color: Color.fromRGBO(230, 230, 230, 0.9),
                            ),
                            child: Text('4g'),
                          ),
                          Container(
                            alignment: Alignment.center,
                            height: ScreenAdapter.height(36),
                            margin: EdgeInsets.only(right: 10),
                            padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
                            decoration: BoxDecoration(
                              borderRadius: BorderRadius.circular(10),
                              color: Color.fromRGBO(230, 230, 230, 0.9),
                            ),
                            child: Text(
                              '126',
                            ),
                          )
                        ],
                      ),
                      Text(
                        '¥${itemModel.price!.toString()}',
                        style: TextStyle(color: Colors.red, fontSize: 16),
                      )
                    ],
                  ),
                ),
              ),
            ],
          ),
        ),
      ],
    );
  }

  //创建商品列表
  Widget _productListWidget() {
    return Container(
      height: ScreenAdapter.getScreenHeight(),
      padding: EdgeInsets.all(10),
      margin: EdgeInsets.only(top: ScreenAdapter.height(80)),
      //配置刷新
      child: EasyRefresh(
        child: ListView.builder(
            itemCount: productList.length,
            itemBuilder: (context, index) {
              //创建列表内容
              return createContent(index);
            }
            ),
        //下拉刷新
        onRefresh: () async{
          _page = 1;
          _getProductListData(false);
        },
        //上拉加载
        onLoad: () async {
          _page += 1;
          if(!_hasMore){
            return;
          }
          _getProductListData(true);
        },
      ),
    );
  }

  //创建升降序的图标
  Widget _showIcon(id){
    if(id==2 || id==3){
      if(_subHeaderList[id-1]['sort'] == 1){
        return Icon(Icons.arrow_drop_down);
      } else {
        return Icon(Icons.arrow_drop_up);
      }
    }
    return Text('');
  }

  //创建头部分组
  Widget _subHeaderWidget() {
    return Positioned(
      top: 0,
      width: ScreenAdapter.getScreenWidth(),
      height: ScreenAdapter.height(80),
      child: Container(
        //分组底部分割线
        decoration: const BoxDecoration(
            border: Border(
                bottom: BorderSide(
                    color: Color.fromRGBO(233, 233, 233, 0.9), width: 1))),
        child: Row(
          children: _subHeaderList.map((value){
            return Expanded(
                flex: 1,
                child: InkWell(
                  onTap: () {
                    _subHeaderChange(value['id']);
                  },
                  child: Padding(
                    padding: EdgeInsets.fromLTRB(
                        0, ScreenAdapter.height(16), 0, ScreenAdapter.height(16)),
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Container(
                          child: Text(
                            value['title'],
                            textAlign: TextAlign.center,
                            style: TextStyle(color: _selectHeaderId == value['id'] ? Colors.red : Colors.black),
                          ),
                        ),
                        _showIcon(value['id'])
                      ],
                    ),
                  ),
                ));
          }).toList(),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: _scaffoldKey,
      //创建导航栏
      appBar: AppBar(
        leading: IconButton(
          onPressed: (){
            Navigator.pop(context);
          },
          icon: Icon(Icons.arrow_back),
        ),
        title: Container(
          //文本输入
          child: TextField(
            controller: this._initKeywordsController,
            autofocus: true,
            decoration: InputDecoration(
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(30),
                borderSide: BorderSide.none
              ),
            ),
            onChanged: (value){
              setState(() {
                //搜索框输入的文字
                _keyWords = value;
              });
            },
          ),
          height: ScreenAdapter.height(68),
          decoration: BoxDecoration(
            color: Color.fromRGBO(233, 233, 233, 0.8),
            borderRadius: BorderRadius.circular(30)
          ),
        ),
        actions: [
          InkWell(
            child: Container(
              width: ScreenAdapter.width(80),
              height: ScreenAdapter.height(68),
              child: Row(
                children: [
                  Text('搜索', style: TextStyle(fontSize: 16),)
                ],
              ),
            ),
            onTap: (){
              //点击搜索框开始搜索,这里只是简单的在综合组搜索
              _subHeaderChange(1);
            },
          )
        ],
      ),
      endDrawer: Drawer(
        child: Container(
          child: Text('实现筛选功能'),
        ),
      ),
      body: !productList.isEmpty ? Stack(
        children: [
          //创建导航栏下分页栏
          _subHeaderWidget(),
          //创建页面
          _productListWidget(),
        ],
      ) : Center(
        child: Text('没有搜索到商品'),
      ),
    );
  }
}
复制代码

实现效果

Simulator Screen Shot - iPhone 12 Pro - 2021-12-23 at 09.14.59.png

猜你喜欢

转载自juejin.im/post/7044716539550892068