Flutter:仿京东项目实战(3)-商品详情页功能实现
在我个人认为学习一门新的语言(快速高效学习) 一定是通过实践,最好的就是做项目,这里我会简单写一个京东的Demo。
Dart2.15版本发布了:mp.weixin.qq.com/s/g-1uCl3up…
前面两篇文章分别完成了首页和分类及商品列表页面功能,这篇文章完成商品详情页的功能,这里用到了以下知识点:
用到的知识点
1. Provider 状态管理
什么是Provider 状态管理?
当我们想在多个页面(组件/Widget)之间共享状态(数据),或者一个页面(组 件/Widget)中的多个子组件之间共享状态(数据),这个时候我们就可以用 Flutter 中的状态管理来管理统一的状态(数据),实现不同组件直接的传值和数据共享。provider
是 Flutter 官方团队
推出的状态管理模式。
具体的使用:
- 配置
provider: ^6.0.1
, - 新建一个文件夹叫 provider,在 provider 文件夹里面放我们对于的状态管理类
- 在 provider 里面新建 counter.dart
- counter.dart 里面新建一个类继承
minxins
的ChangeNotifier
代码如下
import 'package:provider/provider.dart';
class Counter with ChangeNotifier {
int _count;
Counter(this._count);
void add() {
_count++;
notifyListeners();//2
}
get count => _count;//3
}
notifyListeners();
这个方法是通知用到Counter
对象的widget刷新用的
- 找到 main.dart 修改代码,添加
MultiProvider
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return ScreenUtilInit(
//配置设计稿的宽度高度
designSize: Size(750, 1334),
builder:()=> MultiProvider(
providers:[
ChangeNotifierProvider(create: (_) => Counter()),
],
child: MaterialApp(
localizationsDelegates: [
GlobalMaterialLocalizations.delegate, // 指定本地化的字符串和一些其他的值
GlobalCupertinoLocalizations.delegate, // 对应的Cupertino风格
GlobalWidgetsLocalizations.delegate //指定默认的文本排列方向, 由左到右或由右到左
],
supportedLocales: [
Locale("en"),
Locale("zh")
],
initialRoute: '/',
onGenerateRoute: onGenerateRoute)));
}
}
- 获取值、以及设置值
import 'package:provider/provider.dart';
import '../../provider/Counter.dart';
Widget build(BuildContext context) {
final counter = Provider.of<Counter>(context);
return Scaffold(
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: (){
counter.add();
},
),
body: Text("counter 的值:${counter.count}")
);
}
用Provider.of(context).count获取_count的值,Provider.of(context)相当于Provider去查找它管理的Counter(1);
用Provider.of(context).add();调用Counter()中的add()方法;
2. eventBus 广播
-
配置
event_bus: ^2.0.0
-
新建 event_bus.dart 类统一管理
//引入 eventBus 包文件
import 'package:event_bus/event_bus.dart';
//创建EventBus
EventBus eventBus = new EventBus();
//event 监听
class EventFn{
//想要接收的数据时什么类型的,就定义相同类型的变量
dynamic obj;
EventFn(this.obj);
}
- 在需要广播事件的页面引入上面的 EventBus.dart 类 然后配置如下代码
eventBus.fire(new EventFn('数据'));
- 在需要监听广播的地方引入上面的
event_bus.dart
类 然后配置如下代码
void initState() {
super.initState();
//监听广播
eventBus.on<EventFn>().listen((event){
print(event);
});
}
- event_bus 取消事件监听
@override
void dispose() {
super.dispose();
//取消订阅
eventBusFn.cancel();
}
3. flutter_inappwebview 加载网页
- 配置
flutter_inappwebview: ^5.3.2
- 引入包文件
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
- 初始化属性
initialUrl: 被加载的初始URL。
initialOptions:将被加载的初始URL; initialOptions:将被使用的初始WebView选项。将要使用的初始WebView选项。
gestureRecognizers:指定哪些手势应该被WebView消耗。
initialData:初始InAppWebViewInitial数据。将要加载的InAppWebViewInitialData的初始数据,比如一个HTML字符串。
initialFile:将被加载的初始资产文件。
initialHeaders: 将要使用的初始头文件。将要使用的初始头文件。
contextMenu:上下文菜单,包含自定义菜单项。上下文菜单,包含自定义菜单项。
- 常用触发的事件
onLoadStart:当WebView开始加载一个URL时被触发的事件。
onLoadStop:当WebView完成加载一个URL时触发的事件。
onLoadHttpError:当WebView主页面收到一个HTTP错误时被触发的事件。
onConsoleMessage:当WebView收到JavaScript控制台消息(如console.log ,console.error等)时触发的事件。
shouldOverrideUrlLoading:当当前WebView中的URL即将被加载时,给主机应用程序一个控制的机会。
onDownloadStart:当WebView识别到一个可下载的文件时发射的事件。
onReceivedHttpAuthRequest:当WebView接收到HTTP认证请求时触发的事件。默认行为是取消该请求。
onReceivedServerTrustAuthRequest:当WebView需要执行服务器信任认证(证书验证)时被触发的事件。
onPrint:当window.print()从JavaScript端被调用时被触发的事件,默认行为是取消请求;onCreateWindow:当WebView需要进行服务器信任验证(证书验证)时被触发的事件。
onCreateWindow: 当InAppWebView请求主机应用程序创建一个新窗口时,例如当试图打开一个target="_blank"的链接或当window.open()被JavaScript端调用时,事件被触发。
- 简单使用
Expanded(
child: InAppWebView(
initialUrlRequest: URLRequest(url: Uri.parse("https://jdmall.itying.com/pcontent?id=${_id}")),
onProgressChanged: (InAppWebViewController controller, int progress){
if (progress / 100 > 0.9999) {
setState(() {
this._flag = false;
});
}
},
)
)
4. DefaultTabController和TabController
这两个都可以实现顶部导航选项卡,区别就是TabController一般放在有状态组件中使用,而DefaultTabController一般放在无状态组件中使用,这里没有做成上下拉刷新,在这个页面用的是DefaultTabController。
TabController介绍
- 常见的属性
- 常用方法介绍
TabBar属性介绍
const TabBar({
Key key,
@required this.tabs,//必须实现的,设置需要展示的tabs,最少需要两个
this.controller,
this.isScrollable = false,//是否需要滚动,true为需要
this.indicatorColor,//选中下划线的颜色
this.indicatorWeight = 2.0,//选中下划线的高度,值越大高度越高,默认为2
this.indicatorPadding = EdgeInsets.zero,
this.indicator,//用于设定选中状态下的展示样式
this.indicatorSize,//选中下划线的长度,label时跟文字内容长度一样,tab时跟一个Tab的长度一样
this.labelColor,//设置选中时的字体颜色,tabs里面的字体样式优先级最高
this.labelStyle,//设置选中时的字体样式,tabs里面的字体样式优先级最高
this.labelPadding,
this.unselectedLabelColor,//设置未选中时的字体颜色,tabs里面的字体样式优先级最高
this.unselectedLabelStyle,//设置未选中时的字体样式,tabs里面的字体样式优先级最高
this.dragStartBehavior = DragStartBehavior.start,
this.onTap,//点击事件
})
DefaultTabController的使用
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: ScreenAdapter.width(400),
child: TabBar(
indicatorColor: Colors.red,
indicatorSize: TabBarIndicatorSize.label,
labelColor: Colors.red,
unselectedLabelColor: Colors.white,
tabs: [
Tab(
child: Text('商品', style: TextStyle(fontSize: 18,),),
),
Tab(
child: Text('详情', style: TextStyle(fontSize: 18),),),
Tab(
child: Text('评价', style: TextStyle(fontSize: 18),),
)
],
),
)
],
),
),
body:Stack(
children: [
TabBarView(children:
[
ProductContentFirst(_productContentList),
ProductContentSecond(_productContentList),
ProductContentThrid(),
]),
],
),
))
showModalBottomSheet 底部面板
ModalBottomSheet
底部面板,相当于弹出了一个新页面,有点类似于 ActionSheet
ModalBottomSheet的属性:
- context:BuildContext
- builder:WidgetBuilder
- backgroundColor:背景色
- elevation:阴影
- shape:形状
- barrierColor:遮盖背景颜色
- isDismissible:点击遮盖背景是否可消失
- enableDrag:下滑消失
BoxDecoration 的使用说明
BoxDecoration通常用于给Widget组件设置边框效果、阴影效果、渐变色等效果;常用属性如下:
实现效果
具体实现代码
创建product_content_model.dart
类
class ProductContentModel {
late ProductContentitem result;
ProductContentModel({
required this.result,
});
ProductContentModel.fromJson(Map<String, dynamic> json) {
result = ProductContentitem.fromJson(json['result']);
}
Map<String, dynamic> toJson() {
final _data = <String, dynamic>{};
_data['result'] = result.toJson();
return _data;
}
}
class ProductContentitem {
//可为空的字段就设置成可为空空
String? sId;
String? title;
String? cid;
Object? price;
Object? oldPrice;
Object? isBest;
Object? isHot;
Object? isNew;
late List<Attr> attr; //不可为空
Object? status;
late String pic; //不可为空
String? content;
String? cname;
int? salecount;
String? subTitle;
int count=1;
ProductContentitem({
this.sId,
this.title,
this.cid,
this.price,
this.oldPrice,
this.isBest,
this.isHot,
this.isNew,
required this.attr,
this.status,
required this.pic,
this.content,
this.cname,
this.salecount,
this.subTitle,
});
ProductContentitem.fromJson(Map<String, dynamic> json) {
sId = json['_id'];
title = json['title'];
cid = json['cid'];
price = json['price'];
oldPrice = json['old_price'];
isBest = json['is_best'];
isHot = json['is_hot'];
isNew = json['is_new'];
attr =
List<dynamic>.from(json['attr']).map((e) => Attr.fromJson(e)).toList();
status = json['status'];
pic = json['pic'];
content = json['content'];
cname = json['cname'];
salecount = json['salecount'];
subTitle = json['sub_title'];
}
Map<String, dynamic> toJson() {
final _data = <String, dynamic>{};
_data['_id'] = sId;
_data['title'] = title;
_data['cid'] = cid;
_data['price'] = price;
_data['old_price'] = oldPrice;
_data['is_best'] = isBest;
_data['is_hot'] = isHot;
_data['is_new'] = isNew;
_data['attr'] = attr.map((e) => e.toJson()).toList();
_data['status'] = status;
_data['pic'] = pic;
_data['content'] = content;
_data['cname'] = cname;
_data['salecount'] = salecount;
_data['sub_title'] = subTitle;
return _data;
}
}
class Attr {
late String cate;
late List<String> list;
Attr({
required this.cate,
required this.list,
});
Attr.fromJson(Map<String, dynamic> json) {
cate = json['cate'];
list = List<String>.from(json['list']);
}
Map<String, dynamic> toJson() {
final _data = <String, dynamic>{};
_data['cate'] = cate;
_data['list'] = list;
return _data;
}
}
商品详情页的框架页面
class ProductContentPage extends StatefulWidget {
final Map arguments;
ProductContentPage({Key? key, required this.arguments}) : super(key: key);
@override
_ProductContentPageState createState() => _ProductContentPageState();
}
class _ProductContentPageState extends State<ProductContentPage> {
List _productContentList=[];
@override
void initState() {
// TODO: implement initState
super.initState();
_getContentData();
}
//请求商品数据
_getContentData() async{
var api ='${Config.domain}api/pcontent?id=${widget.arguments['id']}';
print(api);
var result = await Dio().get(api);
var productContent = new ProductContentModel.fromJson(result.data);
setState(() {
_productContentList.add(productContent.result);
});
}
@override
Widget build(BuildContext context) {
//实现顶部导航选项卡
return DefaultTabController(length: 3, child: Scaffold(
appBar: AppBar(
title: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: ScreenAdapter.width(400),
child: TabBar(
indicatorColor: Colors.red,
indicatorSize: TabBarIndicatorSize.label,
labelColor: Colors.red,
unselectedLabelColor: Colors.white,
tabs: [
Tab(
child: Text('商品', style: TextStyle(fontSize: 18,),),
),
Tab(
child: Text('详情', style: TextStyle(fontSize: 18),),
),
Tab(
child: Text('评价', style: TextStyle(fontSize: 18),),
)
],
),
)
],
),
actions: [
IconButton(onPressed: (){
//实现菜单选项栏
showMenu(context: context, position: RelativeRect.fromLTRB(ScreenAdapter.width(600), ScreenUtil().statusBarHeight+40, 10, 0), items:
[
PopupMenuItem(
child: Row(
children: [
Icon(Icons.home),
SizedBox(width: 10,),
Text('首页')
],
),
),
PopupMenuItem(
child: Row(
children: [
Icon(Icons.search),
SizedBox(width: 10,),
Text('搜索')
],
),
)
]
);
}, icon: Icon(Icons.more_horiz)),
],
),
body: _productContentList.length > 0 ?
Stack(
children: [
TabBarView(children:
[
//商品页面
ProductContentFirst(_productContentList),
//详情页面
ProductContentSecond(_productContentList),
//评价页面
ProductContentThrid(),
]
),
//页面底部的购物车、加入购物车、立即购买
Positioned(
width: ScreenAdapter.width(750),
height: ScreenAdapter.width(100)+ScreenAdapter.bottomBarHeight+10,
bottom: 0,
child: Container(
decoration: BoxDecoration(
border: Border(
top: BorderSide(
width: 1,
color: Colors.black26
)
),
color: Colors.white
),
child: Container(
margin: EdgeInsets.only(top: 10, bottom: ScreenAdapter.bottomBarHeight),
child: Row(
children: [
Container(
width: 100,
height: ScreenAdapter.height(100),
child: Column(
children: [
Icon(Icons.shopping_cart, size: ScreenAdapter.width(38),),
Text('购物车', style: TextStyle(fontSize:ScreenAdapter.size(24))),
],
),
),
Expanded(
flex: 1,
child: CircleButton(
color: Color.fromRGBO(253, 1, 0, 0.9),
text: '加入购物车',
callBack: (){
print('加入购物车');
},
)
),
Expanded(
flex: 1,
child: CircleButton(
color: Color.fromRGBO(255, 165, 0, 0.9),
text: '立即购买',
callBack: (){
print('立即购买');
},
)
)
],
),
),
),
),
],
) : LoadingWidget(),
));
}
}
商品详情页的商品页面
class ProductContentFirst extends StatefulWidget {
final List _productContentList;
ProductContentFirst(this._productContentList, {Key? key}) : super(key: key);
@override
_ProductContentFirstState createState() => _ProductContentFirstState();
}
class _ProductContentFirstState extends State<ProductContentFirst> {
late ProductContentitem _productContent;
List _attr = [];
String _selectedValue='';
var cartProvider;
@override
void initState() {
// TODO: implement setState
super.initState();
_productContent = widget._productContentList[0];
_attr = _productContent.attr;
_selectedValue = _attr.first.list.first;
}
//实现选项卡功能
_attrBottomSheet(){
showModalBottomSheet(
context: context,
builder: (context){
return Stack(
children: [
Container(
padding: EdgeInsets.only(left: 10),
child: ListView(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: _getAttrWidget(),
),
Divider(),
Container(
margin: EdgeInsets.only(top: 10),
height: ScreenAdapter.height(80),
child: Row(
children: <Widget>[
Text("数量: ",
style: TextStyle(
fontWeight: FontWeight.bold)),
SizedBox(width: 10),
CartNum(this._productContent)
],
),
)
],
),
),
Positioned(
bottom: 0,
width: ScreenAdapter.width(750),
height: ScreenAdapter.height(76)+ScreenAdapter.bottomBarHeight,
child: Container(
color: Colors.white,
padding: EdgeInsets.only(bottom: ScreenAdapter.bottomBarHeight),
child: Row(
children: <Widget>[
Expanded(
flex: 1,
child: Container(
margin: EdgeInsets.fromLTRB(10, 0, 0, 0),
child: CircleButton(
color: Color.fromRGBO(253, 1, 0, 0.9),
text: "加入购物车",
callBack: () async {
print('豪杰是八点就把手');
await CartServices.addCart(this._productContent);
//关闭底部筛选属性
Navigator.of(context).pop();
//调用Provider 更新数据
this.cartProvider.updateCartList();
Fluttertoast.showToast( msg: '加入购物车成功', toastLength: Toast.LENGTH_SHORT,gravity: ToastGravity.CENTER,);
},
),
),
),
Expanded(
flex: 1,
child: Container(
margin: EdgeInsets.fromLTRB(10, 0, 10, 0),
child: CircleButton(
color: Color.fromRGBO(255, 165, 0, 0.9),
text: "立即购买",
callBack: () {
print('立即购买');
},
)),
)
],
),
),
)
],
);
}
);
}
List<Widget> _getAttrWidget(){
List<Widget> attrList = [];
_attr.forEach((attrItem) {
attrList.add(
Wrap(
children: [
Container(
width: ScreenAdapter.width(100),
child: Padding(
padding: EdgeInsets.only(top: ScreenAdapter.height(28)),
child: Text('${attrItem.cate}:', style: TextStyle(fontWeight: FontWeight.bold),textAlign: TextAlign.left),
),
),
Container(
width: ScreenAdapter.width(580),
child: Wrap(
children: _getAttrItemWidget(attrItem),
),
)
],
)
);
});
return attrList;
}
List<Widget> _getAttrItemWidget(attrItem) {
List<Widget> attrItemList = [];
attrItem.list.forEach((item) {
attrItemList.add(Container(
margin: EdgeInsets.all(10),
child: Chip(
label: Text("${item}"),
padding: EdgeInsets.all(10),
),
));
});
return attrItemList;
}
@override
Widget build(BuildContext context) {
this.cartProvider = Provider.of<Cart>(context);
//处理图片
String pic = Config.domain + this._productContent.pic;
pic = pic.replaceAll('\', '/');
//商品页面内容
return Container(
padding: EdgeInsets.all(10),
child: ListView(
children: [
AspectRatio(
aspectRatio: 16/9,
child: Image.network(pic, fit: BoxFit.cover,),
),
Container(
padding: EdgeInsets.only(top: 10),
child: Text(_productContent.title!,
style: TextStyle(color: Colors.black87, fontSize: ScreenAdapter.size(36), fontWeight: FontWeight.bold),),
),
Container(
padding: EdgeInsets.only(top: 10),
child: Text(
_productContent.subTitle!,
style: TextStyle(
color: Colors.black54,
fontSize: ScreenAdapter.size(28))
)
),
SizedBox(height: 10,),
Container(
child: Row(
children: [
Expanded(
child: Row(
children: [
Text('特价:'),
Text('¥${_productContent.price}',style: TextStyle(
color: Colors.red,
fontSize: ScreenAdapter.size(46))),
],
)
),
Expanded(
flex: 1,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text('原价:'),
Text('¥${_productContent.oldPrice}',style: TextStyle(
color: Colors.black38,
fontSize: ScreenAdapter.size(28),
decoration: TextDecoration.lineThrough)),
],
),
)
],
),
),
//筛选
_attr.length > 0
? Container(
margin: EdgeInsets.only(top: 10),
height: ScreenAdapter.height(80),
child: InkWell(
onTap: () {
_attrBottomSheet();
},
child: Row(
children: <Widget>[
Text("已选: ",
style: TextStyle(fontWeight: FontWeight.bold)),
Text("${_selectedValue}")
],
),
),
)
: Text(""),
Divider(),
Container(
height: ScreenAdapter.height(80),
child: Row(
children: <Widget>[
Text("运费: ", style: TextStyle(fontWeight: FontWeight.bold)),
Text("免运费")
],
),
),
Divider(),
],
),
);
}
}
这个就是代码中说的选项卡,也是通过接口返回数据生成的
商品详情页面
class ProductContentSecond extends StatefulWidget {
final List _productContentList;
const ProductContentSecond(this._productContentList, {Key? key}) : super(key: key);
@override
_ProductContentSecondState createState() => _ProductContentSecondState();
}
class _ProductContentSecondState extends State<ProductContentSecond> {
var _flag=true;
var _id;
@override
void initState() {
// TODO: implement initState
super.initState();
_id = widget._productContentList[0].sId;
}
@override
Widget build(BuildContext context) {
return Container(
child: Column(
children: [
_flag ? LoadingWidget() : Text(''),
Expanded(
child: InAppWebView(
initialUrlRequest: URLRequest(url: Uri.parse("https://jdmall.itying.com/pcontent?id=${_id}")),
onProgressChanged: (InAppWebViewController controller, int progress){
if (progress / 100 > 0.9999) {
setState(() {
this._flag = false;
});
}
},
)
)
],
),
);
}
}
后面我会把整个项目的代码放到github.
转载自:https://juejin.cn/post/7046186331940061191