Flutter:仿京東項目實戰(4)-購物車頁面功能實現

語言: CN / TW / HK

highlight: a11y-dark

在我個人認為學習一門新的語言(快速高效學習) 一定是通過實踐,最好的就是做項目,這裏我會簡單寫一個京東的Demo。

第一天 搭建項目框架,實現首頁的功能:https://juejin.cn/editor/drafts/7043351582573854727。

第二天實現 分類和商品列表頁面: https://juejin.cn/post/7044716539550892068。

第三天實現 商品詳情頁功能:https://juejin.cn/editor/drafts/7045849478170935332

Flutter-混合工程的持續集成實踐: https://juejin.cn/post/7042099001679675422

前面實現了首頁、分類頁面、商品列表頁和商品詳情頁的功能,這篇文章實現購物車頁面的功能。

用到的知識點

1. shared_preferences 實現本地數據存儲

shared_preferences 是 Flutter 提供的 key-value 存儲插件,能夠將數據持久化到磁盤中,支持 Android 和 iOS,在 iOS 中是基於 NSUserDefaults,在 Android 中基於SharedPreferences

在項目的 pubspec.yaml 文件中添加依賴:shared_preferences: ^2.0.11,然後執行 pub get, shared_preferences 支持的數據類型有 int、double、bool、string、stringList

services文件裏面定義一個storage.dart,在裏面封裝常用的功能:

```dart import 'package:shared_preferences/shared_preferences.dart';

class Storage { //設置值 static Future setString(key, value) async { SharedPreferences sp = await SharedPreferences.getInstance(); var result = sp.setString(key, value); }

//獲取值 static Future getString(key) async{ SharedPreferences sp = await SharedPreferences.getInstance(); var result = sp.getString(key); return result; }

//刪除值 static Future remove(key) async { SharedPreferences sp = await SharedPreferences.getInstance(); sp.remove(key); }

//清理值 static Future clear() async { SharedPreferences sp = await SharedPreferences.getInstance(); sp.clear(); } } ```

我這裏是用了String類型舉例,封裝了一個類專門管理。在之前的文章中頁講到數據存儲的兩種方式:https://juejin.cn/post/7040986659533357087

2. JSON 轉 Model

在日常開發中JSON的序列化與反序列化是一個常見的操作,如果都是我們手動去解析JSON數據,是很麻煩的。如果能夠自動轉化就省去了很多事情。

工具實現

在iOS上面我就找到了一個工具可以自動轉換 json 數據:https://juejin.cn/post/7026898009900187679 。那在Flutter 中也找到了轉換的工具:https://app.quicktype.io ,相對來講也是比較好用的。

截屏2021-12-28 下午9.54.10.png

這樣就可以實現轉化。由於Flutter禁用運行時反射,才導致沒有像iOS成熟的庫完成解析,比如 MJExtensionYYModel,這裏介紹一個相對成熟的庫 json_serializable 實現轉化。

json_serializable 實現

在項目的 pubspec.yaml 文件中添加依賴:

json_serializable: ^6.1.3 build_runner: ^2.1.7 json_annotation: ^4.4.0

然後執行 pub get。要想使用轉化,首先要先用工具生成模型類,工具地址:https://caijinglong.github.io/json2dart/index_ch.html

截屏2021-12-28 下午9.31.39.png

在項目裏面創建模型類,把工具轉換的代碼拷貝到這個模型類裏面

```dart import 'package:json_annotation/json_annotation.dart';

part 'person.g.dart';

List getpersonList(List list){ List result = []; list.forEach((item){ result.add(person.fromJson(item)); }); return result; } @JsonSerializable() class person extends Object with _$personSerializerMixin{

@JsonKey(name: 'name') String name;

@JsonKey(name: 'age') String age;

@JsonKey(name: 'tele') String tele;

person(this.name,this.age,this.tele,);

factory person.fromJson(Map srcJson) => _$personFromJson(srcJson);

} ```

接下來在終端執行flutter packages pub run build_runner watch,就會在項目裏面生成person.g.dart文件,這個裏面就是轉換好的代碼。

截屏2021-12-28 下午9.38.57.png

也可以執行 flutter packages pub run build_runner build生成 person.g.dart文件,區別在於上面是持續生成,下面這個是一次性生成。

注意上面工具使用時,會按着list裏面第一個map裏面的數據進行解析,假如數組裏面其他map字段比較多,就會存在漏字段的情況,這個還需要注意檢查下。整體使用下來也不是很方便,還不如用工具直接生成簡單:https://app.quicktype.io 。

插件 JsonToDart 實現

https://zhuanlan.zhihu.com/p/163330265 這個插件也可以實現轉換

在 Android Studio 中安裝 JsonToDart 插件,打開 Preferences(Mac)或者 Setting(Window),選擇 Plugins,搜索 JsonToDart

截屏2021-12-28 下午10.08.59.png

點擊 Install 安裝,安裝完成後重啟。這個時候選定目錄,點擊右鍵,選擇 New->Json to Dart,或者使用快捷鍵

Windows:ALT + Shift + D Mac:Option + Shift + D

截屏2021-12-28 下午10.11.24.png

選中 Json To Dart 後,彈出頁面輸入要轉換的json數據

截屏2021-12-28 下午10.10.35.png

點擊完成,就會生成對應的模型文件了

截屏2021-12-28 下午10.10.55.png

這個是三個json轉model方案裏面最簡單的了。

上篇文章實現了五種JSON轉Model的方案:https://juejin.cn/post/7047011637248655396

3. 在不同分辨率的手機上查看UI效果

Flutter 開發最大的優勢就是其跨平台,當開發完成時,想在不同分辨率的手機查看其效果,如果跑每個機型去看效果還是比較麻煩的。這個包 device_preview 可以實現查看不同分辨率手機上的UI效果。

配置 device_preview: ^1.0.0,然後執行 pub get。在 main.dart裏面使用

```dart import 'package:device_preview/device_preview.dart';

void main() => runApp( DevicePreview( enabled: !kReleaseMode,//在非release環境下使用 builder: (context) => MyApp(), // Wrap your app ), );

class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( useInheritedMediaQuery: true, locale: DevicePreview.locale(context), builder: DevicePreview.appBuilder, theme: ThemeData.light(), darkTheme: ThemeData.dark(), home: const HomePage(), ); } } ```

這個包可以實現下列功能: - 更改設備方向 - 動態系統配置:語言,暗模式,文本縮放比例 - 可自由調整分辨率和安全區域的設備 - 保持應用程序狀態 - 截圖

Simulator Screen Shot - iPhone 12 Pro - 2021-12-29 at 18.24.12.png

4. Provider 狀態管理

什麼是Provider 狀態管理?

當我們想在多個頁面(組件/Widget)之間共享狀態(數據),或者一個頁面(組 件/Widget)中的多個子組件之間共享狀態(數據),這個時候我們就可以用 Flutter 中的狀態管理來管理統一的狀態(數據),實現不同組件直接的傳值和數據共享。provider 是 Flutter 官方團隊推出的狀態管理模式。

具體的使用:

  • 配置provider: ^6.0.1
  • 新建一個文件夾叫 provider,在 provider 文件夾裏面放我們對於的狀態管理類
  • 在 provider 裏面新建 cart.dart
  • cart.dart 裏面新建一個類繼承 ChangeNotifier 代碼如下,這裏主要是處理購物車中的數據

```dart class Cart with ChangeNotifier { List _cartList = [];//購物車數據 bool _isCheckAll = false;//全選 double _allPrice = 0;//總價

List get cartList => _cartList; bool get isCheckAll => _isCheckAll; double get allPrice => _allPrice;

Cart(){ this.init(); }

//初始化的時候獲取購物車數據 init() async { String? cartList = await Storage.getString(('cartList')); if(cartList != null){ List cartListData = json.decode(cartList); _cartList = cartListData; } else { _cartList = []; }

//獲取全選的狀態
_isCheckAll = this.isCheckAll;
//計算總價
computeAllPrice();
notifyListeners();

}

updateCartList() { this.init(); }

itemCountChange() { Storage.setString('cartList', json.encode(_cartList)); //計算總價 computeAllPrice(); notifyListeners(); }

//全選 反選 checkAll(value) { for (var i = 0; i < _cartList.length; i++) { _cartList[i]['checked'] = value; } _isCheckAll = value; //計算總價 computeAllPrice(); Storage.setString('cartList', json.encode(_cartList)); notifyListeners(); }

//判斷是否全選 bool isCheckedAll() { if (_cartList.length > 0) { for (var i = 0; i < cartList.length; i++) { if (_cartList[i]['checked'] == false) { return false; } } return true; } return false; }

//監聽每一項的選中事件 itemChage() { if (isCheckAll == true) { _isCheckAll = true; } else { _isCheckAll = false; } //計算總價 computeAllPrice(); Storage.setString('cartList', json.encode(_cartList)); notifyListeners(); }

//計算總價 computeAllPrice() { double tempAllPrice = 0; for (var i = 0; i < _cartList.length; i++) { if (_cartList[i]['checked'] == true) { tempAllPrice += _cartList[i]['price'] * _cartList[i]['count']; } }

_allPrice = tempAllPrice;
notifyListeners();

}

//刪除數據 removeItem() { List tempList=[]; for (var i = 0; i < _cartList.length; i++) { if (_cartList[i]['checked'] == false) { tempList.add(_cartList[i]); } } _cartList=tempList; //計算總價 computeAllPrice(); Storage.setString('cartList', json.encode(_cartList)); notifyListeners(); } } ```

最後別忘記在main.dart中的MultiProvider添加上這個文件

dart providers:[ ChangeNotifierProvider(create: (_) => CheckOut()), ChangeNotifierProvider(create: (_) => Cart()), ],

實現效果

Simulator Screen Shot - iPhone 12 Pro - 2021-12-28 at 20.07.24.png

具體實現代碼

界面框架代碼

```dart class CartPage extends StatefulWidget { CartPage({Key? key}) : super(key: key);

_CartPageState createState() => _CartPageState(); }

class _CartPageState extends State {

bool _isEdit = false;

var checkOutProvider;

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

}

//去結算 doCheckOut() async { //1、獲取購物車選中的數據 List checkOutData = await CartServices.getCheckOutData(); //2、保存購物車選中的數據 this.checkOutProvider.changeCheckOutListData(checkOutData); //3、購物車有沒有選中的數據 if (checkOutData.length > 0) { Navigator.pushNamed(context, '/checkOut'); } else { Fluttertoast.showToast( msg: '購物車沒有選中的數據', toastLength: Toast.LENGTH_SHORT, gravity: ToastGravity.CENTER, ); } }

@override Widget build(BuildContext context) { var cartProvider = Provider.of(context); checkOutProvider = Provider.of(context);

return Scaffold(
  appBar: AppBar(
    title: Text('購物車'),
    actions: [
      IconButton(onPressed: (){

      }, icon: Icon(Icons.launch))
    ],
  ),
  body: cartProvider.cartList.length > 0 ? Stack(
    children: [
      //列表
      ListView(
        children: [
          Column(
            children: [
              Column(
                children: cartProvider.cartList.map((value){
                  //返回生成每個Item
                  return CartItem(value);
                }).toList(),
              ),
              SizedBox(height: ScreenAdapter.height(100))
            ],
          )
        ],
      ),
      //底部的全選和結算按鈕
      Positioned(
          bottom: 0,
          width: ScreenAdapter.width(750),
          height: ScreenAdapter.height(78),
          child: Container(
            decoration: BoxDecoration(
                border: Border(
                  top: BorderSide(width: 1, color: Colors.black12),
                ),
              color: Colors.white
            ),
            width: ScreenAdapter.width(750),
            height: ScreenAdapter.height(78),
            child: Stack(
              children: [
                Align(
                  alignment: Alignment.centerLeft,
                  child: Row(
                    children: [
                      Container(
                        width: ScreenAdapter.width(60),
                        child: Checkbox(
                          value: false,
                          activeColor: Colors.pink,
                          onChanged: (v){

                          },
                        ),
                      ),
                      Text('全選'),
                    ],
                  ),
                ),
                Align(
                  alignment: Alignment.centerRight,
                  child: Container(
                    margin: EdgeInsets.only(right: 10),
                    child: ElevatedButton(
                      child: Text('結算', style: TextStyle(color: Colors.white),),
                      style: ButtonStyle(
                        backgroundColor: MaterialStateProperty.all(Colors.red),
                      ),
                      onPressed: (){
                        doCheckOut();
                      },
                    ),
                  ),
                )
              ],
            ),
          )
      ),
    ],
  ) : Center(
    child: Text("購物車空空的..."),
  ),
);

} } ```

每個Item的實現代碼

截屏2021-12-29 下午5.46.29.png

單獨創建一個cart文件夾,在裏面放在主頁面抽離的代碼

```dart class CartItem extends StatefulWidget { Map _itemData; CartItem(this._itemData,{Key? key}) : super(key: key);

_CartItemState createState() => _CartItemState(); }

class _CartItemState extends State {

//從本地存儲的數據裏面讀取的 late Map _itemData;

@override Widget build(BuildContext context) { //注意:給屬性賦值 this._itemData=widget._itemData; //通過Provider實現了頁面和組件間的數據共享 var cartProvider = Provider.of(context); return Container( height: ScreenAdapter.height(220), padding: EdgeInsets.all(5), decoration: BoxDecoration( border: Border(bottom: BorderSide(width: 1, color: Colors.black12))), child: Row( children: [ Container( width: ScreenAdapter.width(60), child: Checkbox( value: _itemData["checked"], onChanged: (val) { _itemData["checked"]=!_itemData["checked"]; //更新數據 cartProvider.itemChage(); }, activeColor: Colors.pink, ), ), Container( width: ScreenAdapter.width(160), child: Image.network( "${_itemData["pic"]}", fit: BoxFit.cover), ), Expanded( flex: 1, child: Container( padding: EdgeInsets.fromLTRB(10, 10, 10, 5), child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("${_itemData["title"]}", maxLines: 2), Text("${_itemData["selectedAttr"]}", maxLines: 2), Stack( children: [ Align( alignment: Alignment.centerLeft, child: Text("¥${_itemData["price"]}",style: TextStyle( color: Colors.red )), ), Align( alignment: Alignment.centerRight, //實現增加/減少物品件數 child: CartNum(_itemData), ) ], ) ], ), ), ) ], ), ); } } ```

每件商品的數量加/減組件代碼

截屏2021-12-29 下午5.46.45.png

```dart class CartNum extends StatefulWidget { Map _itemData; CartNum(this._itemData,{Key? key}) : super(key: key);

_CartNumState createState() => _CartNumState(); }

class _CartNumState extends State { late Map _itemData; var cartProvider;

@override Widget build(BuildContext context) {

//注意
_itemData=widget._itemData;

cartProvider = Provider.of<Cart>(context);

return Container(
  width: ScreenAdapter.width(168),
  decoration:
  BoxDecoration(border: Border.all(width: ScreenAdapter.width(2), color: Colors.black12)),
  child: Row(
    children: <Widget>[
      _leftBtn(),
      _centerArea(),
      _rightBtn()
    ],
  ),
);

}

//左側按鈕

Widget _leftBtn() { return InkWell( onTap: () { if(_itemData["count"]>1){ _itemData["count"]--; cartProvider.itemCountChange(); } }, child: Container( alignment: Alignment.center, width: ScreenAdapter.width(45), height: ScreenAdapter.height(45), child: Text("-"), ), ); }

//右側按鈕 Widget _rightBtn() { return InkWell( onTap: (){ _itemData["count"]++; cartProvider.itemCountChange(); }, child: Container( alignment: Alignment.center, width: ScreenAdapter.width(45), height: ScreenAdapter.height(45), child: Text("+"), ), ); }

//中間 Widget _centerArea() { return Container( alignment: Alignment.center, width: ScreenAdapter.width(70), decoration: BoxDecoration( border: Border( left: BorderSide(width: ScreenAdapter.width(2), color: Colors.black12), right: BorderSide(width: ScreenAdapter.width(2), color: Colors.black12), )), height: ScreenAdapter.height(45), child: Text("${_itemData["count"]}"), ); } } ```

provider 代碼

通過使用provider實現了數據共享,創建了兩個文件 cart.dartcheck_out.dart

```dart class Cart with ChangeNotifier { List _cartList = [];//購物車數據 bool _isCheckAll = false;//全選 double _allPrice = 0;//總價

List get cartList => _cartList; bool get isCheckAll => _isCheckAll; double get allPrice => _allPrice;

Cart(){ this.init(); }

//初始化的時候獲取購物車數據 init() async { String? cartList = await Storage.getString(('cartList')); if(cartList != null){ List cartListData = json.decode(cartList); _cartList = cartListData; } else { _cartList = []; }

//獲取全選的狀態
_isCheckAll = this.isCheckAll;
//計算總價
computeAllPrice();
notifyListeners();

}

updateCartList() { this.init(); }

itemCountChange() { Storage.setString('cartList', json.encode(_cartList)); //計算總價 computeAllPrice(); notifyListeners(); }

//全選 反選 checkAll(value) { for (var i = 0; i < _cartList.length; i++) { _cartList[i]['checked'] = value; } _isCheckAll = value; //計算總價 computeAllPrice(); Storage.setString('cartList', json.encode(_cartList)); notifyListeners(); }

//判斷是否全選 bool isCheckedAll() { if (_cartList.length > 0) { for (var i = 0; i < cartList.length; i++) { if (_cartList[i]['checked'] == false) { return false; } } return true; } return false; }

//監聽每一項的選中事件 itemChage() { if (isCheckAll == true) { _isCheckAll = true; } else { _isCheckAll = false; } //計算總價 computeAllPrice(); Storage.setString('cartList', json.encode(_cartList)); notifyListeners(); }

//計算總價 computeAllPrice() { double tempAllPrice = 0; for (var i = 0; i < _cartList.length; i++) { if (_cartList[i]['checked'] == true) { tempAllPrice += _cartList[i]['price'] * _cartList[i]['count']; } }

_allPrice = tempAllPrice;
notifyListeners();

}

//刪除數據 removeItem() { List tempList=[]; for (var i = 0; i < _cartList.length; i++) { if (_cartList[i]['checked'] == false) { tempList.add(_cartList[i]); } } _cartList=tempList; //計算總價 computeAllPrice(); Storage.setString('cartList', json.encode(_cartList)); notifyListeners(); } } ```

```dart class CheckOut with ChangeNotifier { List _checkOutListData = []; //購物車數據 List get checkOutListData => _checkOutListData;

changeCheckOutListData(data){ _checkOutListData=data; notifyListeners(); } } ```

以上就是購物車頁面的實現代碼。