Flutter 商城实例 分类列表

列表页_使用Provide控制子类

使用flutter_provide,初步实现点击列表页的大类,改变小类的效果。

继续上次的代码接着写:https://www.cnblogs.com/joe235/p/11633989.html

新写了一个右侧小类,暂时用假数据代替,代码如下:

//右侧小类类别
class RightCategoryNav extends StatefulWidget {
  _RightCategoryNavState createState() => _RightCategoryNavState();
}

class _RightCategoryNavState extends State<RightCategoryNav> {

  List list = ['名酒','宝丰','北京二锅头','舍得','五粮液','茅台','散白'];
  
  @override
  Widget build(BuildContext context) {
    
     return Container(
            height: ScreenUtil().setHeight(80),
            width: ScreenUtil().setWidth(570),
            decoration: BoxDecoration(
              color: Colors.white,
              border: Border(
                bottom: BorderSide(width: 1,color: Colors.black12)
              )
            ),
            child:ListView.builder(
              scrollDirection: Axis.horizontal,
              itemCount: list.length,
              itemBuilder: (context,index){
                return _rightInkWell(list[index]);
              },
            )
     );
  }
  //子类
  Widget _rightInkWell(String item){
    return InkWell(
      onTap: (){},
      child: Container(
        padding:EdgeInsets.fromLTRB(5.0,10.0,5.0,10.0),
        child: Text(
          item,
          style: TextStyle(fontSize:ScreenUtil().setSp(28)),
        ),
      ),
    );
  }    

添加到界面中

category_page.dartCategoryPage类的build方法里,加入右侧子类导航区域.

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text('商品分类'),
    ),
    body: Container(
      child: Row(
        children: <Widget>[
          LeftCategoryNav(),
          Column(
            children: <Widget>[
              RightCategoryNav()
            ],
          )
        ],
      ),
    ),
  );
}

我们就应该能看到效果,但是现在数据都是写死的,还没有实现状态的控制,下一步就是用provide来控制,实现二级分类和一级分类的交互效果。

编写二级分类的Provide文件

我们先设置一个子类的provide,在lib/provide/文件夹下,新建一个child_category.dart文件,这个文件就是控制子类的状态管理文件。代码如下:

import 'package:flutter/material.dart';
import '../model/category.dart';

//ChangeNotifier的混入是不用管理听众
class ChildCategory with ChangeNotifier{

    List<BxMallSubDto> childCategoryList = [];

    getChildCategory(List list){
      childCategoryList=list;
      notifyListeners();
    }
}

引入了category.dart的model文件,这样就可以很好的对象化,先声明了一个泛型的List变量childCategoryList。然后做了个方法,进行赋值。(注意这种形式也是在工作中最常用的一种形式。)

main里进行引入

import './provide/child_category.dart';

void main() {

  var childCategory= ChildCategory();
  providers
  ..provide(Provider<Counter>.value(counter)) //依赖
  ..provide(Provider<ChildCategory>.value(childCategory));
 
  。。。
}

修改二级分类状态

有了Provide类之后,就可以修改二级分类了,这时候先引入child_category.dart文件和provide.dart,再修改左侧大类的InkWell中的onTap方法。 

import 'package:provide/provide.dart';
import '../provide/child_category.dart';
onTap: () {
    var childList = list[index].bxMallSubDto;
    Provide.value<ChildCategory>(context).getChildCategory(childList);
},

编写好后,其实状态已经改变了,那接下来就可以设置二级分类的修改状态了。

二级分类展现

修改右侧二级分类的展示,这个先改变子项的接受数据。把原来的item,改成item.mallSubName,修改后的代码如下:

Widget _rightInkWell(BxMallSubDto item){

    return InkWell(
      onTap: (){},
      child: Container(
        padding:EdgeInsets.fromLTRB(5.0,10.0,5.0,10.0),
        
        child: Text(
          item.mallSubName,
          style: TextStyle(fontSize:ScreenUtil().setSp(28)),
        ),
      ),
    );
  }

子项修改好后哦,再修改build里的Container,我们需要加入一个Provide组件,注意这里使用了泛型。

@override
  Widget build(BuildContext context) {
    return Container(
      width: ScreenUtil().setWidth(570),
      height: ScreenUtil().setHeight(80),
      decoration: BoxDecoration(
        color: Colors.white,
        border: Border(
          bottom: BorderSide(width: 1,color: Colors.black12)
        )
      ),
      child: Provide<ChildCategory>(
        builder: (context,child,childCategory){
          return ListView.builder( //ListView要设置宽高,纵向的可以只设置高,横向的宽高都要设置
            scrollDirection: Axis.horizontal, //横向
            itemCount: childCategory.childCategoryList.length,
            itemBuilder: (content,index){
              return _rightInkWell(childCategory.childCategoryList[index]);
            },
          );
        },
      ),  
    );
  }

修改步骤:

  1. Container Widget外层加入一个Provie widget
  2. 修改ListView WidgetitemCount选项为childCategory.childCategoryList.length
  3. 修改itemBuilder里的传值选项为return _rightInkWell(childCategory.childCategoryList[index]);

交互效果的设置

现在二级分类已经能跟随我们的点击发生变化了,但是大类还没有高亮显示,所以要作一下交互效果,这种交互效果跟其它类或者页面没什么关系,所以我们还是使用最简单的setState来实现了。 这个变化主要在_leftInkWell里,所以操作也基本在这个里边。

  1. 先声明一个变量,用于控制是否高亮显示bool isClick = false;
  2. _leftInkWell接收一个变量,变量是ListView传递过来的Widget _leftInkWel(int index)
  3. 声明一个全局的变量var listIndex = 0; //索引
  4. 对比index和listIndexisClick=(index==listIndex)?true:false;.
  5. 修改为动态显示背景颜色color: isClick?Colors.black26:Colors.white,

在进入页面后的_getCategory里在等到大类数据后,把第一个小类的数据同时进行状态修改。

代码如下:

//得到后台大类数据
  void _getCategory() async{
    await request('post','getCategory').then((val){
      var data = json.decode(val.toString());
      //print(data);
      CategoryModel category = CategoryModel.fromJson(data);
      setState(() {
        list = category.data; 
      });
      Provide.value<ChildCategory>(context).getChildCategory( list[0].bxMallSubDto);
      //print(list[0].bxMallSubDto);
      //list.data.forEach((item)=>print(item.mallCategoryName));
    });
  }

反白显示颜色过重,使用RGBO颜色

这个直接使用Flutter里的RGBO模式就可以了,当然你也完全可以使用Colors.black12

color: isClick ? Color.fromRGBO(236, 238, 239, 1.0) : Colors.white,

添加子类“全部”按钮

原型上在二级分类上是有“全部”字样的,但我们作的这里并没有。其实加上这个全部也非常简单,只要我们在状态管理,改变状态的方法getChildCategory里,现加入一个全部的BxMallSubDto对象就可以了。

代码部分就是修改provide/child_Category.dartgetchildCategory方法。思路是声明一个all对象,然后进行赋值,复制后组成List赋给childCategoryList。然后把list添加到childCategoryList里。

import 'package:flutter/material.dart';
import '../model/category.dart';

//ChangeNotifier的混入是不用管理听众
class ChildCategory with ChangeNotifier{

    List<BxMallSubDto> childCategoryList = [];

    getChildCategory(List<BxMallSubDto> list){
      BxMallSubDto all=  BxMallSubDto();
      all.mallSubId='00';
      all.mallCategoryId='00';
      all.mallSubName = '全部';
      all.comments = 'null';
      childCategoryList=[all];
      childCategoryList.addAll(list);   
      notifyListeners();
    }
}

全部代码如下:

import 'package:flutter/material.dart';
import '../service/service_method.dart';
import 'dart:convert';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../model/category.dart';
import 'package:provide/provide.dart';
import '../provide/child_category.dart';

class CategoryPage extends StatefulWidget {
  _CategoryPageState createState() => _CategoryPageState();
}

class _CategoryPageState extends State<CategoryPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('商品分类')),
      body: Container(
        child: Row(
          children: <Widget>[
            LeftCategoryNav(),
            Column(
              children: <Widget>[
                RightCategoryNav(),

              ],
            )
          ]
        ) 
      )   
    );
  }

}

//左侧大类导航
class LeftCategoryNav extends StatefulWidget {
  _LeftCategoryNavState createState() => _LeftCategoryNavState();
}

class _LeftCategoryNavState extends State<LeftCategoryNav> {
  List list = [];
  var listIndex = 0; //索引

  @override
  void initState() { 
    _getCategory(); //请求接口的数据
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    
    return Container(
      width: ScreenUtil().setWidth(180),
      decoration: BoxDecoration(
        border: Border(
          right: BorderSide(width: 1,color: Colors.black12)
        )
      ),
      child: ListView.builder(
        itemCount: list.length,
        itemBuilder:(context,index){
          return _leftInkWell(index);
        }
      ),   
    );
  }

  Widget _leftInkWell(int index){
    bool isClick = false; //是否点击了 
    isClick = (index==listIndex) ? true : false; //判断当前点击的索引是否为全局变量的索引

    return InkWell(
      onTap: (){
         setState(() {
           listIndex = index; //每次点击了大类就把索引赋值给listIndex
         });
        var childList = list[index].bxMallSubDto;
        Provide.value<ChildCategory>(context).getChildCategory(childList);
      },
      child: Container(
        height: ScreenUtil().setHeight(100),
        padding: EdgeInsets.only(left: 10,top: 13),
        decoration: BoxDecoration(
          color: isClick ? Color.fromRGBO(236,236,236,1.0) : Colors.white,
          border: Border(
            bottom: BorderSide(width: 1,color: Colors.black12)
          )
        ),
        child: Text(list[index].mallCategoryName,style: TextStyle(fontSize: ScreenUtil().setSp(28))),
      ),
    );
  }

  //得到后台大类数据
  void _getCategory()async{
    await request('post','getCategory').then((val){
      var data = json.decode(val.toString());
      //print(data);
      CategoryModel category = CategoryModel.fromJson(data);
      setState(() {
        list = category.data; 
      });
      Provide.value<ChildCategory>(context).getChildCategory( list[0].bxMallSubDto);
      print(list[0].bxMallSubDto);
      list[0].bxMallSubDto.forEach((item) => print(item.mallSubName));
      //list.data.forEach((item)=>print(item.mallCategoryName));
    });
  }

}

//右侧二级分类
class RightCategoryNav extends StatefulWidget {
  _RightCategoryNavState createState() => _RightCategoryNavState();
}

class _RightCategoryNavState extends State<RightCategoryNav> {
  //List list = ['名酒','宝丰','北京二锅头','舍得','五粮液','茅台','散白'];

  @override
  Widget build(BuildContext context) {
    return Container(
      width: ScreenUtil().setWidth(570),
      height: ScreenUtil().setHeight(80),
      decoration: BoxDecoration(
        color: Colors.white,
        border: Border(
          bottom: BorderSide(width: 1,color: Colors.black12)
        )
      ),
      child: Provide<ChildCategory>(
        builder: (context,child,childCategory){
          return ListView.builder( //ListView要设置宽高,纵向的可以只设置高,横向的宽高都要设置
            scrollDirection: Axis.horizontal, //横向
            itemCount: childCategory.childCategoryList.length,
            itemBuilder: (content,index){
              return _rightInkWell(childCategory.childCategoryList[index]);
            },
          );
        },
      ),  
    );
  }

  //子类
  Widget _rightInkWell(BxMallSubDto item){
    return InkWell(
      onTap: (){},
      child: Container(
        padding: EdgeInsets.fromLTRB(5, 10, 5, 10),
        child: Text(
          item.mallSubName,
          style:TextStyle(fontSize:ScreenUtil().setSp(28.0))
        ),
      ),
    );  
  }

}

 现在点击左侧大类,右侧顶部的二级分类会随之变化。

列表页_商品列表接口调试

下面先调通商品分类页里的商品列表接口,这个接口包括上拉加载、大类切换和小类切换的互动。

配置URL路径

对于后台接口的调试,应该有所了解了,第一步就是配置后台接口的路径到统一的配置文件中,这样方便以后的维护。

打开lib\config\service_ulr.dart文件,再最下面加上商品分类的商品列表接口路径,现在的配置文件,代码如下:

const servicePath={
  'homePageContext': serviceUrl+'wxmini/homePageContent', // 商家首页信息
  'homePageBelowConten': serviceUrl+'wxmini/homePageBelowConten', //商城首页热卖商品拉取
  'getCategory': serviceUrl+'wxmini/getCategory', //商品类别信息
  'getMallGoods': serviceUrl+'wxmini/getMallGoods', //商品分类的商品列表
};

测试大类商品列表接口

lib\pages\category_page.dart文件里,新建一个CategoryGoodsList类,这个类我们也将用状态管理进行管理,所以这个类并没有什么其它的耦合,不接收任何参数。

//右侧商品列表,可以上拉加载
class CategoryGoodsList extends StatefulWidget {
  @override
  _CategoryGoodsListState createState() => _CategoryGoodsListState();
}

class _CategoryGoodsListState extends State<CategoryGoodsList> {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Text('商品列表'),
    );
  }
}

有了类以后,我们写一个内部获取后台数据的方法_getGoodList。先声明了一个变量data,用于放入传递的值。然后再把参数传递过去。具体代码如下:

//调试接口
  void _getGoodList() async{
    var data = {
      'categoryId': '4',
      'categorySubId': '',
      'page': 1,
    };
    await request('post', 'getMallGoods', formData: data).then((val){
      var data = json.decode(val.toString());
      print('分类列表===========》${data}');
    });
  }

然后我们在initState中调用一下:

@override
  void initState() { 
    _getGoodList();
    super.initState();   
  }

然后把CategoryGoodsList加入到页面布局。

@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('商品分类')),
      body: Container(
        child: Row(
          children: <Widget>[
            LeftCategoryNav(),
            Column(
              children: <Widget>[
                RightCategoryNav(),
                CategoryGoodsList(),
              ],
            )
          ]
        ) 
      )   
    );
  }

如果一切正常应该可以在终端中看到输出的结果,有正常的列表结果输出,说明一切正常。

列表页_商品列表数据模型的建立

先用快速的方法,生成我们商品分类的商品列表数据模型,然后根据数据模型修改一下,读取后台的方法。

商品列表页数据模型

这里还是使用快速生成的方法,利用https://javiercbk.github.io/json_to_dart/,直接生成。

给出一段JSON数据,当然你页可以自己抓取,这非常的容易。

{"code":"0","message":"success","data":[{"image":"http://images.baixingliangfan.cn/compressedPic/20190116145309_40.jpg","oriPrice":2.50,"presentPrice":1.80,"goodsName":"哈尔滨冰爽啤酒330ml","goodsId":"3194330cf25f43c3934dbb8c2a964ade"},{"image":"http://images.baixingliangfan.cn/compressedPic/20190115185215_1051.jpg","oriPrice":2.00,"presentPrice":1.80,"goodsName":"燕京啤酒8°330ml","goodsId":"522a3511f4c545ab9547db074bb51579"},{"image":"http://images.baixingliangfan.cn/compressedPic/20190121102419_9362.jpg","oriPrice":1.98,"presentPrice":1.80,"goodsName":"崂山清爽8°330ml","goodsId":"bbdbd5028cc849c2998ff84fb55cb934"},{"image":"http://images.baixingliangfan.cn/compressedPic/20180712181330_9746.jpg","oriPrice":2.50,"presentPrice":1.90,"goodsName":"雪花啤酒8°清爽330ml","goodsId":"87013c4315e54927a97e51d0645ece76"},{"image":"http://images.baixingliangfan.cn/compressedPic/20180712180233_4501.jpg","oriPrice":2.50,"presentPrice":2.20,"goodsName":"崂山啤酒8°330ml","goodsId":"86388a0ee7bd4a9dbe79f4a38c8acc89"},{"image":"http://images.baixingliangfan.cn/compressedPic/20190116164250_1839.jpg","oriPrice":2.50,"presentPrice":2.30,"goodsName":"哈尔滨小麦王10°330ml","goodsId":"d31a5a337d43433385b17fe83ce2676a"},{"image":"http://images.baixingliangfan.cn/compressedPic/20180712181139_2653.jpg","oriPrice":2.70,"presentPrice":2.50,"goodsName":"三得利清爽啤酒10°330ml","goodsId":"74a1fb6adc1f458bb6e0788c4859bf54"},{"image":"http://images.baixingliangfan.cn/compressedPic/20190121162731_3928.jpg","oriPrice":2.75,"presentPrice":2.50,"goodsName":"三得利啤酒7.5度超纯啤酒330ml","goodsId":"d52fa8ba9a5f40e6955be9e28a764f34"},{"image":"http://images.baixingliangfan.cn/compressedPic/20180712180452_721.jpg","oriPrice":4.50,"presentPrice":3.70,"goodsName":"青岛啤酒11°330ml","goodsId":"a42c0585015540efa7e9642ec1183940"},{"image":"http://images.baixingliangfan.cn/compressedPic/20190121170407_7423.jpg","oriPrice":4.40,"presentPrice":4.00,"goodsName":"三得利清爽啤酒500ml 10.0°","goodsId":"94ec3df73f4446b5a5f0d80a8e51eb9d"},{"image":"http://images.baixingliangfan.cn/compressedPic/20180712181427_6101.jpg","oriPrice":4.50,"presentPrice":4.00,"goodsName":"雪花勇闯天涯啤酒8°330ml","goodsId":"d80462faab814ac6a7124cec3b868cf7"},{"image":"http://images.baixingliangfan.cn/compressedPic/20180717151537_3425.jpg","oriPrice":4.90,"presentPrice":4.10,"goodsName":"百威啤酒听装9.7°330ml","goodsId":"91a849140de24546b0de9e23d85399a3"},{"image":"http://images.baixingliangfan.cn/compressedPic/20190121101926_2942.jpg","oriPrice":4.95,"presentPrice":4.50,"goodsName":"崂山啤酒8°500ml","goodsId":"3758bbd933b145f2a9c472bf76c4920c"},{"image":"http://images.baixingliangfan.cn/compressedPic/20180712175422_518.jpg","oriPrice":5.00,"presentPrice":4.50,"goodsName":"百威3.6%大瓶9.7°P460ml","goodsId":"dc32954b66814f40977be0255cfdacca"},{"image":"http://images.baixingliangfan.cn/compressedPic/20180717151454_4834.jpg","oriPrice":5.00,"presentPrice":4.50,"goodsName":"青岛啤酒大听装500ml","goodsId":"fc85510c3af7428dbf1cb0c1bcb43711"},{"image":"http://images.baixingliangfan.cn/compressedPic/20180712181007_4229.jpg","oriPrice":5.50,"presentPrice":5.00,"goodsName":"三得利金纯生啤酒580ml 9°","goodsId":"14bd89f066ca4949af5e4d5a1d2afaf8"},{"image":"http://images.baixingliangfan.cn/compressedPic/20190121100752_4292.jpg","oriPrice":6.60,"presentPrice":6.00,"goodsName":"哈尔滨啤酒冰纯白啤(小麦啤酒)500ml","goodsId":"89bccd56a8e9465692ccc469cd4b442e"},{"image":"http://images.baixingliangfan.cn/compressedPic/20180712175656_777.jpg","oriPrice":7.20,"presentPrice":6.60,"goodsName":"百威啤酒500ml","goodsId":"3a94dea560ef46008dad7409d592775d"},{"image":"http://images.baixingliangfan.cn/compressedPic/20180712180754_2838.jpg","oriPrice":7.78,"presentPrice":7.00,"goodsName":"青岛啤酒皮尔森10.5°330ml","goodsId":"97adb29137fb47689146a397e5351926"},{"image":"http://images.baixingliangfan.cn/compressedPic/20190116164149_2165.jpg","oriPrice":7.78,"presentPrice":7.00,"goodsName":"青岛全麦白啤11°500ml","goodsId":"f78826d3eb0546f6a2e58893d4a41b43"}]}

得到快速生成的Model类,在model文件夹下,新建一个文件categoryGoodsList.dart,这时候我们需要修改一下代码,防止产生冲突。修改完成的代码如下:

class CategoryGoodsListModel {
  String code;
  String message;
  List<CategoryListData> data;

  CategoryGoodsListModel({this.code, this.message, this.data});

  CategoryGoodsListModel.fromJson(Map<String, dynamic> json) {
    code = json['code'];
    message = json['message'];
    if (json['data'] != null) {
      data = new List<CategoryListData>();
      json['data'].forEach((v) {
        data.add(new CategoryListData.fromJson(v));
      });
    }
  }

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

class CategoryListData {
  String image;
  double oriPrice;
  double presentPrice;
  String goodsName;
  String goodsId;

  CategoryListData(
      {this.image,
      this.oriPrice,
      this.presentPrice,
      this.goodsName,
      this.goodsId});

  CategoryListData.fromJson(Map<String, dynamic> json) {
    image = json['image'];
    oriPrice = json['oriPrice'];
    presentPrice = json['presentPrice'];
    goodsName = json['goodsName'];
    goodsId = json['goodsId'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['image'] = this.image;
    data['oriPrice'] = this.oriPrice;
    data['presentPrice'] = this.presentPrice;
    data['goodsName'] = this.goodsName;
    data['goodsId'] = this.goodsId;
    return data;
  }
}

然后在category_page.dart里引入:

import '../model/categoryGoodsList.dart';

修改_getGoodList方法

 主要是让从后台得到的数据,可以使用数据模型。

void _getGoodList()async {
    var data={
      'categoryId':'4',
      'categorySubId':"",
      'page':1
    };
    await request('getMallGoods',formData:data ).then((val){
        var  data = json.decode(val.toString());
        CategoryGoodsListModel goodsList=  CategoryGoodsListModel.fromJson(data);
        setState(() {
         list= goodsList.data;
        });
        print('>>>>>>>>>>>>>>>>>>>:${list[0].goodsName}');
    });
  }

写完后测试一下,如果可以在控制台输出,想要的结果,说明我们的Model类建立完成了。

列表页_商品列表UI布局

商品图片方法编写

我们把这个列表拆分成三个内部方法,分别是商品图片、商品名称和商品价格。这样拆分可以减少耦合和维护难度。

先来制作图片的内部方法,代码如下:

//图片
 Widget _goodsImage(index){
    return  Container(
      width: ScreenUtil().setWidth(200),
      child: Image.network(list[index].image),
    );

  }

商品名称方法编写

这个我们直接返回一个Container,然后在里边子组件里放一个Text,需要对Text进行一些样式设置,防止越界。

//商品名称
Widget _goodsName(index){
    return Container( 
      padding: EdgeInsets.all(5.0),
      width: ScreenUtil().setWidth(370),
      child: Text(
        list[index].goodsName,
        maxLines: 2,
        overflow: TextOverflow.ellipsis,
        style: TextStyle(fontSize: ScreenUtil().setSp(28)),
        ),
      );
  }

商品价格方法编写

商品价格我们在Container里放置一个Row,这样就能实现同一排显示,具体可以查看代码。

//商品价格
Widget _goodsPrice(index){
    return  Container( 
      margin: EdgeInsets.only(top:20.0),
      width: ScreenUtil().setWidth(370),
      child:Row(
        children: <Widget>[
            Text(
              '价格:¥${list[index].presentPrice}',
              style: TextStyle(color:Colors.pink,fontSize:ScreenUtil().setSp(30)),
              ),
            Text(
              '¥${list[index].oriPrice}',
              style: TextStyle(
                color: Colors.black26,
                decoration: TextDecoration.lineThrough
              ),
            )
        ]
      )
    );
  }

把方法进行组合

把一个列表项分成了好几个方法,现在需要把每一个方法进行组合。具体代码如下,我会在视频中进行详细讲解

Widget _ListWidget(int index){

    return InkWell(
      onTap: (){},
      child: Container(
        padding: EdgeInsets.only(top: 5.0,bottom: 5.0),
        decoration: BoxDecoration(
          color: Colors.white,
          border: Border(
            bottom: BorderSide(width: 1.0,color: Colors.black12)
          )
        ),
        
        child: Row(
          children: <Widget>[
            _goodsImage(index)
           ,
            Column(
              children: <Widget>[
                _goodsName(index),
                _goodsPrice(index)
              ],
            )
          ],
        ),
      )
    );

  }

ListView的构建

组合完成后,在build方法里,使用ListView来显示表单,记得要正确设置宽和高。

 @override
  Widget build(BuildContext context) {
    return Container(
      width: ScreenUtil().setWidth(570) ,
      height: ScreenUtil().setHeight(1000),
      child: ListView.builder(
        itemCount: list.length,
        itemBuilder: (context,index){
          return _ListWidget(index);
        },
      )
    );
  }

构建好后,就可以进行测试了。

列表页_商品列表交互效果制作

现在页面布局已经基本完成,接下来就要作商品分类页的各种交互效果了,

制作商品列表的Provide

制作Provide是有一个小技巧的,就是页面什么元素需要改变,你就制作什么的provide类,比如现在我们要点击大类,改变商品列表,实质改变的就是List的值,那只制作商品列表List的Provide就可以了。

lib/proive/文件夹下,新建一个category_goods_list.dart文件。

import 'package:flutter/material.dart';
import '../model/categoryGoodsList.dart';

class CategoryGoodsListProvide with ChangeNotifier{

    List<CategoryListData> goodsList = [];
  
    //点击大类时更换商品列表
    getGoodsList(List<CategoryListData> list){
           
      goodsList=list;   
      notifyListeners();
    }
}

先引入了model中的categoryGoodsList.dart文件,管理的状态就是goodsList变量,我们通关过一个方法getGoodsList来改变状态。这样一个Provide类就制作完成了。

将状态放入顶层

Provide编程完成以后,需要把写好的状态管理放到main.dart中,我司叫它为放入顶层,就是全部页面想用这个状态都可以获得。代码如下:

void main(){
  var childCategory= ChildCategory();
  var categoryGoodsListProvide= CategoryGoodsListProvide();

  var counter =Counter();
  var providers  =Providers();
  providers
    ..provide(Provider<ChildCategory>.value(childCategory))
    ..provide(Provider<CategoryGoodsListProvide>.value(categoryGoodsListProvide))
    ..provide(Provider<Counter>.value(counter));

  runApp(ProviderNode(child:MyApp(),providers:providers));
}

声明一个categoryGoodsListProvide变量,然后放入顶层就可以了。

修改category_page.dart页面

这个页面需要伤筋动骨,进行彻底修改结构,步骤较多,请按步骤一步步完成。

1.引入provide文件

lib/pages/category_page.dart文件最上面引入刚写的provide.

import '../provide/category_goods_list.dart';

2.修改_getGoodsList方法

上节课为了布局,把得到商品列表数据的方法,放到了商品列表类里。现在需要把这个方法放到我们的CategoryPage类里,作为一个内部方法,因为我们要在点击大类时,调用后台接口和更新状态。

//得到商品列表数据
void _getGoodList({String categoryId}) async{
    var data = {
      'categoryId': categoryId == null ? '4' : categoryId,
      'categorySubId': '',
      'page': 1,
    };
    await request('post', 'getMallGoods', formData: data).then((val){
      var data = json.decode(val.toString());
      CategoryGoodsListModel goodsList = CategoryGoodsListModel.fromJson(data);

      Provide.value<CategoryGoodsListProvide>(context).getGoodsList(goodsList.data);
    });
  }

首先方法要增加一个可选参数,就是大类ID,如果没有大类ID,我们默认为4,有了参数后到后台获得数据,获得后使用Provide改变状态。

3.使用_getGoodList方法

修改完这个方法后,可以在每次点击大类的时候进行调用。代码如下:

onTap: (){
        setState(() {
          listIndex = index; //每次点击了大类就把索引赋值给listIndex
        });
        var childList = list[index].bxMallSubDto; //当前大类的子类列表
        var categoryId = list[index].mallCategoryId;
        Provide.value<ChildCategory>(context).getChildCategory(childList);
        _getGoodList(categoryId:categoryId);
      },

这段代码,先声明了一个类别IDcategoryId,然后调用了_getGoodList()方法,调用方法时要传递categoryId参数。

4.修改商品列表代码

这个部分的代码修改要多一点,要把原来的setState模式,换成provide模式,所以很多地方都有所不同,但是我们的布局代码时不需要改的。

先去掉list ,然后用Provide widget来监听变化,修改类里的子方法,多接收一个List参数,命名为newList,每个子方法都要加入,这里提醒不要使用state,否则会报错。

修改后的代码如下:

//右侧商品列表 ,可以上拉加载
class CategoryGoodsList extends StatefulWidget {
  CategoryGoodsList({Key key}) : super(key: key);

  _CategoryGoodsListState createState() => _CategoryGoodsListState();
}

class _CategoryGoodsListState extends State<CategoryGoodsList> {
  //List list = [];

  @override
  void initState() { 

    super.initState();   
  }

  @override
  Widget build(BuildContext context) {
    return Provide<CategoryGoodsListProvide>(
      builder:(context, child, data){
        return Container(
          width: ScreenUtil().setWidth(570),
          height: ScreenUtil().setHeight(980),
          child: ListView.builder(
            itemCount: data.goodsList.length,
            itemBuilder: (context,index){
              return _listGoods(data.goodsList,index);
            },
          )
        );
      },
    );
  }

  
  //图片
  Widget _goodsImage(List newList,index){
    return Container(
      width: ScreenUtil().setWidth(200),
      child: Image.network(newList[index].image),
    );
  }
  //商品名称
  Widget _goodsName(List newList,index){
    return Container(
      width: ScreenUtil().setWidth(370),
      padding: EdgeInsets.all(5),
      child: Text(
        newList[index].goodsName,
        maxLines:2,
        overflow:TextOverflow.ellipsis,
        style: TextStyle(fontSize:ScreenUtil().setSp(28)),
      ),
    );
  }
  //商品价格
  Widget _goodsPrice(List newList,index){
    return Container(
      margin: EdgeInsets.only(top: 20),
      width: ScreenUtil().setWidth(370),
      child: Row(
        children: <Widget>[
          Text(
            '价格:${newList[index].presentPrice} ',
            style: TextStyle(color: Colors.blueGrey,fontSize: ScreenUtil().setSp(28)),
          ),
          Text(
            '价格:${newList[index].oriPrice}',
            style: TextStyle(color: Colors.black26,decoration: TextDecoration.lineThrough),
          ),
        ],
      ),
    );
  }
  //商品组合  
  Widget _listGoods(List newList,index){
    return InkWell(
      onTap: (){},
      child: Container(
        padding: EdgeInsets.only(top:5,bottom: 5),
        decoration: BoxDecoration(
          color: Colors.white,
          border: Border(
            bottom: BorderSide(width: 1,color: Colors.black12)
          )
        ),
        child: Row(
          children: <Widget>[
            _goodsImage(newList,index),
            Column(
              children: <Widget>[
                _goodsName(newList,index),
                _goodsPrice(newList,index),
              ],
            )
          ],
        ),
      ),
    );
  }

}

然后使用_getGoodList方法,在_leftInkWell的onTap里修改:

var categoryId = list[index].mallCategoryId;
_getGoodList(categoryId:categoryId);

还要在LeftCategoryNav的initState加上_getGoodList();

class _LeftCategoryNavState extends State<LeftCategoryNav> {
  List list = [];
  var listIndex = 0; //索引

  @override
  void initState() { 
    _getCategory(); //请求接口的数据
-----------添加部分start------------
    _getGoodList();
-----------添加部分end----------------
    super.initState();
  }

总结:这节课算是Provide的高级应用了,如果这个状态管理小伙伴都很熟练了,至少Flutter的状态管理这个知识点是没有问题了。

列表页_小类高亮交互效果制作

Expanded Widget的使用

Expanded Widget 是让子Widget有伸缩能力的小部件,它继承自Flexible,用法也差不多。那为什么要单独拿出来讲一下Expanded Widget那?我们在首页布局和列表页布局时都遇到了高度适配的问题,很多小伙伴出现了高度溢出的BUG,所以这节课开始前先解决一下这个问题。

修改 Category_page.dart里的商品列表页面,不再约束高了,而是使用Expanded Widget包裹外层,修改后的代码如下:

@override
  Widget build(BuildContext context) {
    return Provide<CategoryGoodsListProvide>(
        builder: (context,child,data){
          return Expanded(
            child:Container(
              width: ScreenUtil().setWidth(570) ,
              child:ListView.builder(
                  itemCount: data.goodsList.length,
                  itemBuilder: (context,index){
                    return _ListWidget(data.goodsList,index);
                  },
                ) ,
            ) ,
          ); 
       },
    );
  }

小类高亮效果制作

由于高亮效果也受到大类的控制,不仅仅是子类别的控制,所以这个效果也要用到状态管理来制作。这个状态很简单,没必要单独写一个Provide,所以直接使用以前的类就可以,我们直接在provide/child_category.dart里修改。修改的代码为:

import 'package:flutter/material.dart';
import '../model/category.dart';

//ChangeNotifier的混入是不用管理听众
class ChildCategory with ChangeNotifier{

  List<BxMallSubDto> childCategoryList = [];
  int childIndex = 0; //子类高亮索引

  //点击大类时切换逻辑
  getChildCategory(List<BxMallSubDto> list){
    childIndex = 0; //每次点击切换大类,子类都要归0

    BxMallSubDto all = BxMallSubDto();
    all.mallSubId = '00';
    all.mallCategoryId = '00';
    all.mallSubName = '全部';
    all.comments = 'null';
    childCategoryList = [all];
    childCategoryList.addAll(list);
    notifyListeners();
  }

  //改变子类索引
  changeChildIndex(index){
    childIndex = index;
    notifyListeners();
  } 

}

然后就可以修改UI部分了,UI部分主要是增加索引参数,然后进行判断。

(1)先把_rghtInkWell方法增加一个接收参数int index.这就是修改变量的索引值。

Widget _rightInkWell(int index,BxMallSubDto item)

(2)定义是否高亮变量,再根据状态进行赋值

bool isClick =false; //是否点击
isClick = (index == Provide.value<ChildCategory>(context).childIndex) ? true : false;

(3)用isCheck判断是否高亮

color: isClick ? Colors.blueGrey:Colors.black

(4)_rightInkWell添加index

return _rightInkWell(index,childCategory.childCategoryList[index]);

(5)点击时修改状态_rightInkWell的onTap

Provide.value<ChildCategory>(context).changeChildIndex(index);

到这里,我们的子类高亮就制作完成了,并且当更换大类时,子类自动更改为第一个高亮。

列表页_子类和商品列表切换

修改Provide类

先改动一下child_ategory.dart的Provide类,增加一个大类ID,然后在更改大类的时候改变ID。

import 'package:flutter/material.dart';
import '../model/category.dart';

//ChangeNotifier的混入是不用管理听众
class ChildCategory with ChangeNotifier{

    List<BxMallSubDto> childCategoryList = [];
    int childIndex = 0;
    String categoryId = '4';

    //点击大类时更换
    getChildCategory(List<BxMallSubDto> list,String id){
      
      childIndex=0;
      categoryId=id;
      BxMallSubDto all=  BxMallSubDto();
      all.mallSubId='00';
      all.mallCategoryId='00';
      all.mallSubName = '全部';
      all.comments = 'null';
      childCategoryList=[all];
      childCategoryList.addAll(list);   
      notifyListeners();
    }
    //改变子类索引
    changeChildIndex(index){
       childIndex=index;
       notifyListeners();
    }
} 

修改调用getChildCategory放

增加了参数,以前的调用方法也就都不对了,所以需要修改一下。直接用搜索功能就可以找到getChildCategory方法,一共两处,直接修改就可以了

Provide.value<ChildCategory>(context).getChildCategory(childList,categoryId);
Provide.value<ChildCategory>(context).getChildCategory(list[0].bxMallSubDto,list[0].mallCategoryId);

增加getGoodsList方法

拷贝_getGoodsList方法到子列表类里边,然后把传递参数换成子类的参数categorySubId.代码如下:

//得到商品列表数据
   void _getGoodList(String categorySubId) {
     
    var data={
      'categoryId':Provide.value<ChildCategory>(context).categoryId,
      'categorySubId':categorySubId,
      'page':1
    };
    
    request('getMallGoods',formData:data ).then((val){
        var  data = json.decode(val.toString());
        CategoryGoodsListModel goodsList=  CategoryGoodsListModel.fromJson(data);
        // Provide.value<CategoryGoodsList>(context).getGoodsList(goodsList.data);
        Provide.value<CategoryGoodsListProvide>(context).getGoodsList(goodsList.data);
       
    });
  }

调用方法改版列表

当点击子类时,调用这个方法,并传入子类ID。

onTap: (){
    Provide.value<ChildCategory>(context).changeChildIndex(index);
    _getGoodList(item.mallSubId); //调用子类ID
}, 

子类没有商品时报错

有些小类别里是没有商品的,这时候就会报错。解决这个错误非常简单,只要进行判断就可以了

1、判断状态管理时是否存在数据

首先你要在修改状态的时候,就进行一次判断,方式类型不对,导致整个app崩溃。也就是在点击小类的ontap方法后,当然这里调用了_getGoodList()方法。代码如下:

//得到商品列表数据
   void _getGoodList(String categorySubId) {
     
    var data={
      'categoryId':Provide.value<ChildCategory>(context).categoryId,
      'categorySubId':categorySubId,
      'page':1
    };
    
    request('getMallGoods',formData:data ).then((val){
        var  data = json.decode(val.toString());
        CategoryGoodsListModel goodsList=  CategoryGoodsListModel.fromJson(data);
        // Provide.value<CategoryGoodsList>(context).getGoodsList(goodsList.data);
        if(goodsList.data==null){
         Provide.value<CategoryGoodsListProvide>(context).getGoodsList([]);
        }else{
          Provide.value<CategoryGoodsListProvide>(context).getGoodsList(goodsList.data);
          
        }
    });
  }

2.判断界面输出时是不是有数据

这个主要时给用户一个友好的界面提示,如果没有数据,要提示用户。修改的是商品列表类的build方法,代码如下:

@override
  Widget build(BuildContext context) {
    return Provide<CategoryGoodsListProvide>(
        builder: (context,child,data){
          if(data.goodsList.length>0){
             return Expanded(
                child:Container(
                  width: ScreenUtil().setWidth(570) ,
                    child:ListView.builder(
                      itemCount: data.goodsList.length,
                      itemBuilder: (context,index){
                        return _ListWidget(data.goodsList,index);
                      },
                    ) 
                  )
                ) ,
              ); 
          }else{
            return  Text('暂时没有数据');
          }
       },

    );
  }

把子类ID也Provide化

现在的子类ID,我们还没有形成状态,用的是普通的setState,如果要做下拉刷新,那setState肯定是不行的,因为这样就进行跨类了,没办法传递过去。

1.首先修改provide/child_category.dart类,增加一个状态变量subId,然后在两个方法里都进行修改,代码如下:

import 'package:flutter/material.dart';
import '../model/category.dart';

//ChangeNotifier的混入是不用管理听众
class ChildCategory with ChangeNotifier{

    List<BxMallSubDto> childCategoryList = []; //商品列表
    int childIndex = 0; //子类索引值
    String categoryId = '4'; //大类ID
    String subId =''; //小类ID 
 
    //点击大类时更换
    getChildCategory(List<BxMallSubDto> list,String id){
      categoryId=id;
      childIndex=0;
      subId=''; //点击大类时,把子类ID清空
      BxMallSubDto all=  BxMallSubDto();
      all.mallSubId='00';
      all.mallCategoryId='00';
      all.mallSubName = '全部';
      all.comments = 'null';
      childCategoryList=[all];
      childCategoryList.addAll(list);   
      notifyListeners();
    }
    //改变子类索引 ,
    changeChildIndex(int index,String id){
      //传递两个参数,使用新传递的参数给状态赋值
       childIndex=index;
       subId=id;
       notifyListeners();
    }
}

这就为以后我们作上拉加载效果打下了基础。

列表页_上拉加载功能的制作

现在主要制作一下列表页的上拉加载更多功能,重点会放在上拉加载和Provide结合的方法。

增加page和noMoreText到Provide里

因为无论切换大类或者小类的时候,都需要把page变成1,所以需要在provide/child_category.dart里新声明一个page变量.noMoreText主要用来控制是否显示更多和如果没有数据了,不再向后台请求数据。每一次后台数据的请求都是宝贵的。

int page=1;  //列表页数,当改变大类或者小类时进行改变
String noMoreText=''; //显示更多的标识

声明在切换大类和切换小类的时候都把page变成1,代码如下:

//点击大类时更换
  getChildCategory(List<BxMallSubDto> list,String id){
      isNewCategory=true;
      categoryId=id;
      childIndex=0;
      //------------------关键代码start
      page=1;
      noMoreText = ''; 
      //------------------关键代码end
      subId=''; //点击大类时,把子类ID清空
      noMoreText='';
      BxMallSubDto all=  BxMallSubDto();
      all.mallSubId='00';
      all.mallCategoryId='00';
      all.mallSubName = '全部';
      all.comments = 'null';
      childCategoryList=[all];
      childCategoryList.addAll(list);   
      notifyListeners();
    }
    //改变子类索引 ,
    changeChildIndex(int index,String id){
       isNewCategory=true;
       //传递两个参数,使用新传递的参数给状态赋值
       childIndex=index;
       subId=id;
       //------------------关键代码start
       page=1;
       noMoreText = ''; //显示更多的表示
       //------------------关键代码end
       notifyListeners();
  }

还需要写一个增加page数量的方法,用来实现每次上拉加载后,page随之加一,代码如下:

//增加Page的方法f
  addPage(){
    page++;
  }

在制作一个改变noMoreText方法。

//改变noMoreText数据  
  changeNoMore(String text){
      noMoreText=text;
      notifyListeners();
  }

增加EasyRefresh组件

category_page.dart里增加EasyRefresh组件,首先需要使用import进行引入。

import 'package:flutter_easyrefresh/easy_refresh.dart';

引入之后,可以直接使用EasyRefresh进行包裹,然后加上各种需要的参数,


  GlobalKey<RefreshFooterState> _footerkey = new GlobalKey<RefreshFooterState>(); //定义key

@override Widget build(BuildContext context) {
return Provide<CategoryGoodsListProvide>( builder: (context,child,data){ if(data.goodsList.length>0){ return Expanded( child:Container( width: ScreenUtil().setWidth(570) , child:EasyRefresh( refreshFooter: ClassicsFooter( key:_footerKey, bgColor:Colors.white, textColor:Colors.pink, moreInfoColor: Colors.pink, showMore:true, noMoreText:Provide.value<ChildCategory>(context).noMoreText, moreInfo:'加载中', loadReadyText:'上拉加载' ), child:ListView.builder( itemCount: data.goodsList.length, itemBuilder: (context,index){ return _ListWidget(data.goodsList,index); }, ) , loadMore: ()async{ print('上拉加载更多.......'); }, ) ) , ); }else{ return Text('暂时没有数据'); } }, ); }

修改请求数据的方法

这个类中也需要一个去后台请求数据的方法,这个方法要求从Provide里读出三个参数,大类ID,小类ID和页数。代码如下:

//上拉加载更多的方法
  void _getMoreList(){
     
    Provide.value<ChildCategory>(context).addPage();
     var data={
      'categoryId':Provide.value<ChildCategory>(context).categoryId,
      'categorySubId':Provide.value<ChildCategory>(context).subId,
      'page':Provide.value<ChildCategory>(context).page
    };
    
    request('getMallGoods',formData:data ).then((val){
        var  data = json.decode(val.toString());
        CategoryGoodsListModel goodsList=  CategoryGoodsListModel.fromJson(data);
       
        if(goodsList.data==null){
         Provide.value<ChildCategory>(context).changeNoMore('没有更多了');
        }else{
           
          Provide.value<CategoryGoodsListProvide>(context).addGoodsList(goodsList.data);
          
        }
    });

  }

每次都先调用增加页数的方法,这样请求的数据就是最新的,当没有数据的时候要把noMoreText设置成‘没有更多了’。

切换类别返回顶部

到目前为止,我们应该可以正常展示上拉加载更多的方法了,但是还有一个小Bug,切换大类或者小类的时候,我们的页面没有回到顶部,这个其实很好解决。再build的Provide的构造器里加入下面的代码就可以了。

try{
  if(Provide.value<ChildCategory>(context).page==1){
    scrollController.jumpTo(0.0);
  }
}catch(e){
  print('进入页面第一次初始化:${e}');
}

当然你还要再列表类里进行声明scrollController,如果你不声明是没办法使用的。

var scrollController=new ScrollController();

声明完成后,给ListView加上controller属性。

child:ListView.builder(
  controller: scrollController,
  itemCount: data.goodsList.length,
  itemBuilder: (context,index){
    return _ListWidget(data.goodsList,index);
  },
) ,

这时候再进行测试,应该就可以了。

Fluttertoast组件的介绍

在APP的使用过程中,对用户的友好提示是必不可少的,比如当列表页上拉加载更多的时候,到达了数据的底部,没有更多数据了,就要给用户一个友好的提示。但是这种提示又不能影响用户的使用,一个轻提示组件给大家FlutterToast

Fluttertoast 组件简介

这是一个第三方组件,当你学习的时候可以到Github上查找最新版本。

GitHub地址:https://github.com/PonnamKarthik/FlutterToast

这个组件我觉的还时比较好用的,提供了样式自定义,而且自带的效果页是很酷炫的。

如何使用Fluttertoast

首先需要在pubspec.yaml中进行引入Fluttertoast组件(也叫保持依赖,也叫包管理),主要版本号,请使用最新的,这里不保证时最新版本。

fluttertoast: ^3.1.3

引入后在需要使用的页面使用import引入,引入代码如下:

import 'package:fluttertoast/fluttertoast.dart';

Fluttertoast使用方法

在需要使用的地方直接可以使用,如下代码:

Fluttertoast.showToast(
  msg: "已经到底了",
  toastLength: Toast.LENGTH_SHORT,
  gravity: ToastGravity.CENTER,
  timeInSecForIos: 1,
  backgroundColor: Colors.pink,
  textColor: Colors.white,
  fontSize: 16.0
);
  • msg:提示的文字,String类型。
  • toastLength: 提示的样式,主要是长度,有两个值可以选择:Toast.LENGTH_SHORT :短模式,就是比较短。Toast.LENGTH_LONG : 长模式,就是比较长。
  • gravity:提示出现的位置,分别是上中下,三个选项。ToastGravity.TOP顶部提示,ToastGravit.CENTER中部提示,ToastGravity.BOTTOM底部提示。
  • bgcolor: 背景颜色,跟从Flutter颜色。
  • textcolor:文字的颜色。
  • fontSize: 文字的大小。

小Bug的处理

在列表页还存在着一个小Bug,就是当我们选择子类别后,然后返回全部,这时候会显示没有数据,这个主要是我们在Provide里构造虚拟类别时,传递的参数不对,只要把参数修改成空就可以了。

打开provide/child_category.dart,修改getChildCateg()方法。 修改代码如下:

//点击大类时更换
getChildCategory(List<BxMallSubDto> list,String id){
  isNewCategory=true;
  categoryId=id;
  childIndex=0;
  page=1;
  subId=''; //点击大类时,把子类ID清空
  noMoreText='';
  BxMallSubDto all=  BxMallSubDto();
  //--------修改的关键代码start
  all.mallSubId='';
  //--------修改的关键代码end
  all.mallCategoryId='00';
  all.mallSubName = '全部';
  all.comments = 'null';
  childCategoryList=[all];
  childCategoryList.addAll(list);   
  notifyListeners();
}

现在重新运行就可以了 。

posted on 2020-06-05 15:52  JoeYoung  阅读(2074)  评论(0编辑  收藏  举报