Flutter:仿京東專案實戰(1)-首頁功能實現

語言: CN / TW / HK

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

Scalfold Widget 是用來描述頁面的主結構,一個 MaterialApp 由多個 Scalfold 頁面組成,每個 Scalfold 的普遍結果如下:

  • AppBar:頂部導航欄
  • body:中間內容體
  • BottomNavigationBar:底部導航欄

第一天搭建專案框架,實現首頁的功能。

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

第三天實現 商品詳情頁功能實現:https://juejin.cn/post/7046186331940061191

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

用到的知識點

  1. BottomNavigationBar 底部導航欄基本屬性

截圖2021-12-21 上午9.53.18.png

  1. 命名路由:關於更多內容可以參考 Flutter-導航與路由堆疊詳解

  2. 螢幕適配:使用了flutter_screenutil外掛,具體有以下這些屬性

傳入設計稿的px尺寸 px px px ! ScreenUtil().setWidth(540) //根據螢幕寬度適配尺寸 ScreenUtil().setHeight(200) //根據螢幕高度適配尺寸 ScreenUtil().setSp(24) //適配字型 ScreenUtil().setSp(24, allowFontScalingSelf: true) //適配字型(根據系統的“字型大小”輔助選項來進行縮放) ScreenUtil().setSp(24, allowFontScalingSelf: false) //適配字型(不會根據系統的“字型大小”輔助選項來進行縮放) ScreenUtil().pixelRatio //裝置的畫素密度 ScreenUtil().screenWidth //裝置寬度 ScreenUtil().screenHeight //裝置高度 ScreenUtil().bottomBarHeight //底部安全區距離,適用於全面屏下面有按鍵的 ScreenUtil().statusBarHeight //狀態列高度 劉海屏會更高 單位dp ScreenUtil().textScaleFactor //系統字型縮放比例 ScreenUtil().scaleWidth // 實際寬度的dp與設計稿px的比例 ScreenUtil().scaleHeight // 實際高度的dp與設計稿px的比例 4. 網路請求使用 dio 外掛實現,詳解可以看官網:https://github.com/flutterchina/dio

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

void main() async { var dio = Dio(); final response = await dio.get('https://google.com'); print(response.data); } ```

配置抓包

  • 引入這兩個dio 的標頭檔案 import 'package:dio/adapter.dart'; import 'package:dio/dio.dart';

  • 配置抓包程式碼

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

  1. 輪播圖使用 swiper 外掛實現

截圖2021-12-20 下午8.51.14.png

SwiperPagination 屬性說明:

//如果要將分頁指示器放到其他位置,可以修改這個引數 alignment Alignment.bottomCenter 分頁指示器與容器邊框的距離 margin EdgeInsets.all(10.0) 分頁指示器的樣式,fraction自定義樣式 builder SwiperPagination.dots

  1. 頁面框架使用 ListView 實現列表

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

  1. 商品列表用到了 Wrap ,流式佈局、自動換行

屬性解析 direction:主軸(mainAxis)的方向,預設為水平。 alignment:主軸方向上的對齊方式,預設為start。 spacing:主軸方向上的間距。 runAlignment:run的對齊方式。run可以理解為新的行或者列,如果是水平方向佈局的話,run可以理解為新的一行。 runSpacing:run的間距。 crossAxisAlignment:交叉軸(crossAxis)方向上的對齊方式。 textDirection:文字方向。 verticalDirection:定義了children擺放順序,預設是down,見Flex相關屬性介紹。

專案的搭建

建立 tabs資料夾,裡面新增tabs.dart和4個底導首頁(home.dart、category.dart、cart.dart、user.dart)。

這裡只是貼出主要程式碼:

bottomNavigationBar: BottomNavigationBar( currentIndex:_currentIndex , onTap: (index){ setState(() { _currentIndex=index; }); }, type:BottomNavigationBarType.fixed , fixedColor:Colors.red, items: [ BottomNavigationBarItem( icon: Icon(Icons.home), label: "首頁" ), BottomNavigationBarItem( icon: Icon(Icons.category), label: "分類" ), BottomNavigationBarItem( icon: Icon(Icons.shopping_cart), label: "購物車" ), BottomNavigationBarItem( icon: Icon(Icons.people), label: "我的" ) ], ),

配置命名路由

這裡為了便於統一管理,建立一個routers資料夾,在裡面建立一個router.dart類來管理路由

``` //配置路由 final Map routes = { '/': (context) => Tabs(),arguments,), };

//固定寫法 var onGenerateRoute = (RouteSettings settings) { final String? name = settings.name; final Function? pageContentBuilder = routes[name]; if (pageContentBuilder != null) { if (settings.arguments != null) { final Route route = MaterialPageRoute( builder: (context) => pageContentBuilder(context, arguments: settings.arguments)); return route; } else { final Route route = MaterialPageRoute(builder: (context) => pageContentBuilder(context)); return route; } } }; ```

螢幕適配

這裡我選擇flutter_screenutil實現螢幕適配,flutter_screenutil 預設以750*1334 設計稿為標準 在 pubspec.yaml 新增

flutter_screenutil: ^5.0.0+2

然後點選 Pub get 拉取。建立一個services資料夾,在裡面建立screen_adapter.dart類,在裡面實現常用的功能

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

class ScreenAdapter { //寬、高、字號大小轉換 static height(num value) { return ScreenUtil().setHeight(value); }

static width(num value) { return ScreenUtil().setWidth(value); }

static size(num value) { return ScreenUtil().setSp(value); }

//獲取裝置的物理寬度 static getScreenWidth() { return ScreenUtil().screenWidth; }

//獲取裝置的物理高度 static getScreenHeight() { return ScreenUtil().screenHeight; }

//狀態列高度 static double statusBarHeight = ScreenUtil().statusBarHeight; //底部安全區域距離 static double bottomBarHeight = ScreenUtil().bottomBarHeight; } ```

具體實現

Simulator Screen Shot - iPhone 12 Pro - 2021-12-20 at 20.25.29.png

頁面整體框架使用 ListView 實現,而內容是分下面三大塊實現: - banner區域的輪播圖,通過 Swiper 外掛實現 - 猜你喜歡的橫向滾動列表,通過 ListView 實現 - 熱門推薦的垂直滾動列表,通過 Wrap 實現

實現banner區域的輪播圖

首先引入 flutter_swiper_null_safety: ^1.0.2, 輪播圖是網路獲取的資料,還需要引入 dio: ^4.0.0 進行網路請求,然後執行 pub get 拉取。

建立輪播圖的模型 focus_model.dart,裡面的程式碼實現為

``` class FocusModel { List result=[]; FocusModel({required this.result}); FocusModel.fromJson(Map json) { if (json['result'] != null) {
json['result'].forEach((v) { result.add(new FocusItemModel.fromJson(v)); }); } }

Map toJson() { final Map data = new Map(); data['result'] = this.result.map((v) => v.toJson()).toList(); return data; } }

class FocusItemModel { String? sId; //可空型別 String? title; String? status; String? pic; String? url;

FocusItemModel({this.sId, this.title, this.status, this.pic, this.url}); FocusItemModel.fromJson(Map json) { sId = json['_id']; title = json['title']; status = json['status']; pic = json['pic']; url = json['url']; }

Map toJson() { final Map data = new Map(); data['_id'] = this.sId; data['title'] = this.title; data['status'] = this.status; data['pic'] = this.pic; data['url'] = this.url; return data; } } ```

獲取banner資料

//獲取熱門推薦的資料 _getBestProductData() async { var api = 'https://jdmall.itying.com/api/plist?is_best=1'; var result = await Dio().get(api); var bestProductList = ProductModel.fromJson(result.data); setState(() { this._bestProductList = bestProductList.result; }); }

建立輪播圖

Widget _swiperWidget() { if (this._focusData.length > 0) { return Container( child: AspectRatio( aspectRatio: 2 / 1, child: Swiper( itemBuilder: (BuildContext context, int index) { String pic = this._focusData[index].pic; pic = Config.domain + pic.replaceAll('\', '/'); return new Image.network( "${pic}", fit: BoxFit.fill, ); }, itemCount: this._focusData.length, pagination: new SwiperPagination(), autoplay: true), ), ); } else { return Text('載入中...'); } }

具體程式碼如下:

``` List _focusData = []; @override void initState() { super.initState(); _getFocusData(); }

@override Widget build(BuildContext context) { return ListView( children: [ _swiperWidget(), ], ); } ```

然後執行專案就實現了這樣的效果,輪播圖的功能做好了

Simulator Screen Shot - iPhone 12 Pro - 2021-12-20 at 20.46.25.png

實現猜你喜歡的效果

封裝一個方法,返回一個Widget

Widget _titleWidget(value) { return Container( height: ScreenAdapter.height(32), margin: EdgeInsets.only(left: ScreenAdapter.width(20)), padding: EdgeInsets.only(left: ScreenAdapter.width(20)), decoration: BoxDecoration( border: Border( left: BorderSide( color: Colors.red, width: ScreenAdapter.width(10), ))), child: Text( value, style: TextStyle(color: Colors.black54), ), ); }

獲取猜你喜歡網路資料

//獲取猜你喜歡的資料 _getHotProductData() async { var api = '${Config.domain}api/plist?is_hot=1'; var result = await Dio().get(api); var hotProductList = ProductModel.fromJson(result.data); setState(() { this._hotProductList = hotProductList.result; }); }

建立猜你喜歡橫向列表

//猜你喜歡 Widget _hotProductListWidget() { if (_hotProductList.length > 0) { return Container( height: ScreenAdapter.height(234), padding: EdgeInsets.all(ScreenAdapter.width(20)), child: ListView.builder( //設定滾動方向 scrollDirection: Axis.horizontal, itemBuilder: (contxt, index) { //處理圖片 String sPic = _hotProductList[index].sPic; //得到圖片URL sPic = Config.domain + sPic.replaceAll('\', '/'); return Column( children: <Widget>[ Container( height: ScreenAdapter.height(140), width: ScreenAdapter.width(140), margin: EdgeInsets.only(right: ScreenAdapter.width(21)), child: Image.network(sPic, fit: BoxFit.cover), ), Container( padding: EdgeInsets.only(top: ScreenAdapter.height(10)), height: ScreenAdapter.height(44), child: Text( "¥${_hotProductList[index].price}", style: TextStyle(color: Colors.red), ), ) ], ); }, itemCount: _hotProductList.length, ), ); } else { return Text(""); } }

實現效果

截圖2021-12-20 下午9.10.12.png

實現熱門推薦功能

獲取熱門推薦的資料

_getBestProductData() async { var api = '${Config.domain}api/plist?is_best=1'; var result = await Dio().get(api); var bestProductList = ProductModel.fromJson(result.data); setState(() { this._bestProductList = bestProductList.result; }); }

建立商品列表

``` Widget _recProductListWidget() {

var itemWidth = (ScreenAdapter.getScreenWidth() - 30) / 2; return Container( padding: EdgeInsets.all(10), child: Wrap( runSpacing: 10, spacing: 10, children: this._bestProductList.map((value) {

    //圖片
    String sPic = value.sPic == null ? '' : value.sPic;
    sPic = Config.domain+sPic.replaceAll('\', '/');

    return Container(
      padding: EdgeInsets.all(10),
      width: itemWidth,
      decoration: BoxDecoration(
          border: Border.all(
              color: Color.fromRGBO(233, 233, 233, 0.9), width: 1)),
      child: Column(
        children: <Widget>[
          Container(
            width: double.infinity,
            child: AspectRatio(
              //防止伺服器返回的圖片大小不一致導致高度不一致問題
              aspectRatio: 1 / 1,
              child: Image.network(
                "${sPic}",
                fit: BoxFit.cover,
              ),
            ),
          ),
          Padding(
            padding: EdgeInsets.only(top: ScreenAdapter.height(20)),
            child: Text(
              "${value.title}",
              maxLines: 2,
              overflow: TextOverflow.ellipsis,
              style: TextStyle(color: Colors.black54),
            ),
          ),
          Padding(
            padding: EdgeInsets.only(top: ScreenAdapter.height(20)),
            child: Stack(
              children: <Widget>[
                Align(
                  alignment: Alignment.centerLeft,
                  child: Text(
                    "¥${value.price}",
                    style: TextStyle(color: Colors.red, fontSize: 16),
                  ),
                ),
                Align(
                  alignment: Alignment.centerRight,
                  child: Text( "¥${value.oldPrice}",
                      style: TextStyle(
                          color: Colors.black54,
                          fontSize: 14,
                          decoration: TextDecoration.lineThrough)),
                )
              ],
            ),
          )
        ],
      ),
    );
  }).toList(),
),

); } ```

實現效果

截圖2021-12-20 下午9.11.50.png