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);
},
)
- 更多的使用可以参考:github.com/xuelongqy/f…
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<String, dynamic> json) {
sId = json['_id'];
title = json['title'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['_id'] = this.sId;
data['title'] = this.title;
return data;
}
}
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
],
// ...
)
具体支持国际化更多的方案可以参考:zhuanlan.zhihu.com/p/145992691
具体功能实现
实现的效果
全局配置信息类config.dart
例如可以在里面存放域名
class Config{
static String domain="https://jdmall.itying.com/";
}
分类页面的实现
整体页面左边通过ListView、右边通过GridView实现,然后通过点击左边列表实现右边列表的数据刷新。
定义数据模型
class CateModel {
List<CateItemModel> result = [];
CateModel({required this.result});
CateModel.fromJson(Map<String, dynamic> json) {
if (json['result'] != null) {
json['result'].forEach((v) {
result.add(new CateItemModel.fromJson(v));
});
}
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
if (this.result.length > 0) {
data['result'] = this.result.map((v) => v.toJson()).toList();
}
return data;
}
}
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<String, dynamic> json) {
sId = json['_id'];
title = json['title'];
status = json['status'];
pic = json['pic'];
pid = json['pid'];
sort = json['sort'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['_id'] = this.sId;
data['title'] = this.title;
data['status'] = this.status;
data['pic'] = this.pic;
data['pid'] = this.pid;
data['sort'] = this.sort;
return data;
}
}
实现代码
class CategoryPage extends StatefulWidget {
CategoryPage({Key? key}) : super(key: key);
_CategoryPageState createState() => _CategoryPageState();
}
class _CategoryPageState extends State<CategoryPage> with AutomaticKeepAliveClientMixin{
//当前选中
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: <Widget>[
InkWell(
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: <Widget>[
_leftCateWidget(leftWidth),
_rightCateWidget(rightItemWidth,rightItemHeight)
],
),
);
}
}
实现效果
商品列表页面
创建商品列表Model
class ProductModel {
List<ProductItemModel> result=[];
ProductModel({required this.result});
ProductModel.fromJson(Map<String, dynamic> json) {
if (json['result'] != null) {
json['result'].forEach((v) {
result.add(new ProductItemModel.fromJson(v));
});
}
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
if (this.result != null) {
data['result'] = this.result.map((v) => v.toJson()).toList();
}
return data;
}
}
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<String, dynamic> json) {
sId = json['_id'];
title = json['title'];
cid = json['cid'];
price = json['price'];
oldPrice = json['old_price'];
pic = json['pic'];
sPic = json['s_pic'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['_id'] = this.sId;
data['title'] = this.title;
data['cid'] = this.cid;
data['price'] = this.price;
data['old_price'] = this.oldPrice;
data['pic'] = this.pic;
data['s_pic'] = this.sPic;
return data;
}
}
实现代码
class ProductListPage extends StatefulWidget {
Map arguments;
ProductListPage({Key? key, required this.arguments}) : super(key: key);
@override
_ProductListPageState createState() => _ProductListPageState();
}
class _ProductListPageState extends State<ProductListPage> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
//当前页码
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('没有搜索到商品'),
),
);
}
}
实现效果
转载自:https://juejin.cn/post/7044716539550892068