Flutter:仿京东项目实战(3)-商品详情页功能实现

·  阅读 3991
Flutter:仿京东项目实战(3)-商品详情页功能实现

在我个人认为学习一门新的语言(快速高效学习) 一定是通过实践,最好的就是做项目,这里我会简单写一个京东的Demo。

第一天 搭建项目框架,实现首页的功能juejin.cn/editor/draf…

第二天实现 分类和商品列表页面juejin.cn/post/704471…

Flutter-混合工程的持续集成实践juejin.cn/post/704209…

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;
          });
        }
      },
    )
)
复制代码

更多更详细用法的可以参考这篇文章:juejin.cn/post/686929…

4. DefaultTabController和TabController

这两个都可以实现顶部导航选项卡,区别就是TabController一般放在有状态组件中使用,而DefaultTabController一般放在无状态组件中使用,这里没有做成上下拉刷新,在这个页面用的是DefaultTabController。

TabController介绍

  • 常见的属性

截屏2021-12-26 下午4.31.49.png

  • 常用方法介绍

截屏2021-12-26 下午4.32.08.png

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组件设置边框效果、阴影效果、渐变色等效果;常用属性如下:

截屏2021-12-27 上午9.04.58.png

实现效果

Simulator Screen Shot - iPhone 12 Pro - 2021-12-26 at 16.48.57.png

Simulator Screen Shot - iPhone 12 Pro - 2021-12-26 at 16.49.07.png

具体实现代码

创建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(),
        ],
      ),
    );
  }
}
复制代码

这个就是代码中说的选项卡,也是通过接口返回数据生成的

Simulator Screen Shot - iPhone 12 Pro - 2021-12-26 at 16.49.01.png

商品详情页面

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.

分类:
iOS
标签:
分类:
iOS
标签:
收藏成功!
已添加到「」, 点击更改