zoukankan      html  css  js  c++  java
  • Flutter 商城实例 分类列表

    列表页_使用Provide控制子类

    使用flutter_provide,初步实现点击列表页的大类,改变小类的效果。

    继续上次的代码接着写:https://www.cnblogs.com/joe235/p/11633989.html

    新写了一个右侧小类,暂时用假数据代替,代码如下:

    //右侧小类类别
    class RightCategoryNav extends StatefulWidget {
      _RightCategoryNavState createState() => _RightCategoryNavState();
    }
    
    class _RightCategoryNavState extends State<RightCategoryNav> {
    
      List list = ['名酒','宝丰','北京二锅头','舍得','五粮液','茅台','散白'];
      
      @override
      Widget build(BuildContext context) {
        
         return Container(
                height: ScreenUtil().setHeight(80),
                 ScreenUtil().setWidth(570),
                decoration: BoxDecoration(
                  color: Colors.white,
                  border: Border(
                    bottom: BorderSide( 1,color: Colors.black12)
                  )
                ),
                child:ListView.builder(
                  scrollDirection: Axis.horizontal,
                  itemCount: list.length,
                  itemBuilder: (context,index){
                    return _rightInkWell(list[index]);
                  },
                )
         );
      }
      //子类
      Widget _rightInkWell(String item){
        return InkWell(
          onTap: (){},
          child: Container(
            padding:EdgeInsets.fromLTRB(5.0,10.0,5.0,10.0),
            child: Text(
              item,
              style: TextStyle(fontSize:ScreenUtil().setSp(28)),
            ),
          ),
        );
      }    

    添加到界面中

    category_page.dartCategoryPage类的build方法里,加入右侧子类导航区域.

    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: Text('商品分类'),
        ),
        body: Container(
          child: Row(
            children: <Widget>[
              LeftCategoryNav(),
              Column(
                children: <Widget>[
                  RightCategoryNav()
                ],
              )
            ],
          ),
        ),
      );
    }

    我们就应该能看到效果,但是现在数据都是写死的,还没有实现状态的控制,下一步就是用provide来控制,实现二级分类和一级分类的交互效果。

    编写二级分类的Provide文件

    我们先设置一个子类的provide,在lib/provide/文件夹下,新建一个child_category.dart文件,这个文件就是控制子类的状态管理文件。代码如下:

    import 'package:flutter/material.dart';
    import '../model/category.dart';
    
    //ChangeNotifier的混入是不用管理听众
    class ChildCategory with ChangeNotifier{
    
        List<BxMallSubDto> childCategoryList = [];
    
        getChildCategory(List list){
          childCategoryList=list;
          notifyListeners();
        }
    }

    引入了category.dart的model文件,这样就可以很好的对象化,先声明了一个泛型的List变量childCategoryList。然后做了个方法,进行赋值。(注意这种形式也是在工作中最常用的一种形式。)

    main里进行引入

    import './provide/child_category.dart';
    
    void main() {
    
      var childCategory= ChildCategory();
      providers
      ..provide(Provider<Counter>.value(counter)) //依赖
      ..provide(Provider<ChildCategory>.value(childCategory));
     
      。。。
    }

    修改二级分类状态

    有了Provide类之后,就可以修改二级分类了,这时候先引入child_category.dart文件和provide.dart,再修改左侧大类的InkWell中的onTap方法。 

    import 'package:provide/provide.dart';
    import '../provide/child_category.dart';
    onTap: () {
        var childList = list[index].bxMallSubDto;
        Provide.value<ChildCategory>(context).getChildCategory(childList);
    },

    编写好后,其实状态已经改变了,那接下来就可以设置二级分类的修改状态了。

    二级分类展现

    修改右侧二级分类的展示,这个先改变子项的接受数据。把原来的item,改成item.mallSubName,修改后的代码如下:

    Widget _rightInkWell(BxMallSubDto item){
    
        return InkWell(
          onTap: (){},
          child: Container(
            padding:EdgeInsets.fromLTRB(5.0,10.0,5.0,10.0),
            
            child: Text(
              item.mallSubName,
              style: TextStyle(fontSize:ScreenUtil().setSp(28)),
            ),
          ),
        );
      }

    子项修改好后哦,再修改build里的Container,我们需要加入一个Provide组件,注意这里使用了泛型。

    @override
      Widget build(BuildContext context) {
        return Container(
           ScreenUtil().setWidth(570),
          height: ScreenUtil().setHeight(80),
          decoration: BoxDecoration(
            color: Colors.white,
            border: Border(
              bottom: BorderSide( 1,color: Colors.black12)
            )
          ),
          child: Provide<ChildCategory>(
            builder: (context,child,childCategory){
              return ListView.builder( //ListView要设置宽高,纵向的可以只设置高,横向的宽高都要设置
                scrollDirection: Axis.horizontal, //横向
                itemCount: childCategory.childCategoryList.length,
                itemBuilder: (content,index){
                  return _rightInkWell(childCategory.childCategoryList[index]);
                },
              );
            },
          ),  
        );
      }

    修改步骤:

    1. Container Widget外层加入一个Provie widget
    2. 修改ListView WidgetitemCount选项为childCategory.childCategoryList.length
    3. 修改itemBuilder里的传值选项为return _rightInkWell(childCategory.childCategoryList[index]);

    交互效果的设置

    现在二级分类已经能跟随我们的点击发生变化了,但是大类还没有高亮显示,所以要作一下交互效果,这种交互效果跟其它类或者页面没什么关系,所以我们还是使用最简单的setState来实现了。 这个变化主要在_leftInkWell里,所以操作也基本在这个里边。

    1. 先声明一个变量,用于控制是否高亮显示bool isClick = false;
    2. _leftInkWell接收一个变量,变量是ListView传递过来的Widget _leftInkWel(int index)
    3. 声明一个全局的变量var listIndex = 0; //索引
    4. 对比index和listIndexisClick=(index==listIndex)?true:false;.
    5. 修改为动态显示背景颜色color: isClick?Colors.black26:Colors.white,

    在进入页面后的_getCategory里在等到大类数据后,把第一个小类的数据同时进行状态修改。

    代码如下:

    //得到后台大类数据
      void _getCategory() async{
        await request('post','getCategory').then((val){
          var data = json.decode(val.toString());
          //print(data);
          CategoryModel category = CategoryModel.fromJson(data);
          setState(() {
            list = category.data; 
          });
          Provide.value<ChildCategory>(context).getChildCategory( list[0].bxMallSubDto);
          //print(list[0].bxMallSubDto);
          //list.data.forEach((item)=>print(item.mallCategoryName));
        });
      }

    反白显示颜色过重,使用RGBO颜色

    这个直接使用Flutter里的RGBO模式就可以了,当然你也完全可以使用Colors.black12

    color: isClick ? Color.fromRGBO(236, 238, 239, 1.0) : Colors.white,

    添加子类“全部”按钮

    原型上在二级分类上是有“全部”字样的,但我们作的这里并没有。其实加上这个全部也非常简单,只要我们在状态管理,改变状态的方法getChildCategory里,现加入一个全部的BxMallSubDto对象就可以了。

    代码部分就是修改provide/child_Category.dartgetchildCategory方法。思路是声明一个all对象,然后进行赋值,复制后组成List赋给childCategoryList。然后把list添加到childCategoryList里。

    import 'package:flutter/material.dart';
    import '../model/category.dart';
    
    //ChangeNotifier的混入是不用管理听众
    class ChildCategory with ChangeNotifier{
    
        List<BxMallSubDto> childCategoryList = [];
    
        getChildCategory(List<BxMallSubDto> list){
          BxMallSubDto all=  BxMallSubDto();
          all.mallSubId='00';
          all.mallCategoryId='00';
          all.mallSubName = '全部';
          all.comments = 'null';
          childCategoryList=[all];
          childCategoryList.addAll(list);   
          notifyListeners();
        }
    }

    全部代码如下:

    import 'package:flutter/material.dart';
    import '../service/service_method.dart';
    import 'dart:convert';
    import 'package:flutter_screenutil/flutter_screenutil.dart';
    import '../model/category.dart';
    import 'package:provide/provide.dart';
    import '../provide/child_category.dart';
    
    class CategoryPage extends StatefulWidget {
      _CategoryPageState createState() => _CategoryPageState();
    }
    
    class _CategoryPageState extends State<CategoryPage> {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text('商品分类')),
          body: Container(
            child: Row(
              children: <Widget>[
                LeftCategoryNav(),
                Column(
                  children: <Widget>[
                    RightCategoryNav(),
    
                  ],
                )
              ]
            ) 
          )   
        );
      }
    
    }
    
    //左侧大类导航
    class LeftCategoryNav extends StatefulWidget {
      _LeftCategoryNavState createState() => _LeftCategoryNavState();
    }
    
    class _LeftCategoryNavState extends State<LeftCategoryNav> {
      List list = [];
      var listIndex = 0; //索引
    
      @override
      void initState() { 
        _getCategory(); //请求接口的数据
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        
        return Container(
           ScreenUtil().setWidth(180),
          decoration: BoxDecoration(
            border: Border(
              right: BorderSide( 1,color: Colors.black12)
            )
          ),
          child: ListView.builder(
            itemCount: list.length,
            itemBuilder:(context,index){
              return _leftInkWell(index);
            }
          ),   
        );
      }
    
      Widget _leftInkWell(int index){
        bool isClick = false; //是否点击了 
        isClick = (index==listIndex) ? true : false; //判断当前点击的索引是否为全局变量的索引
    
        return InkWell(
          onTap: (){
             setState(() {
               listIndex = index; //每次点击了大类就把索引赋值给listIndex
             });
            var childList = list[index].bxMallSubDto;
            Provide.value<ChildCategory>(context).getChildCategory(childList);
          },
          child: Container(
            height: ScreenUtil().setHeight(100),
            padding: EdgeInsets.only(left: 10,top: 13),
            decoration: BoxDecoration(
              color: isClick ? Color.fromRGBO(236,236,236,1.0) : Colors.white,
              border: Border(
                bottom: BorderSide( 1,color: Colors.black12)
              )
            ),
            child: Text(list[index].mallCategoryName,style: TextStyle(fontSize: ScreenUtil().setSp(28))),
          ),
        );
      }
    
      //得到后台大类数据
      void _getCategory()async{
        await request('post','getCategory').then((val){
          var data = json.decode(val.toString());
          //print(data);
          CategoryModel category = CategoryModel.fromJson(data);
          setState(() {
            list = category.data; 
          });
          Provide.value<ChildCategory>(context).getChildCategory( list[0].bxMallSubDto);
          print(list[0].bxMallSubDto);
          list[0].bxMallSubDto.forEach((item) => print(item.mallSubName));
          //list.data.forEach((item)=>print(item.mallCategoryName));
        });
      }
    
    }
    
    //右侧二级分类
    class RightCategoryNav extends StatefulWidget {
      _RightCategoryNavState createState() => _RightCategoryNavState();
    }
    
    class _RightCategoryNavState extends State<RightCategoryNav> {
      //List list = ['名酒','宝丰','北京二锅头','舍得','五粮液','茅台','散白'];
    
      @override
      Widget build(BuildContext context) {
        return Container(
           ScreenUtil().setWidth(570),
          height: ScreenUtil().setHeight(80),
          decoration: BoxDecoration(
            color: Colors.white,
            border: Border(
              bottom: BorderSide( 1,color: Colors.black12)
            )
          ),
          child: Provide<ChildCategory>(
            builder: (context,child,childCategory){
              return ListView.builder( //ListView要设置宽高,纵向的可以只设置高,横向的宽高都要设置
                scrollDirection: Axis.horizontal, //横向
                itemCount: childCategory.childCategoryList.length,
                itemBuilder: (content,index){
                  return _rightInkWell(childCategory.childCategoryList[index]);
                },
              );
            },
          ),  
        );
      }
    
      //子类
      Widget _rightInkWell(BxMallSubDto item){
        return InkWell(
          onTap: (){},
          child: Container(
            padding: EdgeInsets.fromLTRB(5, 10, 5, 10),
            child: Text(
              item.mallSubName,
              style:TextStyle(fontSize:ScreenUtil().setSp(28.0))
            ),
          ),
        );  
      }
    
    }

     现在点击左侧大类,右侧顶部的二级分类会随之变化。

    列表页_商品列表接口调试

    下面先调通商品分类页里的商品列表接口,这个接口包括上拉加载、大类切换和小类切换的互动。

    配置URL路径

    对于后台接口的调试,应该有所了解了,第一步就是配置后台接口的路径到统一的配置文件中,这样方便以后的维护。

    打开libconfigservice_ulr.dart文件,再最下面加上商品分类的商品列表接口路径,现在的配置文件,代码如下:

    const servicePath={
      'homePageContext': serviceUrl+'wxmini/homePageContent', // 商家首页信息
      'homePageBelowConten': serviceUrl+'wxmini/homePageBelowConten', //商城首页热卖商品拉取
      'getCategory': serviceUrl+'wxmini/getCategory', //商品类别信息
      'getMallGoods': serviceUrl+'wxmini/getMallGoods', //商品分类的商品列表
    };

    测试大类商品列表接口

    libpagescategory_page.dart文件里,新建一个CategoryGoodsList类,这个类我们也将用状态管理进行管理,所以这个类并没有什么其它的耦合,不接收任何参数。

    //右侧商品列表,可以上拉加载
    class CategoryGoodsList extends StatefulWidget {
      @override
      _CategoryGoodsListState createState() => _CategoryGoodsListState();
    }
    
    class _CategoryGoodsListState extends State<CategoryGoodsList> {
      @override
      Widget build(BuildContext context) {
        return Container(
          child: Text('商品列表'),
        );
      }
    }

    有了类以后,我们写一个内部获取后台数据的方法_getGoodList。先声明了一个变量data,用于放入传递的值。然后再把参数传递过去。具体代码如下:

    //调试接口
      void _getGoodList() async{
        var data = {
          'categoryId': '4',
          'categorySubId': '',
          'page': 1,
        };
        await request('post', 'getMallGoods', formData: data).then((val){
          var data = json.decode(val.toString());
          print('分类列表===========》${data}');
        });
      }

    然后我们在initState中调用一下:

    @override
      void initState() { 
        _getGoodList();
        super.initState();   
      }

    然后把CategoryGoodsList加入到页面布局。

    @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text('商品分类')),
          body: Container(
            child: Row(
              children: <Widget>[
                LeftCategoryNav(),
                Column(
                  children: <Widget>[
                    RightCategoryNav(),
                    CategoryGoodsList(),
                  ],
                )
              ]
            ) 
          )   
        );
      }

    如果一切正常应该可以在终端中看到输出的结果,有正常的列表结果输出,说明一切正常。

    列表页_商品列表数据模型的建立

    先用快速的方法,生成我们商品分类的商品列表数据模型,然后根据数据模型修改一下,读取后台的方法。

    商品列表页数据模型

    这里还是使用快速生成的方法,利用https://javiercbk.github.io/json_to_dart/,直接生成。

    给出一段JSON数据,当然你页可以自己抓取,这非常的容易。

    {"code":"0","message":"success","data":[{"image":"http://images.baixingliangfan.cn/compressedPic/20190116145309_40.jpg","oriPrice":2.50,"presentPrice":1.80,"goodsName":"哈尔滨冰爽啤酒330ml","goodsId":"3194330cf25f43c3934dbb8c2a964ade"},{"image":"http://images.baixingliangfan.cn/compressedPic/20190115185215_1051.jpg","oriPrice":2.00,"presentPrice":1.80,"goodsName":"燕京啤酒8°330ml","goodsId":"522a3511f4c545ab9547db074bb51579"},{"image":"http://images.baixingliangfan.cn/compressedPic/20190121102419_9362.jpg","oriPrice":1.98,"presentPrice":1.80,"goodsName":"崂山清爽8°330ml","goodsId":"bbdbd5028cc849c2998ff84fb55cb934"},{"image":"http://images.baixingliangfan.cn/compressedPic/20180712181330_9746.jpg","oriPrice":2.50,"presentPrice":1.90,"goodsName":"雪花啤酒8°清爽330ml","goodsId":"87013c4315e54927a97e51d0645ece76"},{"image":"http://images.baixingliangfan.cn/compressedPic/20180712180233_4501.jpg","oriPrice":2.50,"presentPrice":2.20,"goodsName":"崂山啤酒8°330ml","goodsId":"86388a0ee7bd4a9dbe79f4a38c8acc89"},{"image":"http://images.baixingliangfan.cn/compressedPic/20190116164250_1839.jpg","oriPrice":2.50,"presentPrice":2.30,"goodsName":"哈尔滨小麦王10°330ml","goodsId":"d31a5a337d43433385b17fe83ce2676a"},{"image":"http://images.baixingliangfan.cn/compressedPic/20180712181139_2653.jpg","oriPrice":2.70,"presentPrice":2.50,"goodsName":"三得利清爽啤酒10°330ml","goodsId":"74a1fb6adc1f458bb6e0788c4859bf54"},{"image":"http://images.baixingliangfan.cn/compressedPic/20190121162731_3928.jpg","oriPrice":2.75,"presentPrice":2.50,"goodsName":"三得利啤酒7.5度超纯啤酒330ml","goodsId":"d52fa8ba9a5f40e6955be9e28a764f34"},{"image":"http://images.baixingliangfan.cn/compressedPic/20180712180452_721.jpg","oriPrice":4.50,"presentPrice":3.70,"goodsName":"青岛啤酒11°330ml","goodsId":"a42c0585015540efa7e9642ec1183940"},{"image":"http://images.baixingliangfan.cn/compressedPic/20190121170407_7423.jpg","oriPrice":4.40,"presentPrice":4.00,"goodsName":"三得利清爽啤酒500ml 10.0°","goodsId":"94ec3df73f4446b5a5f0d80a8e51eb9d"},{"image":"http://images.baixingliangfan.cn/compressedPic/20180712181427_6101.jpg","oriPrice":4.50,"presentPrice":4.00,"goodsName":"雪花勇闯天涯啤酒8°330ml","goodsId":"d80462faab814ac6a7124cec3b868cf7"},{"image":"http://images.baixingliangfan.cn/compressedPic/20180717151537_3425.jpg","oriPrice":4.90,"presentPrice":4.10,"goodsName":"百威啤酒听装9.7°330ml","goodsId":"91a849140de24546b0de9e23d85399a3"},{"image":"http://images.baixingliangfan.cn/compressedPic/20190121101926_2942.jpg","oriPrice":4.95,"presentPrice":4.50,"goodsName":"崂山啤酒8°500ml","goodsId":"3758bbd933b145f2a9c472bf76c4920c"},{"image":"http://images.baixingliangfan.cn/compressedPic/20180712175422_518.jpg","oriPrice":5.00,"presentPrice":4.50,"goodsName":"百威3.6%大瓶9.7°P460ml","goodsId":"dc32954b66814f40977be0255cfdacca"},{"image":"http://images.baixingliangfan.cn/compressedPic/20180717151454_4834.jpg","oriPrice":5.00,"presentPrice":4.50,"goodsName":"青岛啤酒大听装500ml","goodsId":"fc85510c3af7428dbf1cb0c1bcb43711"},{"image":"http://images.baixingliangfan.cn/compressedPic/20180712181007_4229.jpg","oriPrice":5.50,"presentPrice":5.00,"goodsName":"三得利金纯生啤酒580ml 9°","goodsId":"14bd89f066ca4949af5e4d5a1d2afaf8"},{"image":"http://images.baixingliangfan.cn/compressedPic/20190121100752_4292.jpg","oriPrice":6.60,"presentPrice":6.00,"goodsName":"哈尔滨啤酒冰纯白啤(小麦啤酒)500ml","goodsId":"89bccd56a8e9465692ccc469cd4b442e"},{"image":"http://images.baixingliangfan.cn/compressedPic/20180712175656_777.jpg","oriPrice":7.20,"presentPrice":6.60,"goodsName":"百威啤酒500ml","goodsId":"3a94dea560ef46008dad7409d592775d"},{"image":"http://images.baixingliangfan.cn/compressedPic/20180712180754_2838.jpg","oriPrice":7.78,"presentPrice":7.00,"goodsName":"青岛啤酒皮尔森10.5°330ml","goodsId":"97adb29137fb47689146a397e5351926"},{"image":"http://images.baixingliangfan.cn/compressedPic/20190116164149_2165.jpg","oriPrice":7.78,"presentPrice":7.00,"goodsName":"青岛全麦白啤11°500ml","goodsId":"f78826d3eb0546f6a2e58893d4a41b43"}]}

    得到快速生成的Model类,在model文件夹下,新建一个文件categoryGoodsList.dart,这时候我们需要修改一下代码,防止产生冲突。修改完成的代码如下:

    class CategoryGoodsListModel {
      String code;
      String message;
      List<CategoryListData> data;
    
      CategoryGoodsListModel({this.code, this.message, this.data});
    
      CategoryGoodsListModel.fromJson(Map<String, dynamic> json) {
        code = json['code'];
        message = json['message'];
        if (json['data'] != null) {
          data = new List<CategoryListData>();
          json['data'].forEach((v) {
            data.add(new CategoryListData.fromJson(v));
          });
        }
      }
    
      Map<String, dynamic> toJson() {
        final Map<String, dynamic> data = new Map<String, dynamic>();
        data['code'] = this.code;
        data['message'] = this.message;
        if (this.data != null) {
          data['data'] = this.data.map((v) => v.toJson()).toList();
        }
        return data;
      }
    }
    
    class CategoryListData {
      String image;
      double oriPrice;
      double presentPrice;
      String goodsName;
      String goodsId;
    
      CategoryListData(
          {this.image,
          this.oriPrice,
          this.presentPrice,
          this.goodsName,
          this.goodsId});
    
      CategoryListData.fromJson(Map<String, dynamic> json) {
        image = json['image'];
        oriPrice = json['oriPrice'];
        presentPrice = json['presentPrice'];
        goodsName = json['goodsName'];
        goodsId = json['goodsId'];
      }
    
      Map<String, dynamic> toJson() {
        final Map<String, dynamic> data = new Map<String, dynamic>();
        data['image'] = this.image;
        data['oriPrice'] = this.oriPrice;
        data['presentPrice'] = this.presentPrice;
        data['goodsName'] = this.goodsName;
        data['goodsId'] = this.goodsId;
        return data;
      }
    }

    然后在category_page.dart里引入:

    import '../model/categoryGoodsList.dart';

    修改_getGoodList方法

     主要是让从后台得到的数据,可以使用数据模型。

    void _getGoodList()async {
        var data={
          'categoryId':'4',
          'categorySubId':"",
          'page':1
        };
        await request('getMallGoods',formData:data ).then((val){
            var  data = json.decode(val.toString());
            CategoryGoodsListModel goodsList=  CategoryGoodsListModel.fromJson(data);
            setState(() {
             list= goodsList.data;
            });
            print('>>>>>>>>>>>>>>>>>>>:${list[0].goodsName}');
        });
      }

    写完后测试一下,如果可以在控制台输出,想要的结果,说明我们的Model类建立完成了。

    列表页_商品列表UI布局

    商品图片方法编写

    我们把这个列表拆分成三个内部方法,分别是商品图片、商品名称和商品价格。这样拆分可以减少耦合和维护难度。

    先来制作图片的内部方法,代码如下:

    //图片
     Widget _goodsImage(index){
        return  Container(
           ScreenUtil().setWidth(200),
          child: Image.network(list[index].image),
        );
    
      }

    商品名称方法编写

    这个我们直接返回一个Container,然后在里边子组件里放一个Text,需要对Text进行一些样式设置,防止越界。

    //商品名称
    Widget _goodsName(index){
        return Container( 
          padding: EdgeInsets.all(5.0),
           ScreenUtil().setWidth(370),
          child: Text(
            list[index].goodsName,
            maxLines: 2,
            overflow: TextOverflow.ellipsis,
            style: TextStyle(fontSize: ScreenUtil().setSp(28)),
            ),
          );
      }

    商品价格方法编写

    商品价格我们在Container里放置一个Row,这样就能实现同一排显示,具体可以查看代码。

    //商品价格
    Widget _goodsPrice(index){
        return  Container( 
          margin: EdgeInsets.only(top:20.0),
           ScreenUtil().setWidth(370),
          child:Row(
            children: <Widget>[
                Text(
                  '价格:¥${list[index].presentPrice}',
                  style: TextStyle(color:Colors.pink,fontSize:ScreenUtil().setSp(30)),
                  ),
                Text(
                  '¥${list[index].oriPrice}',
                  style: TextStyle(
                    color: Colors.black26,
                    decoration: TextDecoration.lineThrough
                  ),
                )
            ]
          )
        );
      }

    把方法进行组合

    把一个列表项分成了好几个方法,现在需要把每一个方法进行组合。具体代码如下,我会在视频中进行详细讲解

    Widget _ListWidget(int index){
    
        return InkWell(
          onTap: (){},
          child: Container(
            padding: EdgeInsets.only(top: 5.0,bottom: 5.0),
            decoration: BoxDecoration(
              color: Colors.white,
              border: Border(
                bottom: BorderSide( 1.0,color: Colors.black12)
              )
            ),
            
            child: Row(
              children: <Widget>[
                _goodsImage(index)
               ,
                Column(
                  children: <Widget>[
                    _goodsName(index),
                    _goodsPrice(index)
                  ],
                )
              ],
            ),
          )
        );
    
      }

    ListView的构建

    组合完成后,在build方法里,使用ListView来显示表单,记得要正确设置宽和高。

     @override
      Widget build(BuildContext context) {
        return Container(
           ScreenUtil().setWidth(570) ,
          height: ScreenUtil().setHeight(1000),
          child: ListView.builder(
            itemCount: list.length,
            itemBuilder: (context,index){
              return _ListWidget(index);
            },
          )
        );
      }

    构建好后,就可以进行测试了。

    列表页_商品列表交互效果制作

    现在页面布局已经基本完成,接下来就要作商品分类页的各种交互效果了,

    制作商品列表的Provide

    制作Provide是有一个小技巧的,就是页面什么元素需要改变,你就制作什么的provide类,比如现在我们要点击大类,改变商品列表,实质改变的就是List的值,那只制作商品列表List的Provide就可以了。

    lib/proive/文件夹下,新建一个category_goods_list.dart文件。

    import 'package:flutter/material.dart';
    import '../model/categoryGoodsList.dart';
    
    class CategoryGoodsListProvide with ChangeNotifier{
    
        List<CategoryListData> goodsList = [];
      
        //点击大类时更换商品列表
        getGoodsList(List<CategoryListData> list){
               
          goodsList=list;   
          notifyListeners();
        }
    }

    先引入了model中的categoryGoodsList.dart文件,管理的状态就是goodsList变量,我们通关过一个方法getGoodsList来改变状态。这样一个Provide类就制作完成了。

    将状态放入顶层

    Provide编程完成以后,需要把写好的状态管理放到main.dart中,我司叫它为放入顶层,就是全部页面想用这个状态都可以获得。代码如下:

    void main(){
      var childCategory= ChildCategory();
      var categoryGoodsListProvide= CategoryGoodsListProvide();
    
      var counter =Counter();
      var providers  =Providers();
      providers
        ..provide(Provider<ChildCategory>.value(childCategory))
        ..provide(Provider<CategoryGoodsListProvide>.value(categoryGoodsListProvide))
        ..provide(Provider<Counter>.value(counter));
    
      runApp(ProviderNode(child:MyApp(),providers:providers));
    }

    声明一个categoryGoodsListProvide变量,然后放入顶层就可以了。

    修改category_page.dart页面

    这个页面需要伤筋动骨,进行彻底修改结构,步骤较多,请按步骤一步步完成。

    1.引入provide文件

    lib/pages/category_page.dart文件最上面引入刚写的provide.

    import '../provide/category_goods_list.dart';

    2.修改_getGoodsList方法

    上节课为了布局,把得到商品列表数据的方法,放到了商品列表类里。现在需要把这个方法放到我们的CategoryPage类里,作为一个内部方法,因为我们要在点击大类时,调用后台接口和更新状态。

    //得到商品列表数据
    void _getGoodList({String categoryId}) async{
        var data = {
          'categoryId': categoryId == null ? '4' : categoryId,
          'categorySubId': '',
          'page': 1,
        };
        await request('post', 'getMallGoods', formData: data).then((val){
          var data = json.decode(val.toString());
          CategoryGoodsListModel goodsList = CategoryGoodsListModel.fromJson(data);
    
          Provide.value<CategoryGoodsListProvide>(context).getGoodsList(goodsList.data);
        });
      }

    首先方法要增加一个可选参数,就是大类ID,如果没有大类ID,我们默认为4,有了参数后到后台获得数据,获得后使用Provide改变状态。

    3.使用_getGoodList方法

    修改完这个方法后,可以在每次点击大类的时候进行调用。代码如下:

    onTap: (){
            setState(() {
              listIndex = index; //每次点击了大类就把索引赋值给listIndex
            });
            var childList = list[index].bxMallSubDto; //当前大类的子类列表
            var categoryId = list[index].mallCategoryId;
            Provide.value<ChildCategory>(context).getChildCategory(childList);
            _getGoodList(categoryId:categoryId);
          },

    这段代码,先声明了一个类别IDcategoryId,然后调用了_getGoodList()方法,调用方法时要传递categoryId参数。

    4.修改商品列表代码

    这个部分的代码修改要多一点,要把原来的setState模式,换成provide模式,所以很多地方都有所不同,但是我们的布局代码时不需要改的。

    先去掉list ,然后用Provide widget来监听变化,修改类里的子方法,多接收一个List参数,命名为newList,每个子方法都要加入,这里提醒不要使用state,否则会报错。

    修改后的代码如下:

    //右侧商品列表 ,可以上拉加载
    class CategoryGoodsList extends StatefulWidget {
      CategoryGoodsList({Key key}) : super(key: key);
    
      _CategoryGoodsListState createState() => _CategoryGoodsListState();
    }
    
    class _CategoryGoodsListState extends State<CategoryGoodsList> {
      //List list = [];
    
      @override
      void initState() { 
    
        super.initState();   
      }
    
      @override
      Widget build(BuildContext context) {
        return Provide<CategoryGoodsListProvide>(
          builder:(context, child, data){
            return Container(
               ScreenUtil().setWidth(570),
              height: ScreenUtil().setHeight(980),
              child: ListView.builder(
                itemCount: data.goodsList.length,
                itemBuilder: (context,index){
                  return _listGoods(data.goodsList,index);
                },
              )
            );
          },
        );
      }
    
      
      //图片
      Widget _goodsImage(List newList,index){
        return Container(
           ScreenUtil().setWidth(200),
          child: Image.network(newList[index].image),
        );
      }
      //商品名称
      Widget _goodsName(List newList,index){
        return Container(
           ScreenUtil().setWidth(370),
          padding: EdgeInsets.all(5),
          child: Text(
            newList[index].goodsName,
            maxLines:2,
            overflow:TextOverflow.ellipsis,
            style: TextStyle(fontSize:ScreenUtil().setSp(28)),
          ),
        );
      }
      //商品价格
      Widget _goodsPrice(List newList,index){
        return Container(
          margin: EdgeInsets.only(top: 20),
           ScreenUtil().setWidth(370),
          child: Row(
            children: <Widget>[
              Text(
                '价格:${newList[index].presentPrice} ',
                style: TextStyle(color: Colors.blueGrey,fontSize: ScreenUtil().setSp(28)),
              ),
              Text(
                '价格:${newList[index].oriPrice}',
                style: TextStyle(color: Colors.black26,decoration: TextDecoration.lineThrough),
              ),
            ],
          ),
        );
      }
      //商品组合  
      Widget _listGoods(List newList,index){
        return InkWell(
          onTap: (){},
          child: Container(
            padding: EdgeInsets.only(top:5,bottom: 5),
            decoration: BoxDecoration(
              color: Colors.white,
              border: Border(
                bottom: BorderSide( 1,color: Colors.black12)
              )
            ),
            child: Row(
              children: <Widget>[
                _goodsImage(newList,index),
                Column(
                  children: <Widget>[
                    _goodsName(newList,index),
                    _goodsPrice(newList,index),
                  ],
                )
              ],
            ),
          ),
        );
      }
    
    }

    然后使用_getGoodList方法,在_leftInkWell的onTap里修改:

    var categoryId = list[index].mallCategoryId;
    _getGoodList(categoryId:categoryId);

    还要在LeftCategoryNav的initState加上_getGoodList();

    class _LeftCategoryNavState extends State<LeftCategoryNav> {
      List list = [];
      var listIndex = 0; //索引
    
      @override
      void initState() { 
        _getCategory(); //请求接口的数据
    -----------添加部分start------------
        _getGoodList();
    -----------添加部分end----------------
        super.initState();
      }

    总结:这节课算是Provide的高级应用了,如果这个状态管理小伙伴都很熟练了,至少Flutter的状态管理这个知识点是没有问题了。

    列表页_小类高亮交互效果制作

    Expanded Widget的使用

    Expanded Widget 是让子Widget有伸缩能力的小部件,它继承自Flexible,用法也差不多。那为什么要单独拿出来讲一下Expanded Widget那?我们在首页布局和列表页布局时都遇到了高度适配的问题,很多小伙伴出现了高度溢出的BUG,所以这节课开始前先解决一下这个问题。

    修改 Category_page.dart里的商品列表页面,不再约束高了,而是使用Expanded Widget包裹外层,修改后的代码如下:

    @override
      Widget build(BuildContext context) {
        return Provide<CategoryGoodsListProvide>(
            builder: (context,child,data){
              return Expanded(
                child:Container(
                   ScreenUtil().setWidth(570) ,
                  child:ListView.builder(
                      itemCount: data.goodsList.length,
                      itemBuilder: (context,index){
                        return _ListWidget(data.goodsList,index);
                      },
                    ) ,
                ) ,
              ); 
           },
        );
      }

    小类高亮效果制作

    由于高亮效果也受到大类的控制,不仅仅是子类别的控制,所以这个效果也要用到状态管理来制作。这个状态很简单,没必要单独写一个Provide,所以直接使用以前的类就可以,我们直接在provide/child_category.dart里修改。修改的代码为:

    import 'package:flutter/material.dart';
    import '../model/category.dart';
    
    //ChangeNotifier的混入是不用管理听众
    class ChildCategory with ChangeNotifier{
    
      List<BxMallSubDto> childCategoryList = [];
      int childIndex = 0; //子类高亮索引
    
      //点击大类时切换逻辑
      getChildCategory(List<BxMallSubDto> list){
        childIndex = 0; //每次点击切换大类,子类都要归0
    
        BxMallSubDto all = BxMallSubDto();
        all.mallSubId = '00';
        all.mallCategoryId = '00';
        all.mallSubName = '全部';
        all.comments = 'null';
        childCategoryList = [all];
        childCategoryList.addAll(list);
        notifyListeners();
      }
    
      //改变子类索引
      changeChildIndex(index){
        childIndex = index;
        notifyListeners();
      } 
    
    }

    然后就可以修改UI部分了,UI部分主要是增加索引参数,然后进行判断。

    (1)先把_rghtInkWell方法增加一个接收参数int index.这就是修改变量的索引值。

    Widget _rightInkWell(int index,BxMallSubDto item)

    (2)定义是否高亮变量,再根据状态进行赋值

    bool isClick =false; //是否点击
    isClick = (index == Provide.value<ChildCategory>(context).childIndex) ? true : false;

    (3)用isCheck判断是否高亮

    color: isClick ? Colors.blueGrey:Colors.black

    (4)_rightInkWell添加index

    return _rightInkWell(index,childCategory.childCategoryList[index]);

    (5)点击时修改状态_rightInkWell的onTap

    Provide.value<ChildCategory>(context).changeChildIndex(index);

    到这里,我们的子类高亮就制作完成了,并且当更换大类时,子类自动更改为第一个高亮。

    列表页_子类和商品列表切换

    修改Provide类

    先改动一下child_ategory.dart的Provide类,增加一个大类ID,然后在更改大类的时候改变ID。

    import 'package:flutter/material.dart';
    import '../model/category.dart';
    
    //ChangeNotifier的混入是不用管理听众
    class ChildCategory with ChangeNotifier{
    
        List<BxMallSubDto> childCategoryList = [];
        int childIndex = 0;
        String categoryId = '4';
    
        //点击大类时更换
        getChildCategory(List<BxMallSubDto> list,String id){
          
          childIndex=0;
          categoryId=id;
          BxMallSubDto all=  BxMallSubDto();
          all.mallSubId='00';
          all.mallCategoryId='00';
          all.mallSubName = '全部';
          all.comments = 'null';
          childCategoryList=[all];
          childCategoryList.addAll(list);   
          notifyListeners();
        }
        //改变子类索引
        changeChildIndex(index){
           childIndex=index;
           notifyListeners();
        }
    } 

    修改调用getChildCategory放

    增加了参数,以前的调用方法也就都不对了,所以需要修改一下。直接用搜索功能就可以找到getChildCategory方法,一共两处,直接修改就可以了

    Provide.value<ChildCategory>(context).getChildCategory(childList,categoryId);
    Provide.value<ChildCategory>(context).getChildCategory(list[0].bxMallSubDto,list[0].mallCategoryId);

    增加getGoodsList方法

    拷贝_getGoodsList方法到子列表类里边,然后把传递参数换成子类的参数categorySubId.代码如下:

    //得到商品列表数据
       void _getGoodList(String categorySubId) {
         
        var data={
          'categoryId':Provide.value<ChildCategory>(context).categoryId,
          'categorySubId':categorySubId,
          'page':1
        };
        
        request('getMallGoods',formData:data ).then((val){
            var  data = json.decode(val.toString());
            CategoryGoodsListModel goodsList=  CategoryGoodsListModel.fromJson(data);
            // Provide.value<CategoryGoodsList>(context).getGoodsList(goodsList.data);
            Provide.value<CategoryGoodsListProvide>(context).getGoodsList(goodsList.data);
           
        });
      }

    调用方法改版列表

    当点击子类时,调用这个方法,并传入子类ID。

    onTap: (){
        Provide.value<ChildCategory>(context).changeChildIndex(index);
        _getGoodList(item.mallSubId); //调用子类ID
    }, 

    子类没有商品时报错

    有些小类别里是没有商品的,这时候就会报错。解决这个错误非常简单,只要进行判断就可以了

    1、判断状态管理时是否存在数据

    首先你要在修改状态的时候,就进行一次判断,方式类型不对,导致整个app崩溃。也就是在点击小类的ontap方法后,当然这里调用了_getGoodList()方法。代码如下:

    //得到商品列表数据
       void _getGoodList(String categorySubId) {
         
        var data={
          'categoryId':Provide.value<ChildCategory>(context).categoryId,
          'categorySubId':categorySubId,
          'page':1
        };
        
        request('getMallGoods',formData:data ).then((val){
            var  data = json.decode(val.toString());
            CategoryGoodsListModel goodsList=  CategoryGoodsListModel.fromJson(data);
            // Provide.value<CategoryGoodsList>(context).getGoodsList(goodsList.data);
            if(goodsList.data==null){
             Provide.value<CategoryGoodsListProvide>(context).getGoodsList([]);
            }else{
              Provide.value<CategoryGoodsListProvide>(context).getGoodsList(goodsList.data);
              
            }
        });
      }

    2.判断界面输出时是不是有数据

    这个主要时给用户一个友好的界面提示,如果没有数据,要提示用户。修改的是商品列表类的build方法,代码如下:

    @override
      Widget build(BuildContext context) {
        return Provide<CategoryGoodsListProvide>(
            builder: (context,child,data){
              if(data.goodsList.length>0){
                 return Expanded(
                    child:Container(
                       ScreenUtil().setWidth(570) ,
                        child:ListView.builder(
                          itemCount: data.goodsList.length,
                          itemBuilder: (context,index){
                            return _ListWidget(data.goodsList,index);
                          },
                        ) 
                      )
                    ) ,
                  ); 
              }else{
                return  Text('暂时没有数据');
              }
           },
    
        );
      }

    把子类ID也Provide化

    现在的子类ID,我们还没有形成状态,用的是普通的setState,如果要做下拉刷新,那setState肯定是不行的,因为这样就进行跨类了,没办法传递过去。

    1.首先修改provide/child_category.dart类,增加一个状态变量subId,然后在两个方法里都进行修改,代码如下:

    import 'package:flutter/material.dart';
    import '../model/category.dart';
    
    //ChangeNotifier的混入是不用管理听众
    class ChildCategory with ChangeNotifier{
    
        List<BxMallSubDto> childCategoryList = []; //商品列表
        int childIndex = 0; //子类索引值
        String categoryId = '4'; //大类ID
        String subId =''; //小类ID 
     
        //点击大类时更换
        getChildCategory(List<BxMallSubDto> list,String id){
          categoryId=id;
          childIndex=0;
          subId=''; //点击大类时,把子类ID清空
          BxMallSubDto all=  BxMallSubDto();
          all.mallSubId='00';
          all.mallCategoryId='00';
          all.mallSubName = '全部';
          all.comments = 'null';
          childCategoryList=[all];
          childCategoryList.addAll(list);   
          notifyListeners();
        }
        //改变子类索引 ,
        changeChildIndex(int index,String id){
          //传递两个参数,使用新传递的参数给状态赋值
           childIndex=index;
           subId=id;
           notifyListeners();
        }
    }

    这就为以后我们作上拉加载效果打下了基础。

    列表页_上拉加载功能的制作

    现在主要制作一下列表页的上拉加载更多功能,重点会放在上拉加载和Provide结合的方法。

    增加page和noMoreText到Provide里

    因为无论切换大类或者小类的时候,都需要把page变成1,所以需要在provide/child_category.dart里新声明一个page变量.noMoreText主要用来控制是否显示更多和如果没有数据了,不再向后台请求数据。每一次后台数据的请求都是宝贵的。

    int page=1;  //列表页数,当改变大类或者小类时进行改变
    String noMoreText=''; //显示更多的标识

    声明在切换大类和切换小类的时候都把page变成1,代码如下:

    //点击大类时更换
      getChildCategory(List<BxMallSubDto> list,String id){
          isNewCategory=true;
          categoryId=id;
          childIndex=0;
          //------------------关键代码start
          page=1;
          noMoreText = ''; 
          //------------------关键代码end
          subId=''; //点击大类时,把子类ID清空
          noMoreText='';
          BxMallSubDto all=  BxMallSubDto();
          all.mallSubId='00';
          all.mallCategoryId='00';
          all.mallSubName = '全部';
          all.comments = 'null';
          childCategoryList=[all];
          childCategoryList.addAll(list);   
          notifyListeners();
        }
        //改变子类索引 ,
        changeChildIndex(int index,String id){
           isNewCategory=true;
           //传递两个参数,使用新传递的参数给状态赋值
           childIndex=index;
           subId=id;
           //------------------关键代码start
           page=1;
           noMoreText = ''; //显示更多的表示
           //------------------关键代码end
           notifyListeners();
      }

    还需要写一个增加page数量的方法,用来实现每次上拉加载后,page随之加一,代码如下:

    //增加Page的方法f
      addPage(){
        page++;
      }

    在制作一个改变noMoreText方法。

    //改变noMoreText数据  
      changeNoMore(String text){
          noMoreText=text;
          notifyListeners();
      }

    增加EasyRefresh组件

    category_page.dart里增加EasyRefresh组件,首先需要使用import进行引入。

    import 'package:flutter_easyrefresh/easy_refresh.dart';

    引入之后,可以直接使用EasyRefresh进行包裹,然后加上各种需要的参数,

    
    
      GlobalKey<RefreshFooterState> _footerkey = new GlobalKey<RefreshFooterState>(); //定义key

    @override Widget build(BuildContext context) {
    return Provide<CategoryGoodsListProvide>( builder: (context,child,data){ if(data.goodsList.length>0){ return Expanded( child:Container( ScreenUtil().setWidth(570) , child:EasyRefresh( refreshFooter: ClassicsFooter( key:_footerKey, bgColor:Colors.white, textColor:Colors.pink, moreInfoColor: Colors.pink, showMore:true, noMoreText:Provide.value<ChildCategory>(context).noMoreText, moreInfo:'加载中', loadReadyText:'上拉加载' ), child:ListView.builder( itemCount: data.goodsList.length, itemBuilder: (context,index){ return _ListWidget(data.goodsList,index); }, ) , loadMore: ()async{ print('上拉加载更多.......'); }, ) ) , ); }else{ return Text('暂时没有数据'); } }, ); }

    修改请求数据的方法

    这个类中也需要一个去后台请求数据的方法,这个方法要求从Provide里读出三个参数,大类ID,小类ID和页数。代码如下:

    //上拉加载更多的方法
      void _getMoreList(){
         
        Provide.value<ChildCategory>(context).addPage();
         var data={
          'categoryId':Provide.value<ChildCategory>(context).categoryId,
          'categorySubId':Provide.value<ChildCategory>(context).subId,
          'page':Provide.value<ChildCategory>(context).page
        };
        
        request('getMallGoods',formData:data ).then((val){
            var  data = json.decode(val.toString());
            CategoryGoodsListModel goodsList=  CategoryGoodsListModel.fromJson(data);
           
            if(goodsList.data==null){
             Provide.value<ChildCategory>(context).changeNoMore('没有更多了');
            }else{
               
              Provide.value<CategoryGoodsListProvide>(context).addGoodsList(goodsList.data);
              
            }
        });
    
      }

    每次都先调用增加页数的方法,这样请求的数据就是最新的,当没有数据的时候要把noMoreText设置成‘没有更多了’。

    切换类别返回顶部

    到目前为止,我们应该可以正常展示上拉加载更多的方法了,但是还有一个小Bug,切换大类或者小类的时候,我们的页面没有回到顶部,这个其实很好解决。再build的Provide的构造器里加入下面的代码就可以了。

    try{
      if(Provide.value<ChildCategory>(context).page==1){
        scrollController.jumpTo(0.0);
      }
    }catch(e){
      print('进入页面第一次初始化:${e}');
    }

    当然你还要再列表类里进行声明scrollController,如果你不声明是没办法使用的。

    var scrollController=new ScrollController();

    声明完成后,给ListView加上controller属性。

    child:ListView.builder(
      controller: scrollController,
      itemCount: data.goodsList.length,
      itemBuilder: (context,index){
        return _ListWidget(data.goodsList,index);
      },
    ) ,

    这时候再进行测试,应该就可以了。

    Fluttertoast组件的介绍

    在APP的使用过程中,对用户的友好提示是必不可少的,比如当列表页上拉加载更多的时候,到达了数据的底部,没有更多数据了,就要给用户一个友好的提示。但是这种提示又不能影响用户的使用,一个轻提示组件给大家FlutterToast

    Fluttertoast 组件简介

    这是一个第三方组件,当你学习的时候可以到Github上查找最新版本。

    GitHub地址:https://github.com/PonnamKarthik/FlutterToast

    这个组件我觉的还时比较好用的,提供了样式自定义,而且自带的效果页是很酷炫的。

    如何使用Fluttertoast

    首先需要在pubspec.yaml中进行引入Fluttertoast组件(也叫保持依赖,也叫包管理),主要版本号,请使用最新的,这里不保证时最新版本。

    fluttertoast: ^3.1.3

    引入后在需要使用的页面使用import引入,引入代码如下:

    import 'package:fluttertoast/fluttertoast.dart';

    Fluttertoast使用方法

    在需要使用的地方直接可以使用,如下代码:

    Fluttertoast.showToast(
      msg: "已经到底了",
      toastLength: Toast.LENGTH_SHORT,
      gravity: ToastGravity.CENTER,
      timeInSecForIos: 1,
      backgroundColor: Colors.pink,
      textColor: Colors.white,
      fontSize: 16.0
    );
    • msg:提示的文字,String类型。
    • toastLength: 提示的样式,主要是长度,有两个值可以选择:Toast.LENGTH_SHORT :短模式,就是比较短。Toast.LENGTH_LONG : 长模式,就是比较长。
    • gravity:提示出现的位置,分别是上中下,三个选项。ToastGravity.TOP顶部提示,ToastGravit.CENTER中部提示,ToastGravity.BOTTOM底部提示。
    • bgcolor: 背景颜色,跟从Flutter颜色。
    • textcolor:文字的颜色。
    • fontSize: 文字的大小。

    小Bug的处理

    在列表页还存在着一个小Bug,就是当我们选择子类别后,然后返回全部,这时候会显示没有数据,这个主要是我们在Provide里构造虚拟类别时,传递的参数不对,只要把参数修改成空就可以了。

    打开provide/child_category.dart,修改getChildCateg()方法。 修改代码如下:

    //点击大类时更换
    getChildCategory(List<BxMallSubDto> list,String id){
      isNewCategory=true;
      categoryId=id;
      childIndex=0;
      page=1;
      subId=''; //点击大类时,把子类ID清空
      noMoreText='';
      BxMallSubDto all=  BxMallSubDto();
      //--------修改的关键代码start
      all.mallSubId='';
      //--------修改的关键代码end
      all.mallCategoryId='00';
      all.mallSubName = '全部';
      all.comments = 'null';
      childCategoryList=[all];
      childCategoryList.addAll(list);   
      notifyListeners();
    }

    现在重新运行就可以了 。

  • 相关阅读:
    天翼网关获取超级密码
    Wirte-up:攻防世界Web解题过程新手区01-06
    F#周报2019年第12期
    F#周报2019年第11期
    F#周报2019年第10期
    F#周报2019年第9期
    F#周报2019年第8期
    F#周报2019年第7期
    ML.NET 0.10特性简介
    F#周报2019年第6期
  • 原文地址:https://www.cnblogs.com/joe235/p/11648768.html
Copyright © 2011-2022 走看看