在我个人以为学习一门新的语言(快速高效学习) 一定是经过实践,最好的便是做项目,这儿我会简略写一个京东的Demo。

第一天 搭建项目框架,完成主页的功能:/editor/draf…

第二天完成 分类和产品列表页面: /post/704471…

Flutter-混合工程的持续集成实践: /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里边新建一个类承继minxinsChangeNotifier代码如下
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.logconsole.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;
          });
        }
      },
    )
)

更多更详细用法的能够参阅这篇文章:/post/686929…

4. DefaultTabController和TabController

这两个都能够完成顶部导航选项卡,差异便是TabController一般放在有状况组件中运用,而DefaultTabController一般放在无状况组件中运用,这儿没有做成上下拉刷新,在这个页面用的是DefaultTabController。

TabController介绍

  • 常见的特点

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

  • 常用方法介绍

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

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组件设置边框作用、暗影作用、渐变色等作用;常用特点如下:

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

完成作用

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

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

详细完成代码

创立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(),
        ],
      ),
    );
  }
}

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

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

产品概况页面

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.