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(); } } ```

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