Flutter:仿京東專案實戰(2)-分類和商品列表頁面功能實現
在我個人認為學習一門新的語言(快速高效學習) 一定是通過實踐,最好的就是做專案,這裡我會簡單寫一個京東的Demo。
在上篇文章裡面建立了BottomNavigationBar,裡面包含了4個主介面,今天完成第二個主介面,分類頁面的功能和商品列表功能。
用到的知識點
1. 命名路由傳參
- 路由表
routes
裡面增加
'/product_list': (context, {arguments}) => ProductListPage(arguments: arguments),
- 需要跳轉頁面的地方
Navigator.pushNamed(context, '/product_list', arguments: {'cid': _rightCateList[index].sId!});
- 跳轉到的頁面
``` class ProductListPage extends StatefulWidget {
Map arguments;
ProductListPage({Key? key, required this.arguments}) : super(key: key);
@override _ProductListPageState createState() => _ProductListPageState(); } ```
2. 配置抓包
-
引入這兩個
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";
};
};
}
- 配置好後的抓包的效果:
3. 上拉載入下拉重新整理通過 flutter_easyrefresh
實現
- 這裡是 flutter_easyrefresh 官網列出的幾種實現方式:
import 'package:flutter_easyrefresh/easy_refresh.dart';
....
// 方式一
EasyRefresh(
child: ScrollView(),
onRefresh: () async{
....
},
onLoad: () async {
....
},
)
// 方式二
EasyRefresh.custom(
slivers: <Widget>[],
onRefresh: () async{
....
},
onLoad: () async {
....
},
)
// 方式三
EasyRefresh.builder(
builder: (context, physics, header, footer) {
return CustomScrollView(
physics: physics,
slivers: <Widget>[
...
header,
...
footer,
],
);
}
onRefresh: () async{
....
},
onLoad: () async {
....
},
)
- 在商品列表中的使用
EasyRefresh(
child: ListView.builder(
itemCount: productList.length,
itemBuilder: (context, index) {
//建立列表內容
return createContent(index);
}
),
//下拉重新整理
onRefresh: () async{
_page = 1;
_getProductListData(false);
},
//上拉載入
onLoad: () async {
_page += 1;
if(!_hasMore){
return;
}
_getProductListData(true);
},
)
- 更多的使用可以參考:https://github.com/xuelongqy/flutter_easyrefresh
4. 保持頁面狀態 AutomaticKeepAliveClientMixin
Flutter切換tabar後不會保留tabbar狀態 ,為了節約記憶體不會儲存widget的狀態,widget都是臨時變數。當我們使用TabBar,切換tabar,initState又會被呼叫一次。可以使用 AutomaticKeepAliveClientMixin
解決這個問題
-
當前類要繼承 AutomaticKeepAliveClientMixin
class _CategoryPageState extends State<CategoryPage> with AutomaticKeepAliveClientMixin
-
實現這個方法
bool get wantKeepAlive =>true;
- 新增 super.build(context)
@override Widget build(BuildContext context) { super.build(context); return Container(); }
5. 資料和模型的轉換
這裡只是簡單的資料模型轉換,我採用手動的方式實現了
``` class ProductItemModel { String? sId; String? title;
ProductItemModel({this.sId, this.title,});
ProductItemModel.fromJson(Map
Map
6.ListView
的使用
7. GridView
網格佈局的實現
8. Image
常用方法
加入圖片的幾種方式:
Image.asset:載入本地資源圖片
Image.network:載入網路資源圖片
Image.file:載入本地檔案中的圖片
9. 本地專案國際化
flutter_localizations:
sdk: flutter
``` import 'package:flutter_localizations/flutter_localizations.dart';
new MaterialApp( localizationsDelegates: [ // ... app-specific localization delegate[s] here GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, ], supportedLocales: [ const Locale('en', 'US'), // English const Locale('he', 'IL'), // Hebrew // ... other locales the app supports ], // ... ) ```
具體支援國際化更多的方案可以參考:https://zhuanlan.zhihu.com/p/145992691
具體功能實現
實現的效果
全域性配置資訊類config.dart
例如可以在裡面存放域名
class Config{
static String domain="https://jdmall.itying.com/";
}
分類頁面的實現
整體頁面左邊通過ListView、右邊通過GridView實現,然後通過點選左邊列表實現右邊列表的資料重新整理。
定義資料模型
```
class CateModel {
List
CateModel({required this.result});
CateModel.fromJson(Map
Map
class CateItemModel { String? sId; //String? 表示可空型別 String? title; Object? status; String? pic; String? pid; String? sort;
CateItemModel( {this.sId, this.title, this.status, this.pic, this.pid, this.sort});
CateItemModel.fromJson(Map
Map
實現程式碼
``` class CategoryPage extends StatefulWidget { CategoryPage({Key? key}) : super(key: key);
_CategoryPageState createState() => _CategoryPageState(); }
class _CategoryPageState extends State
//當前選中 int _selectIndex=0; //左側列表資料 List _leftCateList=[]; //右側列表資料 List _rightCateList=[];
@override // TODO: implement wantKeepAlive 快取當前頁面 bool get wantKeepAlive =>true;
@override void initState() { // TODO: implement initState super.initState();
_getLeftCateData();
}
//左側分類的資料 _getLeftCateData() async{ var api = '${Config.domain}api/pcate'; var result = await Dio().get(api); var leftCateList = new CateModel.fromJson(result.data); setState(() { this._leftCateList = leftCateList.result; }); _getRightCateData(leftCateList.result[0].sId); }
//右側分類資料 _getRightCateData(pid) async{ var api = '${Config.domain}api/pcate?pid=${pid}'; var result = await Dio().get(api); var rightCateList = new CateModel.fromJson(result.data); setState(() { this._rightCateList = rightCateList.result; }); }
//左側列表佈局
Widget _leftCateWidget(leftWidth){
if(_leftCateList.length>0){
return Container(
width: leftWidth,
height: double.infinity,
// color: Colors.red,
child: ListView.builder(
itemCount: _leftCateList.length,
itemBuilder: (context,index){
return Column(
children:
onTap: (){
setState(() {
//重新整理右側列表的資料
_selectIndex= index;
_getRightCateData(_leftCateList[index].sId);
});
},
child: Container(
width: double.infinity,
height: ScreenAdapter.height(84),
padding: EdgeInsets.only(top:ScreenAdapter.height(24)),
child: Text("${_leftCateList[index].title}",textAlign: TextAlign.center),
color: _selectIndex==index? Color.fromRGBO(240, 246, 246, 0.9):Colors.white,
),
),
Divider(height: 1),
],
);
},
),
);
} else {
return Container(
width: leftWidth,
height: double.infinity
);
}
}
//建立右側列表 Widget _rightCateWidget(rightItemWidth,rightItemHeight){ if(_rightCateList.length>0){ return Expanded( flex: 1, child: Container( padding: EdgeInsets.all(10), height: double.infinity, color: Color.fromRGBO(240, 246, 246, 0.9), child: GridView.builder( gridDelegate:SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount:3, childAspectRatio: rightItemWidth/rightItemHeight, crossAxisSpacing: 10, mainAxisSpacing: 10 ), itemCount: _rightCateList.length, itemBuilder: (context,index){ //處理圖片 String pic = _rightCateList[index].pic; pic = Config.domain+pic.replaceAll('\', '/');
return InkWell(
onTap: (){
Navigator.pushNamed(context, '/product_list', arguments: {'cid': _rightCateList[index].sId!});
},
child: Container(
// padding: EdgeInsets.all(10),
child: Column(
children: <Widget>[
AspectRatio(
aspectRatio: 1/1,
child: Image.network("${pic}",fit: BoxFit.cover),
),
Container(
height: ScreenAdapter.height(28),
child: Text("${_rightCateList[index].title}"),
)
],
),
),
);
},
)
),
);
} else {
return Expanded(
flex: 1,
child: Container(
padding: EdgeInsets.all(10),
height: double.infinity,
color: Color.fromRGBO(240, 246, 246, 0.9),
child: Text("載入中..."),
)
);
}
}
@override
Widget build(BuildContext context) {
//左側寬度
var leftWidth=ScreenAdapter.getScreenWidth()/4;
//右側每一項寬度=(總寬度-左側寬度-GridView外側元素左右的Padding值-GridView中間的間距/3
var rightItemWidth=(ScreenAdapter.getScreenWidth()-leftWidth-20-20)/3;
//獲取計算後的寬度
rightItemWidth=ScreenAdapter.width(rightItemWidth);
//獲取計算後的高度
var rightItemHeight=rightItemWidth+ScreenAdapter.height(28);
return Scaffold(
appBar: AppBar(
title: Text('分類頁面'),
),
body: Row(
children:
實現效果
商品列表頁面
建立商品列表Model
```
class ProductModel {
List
ProductModel({required this.result});
ProductModel.fromJson(Map
Map
class ProductItemModel { String? sId; //String? 表示可空型別 String? title; String? cid; Object? price; //所有的型別都繼承 Object String? oldPrice; String? pic; String? sPic;
ProductItemModel( {this.sId, this.title, this.cid, this.price, this.oldPrice, this.pic, this.sPic});
ProductItemModel.fromJson(Map
Map
實現程式碼
``` class ProductListPage extends StatefulWidget {
Map arguments;
ProductListPage({Key? key, required this.arguments}) : super(key: key);
@override _ProductListPageState createState() => _ProductListPageState(); }
class _ProductListPageState extends State
final GlobalKey
//當前頁碼 int _page = 1; //每次請求返回多少條資料 int _pageSize = 10; //排序 String _sort = ''; //是否還有更多 bool _hasMore = true; //每組ID int _selectHeaderId = 1; //頁面列表資料 List productList = []; //搜尋關鍵字 String _keyWords = ''; //文字輸入框的控制器 var _initKeywordsController = TextEditingController();
/二級導航資料/ List subHeaderList = [ {"id": 1, "title": "綜合", "fileds": "all", "sort": -1,}, //排序 升序:price_1 {price:1} 降序:price-1 {price:-1} {"id": 2, "title": "銷量", "fileds": 'salecount', "sort": -1}, {"id": 3, "title": "價格", "fileds": 'price', "sort": -1}, {"id": 4, "title": "篩選"} ];
@override void initState() { // TODO: implement initState super.initState();
_getProductListData(false);
}
//請求列表資料,非同步請求 _getProductListData(bool isMore) async {
var api;
if(_keyWords.isEmpty){
api = '${Config.domain}api/plist?cid=${widget.arguments["cid"]}&page=${_page}&sort=${_sort}&pageSize=${_pageSize}';
} else {
api = '${Config.domain}api/plist?cid=${widget.arguments["cid"]}&page=${_page}&sort=${_sort}&pageSize=${_pageSize}&search=${_keyWords}';
}
//設定只在debug模式下抓包
final kReleaseMode = false;
final Dio dio = Dio();
if (!kReleaseMode){
//設定代理 抓包用
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (HttpClient client) {
client.findProxy = (uri) {
return "PROXY localhost:8888";
};
};
}
var result = await dio.get(api);
//解析json資料,目前我都還是採用手動解析
var dataList = ProductModel.fromJson(result.data).result;
if(dataList.length > 10){
_hasMore = true;
}
setState(() {
if(isMore){
productList.addAll(dataList);
} else {
productList = dataList;
}
});
}
//改變分組的處理 subHeaderChange(id){ if(id==4){ setState(() { _selectHeaderId = id; _scaffoldKey.currentState!.openEndDrawer(); }); } else { setState(() { _selectHeaderId = id; _sort = "${_subHeaderList[id - 1]["fileds"]}${_subHeaderList[id - 1]["sort"]}"; _page = 1; productList = []; //改變sort排序 _subHeaderList[id - 1]['sort'] = _subHeaderList[id - 1]['sort'] * -1; _hasMore = true; _getProductListData(false); }); } }
//列表的內容 Widget createContent(index) { ProductItemModel itemModel = productList[index]; String pic = ''; if(itemModel.pic != null){ //Config存放全域性配置的類 //由於這個圖片連結有問題才這樣處理 pic = Config.domain + itemModel.pic!.replaceAll('\', '/'); } return Column( children: [ InkWell( onTap: (){ //push到下個頁面並傳參 Navigator.pushNamed(context, '/product_content', arguments: {'id' : itemModel.sId}); }, child: Row( children: [ Container( margin: EdgeInsets.only(left: 10), width: ScreenAdapter.width(180), height: ScreenAdapter.height(180), child: Image.network( pic, fit: BoxFit.cover, ), ), Expanded( flex: 1, child: Container( margin: EdgeInsets.all(10), height: ScreenAdapter.height(180), child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( itemModel.title!, maxLines: 2, overflow: TextOverflow.ellipsis, ), Row( children: [ Container( alignment: Alignment.center, height: ScreenAdapter.height(36), margin: EdgeInsets.only(right: 10), padding: EdgeInsets.fromLTRB(10, 0, 10, 0), decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), color: Color.fromRGBO(230, 230, 230, 0.9), ), child: Text('4g'), ), Container( alignment: Alignment.center, height: ScreenAdapter.height(36), margin: EdgeInsets.only(right: 10), padding: EdgeInsets.fromLTRB(10, 0, 10, 0), decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), color: Color.fromRGBO(230, 230, 230, 0.9), ), child: Text( '126', ), ) ], ), Text( '¥${itemModel.price!.toString()}', style: TextStyle(color: Colors.red, fontSize: 16), ) ], ), ), ), ], ), ), ], ); }
//建立商品列表 Widget _productListWidget() { return Container( height: ScreenAdapter.getScreenHeight(), padding: EdgeInsets.all(10), margin: EdgeInsets.only(top: ScreenAdapter.height(80)), //配置重新整理 child: EasyRefresh( child: ListView.builder( itemCount: productList.length, itemBuilder: (context, index) { //建立列表內容 return createContent(index); } ), //下拉重新整理 onRefresh: () async{ _page = 1; _getProductListData(false); }, //上拉載入 onLoad: () async { _page += 1; if(!_hasMore){ return; } _getProductListData(true); }, ), ); }
//建立升降序的圖示 Widget _showIcon(id){ if(id==2 || id==3){ if(_subHeaderList[id-1]['sort'] == 1){ return Icon(Icons.arrow_drop_down); } else { return Icon(Icons.arrow_drop_up); } } return Text(''); }
//建立頭部分組 Widget _subHeaderWidget() { return Positioned( top: 0, width: ScreenAdapter.getScreenWidth(), height: ScreenAdapter.height(80), child: Container( //分組底部分割線 decoration: const BoxDecoration( border: Border( bottom: BorderSide( color: Color.fromRGBO(233, 233, 233, 0.9), width: 1))), child: Row( children: _subHeaderList.map((value){ return Expanded( flex: 1, child: InkWell( onTap: () { _subHeaderChange(value['id']); }, child: Padding( padding: EdgeInsets.fromLTRB( 0, ScreenAdapter.height(16), 0, ScreenAdapter.height(16)), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( child: Text( value['title'], textAlign: TextAlign.center, style: TextStyle(color: _selectHeaderId == value['id'] ? Colors.red : Colors.black), ), ), _showIcon(value['id']) ], ), ), )); }).toList(), ), ), ); }
@override Widget build(BuildContext context) { return Scaffold( key: _scaffoldKey, //建立導航欄 appBar: AppBar( leading: IconButton( onPressed: (){ Navigator.pop(context); }, icon: Icon(Icons.arrow_back), ), title: Container( //文字輸入 child: TextField( controller: this._initKeywordsController, autofocus: true, decoration: InputDecoration( border: OutlineInputBorder( borderRadius: BorderRadius.circular(30), borderSide: BorderSide.none ), ), onChanged: (value){ setState(() { //搜尋框輸入的文字 _keyWords = value; }); }, ), height: ScreenAdapter.height(68), decoration: BoxDecoration( color: Color.fromRGBO(233, 233, 233, 0.8), borderRadius: BorderRadius.circular(30) ), ), actions: [ InkWell( child: Container( width: ScreenAdapter.width(80), height: ScreenAdapter.height(68), child: Row( children: [ Text('搜尋', style: TextStyle(fontSize: 16),) ], ), ), onTap: (){ //點選搜尋框開始搜尋,這裡只是簡單的在綜合組搜尋 _subHeaderChange(1); }, ) ], ), endDrawer: Drawer( child: Container( child: Text('實現篩選功能'), ), ), body: !productList.isEmpty ? Stack( children: [ //建立導航欄下分頁欄 _subHeaderWidget(), //建立頁面 _productListWidget(), ], ) : Center( child: Text('沒有搜尋到商品'), ), ); } } ```
實現效果
- Flutter:仿京東專案實戰(4)-購物車頁面功能實現
- Flutter整合原生遇到的問題彙總
- Flutter:仿京東專案實戰(3)-商品詳情頁功能實現
- Flutter-Dart中的非同步和多執行緒講解
- iOS-底層原理分析之Block本質
- Flutter-官方推薦的Flutter與原生互動外掛Pigeon
- Flutter-flutter_sound錄音與播放
- iOS-CocoaPods的原理及Podfile.lock問題
- iOS配置多環境的三種方案
- iOS-各種Crash防護
- iOS-Swift中常見的幾種閉包
- Flutter:仿京東專案實戰(2)-分類和商品列表頁面功能實現
- Flutter:仿京東專案實戰(1)-首頁功能實現
- Flutter-JSON轉Model的四種便捷方案
- Flutter-導航與路由堆疊詳解
- Flutter 與原生通訊的三種方式
- iOS-記憶體洩漏檢測
- Fastlane實現自動打包
- 懶人必備神器-Xcode程式碼塊
- Jenkins實現自動化打包