Flutter 商城实例 购物车

开始制作购物车部分的内容了。这也算是最复杂的一个部分,也是我们基本掌握Flutter实战技巧的关键,当然我会还是采用UI代码和业务逻辑完全分开的形式,让代码完全解耦。

1、购物车_添加商品

Provide的建立

因为要UI和业务进行分离,所以还是需要先建立一个Provide文件,在lib/provide/文件夹下,建立一个cart.dart文件。

思路是先把List转换成字符串,然后再进行持久化,修改的时候再转换成LIst的l。

先引入下面三个文件和包:

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';

引进后建立一个类,并在里边写一个字符串变量(后期会换成对象)。代码如下:

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';

class CartProvide with ChangeNotifier{

  String cartString="[]";

}

添加商品到购物车

先来制作把商品添加到购物车的方法。思路是这样的,利用shared_preferences可以保存字符串的特点,我们先把List<Map>传换成字符串,然后操作的时候,我们再转换回来。说简单点就是持久化的只是一串字符串,然后需要操作的时候,我们变成List,操作List的每一项就可以了。

save(goodsId,goodsName,count,price,images) async{
    //初始化SharedPreferences
    SharedPreferences prefs = await  SharedPreferences.getInstance();
    cartString=prefs.getString('cartInfo');  //获取持久化存储的值
    //判断cartString是否为空,为空说明是第一次添加,或者被key被清除了。
    //如果有值进行decode操作
    var temp=cartString==null?[]:json.decode(cartString.toString());
    //把获得值转变成List
    List<Map> tempList= (temp as List).cast();
    //声明变量,用于判断购物车中是否已经存在此商品ID
    var isHave= false;  //默认为没有
    int ival=0; //用于进行循环的索引使用
    tempList.forEach((item){//进行循环,找出是否已经存在该商品
      //如果存在,数量进行+1操作
      if(item['goodsId']==goodsId){
        tempList[ival]['count']=item['count']+1;=true;
      }
      ival++;
    });
    //  如果没有,进行增加
    if(!isHave){
      tempList.add({
        'goodsId':goodsId,
        'goodsName':goodsName,
        'count':count,
        'price':price,
        'images':images
      });
    }
    //把字符串进行encode操作,
    cartString= json.encode(tempList).toString();
    print(cartString);
    prefs.setString('cartInfo', cartString);//进行持久化
   
  }

清空购物车

为了测试方便,再顺手写一个清空购物车的方法,这个还没有谨慎思考,只是为了测试使用。

remove() async{
    SharedPreferences prefs = await SharedPreferences.getInstance();
    //prefs.clear();//清空键值对
    prefs.remove('cartInfo');
    print('清空完成-----------------');
    notifyListeners();
  }

注册全局依赖

main.dart文件中注册全局依赖,先引入cart.dart文件.

import './provide/cart.dart';

然后在main区域进行声明

var cartProvide = CartProvide();

进行注入:

..provide(Provider<CartProvide>.value(cartProvide))

 业务逻辑加入到UI

details_bottom.dart文件里,加入Provide,先进行引入。

import 'package:provide/provide.dart';
import '../../provide/cart.dart';
import '../../provide/details_info.dart';

然后声明provide的save方法中需要的参数变量。

var goodsInfo = Provide.value<DetailsInfoProvide>(context).goodsInfo.data.goodInfo;
var goodsId= goodsInfo.goodsId;
var goodsName =goodsInfo.goodsName;
var count =1;
var price =goodsInfo.presentPrice;
var images= goodsInfo.image1;

然后在加入购物车的按钮的onTap方法中,加入下面代码.

onTap: ()async {
  await Provide.value<CartProvide>(context).save(goodsID,goodsName,count,price,images);
 },

先暂时把“马上结账”按钮方式清除购物车的方法,方便我们测试。

onTap: ()async{
  await Provide.value<CartProvide>(context).remove();
},

做完这些,我们就要查看一下效果了,看看是否可以真的持久化。

完成代码:

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:provide/provide.dart';
import '../../provide/cart.dart';
import '../../provide/detail_info.dart';

class DetailsBottom extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //声明变量,商品详细信息
    var goodsInfo = Provide.value<DetailsInfoProvide>(context).goodsInfo.data.goodInfo;
    var goodsId = goodsInfo.goodsId;
    var goodsName = goodsInfo.goodsName;
    var count = 1;
    var price = goodsInfo.presentPrice;
    var images = goodsInfo.image1;

    return Container(
      width: ScreenUtil().setWidth(750),
      height: ScreenUtil().setHeight(80),
      color: Colors.white,
      child: Row(
        children: <Widget>[
          InkWell(
            onTap: () {},
            child: Container(
              width: ScreenUtil().setWidth(110),
              alignment: Alignment.center,
              child: Icon(Icons.shopping_cart,size: 35,color: Colors.red,),
            ),
          ),
          InkWell(
            onTap: () async{
              await Provide.value<CartProvide>(context).save(goodsId, goodsName, count, price, images);
            },
            child: Container(
              width: ScreenUtil().setWidth(320),
              height: ScreenUtil().setHeight(80),
              alignment: Alignment.center,
              color: Colors.green,
              child: Text(
                '加入购物车',
                style:TextStyle(color:Colors.white,fontSize:ScreenUtil().setSp(28))
              ),
            ),
          ),
          InkWell(
            onTap: () async{
              await Provide.value<CartProvide>(context).remove();
            },
            child: Container(
              width: ScreenUtil().setWidth(320),
              height: ScreenUtil().setHeight(80),
              alignment: Alignment.center,
              color: Colors.red,
              child: Text(
                '立即购买',
                style:TextStyle(color:Colors.white,fontSize:ScreenUtil().setSp(28))
              ),
            ),
          ),
        ],
      ),
    );
  }
}

2、购物车_建立数据模型

刚才使用了字符串进行持久化,然后输出的时候都是Map,但是在真实工作中为了减少异常的发生,都要进行模型化处理,就是把Map转变为对象。

建立模型文件

得到的购物车数据,如下:

{"goodsId":"2171c20d77c340729d5d7ebc2039c08d","goodsName":"五粮液52°500ml","count":1,"price":830.0,"images":"http://images.baixingliangfan.cn/shopGoodsImg/20181229/20181229211422_8507.jpg"}

拷贝到自动生成mode的页面上,网址是:

https://javiercbk.github.io/json_to_dart/

生成后,在model文件夹下,建立一个新文件cartInfo.dart,然后把生成的mode文件进行改写,代码如下:

class CartInfoModel {
  String goodsId;
  String goodsName;
  int count;
  double price;
  String images;

  CartInfoModel(
      {this.goodsId, this.goodsName, this.count, this.price, this.images});

  CartInfoModel.fromJson(Map<String, dynamic> json) {
    goodsId = json['goodsId'];
    goodsName = json['goodsName'];
    count = json['count'];
    price = json['price'];
    images = json['images'];
  }

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

这个相对于以前其它Model文件简单很多。也可以手写练习一下。

在provide里使用模型

有了模型文件之后,需要先引入provide里,然后进行改造。引入刚刚写好的模型层文件。

import '../model/cartInfo.dart';

provide类的最上部新声明一个List变量,这就是购物车页面用于显示的购物车列表了.

List<CartInfoModel> cartList = [];

然后改造save方法,让他支持模型类,但是要注意,原来的字符串不要改变,因为shared_preferences不持支对象的持久化。

save(goodsId,goodsName,count,price,images) async{
    //初始化SharedPreferences
    SharedPreferences prefs = await  SharedPreferences.getInstance();
    cartString=prefs.getString('cartInfo');  //获取持久化存储的值
    //判断cartString是否为空,为空说明是第一次添加,或者被key被清除了。
    //如果有值进行decode操作
    var temp=cartString==null?[]:json.decode(cartString.toString());
    //把获得值转变成List
    List<Map> tempList= (temp as List).cast();
    //声明变量,用于判断购物车中是否已经存在此商品ID
    var isHave= false;  //默认为没有
    int ival=0; //用于进行循环的索引使用
    tempList.forEach((item){//进行循环,找出是否已经存在该商品
      //如果存在,数量进行+1操作
      if(item['goodsId']==goodsId){
        tempList[ival]['count']=item['count']+1;
         //关键代码-----------------start
        cartList[ival].count++;
         //关键代码-----------------end
        isHave=true;
      }
      ival++;
    });
    // 如果没有,进行增加
    if(!isHave){
       //关键代码-----------------start
          Map<String, dynamic> newGoods={
             'goodsId':goodsId,
            'goodsName':goodsName,
            'count':count,
            'price':price,
            'images':images
          };
          tempList.add(newGoods);
          cartList.add(new CartInfoModel.fromJson(newGoods));
       //关键代码-----------------end
    }
    //把字符串进行encode操作,
    cartString= json.encode(tempList).toString();
    print('字符串>>>>>>>>>>>${cartString}');
    print('数据模型>>>>>>>>>>>${cartList}');
    print(cartList.toString());
    prefs.setString('cartInfo', cartString);//进行持久化
    notifyListeners();
  }

得到购物车中商品方法

有了增加方法,我们还需要写一个得到购物车中的方法,现在就学习一下结合Model如何得到持久化的数据。

//得到购物车中的商品
  getCartInfo() async {
     SharedPreferences prefs = await SharedPreferences.getInstance();
     //获得购物车中的商品,这时候是一个字符串
     cartString=prefs.getString('cartInfo'); 
     //把cartList进行初始化,防止数据混乱 
     cartList=[];
     //判断得到的字符串是否有值,如果不判断会报错
     if(cartString==null){
       cartList=[];
     }else{
       List<Map> tempList= (json.decode(cartString.toString()) as List).cast();
       tempList.forEach((item){
          cartList.add(new CartInfoModel.fromJson(item));
       });

     }
      notifyListeners();
  }

 cart.dart完整代码:

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
import '../model/cartInfo.dart';

class CartProvide with ChangeNotifier{
  String cartString = '[]'; //声明一个变量 做持久化的存储
  List<CartInfoModel> cartList = [];

  //加入购物车
  save(goodsId, goodsName, count, price, images) async{
    SharedPreferences prefs = await SharedPreferences.getInstance(); //初始化
    cartString = prefs.getString('cartInfo'); //获取持久化存储的值
    //判断cartString是否为空,为空说明是第一次添加,或者被key被清除了。
    //如果有值进行decode操作
    var temp = cartString == null ? [] : json.decode(cartString.toString());
    //把获得值转变成List
    List<Map> tempList = (temp as List).cast(); //temp转化为List
    //声明变量,用于判断购物车中是否已经存在此商品ID
    bool isHave = false; //默认为没有
    int ival = 0; //用于进行循环的索引使用
    tempList.forEach((item){ //进行循环,找出是否已经存在该商品
      //如果存在,数量进行+1操作
      if(item['goodsId'] == goodsId){
        tempList[ival] ['count'] = item['count'] +1;
        cartList[ival].count++;
        isHave = true;
      }
      ival++;
    });
    //如果没有,进行增加
    if(!isHave){
      Map<String, dynamic> newGoods = {
        'goodsId':goodsId,
        'goodsName':goodsName,
        'count':count,
        'price':price,
        'images':images,
      };
      tempList.add(newGoods);
      cartList.add(CartInfoModel.fromJson(newGoods)); //fromJson变成对象
    }
    //把字符串进行encode操作
    cartString = json.encode(tempList).toString(); //json数据转字符串
    print('字符串>>>>>>>>>>>${cartString}');
    print('数据模型>>>>>>>>>>>${cartList}');
    prefs.setString('cartInfo', cartString); //进行持久化
    notifyListeners(); //通知
  }
  //清空购物车
  remove() async{
    SharedPreferences prefs=await SharedPreferences.getInstance(); //初始化
    prefs.remove('cartInfo');
    cartList = [];
    print('清空完成------------');
    notifyListeners(); //通知
  }
  //得到购物车中的商品
  getCartInfo() async{
    SharedPreferences prefs=await SharedPreferences.getInstance(); //初始化
    //获得购物车中的商品,这时候是一个字符串
    cartString = prefs.getString('cartInfo');
    //把cartList进行初始化,防止数据混乱 
    cartList = [];
    //判断得到的字符串是否有值,如果不判断会报错
    if(cartString == null){
      cartList = [];
    }else{
      List<Map> tempList = (json.decode(cartString.toString()) as List).cast(); //字符串转为List<Map>类型
      tempList.forEach((item){
        cartList.add(CartInfoModel.fromJson(item)); //json转成对象,加入到cartList中
      });
    }
    notifyListeners(); //通知
  }

}

3、购物车_大体结构布局

下面开始制作页面。其实在实际开发中也有很多这样的情况。就是先得到数据,再调试页面。

页面基本结构搭建

cart_page.dart清空原来写的持久化的代码。建立页面的基本接口,还是使用脚手架组件Scaffold来进行操作。代码如下:

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:provide/provide.dart';
import '../provide/cart.dart';

class CartPage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('购物车'),
      ),
      body:Text('测试')
    );
  }
}

Future方法编写(创建Future方法获取购物车持久化数据)

使用了Future组件,自然需要一个返回Future的方法了,在这个方法里,我们使用Provide取出本地持久化的数据,然后进行变化。

Future<String> _getCartInfo(BuildContext context) async{
     await Provide.value<CartProvide>(context).getCartInfo();
     return 'end';
 }

再body区域我们使用Future Widget,因为就算是本地持久化,还是有一个时间的,当然这个时间可能你肉眼看不见。不过这样控制台可能会把错误信息返回回来。

body: FutureBuilder(
    future:_getCartInfo(context),
    builder: (context,snapshot){

      if(snapshot.hasData){
        List cartList=Provide.value<CartProvide>(context).cartList;
      }else{
        return Text('正在加载');
      }
    },
  ),
  );
}

用ListView简单输出

return ListView.builder(
  itemCount: cartList.length,
  itemBuilder: (context,index){
    return ListTile(
      title:Text(cartList[index].goodsName)
    );
  },
);

现在可以简单的进行预览,当然页面还是很丑的,下面会继续进行美化。会把列表的子项单独拿出一个文件,这样会降低以后的维护成本。 

4、购物车_商品列表子项组件编写

上面把购物车页面的大体结构编写好,并且也可以获得购物车中的商品列表信息了,但是页面不够美观,现在继续完成子项的UI美化。

编写购物车单项方法

为了以后维护方便,我们还是采用单独编写的方式,把购物车里边的每一个子项统一作一个组件出来。

现在lib\pages下建立一个新文件夹cart_page,然后在新文件夹下面家里一个cart_item.dart文件。先引入几个必要的文件.

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../model/cartInfo.dart';

然后声明一个stateLessWidget 类,名字叫CartItem并设置接收参数,这里的接收参数就是cartInfo对象,也就是每个购物车商品的子项。代码如下:

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../model/cartInfo.dart';

class CartItem extends StatelessWidget {
  final CartInfoMode item;
  CartItem(this.item);

  @override
  Widget build(BuildContext context) {
    print(item);
    return Container(
        margin: EdgeInsets.fromLTRB(5.0,2.0,5.0,2.0),
        padding: EdgeInsets.fromLTRB(5.0,10.0,5.0,10.0),
        decoration: BoxDecoration(
          color: Colors.white,
          border: Border(
            bottom: BorderSide(width:1,color:Colors.black12)
          )
        ),
        child: Row(
          children: <Widget>[
          
          ],
        ),
      );
  }

}

编写多选按钮方法

//多选按钮
  Widget _cartCheckBt(item){
    return Container(
      child: Checkbox(
        value: true,
        activeColor:Colors.pink,
        onChanged: (bool val){},
      ),
    );
  }

编写商品图片方法

//商品图片 
  Widget _cartImage(item){
    return Container(
      width: ScreenUtil().setWidth(150),
      padding: EdgeInsets.all(3.0),
      decoration: BoxDecoration(
        border: Border.all(width: 1,color:Colors.black12)
      ),
      child: Image.network(item.images),
    );
  }

编写商品名称方法

//商品名称
  Widget _cartGoodsName(item){
    return Container(
      width: ScreenUtil().setWidth(300),
      padding: EdgeInsets.all(10),
      alignment: Alignment.topLeft,
      child: Column(
        children: <Widget>[
          Text(item.goodsName)
        ],
      ),
    );
  }

编写商品价格方法

//商品价格
  Widget _cartPrice(item){
    return Container(
        width:ScreenUtil().setWidth(150) ,
        alignment: Alignment.centerRight,
        
        child: Column(
          children: <Widget>[
            Text('¥${item.price}'),
            Container(
              child: InkWell(
                onTap: (){},
                child: Icon(
                  Icons.delete_forever,
                  color: Colors.black26,
                  size: 30,
                ),
              ),
            )
          ],
        ),
      );
  }

进行整合

child: Row(
  children: <Widget>[
    _cartCheckBt(item),
    _cartImage(item),
    _cartGoodsName(item),
    _cartPrice(item)
  ],
),

全部代码如下:

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../model/cartInfo.dart';

class CartItem extends StatelessWidget {
  final CartInfoModel item; 
  CartItem(this.item); //构造方法接收参数

  @override
  Widget build(BuildContext context) {
    print(item);
    return Container(
      margin: EdgeInsets.fromLTRB(5.0, 2.0, 5.0, 2.0),
      padding: EdgeInsets.fromLTRB(5.0, 10.0, 5.0, 10.0),
      decoration: BoxDecoration(
        color: Colors.white,
        border: Border(
          bottom: BorderSide(width: 1,color: Colors.black12)
        )
      ),
      child: Row(
        children: <Widget>[
          _cartCheckBt(item),
          _cartImage(item),
          _cartGoodsName(item),
          _cartPirce(item),
        ],
      ),
    );
  }

  //多选框
  Widget _cartCheckBt(item){
    return Container(
      child: Checkbox(
        value: true, //先默认选中
        activeColor:Colors.red, //选中颜色
        onChanged: (bool val){}, 
      ),
    );
  }
  //商品图片
  Widget _cartImage(item){
    return Container(
      width: ScreenUtil().setWidth(150),
      padding: EdgeInsets.all(3.0),
      decoration: BoxDecoration(
        border: Border.all(width: 1,color: Colors.black12)
      ),
      child: Image.network(item.images),
    );
  }
  //商品名称
  Widget _cartGoodsName(item){
    return Container(
      width: ScreenUtil().setWidth(300),
      padding: EdgeInsets.all(10),
      alignment: Alignment.topLeft,
      child: Column(
        children: <Widget>[
          Text(item.goodsName),

        ],
      ),
    );
  }
  //商品价格
  Widget _cartPirce(item){
    return Container(
      width: ScreenUtil().setWidth(150),
      alignment: Alignment.centerRight,
      child: Column(
        children: <Widget>[
          Text('¥${item.price}'),
          Container(
            child: InkWell(
              onTap: (){},
              child: Icon(Icons.delete_forever,color: Colors.black26,size: 30),
            ),
          )
        ],
      ),
    );
  }

}

然后在cart_page.dart页面引入cart_item.dart

import './cart_page/cart_item.dart';

修改代码:

// return ListTile(
//   title: Text(cartList[index].goodsName),
// );
return CartItem(cartList[index]);

运行后可以查看效果。

5、购物车_制作底部结算栏的UI

这个要使用Stack Widget

建立底部结算栏页面

lib/pages/cart_page文件夹下,新建一个cart_bottom.dart文件。文件建立好以后,先引入下面的基础package

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';

引入完成后,用快捷的方式建立一个StatelessWidget,建立后,我们使用Row来进行总体布局,并给Container一些必要的修饰.代码如下:

class CartBottom extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.all(5.0),
      color: Colors.white,
      width: ScreenUtil().setWidth(750),
      child: Row(
        children: <Widget>[
        
        ],
      ),
    );
  }
}

这就完成了一个底部结算栏的大体结构确定,大体结构完成后,我们还是把里边的细节,拆分成不同的方法返回对象的组件。

全选按钮方法

先来制作全选按钮方法,这个外边采用Container,里边使用了一个Row,这样能很好的完成横向布局的需求.

//全选按钮
  Widget selectAllBtn(){
    return Container(
      child: Row(
        children: <Widget>[
          Checkbox(
            value: true,
            activeColor: Colors.pink,
            onChanged: (bool val){},
          ),
          Text('全选')
        ],
      ),
    );
  }

合计区域方法

合计区域由于布局对齐方式比较复杂,所以这段代码虽然很简单,但是代码设计的样式比较多,需要你有很好的样式编写能力.代码如下:

 // 合计区域
 Widget allPriceArea(){
    return Container(
      width: ScreenUtil().setWidth(430),
      alignment: Alignment.centerRight,
      child: Column(
        children: <Widget>[
          Row(
            children: <Widget>[
              Container(
                alignment: Alignment.centerRight,
                width: ScreenUtil().setWidth(280),
                child: Text(
                  '合计:',
                  style:TextStyle(
                    fontSize: ScreenUtil().setSp(36)
                  )
                ), 
              ),
              Container(
                 alignment: Alignment.centerLeft,
                 width: ScreenUtil().setWidth(150),
                 child: Text(
                  '¥1922',
                  style:TextStyle(
                    fontSize: ScreenUtil().setSp(36),
                    color: Colors.red,
                  )
                ),
              )
              
            ],
          ),
          Container(
            width: ScreenUtil().setWidth(430),
            alignment: Alignment.centerRight,
            child: Text(
              '满10元免配送费,预购免配送费',
              style: TextStyle(
                color: Colors.black38,
                fontSize: ScreenUtil().setSp(22)
              ),
            ),
          )
          
        ],
      ),
    );

  }

结算按钮方法

这个方法里边的按钮,我们并没有使用Flutter Button Widget 而是使用InkWell自己制作一个组件。这样作能很好的控制按钮的形状,还可以解决水波纹的问题,一举两得。代码如下:

//结算按钮
  Widget goButton(){
    return Container(
      width: ScreenUtil().setWidth(160),
      padding: EdgeInsets.only(left: 10),
      child:InkWell(
        onTap: (){},
        child: Container(
          padding: EdgeInsets.all(10.0),
          alignment: Alignment.center,
          decoration: BoxDecoration(
             color: Colors.red,
             borderRadius: BorderRadius.circular(3.0)
          ),
          child: Text(
            '结算(6)',
            style: TextStyle(
              color: Colors.white
            ),
          ),
        ),
      ) ,
    );
    
  }

加入到页面中

组件样式基本都各自完成后,接下来就是组合和加入到页面中了,我们先把个个方法组合到底部结算区域,也就是放到build方法里。

Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.all(5.0),
      color: Colors.white,
      width: ScreenUtil().setWidth(750),
      child: Row(
        children: <Widget>[
          selectAllBtn(),
          allPriceArea(),
          goButton()
        ],
      ),
    );
  }

完成后就是到lib/pages/cart_page.dart文件中,加入底部结算栏的操作了,这里我们需要使用Stack Widget组件。

首先需要引入cart_bottom.dart

import './cart_page/cart_bottom.dart';

然后改写FutureBuilder Widget里边的builder方法,这时候返回的是一个Stack Widget。代码如下:

import 'package:flutter/material.dart';
import 'package:provide/provide.dart';
import '../provide/cart.dart';
import './cart_page/cart_item.dart';
import './cart_page/cart_bottom.dart';

class CartPage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('购物车'),
      ),
      body: FutureBuilder(
        future:_getCartInfo(context),
        builder: (context,snapshot){
          if(snapshot.hasData && cartList!=null){
            List cartList=Provide.value<CartProvide>(context).cartList;
            //关键代码-------------------start
            return Stack(
              children: <Widget>[
                ListView.builder(
                  itemCount: cartList.length,
                  itemBuilder: (context,index){
                    return CartItem(cartList[index]);
                  },
                ),
                Positioned(
                  bottom:0,
                  left:0,
                  child: CartBottom(),
                )
              ],
            );
            //关键代码-----------------end
          
          }else{
            return Text('正在加载');
          }
        },
      ),
    );
  }

  Future<String> _getCartInfo(BuildContext context) async{
     await Provide.value<CartProvide>(context).getCartInfo();
     return 'end';
  }
  
}

这步做完之后,就可以进行预览了。可以试着把这个页面布局成自己想要的样子。

6、购物车_制作数量加减按钮UI

购物车的UI界面已经基本完成了,只差最后一个数量加载的部分没有进行布局,现在就把这个部分的布局制作完成。

建立组件和基本结构

lib/pages/cart_page/文件夹下,建立一个新的文件cart_count.dart。先引入两个布局使用的基本文件。

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';

class CartCount extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      
    );
  }
}

然后开始写基本结构,我们这里使用ContainerRow的形式。

Widget build(BuildContext context) {
    return Container(
      width: ScreenUtil().setWidth(165),
      margin: EdgeInsets.only(top:5.0),
      decoration: BoxDecoration(
        border:Border.all(width: 1 , color:Colors.black12)
      ),
      child: Row(
        children: <Widget>[
        ],
      ),
    );
  }

写完这个,再把Row里边的每个子元素进行拆分.

减少按钮UI编写

// 减少按钮
  Widget _reduceBtn(){
    return InkWell(
      onTap: (){},
      child: Container(
        width: ScreenUtil().setWidth(45),
        height: ScreenUtil().setHeight(45),
        alignment: Alignment.center,
       
        decoration: BoxDecoration(
          color: Colors.white,
          border:Border(
            right:BorderSide(width:1,color:Colors.black12)
          )
        ),
        child: Text('-'),
      ),
    );
  }

添加按钮UI编写

//添加按钮
  Widget _addBtn(){
    return InkWell(
      onTap: (){},
      child: Container(
        width: ScreenUtil().setWidth(45),
        height: ScreenUtil().setHeight(45),
        alignment: Alignment.center,
       
         decoration: BoxDecoration(
          color: Colors.white,
          border:Border(
            left:BorderSide(width:1,color:Colors.black12)
          )
        ),
        child: Text('+'),
      ),
    );
  }

数量区域UI编写

//中间数量显示区域
  Widget _countArea(){
    return Container(
      width: ScreenUtil().setWidth(70),
      height: ScreenUtil().setHeight(45),
      alignment: Alignment.center,
      color: Colors.white,
       child: Text('1'),
    );
  }

进行组合

组件都写好后,要进行组合和加入到页面中的操作。

组合:直接在build区域的Row数组中进行组合。

 Widget build(BuildContext context) {
    return Container(
      width: ScreenUtil().setWidth(165),
      margin: EdgeInsets.only(top:5.0),
      decoration: BoxDecoration(
        border:Border.all(width: 1 , color:Colors.black12)
      ),
      child: Row(
        //关键代码----------------start
        children: <Widget>[
          _reduceBtn(),
          _countArea(),
          _addBtn(),
        ],
        //关键代码----------------end
      ),
      
    );
  }

这个不完成后,再到同级目录下的cart_item.dart,引入和使用。先进行文件的引入.

import './cart_count.dart';

引入后,再商品名称的方法中直接引入就。

//商品名称
  Widget _cartGoodsName(item){
    return Container(
      width: ScreenUtil().setWidth(300),
      padding: EdgeInsets.all(10),
      alignment: Alignment.topLeft,
      child: Column(
        children: <Widget>[
          Text(item.goodsName),
          //关键代码---------start
          CartCount()
          //关键代码---------end
        ],
      ),
    );
  }

完成后就可以进行预览了。

7、购物车_在Model中增加选中字段

通过布局,我们可以看到是有选中和多选操作的,但是在设计购物车模型时并没有涉及这个操作,现在修改一下。

修改Model文件

首先我们打开lib/model/cartInfo.dart文件,增加一个新的变量isCheck

class CartInfoMode {
  String goodsId;
  String goodsName;
  int count;
  double price;
  String images;
  //------新添加代码----start
  bool isCheck;
  //------新添加代码----end

  CartInfoMode(
      //需要修改---------start-----
      {this.goodsId, this.goodsName, this.count, this.price, this.images,this.isCheck});
      //修改需改--------end------

  CartInfoMode.fromJson(Map<String, dynamic> json) {
    goodsId = json['goodsId'];
    goodsName = json['goodsName'];
    count = json['count'];
    price = json['price'];
    images = json['images'];
    //------新添加代码----start
    isCheck = json['isCheck'];
    //------新添加代码----end
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['goodsId'] = this.goodsId;
    data['goodsName'] = this.goodsName;
    data['count'] = this.count;
    data['price'] = this.price;
    data['images'] = this.images;
    //------新添加代码----start
    data['isCheck']= this.isCheck;
    /------新添加代码----end
    return data;
  }
}

在增加时加入isCheck

打开lib/provide/cart.dart文件,找到添加购物车商品的方法save,修改增加的部分代码。

Map<String, dynamic> newGoods={
  'goodsId':goodsId,
  'goodsName':goodsName,
  'count':count,
  'price':price,
  'images':images,
  //-----新添加代码-----start
  'isCheck': true  //是否已经选择
  //-----新添加代码-----end
};

修改UI的值

之前UI中多选按钮的值,我们是写死的,现在就可以使用这个动态的值了。打开lib/pages/cart_page/cart_item.dart文件,找到多选按钮的部分,修改val的值.

Widget _cartCheckBt(context,item){
  return Container(
    child: Checkbox(
      //修改部分--------start----
      value: item.isCheck,
      //修改部分--------end------
      activeColor:Colors.pink,
      onChanged: (bool val){
      },
    ),
  );
}

记得修改完成后,要把原来的持久化购物车的数据清除掉,删除掉后再次填入新的商品到购物车,就可以正常显示了。

8、购物车_删除单个商品功能制作

页面终于制作完成了,剩下来就是逐步完善购物车中的各项功能,这部分的视频可能拆分的比较细致。

编写删除方法

直接在provide中的cart.dart文件里,增加一个deleteOneGoods方法。编写思路是这样的,先从持久化数据里得到数据,然后把纯字符串转换成字List,转换之后进行循环,如果goodsId,相同,说明就是要删除的项,把索引进行记录,记录之后用removeAt方法进行删除,删除后再次进行持久化,并重新获得数据。 主要代码如下:

//删除单个购物车商品
  deleteOneGoods(String goodsId) async{
    SharedPreferences prefs = await SharedPreferences.getInstance(); //初始化
    cartString = prefs.getString('cartInfo'); //获取持久化
    List<Map> tempList = (json.decode(cartString.toString()) as List).cast(); //字符串转为List<Map>类型
    int tempIndex = 0; //循环使用的索引
    int delIndex = 0; //删除第几个的索引
    tempList.forEach((item){
      if(item['goodsId'] == goodsId){
        delIndex = tempIndex;
      }
      tempIndex++;
    });  

    tempList.removeAt(delIndex); //删除索引项
    cartString = json.encode(tempList).toString(); //转换cartString持久化
    prefs.setString('cartInfo', cartString); //修改cartString持久化
    await getCartInfo(); //持久化成功后刷新列表

  }

注意,这个部分为什么循环时不进行删除,因为dart语言不支持迭代时进行修改,这样可以保证在循环时不出错。

修改UI界面,实现效果

UI界面主要时增加Proivde组件,就是当值法伤变化时,界面也随着变化。打开cart_page.dart文件,主要修改build里的ListView区域,代码如下:

import 'package:flutter/material.dart';
import 'package:provide/provide.dart';
import '../provide/cart.dart';
import './cart_page/cart_item.dart';
import './cart_page/cart_bottom.dart';

class CartPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('购物车'),
      ),
      body: FutureBuilder(
        future:_getCartInfo(context),
        builder: (context,snapshot){
          List cartList=Provide.value<CartProvide>(context).cartList;
          if(snapshot.hasData && cartList!=null){
              return Stack(
                children: <Widget>[
                  //主要代码--------------------start--------
                  Provide<CartProvide>(
                    builder: (context,child,childCategory){
                       cartList= Provide.value<CartProvide>(context).cartList;
                      print(cartList);
                      return ListView.builder(
                        itemCount: cartList.length,
                        itemBuilder: (context,index){
                          return CartItem(cartList[index]);
                        },
                      );
                    }
                  ), 
                  //主要代码------------------end---------
                  Positioned(
                    bottom:0,
                    left:0,
                    child: CartBottom(),
                  )
                ],
              );

          }else{
            return Text('正在加载');
          }
        },
      ),
    );
  }

  Future<String> _getCartInfo(BuildContext context) async{
     await Provide.value<CartProvide>(context).getCartInfo();
     return 'end';
  }

}

增加删除响应事件

cart_item.dart文件中,增加删除响应事件,由于所有业务逻辑都在Provide中,所以需要引入下面两个文件。

import 'package:provide/provide.dart';
import '../../provide/cart.dart';

有了这两个文件后,可以修改对应的方法_cartPrice。首先要加入context选项,然后修改里边的onTap方法。具体代码如下:

//商品价格
  Widget _cartPrice(context,item){
    return Container(
        width:ScreenUtil().setWidth(150) ,
        alignment: Alignment.centerRight,
        child: Column(
          children: <Widget>[
            Text('¥${item.price}'),
            Container(
              child: InkWell(
                onTap: (){
                  //主要代码---------------start----------
                  Provide.value<CartProvide>(context).deleteOneGoods(item.goodsId);
                  //主要代码--------------end-----------
                },
                child: Icon(
                  Icons.delete_forever,
                  color: Colors.black26,
                  size: 30,
                ),
              ),
            )
          ],
        ),
      );
  }

这步做完,已经有了删除功能,可以进行测试了.。

9、购物车_计算商品价格和数量

购物车中都有自动计算商品价格和商品数量的功能,下面就把这两个小功能实现一下。

增加Provide变量

lib/provide/cart.dart文件的类头部,增加总价格allPrice和总商品数量allGoodsCount两个变量.

class CartProvide with ChangeNotifier{

  String cartString="[]";
  List<CartInfoMode> cartList=[]; //商品列表对象
  //新代码----------start
  double allPrice =0 ;   //总价格
  int allGoodsCount =0;  //商品总数量
  
  ...

修改getCartInfo()方法

主要是在循环是累计增加数量和价格,这里给出全部增加的代码,并标注了修改部分。

getCartInfo() async {
     SharedPreferences prefs = await SharedPreferences.getInstance();
     //获得购物车中的商品,这时候是一个字符串
     cartString=prefs.getString('cartInfo'); 
     
     //把cartList进行初始化,防止数据混乱 
     cartList=[];
     //判断得到的字符串是否有值,如果不判断会报错
     if(cartString==null){
       cartList=[];
     }else{
       List<Map> tempList= (json.decode(cartString.toString()) as List).cast();
        //---------修改代码------start-------------
       allPrice=0;
       allGoodsCount=0;
        //---------修改代码------end-------------

       tempList.forEach((item){
           //---------修改代码------start-------------
          if(item['isCheck']){
             allPrice += (item['count']*item['price']);
             allGoodsCount += item['count'];
          }
           //---------修改代码------end-------------
         
          cartList.add(new CartInfoMode.fromJson(item));

       });

     }
      notifyListeners();
  }

修改UI界面 显示结果

有了业务逻辑,就应该可以正常的显示出界面效果了。但是需要把原来我们写死的值,都改成动态的。

打开lib/pages/cart_page/cart_bottom.dart文件,先用import引入provide package

import 'package:provide/provide.dart';
import '../../provide/cart.dart';

然后把底部的三个区域方法都加上context上下文参数,因为Provide的使用,必须有上下文参数。

Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.all(5.0),
      color: Colors.white,
      width: ScreenUtil().setWidth(750),
      child: Provide<CartProvide>(
        builder: (context,child,childCategory){
          return  Row(
            children: <Widget>[
              //修改部分--------start----------
              selectAllBtn(context),
              allPriceArea(context),
              goButton(context)
              //修改部分--------end-----------
            ],
          );
        },
      )
    );
  }

然后在两个方法中都从Provide里动态获取变量,就可以实现效果了。

合计区域的方法修改代码:

// 合计区域
  Widget allPriceArea(context){
    //修改代码---------------start------------
    double allPrice = Provide.value<CartProvide>(context).allPrice;
    //修改代码---------------end------------
    return Container(
      width: ScreenUtil().setWidth(430),
      alignment: Alignment.centerRight,
      child: Column(
        children: <Widget>[
          Row(
            children: <Widget>[
              Container(
                alignment: Alignment.centerRight,
                width: ScreenUtil().setWidth(280),
                child: Text(
                  '合计:',
                  style:TextStyle(
                    fontSize: ScreenUtil().setSp(36)
                  )
                ), 
              ),
              Container(
                 alignment: Alignment.centerLeft,
                width: ScreenUtil().setWidth(150),
                 //修改代码---------------start------------
                child: Text(
                  '¥${allPrice}',
                  style:TextStyle(
                    fontSize: ScreenUtil().setSp(36),
                    color: Colors.red,
                  )
                ),
                 //修改代码---------------end------------
                
              )
            ],
          ),
          Container(
            width: ScreenUtil().setWidth(430),
            alignment: Alignment.centerRight,
            child: Text(
              '满10元免配送费,预购免配送费',
              style: TextStyle(
                color: Colors.black38,
                fontSize: ScreenUtil().setSp(22)
              ),
            ),
          )
        ],
      ),
    );
  }

结算按钮区域修改代码:

//结算按钮
  Widget goButton(context){
    //修改代码---------------start------------
    int allGoodsCount =  Provide.value<CartProvide>(context).allGoodsCount;
    //修改代码---------------end--------------
    return Container(
      width: ScreenUtil().setWidth(160),
      padding: EdgeInsets.only(left: 10),
      child:InkWell(
        onTap: (){},
        child: Container(
          padding: EdgeInsets.all(10.0),
          alignment: Alignment.center,
          decoration: BoxDecoration(
             color: Colors.red,
             borderRadius: BorderRadius.circular(3.0)
          ),
          //修改代码---------------start------------
          child: Text(
            '结算(${allGoodsCount})',
            style: TextStyle(
              color: Colors.white
            ),
          ),
          //修改代码---------------end------------
        ),
      ) ,
    );
  }

点击删除,总价和商品的数量还没有变化。

这是因为没有包裹provide 的原因,我们需要把这里的Row嵌套到Provide里面去

class CartBottom extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      padding:EdgeInsets.all(5.0),
      color:Colors.white,
      //修改代码---------------start------------
      child:Provide<CartProvide>(
        builder: (context,child,val){
          return Row(
            children:<Widget>[
              selectAllBtn(context),
              allPriceArea(context),
              goButton(context),
            ]
          );
        },
      ),
      //修改代码---------------end------------
    );
  }

这步完成后,就应该可以正常动态显示购物车中的商品数量和商品价格了。

10、购物车_商品选中功能制作

在购物车里是有选择和取消选择,还有全选的功能按钮的。当我们选择时,价格和数量都是跟着自动计算的,列表也是跟着刷新的。这节课主要完成单选和全选按钮的交互效果。

制作商品单选按钮的交效果

这些业务逻辑代码,当然需要写到Provide中,打开lib/provide/cart.dart文件。新建一个changeCheckState方法:

changeCheckState(CartInfoMode cartItem) async{
     SharedPreferences prefs = await SharedPreferences.getInstance();
     cartString=prefs.getString('cartInfo');  //得到持久化的字符串
     List<Map> tempList= (json.decode(cartString.toString()) as List).cast(); //声明临时List,用于循环,找到修改项的索引
     int tempIndex =0;  //循环使用索引
     int changeIndex=0; //需要修改的索引
     tempList.forEach((item){
         
         if(item['goodsId']==cartItem.goodsId){
          //找到索引进行赋值
          changeIndex=tempIndex;
         }
         tempIndex++;
     });
   //tempList里面是Map,而传递过来的cartItem里是对象 tempList[changeIndex]
=cartItem.toJson(); //把对象变成Map值 cartString= json.encode(tempList).toString(); //变成字符串 prefs.setString('cartInfo', cartString);//进行持久化 await getCartInfo(); //重新读取列表 }

注意,这个部分为什么循环时不进行修改,因为dart语言不支持迭代时进行修改,就是说循环时不允许修改循环项里的所有属性,这样可以保证在循环时不出错。

业务逻辑写完后到到UI层进行修改,打开lib/pages/cart_page/cart_item.dart文件,修改多选按钮的onTap方法。

//多选按钮
  Widget _cartCheckBt(context,item){
    return Container(
      child: Checkbox(
        value: item.isCheck, //是否选中状态 
        activeColor:Colors.pink,
        //-------新增代码--------start---------
        onChanged: (bool val){
          item.isCheck = val;
          Provide.value<CartProvide>(context).changeCheckState(item);
        },
        //-------新增代码--------end---------
      ),
    );
  }

修改完成后,可以点击测试一下效果,如果一切正常,就可以进行选中和取消的交互了。

全选按钮交互效果制作

在cart.dart文件中声明一个状态变量isAllCheck,然后在读取购物车商品数据时进行更改。

bool isAllCheck= true; //是否全选

 修改getCartInfo方法,就是获取购物车列表的方法.

 //得到购物车中的商品
  getCartInfo() async {
     SharedPreferences prefs = await SharedPreferences.getInstance();
     //获得购物车中的商品,这时候是一个字符串
     cartString=prefs.getString('cartInfo'); 
     //把cartList进行初始化,防止数据混乱 
     cartList=[];
     //判断得到的字符串是否有值,如果不判断会报错
     if(cartString==null){
       cartList=[];
     }else{
       List<Map> tempList= (json.decode(cartString.toString()) as List).cast();
       allPrice=0;
       allGoodsCount=0;
       //--------新增代码----------start--------
       isAllCheck=true; //全选初始化默认true
       //--------新增代码----------end--------
       tempList.forEach((item){
           //--------新增代码----------start--------
          if(item['isCheck']){
             allPrice+=(item['count']*item['price']);
             allGoodsCount+=item['count'];
          }else{
            isAllCheck=false;  //当不选中时,全选为false
          }
          //--------新增代码----------end--------
         
          cartList.add(new CartInfoMode.fromJson(item));

       });

     }
      notifyListeners();
  }

完成后,到UI界面加入交互效果,打开lib/pages/cart_page/cart_bottom.dart文件,修改selectAllBtn(context)方法。

//全选按钮
  Widget selectAllBtn(context){
    //--------新增代码----------end--------
    bool isAllCheck = Provide.value<CartProvide>(context).isAllCheck;
    //--------新增代码----------end--------
    return Container(
      child: Row(
        children: <Widget>[
          Checkbox(
            //--------修改代码----------end--------
            value: isAllCheck,
            //--------修改代码----------end--------
            activeColor: Colors.red,
            onChanged: (bool val){},
          ),
          Text('全选')
        ],
      ),
    );
  }

可以看下效果,加入购物车的商品默认都是选中状态,下面的全选按钮也是选中状态;取消一个上面的选中状态,下面的全选按钮状态也是跟着取消的。

全选按钮的方法和当个商品很类似,也是在Provide中,新建一个changeAllCheckBtnState方法,写入下面的代码.

changeAllCheckBtnState(bool isCheck) async{
    SharedPreferences prefs = await SharedPreferences.getInstance(); //初始化
    cartString = prefs.getString('cartInfo'); //得到持久化的字符串
    List<Map> tempList= (json.decode(cartString.toString()) as List).cast(); //声明临时List,用于循环,找到修改项的索引
    List<Map> newList = []; //新建一个List,用于组成新的持久化数据。
    for(var item in tempList){
      var newItem = item; //把老item赋值给新的item 因为Dart不让循环时修改原值
      newItem['isCheck'] = isCheck; //改变选中状态
      newList.add(newItem); //把newItem新的值add到新数组newList里
    }

    cartString = json.encode(newList).toString(); //形成字符串
    prefs.setString('cartInfo', cartString); //存储持久化
    await getCartInfo(); //持久化成功后刷新列表
  }

完成后,到UI界面加入交互效果,打开lib/pages/cart_page/cart_bottom.dart文件,修改selectAllBtn(context)方法。

//全选按钮
  Widget selectAllBtn(context){
    //--------新增代码----------start--------
    bool isAllCheck = Provide.value<CartProvide>(context).isAllCheck;
    //--------新增代码----------end--------
    return Container(
      child: Row(
        children: <Widget>[
          Checkbox(
            value: isAllCheck,
            activeColor: Colors.pink,
            //--------新增代码----------start--------
            onChanged: (bool val){
              Provide.value<CartProvide>(context).changeAllCheckBtnState(val);
            },
            //--------新增代码----------end--------
          ),
          Text('全选')
        ],
      ),
    );
  }

做完这步,就可以测试一下交互效果了。

11、购物车_商品数量的加减操作

现在基本购物车页面只差一个商品数量的加减操作了,下面开始。

编写业务逻辑方法

直接在lib/provide/cart.dart文件中,新建立一个方法addOrReduceAction()方法。方法接收两个参数.

  • cartItem:要修改的项.
  • todo: 是加还是减。

代码如下:

//商品数量的加减
  addOrReduceAction(var cartItem, String todo) async{ 
    SharedPreferences prefs = await SharedPreferences.getInstance(); //初始化
    cartString = prefs.getString('cartInfo'); //得到持久化的字符串
    List<Map> tempList= (json.decode(cartString.toString()) as List).cast(); //声明临时List,用于循环,找到修改项的索引
    int tempIndex = 0; //临时的 循环使用的索引
    int changeIndex = 0; //需要修改的索引
    tempList.forEach((item){
      if(item['goodsId'] == cartItem.goodsId){
        //找到索引进行赋值
        changeIndex = tempIndex;
      }
      tempIndex++;
    });
    //判断加还是减
    if(todo == 'add'){
      cartItem.count++;
    }else if(cartItem.count > 1){
      cartItem.count--;
    }

    //tempList里面是Map,而传递过来的cartItem里是对象
    tempList[changeIndex] = cartItem.toJson(); //cartItem.toJson()变成Map值然后赋值给tempList
    cartString = json.encode(tempList).toString(); //变成字符串
    prefs.setString('cartInfo', cartString); //存储持久化
    await getCartInfo(); //持久化成功后刷新列表
  }

方法写完后,就可以修改UI部分了,让其有交互效果.

UI交互效果的修改

现在页面中引入Provide相关的文件

import 'package:provide/provide.dart';
import '../../provide/cart.dart';

然后设置接收参数,接收item就可以了

var item;
CartCount(this.item);

然后把组件的内部方法都加入参数context,这里直接给出所有代码:

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:provide/provide.dart';
import '../../provide/cart.dart';

class CartCount extends StatelessWidget {
  //--------------新增加代码------------start--------
  var item;
  CartCount(this.item);
  //--------------新增加代码------------end--------

  @override
  Widget build(BuildContext context) {
    return Container(
      width: ScreenUtil().setWidth(165),
      margin: EdgeInsets.only(top:5.0),
      decoration: BoxDecoration(
        border:Border.all(width: 1 , color:Colors.black12)
      ),
      child: Row(
        children: <Widget>[
          //--------------新增加代码------------start--------
          _reduceBtn(context),
          _countArea(),
          _addBtn(context),
          //--------------新增加代码------------end--------
        ],
      ),
      
    );
  }
  // 减少按钮
  Widget _reduceBtn(context){
    return InkWell(
      onTap: (){
        //--------------新增加代码------------start--------
        Provide.value<CartProvide>(context).addOrReduceAction(item,'reduce');
        //--------------新增加代码------------end--------
      },
      child: Container(
        width: ScreenUtil().setWidth(45),
        height: ScreenUtil().setHeight(45),
        alignment: Alignment.center,
       
        decoration: BoxDecoration(
          //--------------新增加代码------------start--------
          color: item.count>1?Colors.white:Colors.black12,
          //--------------新增加代码------------end--------
          border:Border(
            right:BorderSide(width:1,color:Colors.black12)
          )
        ),
        //--------------新增加代码------------start--------
        child:item.count>1? Text('-'):Text(' '),
        //--------------新增加代码------------end--------
      ),
    );
  }

  //添加按钮
  Widget _addBtn(context){
    return InkWell(
      onTap: (){
       //--------------新增加代码------------start--------
        Provide.value<CartProvide>(context).addOrReduceAction(item,'add');
        //--------------新增加代码------------end--------
      },
      child: Container(
        width: ScreenUtil().setWidth(45),
        height: ScreenUtil().setHeight(45),
        alignment: Alignment.center,
       
         decoration: BoxDecoration(
          color: Colors.white,
          border:Border(
            left:BorderSide(width:1,color:Colors.black12)
          )
        ),
        child: Text('+'),
      ),
    );
  }

  //中间数量显示区域
  Widget _countArea(){
    return Container(
      width: ScreenUtil().setWidth(70),
      height: ScreenUtil().setHeight(45),
      alignment: Alignment.center,
      color: Colors.white,
      //--------------新增加代码------------start--------
       child: Text('${item.count}'),
      //--------------新增加代码------------end--------
    );
  }

}

全部改完后,还需要到cart_item.dart里的_cartGoodsName里的调用组件的方法。

//商品名称
  Widget _cartGoodsName(item){
    return Container(
      width: ScreenUtil().setWidth(300),
      padding: EdgeInsets.all(10),
      alignment: Alignment.topLeft,
      child: Column(
        children: <Widget>[
          Text(item.goodsName),
          //-----------修改关键代码------start-------
          CartCount(item)
          //-----------修改关键代码------end-------
        ],
      ),
    );
  }

重新运行后,就应该可以实现商品数量的加减交互了。

12、购物车_首页Provide化 让跳转随心所欲

开始学习的时候,底部导航跳转并没有使用Provide,而是使用了简单的变量,这样作的结果就是其它页面没办法控制首页底部导航的跳转,让项目的跳转非常笨拙,缺乏灵活性。这节课就通过我们小小的改造,把首页index_page.dart,加入Provide控制。

编写Provide文件

先在lib/provide文件夹下面,新建一个currentIndex.dart文件,然后声明一个索引变量,这个变量就是控制底部导航和页面跳转的。也就是说我们只要把这个索引进行状态管理,那所以的页面可以轻松的控制首页的跳转了。代码如下:

import 'package:flutter/material.dart';

class CurrentIndexProvide with ChangeNotifier{
  int currentIndex = 0; //当前页面的索引
  
  changeIndex(int newIndex){ //新的索引
    currentIndex = newIndex;
    notifyListeners(); //通知
  }

}

全局注入

main.dart页引入

import './provide/currentIndex.dart';

下面增加代码:

var currentIndexProvide = CurrentIndexProvide();
...
...
..provide(Provider<CurrentIndexProvide>.value(currentIndexProvide));

重新编写首页

现在就要改造首页了,这次改动的地方比较多,所以干脆先注释掉所有代码,然后重新进行编写。

修改思路是这样的,把原来的statfulWidget换成静态的statelessWeidget然后进行主要修改build方法里。加入Provide Widget,然后再每次变化时得到索引,点击下边导航时改变索引.

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'home_page.dart';
import 'category_page.dart';
import 'cart_page.dart';
import 'member_page.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:provide/provide.dart';
import '../provide/currentIndex.dart';

class IndexPage extends StatelessWidget {
  final List<BottomNavigationBarItem> bottomTabs = [
    BottomNavigationBarItem(
      icon:Icon(CupertinoIcons.home),
      title:Text('首页')
    ),
    BottomNavigationBarItem(
      icon:Icon(CupertinoIcons.search),
      title:Text('分类')
    ),
    BottomNavigationBarItem(
      icon:Icon(CupertinoIcons.shopping_cart),
      title:Text('购物车')
    ),
     BottomNavigationBarItem(
      icon:Icon(CupertinoIcons.profile_circled),
      title:Text('会员中心')
    ),
  ];

   final List<Widget> tabBodies = [
      HomePage(),
      CategoryPage(),
      CartPage(),
      MemberPage()
   ];

  @override
  Widget build(BuildContext context) {
   
    ScreenUtil.instance = ScreenUtil(width: 750, height: 1334)..init(context);  //初始化设计尺寸
  
    return Provide<CurrentIndexProvide>(

      builder: (context,child,val){
        //------------关键代码----------start---------
        int currentIndex= Provide.value<CurrentIndexProvide>(context).currentIndex;
        // ----------关键代码-----------end ----------
        return Scaffold(
            backgroundColor: Color.fromRGBO(244, 245, 245, 1.0),
            bottomNavigationBar: BottomNavigationBar(
              type:BottomNavigationBarType.fixed, //图标+文字
              currentIndex: currentIndex,
              items:bottomTabs,
              onTap: (index){
                //------------关键代码----------start---------
                Provide.value<CurrentIndexProvide>(context).changeIndex(index);
                // ----------关键代码-----------end ----------
              },
            ),
             body: IndexedStack( //保持页面
                    index: currentIndex,
                    children: tabBodies
             ),
        ); 
      }
    );
     
  }
}

修改商品详细页,实现跳转

打开/lib/pages/details_page/details_bottom.dart文件,先引入curretnIndex.dart文件.

import '../../provide/currentIndex.dart';

然后修改build方法里的购物车图标区域.在图标的onTap方法里,加入下面的代码.

InkWell(
  onTap: (){
      //--------------关键代码----------start-----------
      Provide.value<CurrentIndexProvide>(context).changeIndex(2);
      Navigator.pop(context);
      //-------------关键代码-----------end--------
  },
  child: Container(
      width: ScreenUtil().setWidth(110) ,
      alignment: Alignment.center,
      child:Icon(
            Icons.shopping_cart,
            size: 35,
            color: Colors.red,
          ), 
    ) ,
),

这步做完,可以试着测试一下了,看看是不是可以从详细页直接跳转到购物车页面了。

13、购物车_详细页显示购物车商品数量

现在购物车的基本功能都已经做完了,但是商品详细页面还有一个小功能没有完成,就是在商品详细页添加商品到购物车时,购物车的图标要动态显示出此时购物车的数量。

修改文件结构

打开/lib/pages/details_page/details_bottom.dart文件,修改图片区域,增加层叠组件Stack Widget,然后在右上角加入购物车现有商品数量。

 

children: <Widget>[
         //关键代码--------------------start--------------
          Stack(
            children: <Widget>[
              InkWell(
                onTap: () {
                  Provide.value<CurrentIndexProvide>(context).changeIndex(2); //购物车索引是2
                  Navigator.pop(context); //跳转
                },
                child: Container(
                  width: ScreenUtil().setWidth(110),
                  alignment: Alignment.center,
                  child: Icon(Icons.shopping_cart,size: 35,color: Colors.red,),
                ),
              ),
              
              Provide<CartProvide>(
                builder: (context,child,val){
                  int goodsCount = Provide.value<CartProvide>(context).allGoodsCount; //获取总数量
                  return Positioned(
                    top:0,
                    right:5,
                    child: Container(
                      padding:EdgeInsets.fromLTRB(6, 3, 6, 3),
                      decoration: BoxDecoration(
                        color:Colors.red,
                        border: Border.all(width: 2,color: Colors.white),
                        borderRadius: BorderRadius.circular(12.0)
                      ),
                      child: Text(
                        '${goodsCount}',
                        style: TextStyle(fontSize:ScreenUtil().setSp(22),color: Colors.white),
                      ),
                    ),
                  );
                },
              )

            ],
          ),
          //关键代码--------------------end----------------
          InkWell(   
          ......
        

重新运行可以看到详情页底部购物车图标的右上角有个总数量了,点击购物车图标跳转到购物车页面。不过‘加入购物车’还没有反应,需要修改cart.dart页面。

修改provide/cart.dart文件

因为我们要实现动态展示,所以在添加购物车商品时,应该也有数量的变化,所以需要修改cart.dart文件里的save()方法。

save(goodsId, goodsName, count, price, images) async{
    SharedPreferences prefs = await SharedPreferences.getInstance(); //初始化
    cartString = prefs.getString('cartInfo'); //获取持久化存储的值
    //判断cartString是否为空,为空说明是第一次添加,或者被key被清除了。
    //如果有值进行decode操作
    var temp = cartString == null ? [] : json.decode(cartString.toString());
    //把获得值转变成List
    List<Map> tempList = (temp as List).cast(); //temp转化为List
    //声明变量,用于判断购物车中是否已经存在此商品ID
    bool isHave = false; //默认为没有
    int ival = 0; //用于进行循环的索引使用
    allPrice = 0; //总价格初始化为0
    allGoodsCount = 0; //把商品总数量初始化为0
    tempList.forEach((item){ //进行循环,找出是否已经存在该商品
      //如果存在,数量进行+1操作
      if(item['goodsId'] == goodsId){
        tempList[ival] ['count'] = item['count'] +1;
        cartList[ival].count++;
        isHave = true;
      }
      //点击为真时,总价格和总数更新
      if(item['isCheck']){
        allPrice += (cartList[ival].price * cartList[ival].count); //总价格=(单价*数量)+(单价*数量)+...
        allGoodsCount += cartList[ival].count;
      }

      ival++;
    });
    //如果没有,进行增加
    if(!isHave){
      Map<String, dynamic> newGoods = {
        'goodsId':goodsId,
        'goodsName':goodsName,
        'count':count,
        'price':price,
        'images':images,
        'isCheck':true,  //是否已经选择
      };
      tempList.add(newGoods);
      cartList.add(CartInfoModel.fromJson(newGoods)); //fromJson变成对象
      //没有时,总价格和总数同样更新
      allPrice += (price * count); //总价格=(单价*数量)+(单价*数量)+...
      allGoodsCount += count;
    }
    //把字符串进行encode操作
    cartString = json.encode(tempList).toString(); //json数据转字符串
    // print('字符串>>>>>>>>>>>${cartString}');
    // print('数据模型>>>>>>>>>>>${cartList}');
    prefs.setString('cartInfo', cartString); //进行持久化
    notifyListeners(); //通知
  }

完成后,就可以实现商品详细页购物车中商品数量的动态展示了。也算我们购物车区域所有功能都已经完成了。

购物车页面返回商品页

一般商城在购物车页面,点击商品,还可以跳回到商品页面,下面添加下这个小功能。

修改cart_item.dart文件,先引入路由:

import '../../routers/application.dart';

然后修改商品图片代码:

//商品图片
  Widget _cartImage(context,item){
    //-----------------关键代码---------start---------
    return InkWell(
      onTap: (){
        Application.router.navigateTo(context, '/detail?id=${item.goodsId}');
      },
      child: Container(
        width: ScreenUtil().setWidth(150),
        padding: EdgeInsets.all(3.0),
        decoration: BoxDecoration(
          border: Border.all(width: 1,color: Colors.black12)
        ),
        child: Image.network(item.images),
      )
    );
    //-----------------关键代码---------end---------
  }

同时上面build也要加上contenx

_cartImage(context,item),

重新运行,进入购物车页面,点击商品图片,可以调回到商品详情页了。。。

posted on 2020-05-29 16:19  JoeYoung  阅读(2254)  评论(1编辑  收藏  举报