Flutter移动电商实战

Flutter-shop

春节过完了,新年新气象,年前一直在住院和养病。现在病已经养好,那就来个大招吧。《Flutter实战移动电商》开始更新,小伙伴快来学习吧。

有小伙伴肯定会问我,为什么这套视频要收费?

其实每年技术胖都会出一套收费视频,收益是为了维护博客的正常运营、服务器费用和买一些录制课程的设备。(社会太现实,0盈利无法生存。另外求大佬赞助或投资,实现真正的全免费。)

技术胖的初心并没有变,目标是录制1000集免费视频,这个目标可能要10年才能达到,今年目标是录制100集免费视频。

古语有云:“兵马未动,粮草先行。年年防歉,夜夜防贼。”,那这套视频就是攒粮草的,为了实现1000集免费视频教程的目标。

所以希望大家对收费的理解,也感谢大家的支持,你的一次付费购买,就会帮助技术胖出更多的免费视频教程,也间接帮助了中国前端生态圈的崛起。

如果你对Flutter感兴趣,可以加入Flutter群:

学习讨论QQ群:806799257

入群问题:Flutter出自于哪个公司?

入群答案:google (注意全小写,最好用电脑端加入,移动端Bug)

#第01节:课程介绍


图片地址

#Flutter实战真实接口全网首发

Flutter实战电商开始预售了,课程采用了Flutter1.x版本(最新版),采用真实接口开发,前后端接口联调和真实工作一样,目前市面首发(其他视频多是界面布局,没有接口调试部分)。

教程中将采用真实接口,使用了Fiddle进行项目接口的拦截和发送处理,最终给大家呈现一个简单易用的后台接口文档。因为是真实项目,所以接口会一直改变,正版教程也会进行一年的接口维护工作,官方改变,我们会制作新课跟着改变。(这是一个生产环境的项目,接口变化至少2个星期就会变化一次,所以建议跟着技术胖现在就开始作,不要等到全部出完,全部出完可能接口都变化了,会对你学习产生障碍)

所以如果你购买盗版将没有这些接口改变的后续服务,你根本不可能做出视频中的效果(请购买正版教程)。

#你能学到的知识点

在详细说明之前,把所有你能学到的知识点作了一张梳理图,可以帮助小伙伴更好的了解课程概况。

知识点梳理

  • Dio2.0:Dio是一个强大的Dart Http请求库,支持Restful API、FormData、拦截器、请求取消等操作。视频中将全面学习和使用Dio的操作。

  • Swiper:swiper滑动插件的使用,使用Swiper插件图片的切换效果。

  • 路由Fluro:Flutter的路由机制很繁琐,如果是小型应用还勉强,但是真实开发我们都会使用企业级的路由机制,让路由清晰可用。视频中也会使用Fluro进行路由配置.Fluro也是目前最好的企业级Flutter路由。

  • 屏幕适配:手机屏幕大小不同,布局难免有所不同,在视频中将重点讲述Flutter的开发适配,一次开发适配所有屏幕,学完后可以都各种屏幕做到完美适配。

  • **上拉加载 **:如果稍微熟悉Flutter一点的小伙伴一定知道Flutter没有提供上拉加载这种插件,自己开发时非常麻烦的。在课程中我将带着大家制作上拉加载效果。

  • 本地存储:本地存储是一个App的必要功能,在项目中也大量用到了本地存储功能。

  • 复杂页面的布局:会讲到如何布局复杂页面,如果解决多层嵌套地狱,如何写出优雅的代码。

  • 其他知识点:还会设计到很多其他知识点,基本的Widget操作就超过50个,是目前市面教程中最多的实战课程。

  • 随时增加的知识技巧:如果你参加了预售,你可以根据自己的需求,提交需要增加的知识点,会根据需求的普遍性 ,随时增加知识点(全部视频60集左右)。

#组件化开发,完美复原APP核心页面

我们会最大程度的复原原来APP的UI界面和交互功能,让你熟练掌握Flutter的实战操作。

  • Fluter实战预热:环境的配置、项目代码结构的说明、dart文件的组件、路由的学习配置、项目代码的初始化。

  • APP首页开发:Header区域的制作、首页轮播效果的制作、图标区域实现、推荐区域制作、Bannder区域的制作,呼叫店长功能、楼层组件开发,火爆专区列表。

  • 商品分类页面:动态组件的极致运用,一级分类的区域制作、二级分类的区域制作、商品列表组件开发、上拉加载更多功能的制作。

  • 商品详情页面:路由的使用、商品图片制作、商品详情Webview组件、tab的真实开发。

  • 购物车页面 : 包含购物车的整套功能,增加商品,调整数量,删除商品,运费计算,结账显示合计功能,超过运费的UI组件编写。

  • 会员中心页面:顶部头像制作、订单区域通知功能、会员中心列表功能。

  • 调试与上线:项目后台接口的调试技巧,真机如何测试,打包上线,后续学习指南。

  • 接口文档:接口文档根据官方文档按时更新,只有正版学员才可以享受,让你做出一个拿的出去手的项目。

#物超所值的精华课程,让你达到一线水平

  • 从理论到项目,在熟悉了理论之后,应该专注于真实项目的知识和技巧。
  • 循序渐进的讲解,跟着技术胖花点时间,一点点用代码敲出一个真实项目,项目中的每一行代码都手敲和详细讲解,让你不在懵逼。
  • 贴近企业项目,完全按着企业级代码标准和工程开发的流程进行授课,让你尽早熟悉企业级Flutter的开发形式。
  • 代码、基础、项目流程、工作技巧、多维度提升,让你全方面提高工作能力。

#学员专享增值服务

  • 微信群问答辅导:每天晚上半小时技术胖在线集中答疑,搭建提问区,学员可以随时提问,集中回答。

  • 源码开放: 对于正版学员课程案例代码完全开发给你,你可以根据所学知识自行修改。

  • 进入Google官方Flutter群:群内都是一线Flutter高手,包括咸鱼技术总监,Flutter核心开发者,京东Flutter技术总监.....国内顶尖Flutter高手。(必须学完并开发完成,才能进入)

  • 工作内推机会:了解我的小伙伴都知道,我每年帮助200多人找到工作(内推),其中很多人都进入了一线大厂。(学完后优秀学员,提供内推机会)。

#第02节:建立项目和编写入口文件


这节课正式开始我们的实战学习,请小伙伴们备好电脑,拿好小板凳,买好瓜子花生米,好戏正式开始了!!!!!

在学习这门课程时,我会默认你已经学习了Flutter的基础知识。(如果你还没学过,那这里为你准备了Flutter45集免费基础视频)

  • 第一季Flutter视频教程地址:https://jspang.com/posts/2019/01/20/flutter-base.html
  • 第二季Flutter视频教程地址:https://jspang.com/posts/2019/01/21/flutter-base2.html
  • 第三季Flutter视频教程地址:https://jspang.com/posts/2019/01/28/flutter-base3.html
  • 第四季Flutter视频教程地址:https://jspang.com/posts/2019/02/01/flutter-base4.html
  • 20个Flutter实例视频教程:https://jspang.com/posts/2019/02/22/flutterdemo.html

-Dart中文文档:https://www.kancloud.cn/marswill/dark2_document/709087

#用Flutter命令建立项目

在你的电脑上找一个喜欢的位置,建立一个文件夹。比如我在E:\flutter_shop(你完全可以和我不一样)。然后打开终端(也叫命令行),进入E盘,用Flutter命令直接建立flutter_shop项目。

命令行操作如下:

e: flutter create flutter_shop

注意: flutter建议使用flutter_shop这种命名方式,所以就不要用什么大驼峰,小驼峰这种方式了。其实我觉的下划线这种方式还是比较好的,至少会增加代码的长度,男人还是需要长度的,女人也会更喜欢长度。所以记得我们用下划线的方式命名。

当看到ALL Done字样的时候,就说明项目建立好了。然后进入VSCode,打开项目文件夹,可以看到项目的结构了。

这时候我们的项目已经建立好了。

#入口文件的编写

进入lib目录下,可以看到一个main.dart文件,打开这个文件,写入下面的代码,代码都很简单,这里就不写文字说明了,视频中会作详细的代码介绍。

import 'package:flutter/material.dart'; 
void main()=>runApp(MyApp()); 
class MyApp extends StatelessWidget {  @override  Widget build(BuildContext context) {    return Container(      child: MaterialApp(        title:'百姓生活+',        debugShowCheckedModeBanner: false,        theme: ThemeData(          primaryColor:Colors.pink        ),        home:IndexPage()      ),    );  } }

入口文件相当的简单,如果你现在还不能看懂这段代码,那你需要练习一下基础知识。

#建立主页文件和目录结构

我们在lib目录下建立一个pages目录,这个目录主要放置项目所用的所有UI界面的文件,在page目录下,建立index_page.dart文件。

有了这个文件,我们先建立一个静态Widget,主要是检验我们的入口文件是否可用。

import 'package:flutter/material.dart'; 
class IndexPage extends StatelessWidget {  @override  Widget build(BuildContext context) {    return Scaffold(      appBar: AppBar(title: Text('百姓生活+'),),      body: Center(        child:Text(' 百姓生活+')      ),    );  } }

代码写完后,记得在main.dart(入口文件),用impoart引入index_page.dart文件。

import './pages/index_page.dart';

这时候在终端里运行Flutter run就可以看到效果了,当然你要预先开启虚拟机或者用真机调试,我工作中也都是用真机调试的,虚拟机毕竟占用内存控件,让电脑变慢。也没有真机测试测试起来方便。

课程总结:

这节课是第一节,所以还是比较简单的,让大家可以在轻松愉快中开始一个新的项目。这节学习了Flutter项目的命令行建立方法、flutter入口文件的编写和引入文件的方法。

重点要说的是,虽然超级简单,你还是要跟着动手作一下,一课一作保证你有问题可以及时得到解决。这样也能跟着我一起把项目顺利的做出来。

#第03节:底部导航栏制作

完全模拟工作开发流程的Flutter实战 全网首发。


接下来两节课我们要把项目和页面的大体结构定下来,并利用底部导航栏把这些页面贯穿起来,让我们的项目看起来像一个丰富页面的项目(当然这只是看起来)。

#cupertino_IOS风格介绍

在Flutter里是有两种内置风格的:

  • material风格: Material Design 是由Google推出的全新设计语言,这种设计语言是为手机、平板电脑、台式机和其他平台提供一致,更广泛的外观和感觉。我喜欢称它为纸墨设计。Material Design 风格是一种非常有质感的设计风格,并会提供一些默认的交互动画。
  • cupertino风格:即IOS风格组件,它重现了很多经典的有IOS特性的交互和界面风格,让适用于IOS的人感觉亲切和友好。

有些小伙伴误认为你的APP选择了一种风格,就要一直使用这种风格,但事实是你可以一起使用,兼顾两个风格的特点,比如我这里就觉得cupertino风格的图标比较细致和美观。所以在index_page.dart页面,先引入了cupertino.dart,然后又引入了material.dart。当然这两个引入是不分先后顺序的。

index_page.dart文件

import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart';

#使用动态StatefulWidget

上节课为了测试入口文件,我们在index_page.dart文件里使用了静态组件(也就是继承了StatelessWidget)。因为底部导航栏是要根据用户操作不断变化的,所以我们使用动态组件(StatefulWidget)。

这里我使用了快捷键stful快速生成,如果你要使用这个快速生成需要在VSCode里安装Awesome Flutter Snippets。安装完插件需要重新启动一下VSCode,然后就可以快乐的使用快捷方法生成代码了。(Flutter开发必备,建议安装)

生成代码如下:

class IndexPage extends StatefulWidget {  _IndexPageState createState() => _IndexPageState(); }
 class _IndexPageState extends State {  @override  Widget build(BuildContext context) {    return Container(       child: child,    );  } }

#bottomTabs List

有了动态组件,接下来可以在State部分先声明一个List变量,变量名称为boottomTabs。这个变量的属性为BottomNavigationBarItem

其实这个List变量就定义了底部导航的文字和使用的图标。

代码如下:

final List bottomTabs = [    BottomNavigationBarItem(      icon:Icon(CupertinoIcons.home),      title:Text('首页')    ),    BottomNavigationBarItem(      icon:Icon(CupertinoIcons.search),      title:Text('分类')    ),    BottomNavigationBarItem(      icon:Icon(CupertinoIcons.shopping_cart),      title:Text('购物车')    ),     BottomNavigationBarItem(      icon:Icon(CupertinoIcons.profile_circled),      title:Text('会员中心')    ),  ];

总结:这节课我们就先到这里建立好List就算完成任务。学完这节你应该学会下面知识点:

  1. Flutter两种UI风格的区别和如何一起使用。
  2. Flutter动态小部件的基本使用-StatefulWidget。
  3. 声明一个BottomNavigationBarItem类型的List,并设置文字和图标。

#第04节:打通底部导航栏

这节课我们先新建几个页面,页面内容都是简单放入一个TextWidget就算完事,目的是让底部导航栏可以使用和在页面之间进行切换。


#新建四个基本dart文件

pages目录下,我们新建下面四个dart文件。

  • home_page.dart :商城首页UI页面,首页相关的UI我们都会放到这个文件里。
  • category_page.dart: 商城分类UI页面,这个页面会有复杂的动态组件切换。
  • cart_page.dart:商城购物车UI页面,这个页面会包括购物车的全套功能。
  • member_page.dart:商城会员中心页面,这个页面我们会制作会员中心的全部UI效果。

其实这一部就是建立了底部导航栏需要的四个基本页面,有了这四个基本页面就可以制作底部tab的切换功能了。

这里我只给一个页面(home_page.dart)的基础代码(后期这些代码都要更换,这里只是为了看到效果使用),然后你可以暗装一个的代码,复制到其它页面,进行修改。(具体可查看视频)

import 'package:flutter/material.dart'; 
class HomePage extends StatelessWidget {  @override  Widget build(BuildContext context) {    return Scaffold(      body:Center(        child: Text('商城首页'),      )    );  } }

记得其他三个页面都进行复制,并修改类名和Text文本属性。

#引入页面并建立List

页面创建好以后,要使用import引入到index_page.dart中,引入后才可以使用,如果不引入,VScode会很智能的报错。代码如下。

import 'home_page.dart'; import 'category_page.dart'; import 'cart_page.dart'; import 'member_page.dart';

引入后声明一个List型变量,这个变量主要用于切换的,我们把页面里的类,放到了这个List中。

 final List tabBodies = [    HomePage(),    CategoryPage(),    CartPage(),    MemberPage()  ];

声明两个变量,并进行initState初始化:

  • currentIndex: int类型,负责tabBodies的List索引,改变索引就相当于改变了页面。
  • currentPage: 利用currentIndex得到当前选择的页面,并进行呈现出来。

代码如下:

 int currentIndex = 0;  var currentPage;  @override  void initState() {    currentPage=tabBodies[currentIndex];    super.initState();  }

#build方法的编写

build方法我们会返回一个Scaffold部件,在部件里我们会添加底部导航栏,并利用onTap事件(单击事件),来改变导航栏的状态和切换页面。因为有界面变化,所以这也是要使用动态组件的原因。

@override  Widget build(BuildContext context) {    return Scaffold(      backgroundColor: Color.fromRGBO(244, 245, 245, 1.0),      bottomNavigationBar: BottomNavigationBar(        type:BottomNavigationBarType.fixed,        currentIndex: currentIndex,        items:bottomTabs,        onTap: (index){          setState(() {           currentIndex = index;           currentPage = tabBodies[currentIndex];           });        },      ),      body:currentPage    );  }

这里有句代码type:BottomNavigationBarType.fixed,这个是设置底部tab的样式,它有两种样式fixedshifting,只有超过3个才会有区别,国人的习惯一般是使用fixed的。感兴趣的小伙伴可以自行折腾shifting模式。

这时候就可以启动虚拟机,进行预览了。为了更好的让小伙伴们学习,在这里给出index_page.dart文件的源码。

index_page.dart文件

import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'home_page.dart'; import 'category_page.dart'; import 'cart_page.dart'; import 'member_page.dart'; 

class IndexPage extends StatefulWidget {  _IndexPageState createState() => _IndexPageState(); }
 class _IndexPageState extends State {  final List bottomTabs = [    BottomNavigationBarItem(      icon:Icon(CupertinoIcons.home),      title:Text('首页')    ),    BottomNavigationBarItem(      icon:Icon(CupertinoIcons.search),      title:Text('分类')    ),    BottomNavigationBarItem(      icon:Icon(CupertinoIcons.shopping_cart),      title:Text('购物车')    ),     BottomNavigationBarItem(      icon:Icon(CupertinoIcons.profile_circled),      title:Text('会员中心')    ),  ];  final List tabBodies = [    HomePage(),    CategoryPage(),    CartPage(),    MemberPage()  ];  int currentIndex= 0;  var currentPage ;  @override  void initState() {   currentPage=tabBodies[currentIndex];    super.initState();  }  @override  Widget build(BuildContext context) {    return Scaffold(      backgroundColor: Color.fromRGBO(244, 245, 245, 1.0),      bottomNavigationBar: BottomNavigationBar(        type:BottomNavigationBarType.fixed,        currentIndex: currentIndex,        items:bottomTabs,        onTap: (index){          setState(() {           currentIndex=index;           currentPage =tabBodies[currentIndex];           });        },      ),      body: currentPage,    );  } }
 

总结:通过这节课的学习,应该掌握如下知识点:

  1. 页面切换的技巧和变量如何定义。
  2. BottomNavigationBar部件的使用,最终作成底部切换效果。

#第05节:dio基础_引入和简单的Get请求

这套课程和现在市面上其它Flutter实战教程的区别就是我们采用了真实接口,用贴近真实工作的开发流程和模式来进行授课。可以简单的认为,就是咱们一起来完成一个项目。那真实的接口,就需要使用一个可以调用接口和从接口返回数据的工具(当然Flutter提供了这样的工具,但是普遍认为不够简单话,也许都是喜欢用再封装一下的插件吧)。所以从这节课我们学习Dart的第三方Http请求库dio。这是国人开源的一个项目,截至到我写这篇文章时,有2300多Star。也是国内用的最广泛的Dart Http请求库。


#dio介绍和引入

dio是一个强大的Dart Http请求库,支持Restful API、 FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时和自定义适配器等。

我相信很多人都已经接触或者了解dio了,但是还是需要把它拿出来单独讲解一下,因为在Flutter编程工作中,每天都需要和它打交道,本套教程也会大量的使用dio库来进行接口的调用和数据交换。

#添加dio依赖:

其实Flutter或者说Dart也为我们提供了第三方包管理工具,就和前端经常使用的npm包管理类似。Dart的包管理文件叫做pubspec.yaml,其实它统管整个项目,操作最多的就是第三方插件和静态文件(文件在项目根目录下),如果我们要引入第三方包需要在dependencies里写明。例如我们要加入dio,代码如下:

dependencies:    dio: ^2.0.7

这个写后好,只要我们已保存文件VSCode就会给我们自动进行包的下载,当然有些网络下载会稍微慢点,这可能根你的服务商有关,比如我公司用的是联通,下载就是一瞬间;但家里用的是当地的油田通讯(说是移动服务商,具体不详),下载就相当忙,在20分钟左右。

需要注意的是: 现在dio的版本已经是2.x版本,所以不要在使用1.x版本,可能是我使用的比较早,以前使用的是1.x版本,在项目原始PC上是可以运行的,但是移动到其它PC上就不能传递参数了。这个问题当时找了两天时间,算是一个坑。也就是说它升级了2.x版本后1.x版本不管用了,不能携带参数。(也希望作者在升级版本时要考虑老版本的稳定性)

#dio发送get请求

了解dio后,我们就先上手一个最简单小Demo,练一下手。

案例是这样的,我们模拟去大保健(啥是大保健,别装单纯了,这也是个成人课好吗?),这时候妈妈就是我们的接口,我们需要告诉妈妈我们需要什么样的人为我们服务,然后什么样人就来到房间。用程序来解释,就是我们发送一个get请求,服务端得到请求后会根据我们发送的参数,给我一个返回一个我们需要的数据。

我在easyMock上作了一个超级简单的数据,其实只是为了作这个小案例,所以不是那么复杂,EasyMock接口地址如下。

https://www.easy-mock.com/mock/5c60131a4bed3a6342711498/baixing/dabaojian

当然你也可以自己写一个这样的接口。

有了这样的接口后,你就可以在Flutter里访问这个请求了。不过你需要在使用的文件最上方用import引入dio.dart文件才可以。

import 'package:dio/dio.dart';

然后写一个基本get请求方法,我们暂时命名为getHttp(),方法中我们用了异步的方法,因为这样会防止后面的程序堵塞,具体代码如下:

void getHttp()async{    try{      Response response;      var data={'name':'技术胖'};      response = await Dio().get(        "https://www.easy-mock.com/mock/5c60131a4bed3a6342711498/baixing/dabaojian?name=大胸美女",        //  queryParameters:data      );      return print(response);    }catch(e){      return print(e);    }  }

为了大家学习方便,我给出整个页面的代码,这样更有助于大家学习,所有代码如下:

import 'package:flutter/material.dart'; import 'package:dio/dio.dart'; 
class HomePage extends StatelessWidget {  @override  Widget build(BuildContext context) {    getHttp();    return Scaffold(      body:Center(        child: Text('商城首页'),      )    );  }   void getHttp()async{    try{      Response response;      var data={'name':'技术胖'};      response = await Dio().get(        "https://www.easy-mock.com/mock/5c60131a4bed3a6342711498/baixing/dabaojian?name=大胸美女",        //  queryParameters:data      );      return print(response);    }catch(e){      return print(e);    }  } 
}

目前我们还只能显示在终端里,这太反人类了,下节课我们就来终止这个,让界面和我们进行同步。

**总结:**本节课学会的知识点:

  • 认识Dio库:dio是一个dart的 http请求通用库,目前也是大陆使用最广泛的库,国人开发,完全开源。
  • flutter的插件包管理:学了引入dio包,并简单的学习了pubspec.yaml的结构和编写注意事项。
  • get请求的编写:我们以一个充满正能量的小Demo讲述了get请求的实现,并成功的返回了结果。

#第06节:dio基础_Get请求和动态组件协作

这节课算是一个补充课程,昨天群里有几个小伙伴一直问我,如何Get请求后界面发生变化?如何使用Flutter里的动态小部件StatefulWidget?我当时并没有回答,因为这个不是用文字很好表达清楚的。不回答并不代表我置之不理,而是我准备了一晚上,今天给大家用视频的形式进行演示(为了回答小伙伴们的问题,我在原有课程知识点中提高了难度,融入了大家的问题)。

所以本节就针对于这两个问题作一个小案例,当然这也是为以后的实战作基础准备,基础打牢,我们才能飞速前进。


#案例说明

我们还是作去“大保健”选择服务对象这个例子,不过这次我们使用按钮和动态组件来实现。具体业务逻辑是这样的:

  1. 我们制作一个文本框,用于输入需要什么样的美女为我们服务
  2. 然后点击按钮,相当于去后端请求数据
  3. 后端返回数据后,根据你的需要美女就会走进房间

一图顶千言

http://blogimages.jspang.com/flutter_meihaorenjian.png

#生成动态组件

可以使用stful的快捷方式,在VSCode里快速生成StatefulWidget的基本结构,我们只需要改一下类的名字就可以了,就会得到如下代码.

class HomePage extends StatefulWidget {  _HomePageState createState() => _HomePageState(); }
 class _HomePageState extends State {  @override  Widget build(BuildContext context) {    return Container(       child: child,    );  } }

#加入文本框Widget

有了动态组件,咱们先把界面布局作一下,因为大家都有一定的Flutter基础了,文字教程中我就不作过多的解释了,视频教程中我会详细解释。

Widget build(BuildContext context) {    return Container(               child: Scaffold(          appBar: AppBar(title: Text('美好人间'),),          body:Container(            height: 1000,            child: Column(              children: [                TextField(                  controller:typeController,                  decoration:InputDecoration (                    contentPadding: EdgeInsets.all(10.0),                    labelText: '美女类型',                    helperText: '请输入你喜欢的类型'                  ),                  autofocus: false,                ),                RaisedButton(                  onPressed:_choiceAction,                  child: Text('选择完毕'),                ),                              Text(                  showText,                    overflow:TextOverflow.ellipsis,                    maxLines: 2,                ),                ],            ),          )         ),    );  }

#Dio的get方法

布局完成后,可以先编写一下远程接口的调用方法,跟上节课的内容类似,不过这里返回值为一个Future,这个对象支持一个等待回掉方法then。具体代码如下:

详细解释见视频吧,收费总要有点特权吧。

 Future getHttp(String TypeText)async{    try{      Response response;      var data={'name':TypeText};      response = await Dio().get(        "https://www.easy-mock.com/mock/5c60131a4bed3a6342711498/baixing/dabaojian",          queryParameters:data      );      return response.data;    }catch(e){      return print(e);    }  }

#得到数据后的处理

当我们写完内容后,要点击按钮,按钮会调用方法,并进行一定的判断。比如判断文本框是不是为空。然后当后端返回数据时,我们用setState方法更新了数据。具体代码如下:

void _choiceAction(){    print('开始选择你喜欢的类型............');    if(typeController.text.toString()==''){      showDialog(        context: context,        builder: (context)=>AlertDialog(title:Text('美女类型不能为空'))      );    }else{        getHttp(typeController.text.toString()).then((val){         setState(() {           showText=val['data']['name'].toString();         });        });    }   } 

#案例全部代码

import 'package:flutter/material.dart'; import 'package:dio/dio.dart'; 

class HomePage extends StatefulWidget {  _HomePageState createState() => _HomePageState(); }
 class _HomePageState extends State {   TextEditingController typeController = TextEditingController();  String showText = '欢迎你来到美好人间';  @override  Widget build(BuildContext context) {    return Container(               child: Scaffold(          appBar: AppBar(title: Text('美好人间'),),          body:Container(            height: 1000,            child: Column(              children: [                TextField(                  controller:typeController,                  decoration:InputDecoration (                    contentPadding: EdgeInsets.all(10.0),                    labelText: '美女类型',                    helperText: '请输入你喜欢的类型'                  ),                  autofocus: false,                ),                RaisedButton(                  onPressed:_choiceAction,                  child: Text('选择完毕'),                ),                              Text(                  showText,                    overflow:TextOverflow.ellipsis,                    maxLines: 2,                ),                              ],            ),          )         ),    );  }   void _choiceAction(){    print('开始选择你喜欢的类型............');    if(typeController.text.toString()==''){      showDialog(        context: context,        builder: (context)=>AlertDialog(title:Text('美女类型不能为空'))      );    }else{        getHttp(typeController.text.toString()).then((val){         setState(() {           showText=val['data']['name'].toString();         });        });    }   }   Future getHttp(String TypeText)async{    try{      Response response;      var data={'name':TypeText};      response = await Dio().get(        "https://www.easy-mock.com/mock/5c60131a4bed3a6342711498/baixing/dabaojian",          queryParameters:data      );      return response.data;    }catch(e){      return print(e);    }  } }
 

**总结:**通过这节课的学习,我们应该掌握如下知识点

  1. 对Flutter动态组件的深入了解
  2. Future对象的使用
  3. 改变状态和界面的setState的方法应用
  4. TextField Widget的基本使用

#第07节:dio基础_POST请求的使用

这节学习一下POST请求的使用,其实POST和Get请求都是在工作中最重要的两种请求。比如我们要传递一组表单数据过去,这时候用Get请求就是不太合适的,使用POST比较好。


#SingleChildScrollView Widget

在学习新内容之前,先来填一个昨天的坑,其实昨天的代码在最后演示是,是由一个异常的,异常内容如下:

I/flutter ( 6889):   verticalDirection: down I/flutter ( 6889): ◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤ I/flutter ( 6889): ════════════════════════════════════════════════════════════════════════════════════════════════════ 

但是因为上节课已经录制了25分钟,我就没来得及解决这个异常。异常大概就是说你在纵向超出了边界,界面显示不下了,如果你放大看,还会告诉超出了多少像素。这个异常在工作中是经常出现的,解决方案是非常简单。

解决方案:只要在超出的外层包裹一个SingleChildScrollView小部件就可以了,其实它就是一个可以滚动的widget框,没有组件实体(就是你看不出什么UI界面来)。代码如下:

 Widget build(BuildContext context) {    return Container(              child: Scaffold(          appBar: AppBar(title: Text('美好人间'),),          body:SingleChildScrollView(            child: Container(              child: Column(                children: [                  TextField(                    controller:typeController,                    decoration:InputDecoration (                      contentPadding: EdgeInsets.all(10.0),                      labelText: '美女类型',                      helperText: '请输入你喜欢的类型'                    ),                    autofocus: false,                  ),                  RaisedButton(                    onPressed:_choiceAction,                    child: Text('选择完毕'),                  ),                  Text(                    showText,                      overflow:TextOverflow.ellipsis,                      maxLines: 2,                  ),                  ],              ),            )           )        ),    );  }

这时候我们越界的那个警告就已经没有了,我们也可以开心的继续学习了。

#EasyMock动态参数的实现

EasyMock在工作中我使用的也是比较多,因为要和后台同步开发,后台编写慢的时候,就需要我们先自己设置(应该说是模拟)需要的数据。那固定死的mock数据作起来很简单,我就不在这里讲了,动态数据如何处理,我在这里给出代码,视频中会有所讲解。

{  success: true,  data: {    default: "jspang",    _req: function({      _req    }) {      return _req    },    name: function({      _req,      Mock    }) {      if (_req.query.name) {        return _req.query.name + '走进了房间,来为你亲情服务';      } else {        return '随便来个妹子,服务就好';      }    }  } }

视频中我也会带着你建立一个这样的POST接口,如果学习文字版,这部分自己建立吧。总要给上帝一些特权吧。

#Dio的POST使用

其实Post的使用非常简单,主题代码并没有什么改动,只是把原来的get换成Post就可以了。代码如下:

 Future getHttp(String TypeText)async{    try{      Response response;      var data={'name':TypeText};      response = await Dio().post(        "地址隐藏了,地址会单独发送给正版视频者",          queryParameters:data      );      return response.data;    }catch(e){      return print(e);    }  }

我们这样程序就可以继续使用了,我们的大保健程序还是可以完美运行的。

**本节总结:**这节课程所学到的知识点.

  • SingleChildScrollView: SingleChildScrollView小部件的使用技巧。
  • EasyMock动态参数的实现:我们讲解了一个EasyMock动态参数的实现方法。
  • Dio的Post请求: 学会利用dio的post请求。

#第08节:dio基础_伪造请求头获取数据

在很多时候,后端为了安全都会有一些请求头的限制,只有请求头对了,才能正确返回数据。这虽然限制了一些人恶意请求数据,但是对于我们聪明的程序员来说,就是形同虚设。这节课就以极客时间 为例,讲一下通过伪造请求头,来获取极客时间首页主要数据。(不保证接口和安全措施一直可用哦,赶快练习吧)

这节学完,大家就应该知道如何读取别人的端口数据了,比如你学完这个实战课,想自己作一个掘金或者极客时间,这都是很简单的事情了。


#查看极客时间的数据端口

如果你是一个前端,这套流程可能已经烂熟于心,先找出掘金的一个端口,来进行分析。

首先在浏览器端打开掘金网站(我用的是chrome浏览器):https://time.geekbang.org/ ,然后按F12打开浏览器控制台,来到NetWork选项卡,再选择XHR选项卡,这时候刷新页面就会出现异步请求的数据。我们选择newAll这个接口来进行查看。

拷贝地址:https://time.geekbang.org/serv/v1/column/newAll

我们就以这个接口为案例,来获取它的数据。

#非法请求的实现

有了接口,我们把上节课的页面进行一下改造。注意的是,这时候我们并没有设置请求头,为的是演示我们不配置请求头时,是无法获取数据的,它会返回一个451的错误。

451:就是非法请求,你的请求不合法,服务器决绝了请求,也什么都没给我们返回。

代码如下:

import 'package:flutter/material.dart'; import 'package:dio/dio.dart'; 

class HomePage extends StatefulWidget {  _HomePageState createState() => _HomePageState(); }
 class _HomePageState extends State {  String showText='还没有请求数据';  @override  Widget build(BuildContext context) {    return Container(       child: Scaffold(         appBar: AppBar(title: Text('请求远程数据'),),         body: SingleChildScrollView(           child: Column(             children: [               RaisedButton(                 onPressed: _jike,                 child: Text('请求数据'),               ),               Text(showText)             ],           ),         ),       ),    );  }   void _jike(){    print('开始向极客时间请求数据..................');    getHttp().then((val){      setState(() {       showText=val['data'].toString();      });    });  } 
  Future getHttp()async{    try{      Response response;      Dio dio = new Dio();       response =await dio.get("https://time.geekbang.org/serv/v1/column/newAll");      print(response);      return response.data;    }catch(e){      return print(e);    }  } 
} 

这时候我们预览,会返现控制台无情的输出了异常消息。

I/flutter ( 6942): DioError [DioErrorType.RESPONSE]: Http status error [451] E/flutter ( 6942): [ERROR:flutter/shell/common/shell.cc(184)] Dart Error: Unhandled exception:

#伪造请求头

新建一个文件夹,起名叫作config,然后在里边新建一个文件httpHeaders.dart,把请求头设置好,请求头可以在浏览器中轻松获得,获得后需要进行改造。

const httpHeaders={  'Accept': 'application/json, text/plain, */*', 'Accept-Encoding': 'gzip, deflate, br', 'Accept-Language': 'zh-CN,zh;q=0.9', 'Connection': 'keep-alive', 'Content-Type': 'application/json', 'Cookie': '_ga=GA1.2.676402787.1548321037; GCID=9d149c5-11cb3b3-80ad198-04b551d; _gid=GA1.2.359074521.1550799897; _gat=1; Hm_lvt_022f847c4e3acd44d4a2481d9187f1e6=1550106367,1550115714,1550123110,1550799897; SERVERID=1fa1f330efedec1559b3abbcb6e30f50|1550799909|1550799898; Hm_lpvt_022f847c4e3acd44d4a2481d9187f1e6=1550799907', 'Host': 'time.geekbang.org', 'Origin': 'https://time.geekbang.org', 'Referer': 'https://time.geekbang.org/', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36' };

有了请求头文件后,可以修改主体文件,修改就是引入请求头文件,并进行设置,主要代码就这两句。

import '../config/httpHeaders.dart'; dio.options.headers= httpHeaders;

全部代码如下:

import 'package:flutter/material.dart'; import 'package:dio/dio.dart'; import '../config/httpHeaders.dart'; 
class HomePage extends StatefulWidget {  _HomePageState createState() => _HomePageState(); }
 class _HomePageState extends State {  String showText='还没有请求数据';  @override  Widget build(BuildContext context) {    return Container(       child: Scaffold(         appBar: AppBar(title: Text('请求远程数据'),),         body: SingleChildScrollView(           child: Column(             children: [               RaisedButton(                 onPressed: _juejin,                 child: Text('请求数据'),               ),               Text(showText)             ],           ),         ),       ),    );  }   void _juejin(){    print('开始向极客时间请求数据..................');    getHttp().then((val){      setState(() {       showText=val['data'].toString();      });    });  } 
  Future getHttp()async{    try{      Response response;      Dio dio = new Dio();      dio.options.headers= httpHeaders;      response =await dio.get("https://time.geekbang.org/serv/v1/column/newAll");      print(response);      return response.data;    }catch(e){      return print(e);    }  } 
}

现在就可以正常获取数据了。

课程总结: 本节主要学习了Dio中如何通过伪造请求头来获取别人接口的数据,学会了这个是非常有用的,以后我们想自己作练习Demo时就不用为后端接口而犯愁了。当然课程里查看接口的方法比较初级,我们可以使用向Fiddler这样的专用软件来获得接口。因为Fiddler不是课程内容,所以感兴趣的小伙伴就自行学习吧。

#第09节:移动商城数据请求实战(好戏开始)

前几节已经对Dio的基础知识作了讲解,当然Dio还有一些比较高级的用法,这些用法就不单独拿出来讲了,在项目中遇到后再详细讲解。从这节开始,我们来制作商城的首页,那制作商城的首页第一步还是需要从后端接口获取需要使用的记录。


#URL接口管理文件建立

第一步需要在建立一个URL的管理文件,因为课程的接口会一直进行变化,所以单独拿出来会非常方便变化接口。当然工作中的URL管理也是需要这样配置的,以为我们会不断的切换好几个服务器,组内服务器,测试服务器,内测服务器,公测上线服务器。

所以说一定要单独把这个文件配置出来,这也算是一个开发经验之谈吧。

在/lib/config文件夹下,建立一个service_url.dart文件,然后写入如下代码:

const serviceUrl= 'xxxxxx';//此端口针对于正版用户开放,可自行fiddle获取。 const servicePath={  'homePageContext': serviceUrl+'wxmini/homePageContent', // 商家首页信息 };

接口的详细说明文件,我会在文章下方有一个接口文档给大家。以后的接口URL都会放到这个里边。

#接口读取文件和方法的建立

URL的配置管理文件建立好了,接下来需要建立一个数据接口读取的文件,以后所有跟后台请求数据接口的方法,都会放到这个文件里。

有小伙伴会问了,为什么不耦合在UI页面里?这样看起来更直观。其实如果公司人少,耦合在页面里是可以的,而且效率会更高。但是大公司一个项目会有很多人参与,有时候对接后台接口的是专门一个人或者几个人,那这时候把文件单独出来,效率就更高。

那我们尽力贴合大公司的开放流程,所以把这个文件也单独拿出来,便于以后扩展。 新建一个service文件夹,然后建立一个service_method.dart文件。

首先我们引入三个要使用的包和上边写的一个文件文件,代码如下:

import "package:dio/dio.dart"; import 'dart:async'; import 'dart:io'; import '../config/service_url.dart';

然后编写一个getHomePageContent方法,方法返回一个Future对象。具体代码如下:

import "package:dio/dio.dart"; import 'dart:async'; import 'dart:io'; import '../config/service_url.dart'; 

 Future getHomePageContent() async{   try{    print('开始获取首页数据...............');    Response response;    Dio dio = new Dio();    dio.options.contentType=ContentType.parse("application/x-www-form-urlencoded");    var formData = {'lon':'115.02932','lat':'35.76189'};    response = await dio.post(servicePath['homePageContext'],data:formData);    if(response.statusCode==200){      return response.data;    }else{      throw Exception('后端接口出现异常,请检测代码和服务器情况.........');    }  }catch(e){    return print('ERROR:======>${e}');  } 
}

这个就是我们于后端对接的接口,一般在公司需要一个既会前端有懂后端的人来作,这也是为什么好多公司招聘前端时,需要你懂一个后端语言的主要原因(小公司既作前端又作后端的忽略)。 这个文件完成,就可以回答home_page.dart,来获取数据了。

#home_page.dart 获取首页数据

删除学基础知识的所有代码,在home_page.dart里编写真正的项目代码。代码如下,因为这些知识都已经讲过,所以只贴出代码,当然视频中会有非常详细的讲解。

import 'package:flutter/material.dart'; import '../service/service_method.dart'; 

class HomePage extends StatefulWidget {  _HomePageState createState() => _HomePageState(); 
} 
class _HomePageState extends State {  String homePageContent='正在获取数据';  @override  void initState() {    getHomePageContent().then((val){      setState(() {           homePageContent=val.toString();      });          });    super.initState();  }   @override  Widget build(BuildContext context) {    return Scaffold(      appBar: AppBar(        title: Text('百姓生活+'),      ),      body:SingleChildScrollView(        child:  Text(homePageContent) ,      )              );  } }

写完后,就可以使用flutter run进行测试了。如果能读取远程数据,说明我们编写成功。

本节总结:

  • 和后端接口对接的一些实战技巧,这些技巧可以大大增加项目的灵活性和减少维护成本。
  • 真实项目接口数据的获取,为我们的项目提供后端数据支持。

#第10节:使用FlutterSwiper制作轮播效果

已经有了项目需要的数据,只是现在看起来比较乱(一坨一坨的),有很多格式化JSON的方法,这里我就不给大家墨迹了(要不又有人说我骗时长了)。如果说格式化也懒得格式化,你就直接看博客文章后方的API就可以了。如果你API都懒得看,那就泡杯茶,看视频吧。


#引入flutter_swiper插件

flutter最强大的siwiper, 多种布局方式,无限轮播,Android和IOS双端适配.

好牛X得介绍,一般敢用“最”的一般都是神级大神,看到这个介绍后我也是吃了碗贾玲代言的方便面(一桶半),压了压我激动的心情。

Flutter_swiper的GitHub地址:https://github.com/best-flutter/flutter_swiper

了解flutter_swiper后,需要作的第一件事就再pubspec.yaml文件中引入这个插件(录课时flutter_swiper插件的版本文v1.1.4,以后可能会有更新)。

flutter_swiper : ^1.1.4  (记得使用最新版)

引入后再VSCode中保存,会自动为我们下载包。开着点代理,有一次没开代理死活下不下来。

#首页轮播效果的编写

我们新定义一个类,当然你甚至可以新起一个文件,完全独立出来。这样一个页面就可以分为多个类,然后写完后由项目组长统一整合起来。

当然作练习就没必要每一个模块都分一个dart文件了,要不太乱,自己反而降低编写效率。所以就写在同一个目录里了。

首先引入flutter_swiper插件,然后就可以编写自定义轮播类了。

新写了一个SwiperDiy的类,当然这个类用静态类就完全可以了,这个类是需要接受一个List参数的,所以我们定义了一个常量swiperDataList,然后返回一个组件,这个组件其实就是Swiper组件,不过我们在Swiper组件外边包裹了一个Container

代码如下:

// 首页轮播组件编写 class SwiperDiy extends StatelessWidget {  final List swiperDataList;  SwiperDiy({Key key,this.swiperDataList}):super(key:key);   @override  Widget build(BuildContext context) {    return Container(      height: 333.0,      child: Swiper(        itemBuilder: (BuildContext context,int index){          return Image.network("${swiperDataList[index]['image']}",fit:BoxFit.fill);        },        itemCount: swiperDataList.length,        pagination: new SwiperPagination(),        autoplay: true,      ),    );  } }
 

#FutureBuilder Widget

这是一个Flutter内置的组件,是用来等待异步请求的。现在可以使用FuturerBuilder来改造HomePage类里的build方法,具体代码细节在视频中进行讲解。

代码如下:

@override  Widget build(BuildContext context) {     return Scaffold(      appBar: AppBar(title: Text('百姓生活+'),),      body:FutureBuilder(        future:getHomePageContent(),        builder: (context,snapshot){          if(snapshot.hasData){             var data=json.decode(snapshot.data.toString());             List swiperDataList = (data['data']['slides'] as List).cast(); // 顶部轮播组件数             return Column(               children: [                   SwiperDiy(swiperDataList:swiperDataList ),   //页面顶部轮播组件               ],             );          }else{            return Center(              child: Text('加载中'),            );          }        },      )    );   }

有了这个方法,我们就没必要再用initState了,删除了就可以了。

全体代码如下:

import 'package:flutter/material.dart'; import '../service/service_method.dart'; import 'package:flutter_swiper/flutter_swiper.dart'; import 'dart:convert'; 

class HomePage extends StatefulWidget {  _HomePageState createState() => _HomePageState(); 
} 
class _HomePageState extends State {       @override  Widget build(BuildContext context) {     return Scaffold(      appBar: AppBar(title: Text('百姓生活+'),),      body:FutureBuilder(        future:getHomePageContent(),        builder: (context,snapshot){          if(snapshot.hasData){             var data=json.decode(snapshot.data.toString());             List swiperDataList = (data['data']['slides'] as List).cast(); // 顶部轮播组件数             return Column(               children: [                   SwiperDiy(swiperDataList:swiperDataList ),   //页面顶部轮播组件               ],             );          }else{            return Center(              child: Text('加载中'),            );          }        },      )    );   } }
// 首页轮播组件编写 class SwiperDiy extends StatelessWidget {  final List swiperDataList;  SwiperDiy({Key key,this.swiperDataList}):super(key:key);   @override  Widget build(BuildContext context) {    return Container(      height: 333.0,      child: Swiper(        itemBuilder: (BuildContext context,int index){          return Image.network("${swiperDataList[index]['image']}",fit:BoxFit.fill);        },        itemCount: swiperDataList.length,        pagination: new SwiperPagination(),        autoplay: true,      ),    );  } }

课程总结:

  • flutter_Swiper:学习了flutter_swiper组件的简单使用方法,当然你还可以自己学习。
  • FutureBuilder: 这个布局可以很好的解决异步渲染的问,实战中我们讲了很多使用的技巧,注意反复学习。
  • 自定义类接受参数:我们复习了类接受参数的方法。学会了这个技巧就可以把我们的页面分成很多份,让很多人来进行编写,最后再整合到一起。

#第11节:首页_屏幕适配方案和制作

移动端的屏幕大小不一,IOS端就有很多种,Android端更是多如牛毛。美工或UI妹子也会经常,甜甜的问我们:“哥,设计用啥尺寸的?” 作为一个公司的技术和颜值担当,你一定要很轻松的回答这个问题。你回答后会不会心里胆怯,不用怕,学完今天这节课,你就可以轻松的回答这个问题。


#flutter_ScreenUtil插件简介

flutter_ScreenUtil屏幕适配方案,让你的UI在不同尺寸的屏幕上都能显示合理的布局。

插件会让你先设置一个UI稿的尺寸,他会根据这个尺寸,根据不同屏幕进行缩放,能满足大部分屏幕场景。

github:https://github.com/OpenFlutter/flutter_ScreenUtil

目前github的star数是:247

这个轮子功能还不是很完善,但是也在一点点的进步,这也算是国内现在最好的Flutter屏幕适配插件了,又不足的地方你可以自己下载源码进行修改,并使用。

个人觉的今年在国内应该是Flutter的爆发年,也会有更多更好用的插件诞生。

#flutter_ScreenUtil的引入和使用

因为是第三方包,所以还需要在pubspec.yaml文件中进行注册依赖。在填写依赖之前,最好到github上看一下最新版本,因为这个插件也存在着升级后,以前版本不可用的问题。

dependencies:     flutter_screenutil: ^0.5.1

需要注意的是,一定要注意使用最新版本,这个插件版本升级还是挺快的,基本每周都有升级。

需要在每个使用的地方进行导入:

import 'package:flutter_screenutil/flutter_screenutil.dart';

初始化设置尺寸

在使用之前请设置好设计稿的宽度和高度,传入设计稿的宽度和高度,注意单位是px。

我们公司一般会以Iphone6的屏幕尺寸作设计稿,这个习惯完全是当初公司组长的手机是Iphone6的,审核美工稿的时候,可以完美呈现,所以就沿用下来了(我想估计老总的手机早升级了)。

 ScreenUtil.instance = ScreenUtil(width: 750, height: 1334)..init(context);

这句话的引入一定要在有了界面UI树建立以后执行,如果还没有UI树,会报错的。比如我们直接放在类里,就会报错,如果昉在build方法里,就不会报错。

适配尺寸

这时候我们使用的尺寸是px.

  • 根据屏幕宽度适配:width:ScreenUtil().setWidth(540);
  • 根据屏幕高度适配:height:ScreenUtil().setHeight(200);
  • 适配字体大小:fontSize:ScreenUtil().setSp(28,false);

配置字体大小的参数false是不会根据系统的"字体大小"辅助选项来进行缩放。

根据学到的知识,来设置一下昨天的轮播图片问题。

  • 首先在home_page.dart里,用import进行引入。
  • build方法里,初始化设计稿尺寸,ScreenUtil.instance = ScreenUtil(width: 750, height: 1334)..init(context);.
  • Container设置高和宽的值height: ScreenUtil().setHeight(333),width: ScreenUtil().setWidth(750),

全部代码如下:

import 'package:flutter/material.dart'; import '../service/service_method.dart'; import 'package:flutter_swiper/flutter_swiper.dart'; import 'dart:convert'; import 'package:flutter_screenutil/flutter_screenutil.dart'; 

class HomePage extends StatefulWidget {  _HomePageState createState() => _HomePageState(); 
} 
class _HomePageState extends State {       @override  Widget build(BuildContext context) {       return Scaffold(      appBar: AppBar(title: Text('百姓生活+'),),      body:FutureBuilder(        future:getHomePageContent(),        builder: (context,snapshot){          if(snapshot.hasData){             var data=json.decode(snapshot.data.toString());             List swiperDataList = (data['data']['slides'] as List).cast(); // 顶部轮播组件数             return Column(               children: [                   SwiperDiy(swiperDataList:swiperDataList ),   //页面顶部轮播组件               ],             );          }else{            return Center(              child: Text('加载中'),            );          }        },      )    );   } }
// 首页轮播组件编写 class SwiperDiy extends StatelessWidget {  final List swiperDataList;  SwiperDiy({Key key,this.swiperDataList}):super(key:key);   @override  Widget build(BuildContext context) {     ScreenUtil.instance = ScreenUtil(width: 750, height: 1334)..init(context);    return Container(      height: ScreenUtil().setHeight(333),      width: ScreenUtil().setWidth(750),      child: Swiper(        itemBuilder: (BuildContext context,int index){          return Image.network("${swiperDataList[index]['image']}",fit:BoxFit.fill);        },        itemCount: swiperDataList.length,        pagination: new SwiperPagination(),        autoplay: true,      ),    );  } }

写完这个代码以后,可以查看界面的变化,甚至你可以多测试几个手机的效果。查看一下屏幕的适配效果如何。

#API其他属性简介

我们在简单的学习一下ScreenUtil的其他属性,有助于你在工作中的灵活使用。

  • ScreenUtil.pixelRatio : 设备的像素密度
  • ScreenUtil.screenWidth : 设备的宽度
  • ScreenUtil.screenHeight : 设备高度

我们就简单介绍这三个吧,剩下的有些API如果感兴趣,可以到github上自行学习一下。

 ScreenUtil.instance = ScreenUtil(width: 750, height: 1334)..init(context); print('设备宽度:${ScreenUtil.screenWidth}'); print('设备高度:${ScreenUtil.screenHeight}'); print('设备像素密度:${ScreenUtil.pixelRatio}');

重新用大R刷新一下界面,可以看到控制台已经显示出了这三个基本值了。

本节总结:这节课主要学习了使用flutter_ScreenUtil来视频Flutter的APP应用,需要注意的是这个插件再不断升级中,所以使用的时候要使用最新版。

#第12节:首页导航区域编写

导航区是每个APP(爱啪啪,今天同事教我的,我觉的生动形象,充满娱乐性)必备的一个功能。这节课就利用GridView 小部件进行制作,当然制作中我们也会讲到一些布局技巧。


#导航单元素的编写

从外部看,导航是一个GridView部件,但是每一个导航又是一个上下关系的Column。小伙伴们都知道Flutter有多层嵌套的问题,如果我们都写在一个组件里,那势必造成嵌套严重,不利于项目以后的维护工作。所以我们单独把每一个自元素导航拿出来,一个方法,返回一个组件。

代码如下:详细的解释可以观看视频。

class TopNavigator extends StatelessWidget {  final List navigatorList;  TopNavigator({Key key, this.navigatorList}) : super(key: key);   Widget _gridViewItemUI(BuildContext context,item){    return InkWell(      onTap: (){print('点击了导航');},      child: Column(        children: [          Image.network(item['image'],width:ScreenUtil().setWidth(95)),          Text(item['mallCategoryName'])        ],      ),    );  } }

#GridView制作导航

这个制作我们还是在外层嵌套一个Container组件,然后直接使用GridView。代码如下:

  @override  Widget build(BuildContext context) {    return Container(      height: ScreenUtil().setHeight(320),      padding:EdgeInsets.all(3.0),      child: GridView.count(        crossAxisCount: 5,        padding: EdgeInsets.all(4.0),        children: navigatorList.map((item){          return _gridViewItemUI(context, item);        }).toList(),      ),    );  }

需要注意的是children属性,我们使用了map循环,然后再使用toList()进行转换。

#数据处理和Bug解决

HomePagebuild方法里声明一个List变量,然后把数据进行List转换。再调用TopNavigator自定义组件。

 List navigatorList =(data['data']['category'] as List).cast(); //类别列表  TopNavigator(navigatorList:navigatorList),  //导航组件

这时候进行预览界面,你会发现界面有些问题,就是多了一个类别,并不是我们想要的10个列表,其实如果正常,这应该是后端给数据的一个Bug。但是我们是没办法去找后端麻烦的,所以只能自己想办法解决这个问题。

解决的办法就是把List进行截取,方法如下。

 if(navigatorList.length>10){      navigatorList.removeRange(10, navigatorList.length);    } 

这节主要是以导航功能为例子,讲解了一下布局的技巧。其实知识我们都已经在基础部分学过了,主要练习的是我们综合运用的能力。这种能力要多进行练习,你才能在实际项目中灵活布局。

#第13节:ADBanner组件的编写

这节课的内容相对简单一点,只要制作一个广告的Bannder条就可以了。


#AdBanner组件的编写

我们还是把这部分单独出来,需要说明的是,这个Class你也是可以完全独立成一个dart文件的。代码如下:

//广告图片 class AdBanner extends StatelessWidget {  final String advertesPicture;   AdBanner({Key key, this.advertesPicture}) : super(key: key);   @override  Widget build(BuildContext context) {       return Container(      child: Image.network(advertesPicture),    );  } }

#数据准备和调用组件

我们先把广告的图片准备好,准备好后就可以调用图片组件了。

String advertesPicture = data['data']['advertesPicture']['PICTURE_ADDRESS']; //广告图片 
AdBanner(advertesPicture:advertesPicture),   //广告组件  

这时候进行预览就会得到你想要的效果了,这节课虽然很短,但是你要知道一直知识,就是如何把一个复杂的页面,拆解成一个个Widget,这样有助于我们多人的协作开发,适应现在的开发流程。

#说说新版本的升级

我录课的时候使用的是Flutter1.0版本,但这两天正好升级了1.2版本,而且有一些盼望已久的功能,就有很多小伙伴问我,到底该不该升级。

对于升级这个问题我是这样认为的:

  • 学习项目:尽快升级,体验最新的版本,对我们的职业技能非常有好处。
  • 生产项目:谨慎升级,一般生产的正式项目开发周期比较长,使用插件比较多,追求稳定是一项重要工作,所以等版本稳定,插件跟上后,我们再进行升级。

升级方法有两种:

  • 直接在控制台使用flutter upgrade,这种方法需要开启科学上网,如果中途卡死或者出错,可以使用下面的方法。
  • 直接删除原来下载的Flutter SDK,然后下载最新版本,放置到原来SDK的位置,就可以升级成功。

总结:这节课的内容比较少,主要两个方面,一是图片广告的添加,二是关于是否升级最新版本的问题。下节课我们主要讲一下切换

#第14节:首页_拨打电话操作

拨打电话的功能在app里也很常见,比如一般的外卖app都会有这个才做。其实Flutter本身是没给我们提供拨打电话的能力的,那我们如何来拨打电话那?这节课我们就使用url_launcher来制作拨打电话的效果。


#编写店长电话模块

这个小伙伴们一定轻车熟路了,我也就不再多介绍吧。直接看代码,相信都能看懂。

class LeaderPhone extends StatelessWidget {  final String leaderImage; //店长图片  final String leaderPhone; //店长电话   LeaderPhone({Key key, this.leaderImage,this.leaderPhone}) : super(key: key);   @override  Widget build(BuildContext context) {    return Container(      child: InkWell(        onTap: (){},        child: Image.network(leaderImage),      ),    );  } }

获取需要的数据

HomePage里获取获取店长图片和电话数据,并形成变量。

String  leaderImage= data['data']['shopInfo']['leaderImage'];  //店长图片 String  leaderPhone = data['data']['shopInfo']['leaderPhone']; //店长电话 

有了数据之后,就可以调用这个自己写的组件了。调用方法如下:

  LeaderPhone(leaderImage:leaderImage,leaderPhone: leaderPhone)  //广告组件  

#url_launcher的简介

官方介绍:

A Flutter plugin for launching a URL in the mobile platform. Supports iOS and Android.

意思是用于在移动平台中启动URL的Flutter插件,适用于IOS和Android平台。他可以打开网页,发送邮件,还可以拨打电话。

github地址:https://github.com/flutter/plugins/tree/master/packages/url_launcher

引入依赖

pubspec.yaml文件里注册依赖,并保存下载包。请注意使用最新版。

url_launcher: ^5.0.1

在需要使用的页面在使用import引入具体的url_launcher包。

import 'package:url_launcher/url_launcher.dart';

#改造店长电话组件

有了url_launcher插件就后,我们就可以实现拨打电话功能了,不过要简单的改造一下拨打电话模块的代码,改造后的代码如下。

class LeaderPhone extends StatelessWidget {  final String leaderImage; //店长图片  final String leaderPhone; //店长电话   LeaderPhone({Key key, this.leaderImage,this.leaderPhone}) : super(key: key);   @override  Widget build(BuildContext context) {    return Container(      child: InkWell(        onTap:_launchURL,        child: Image.network(leaderImage),      ),    );  }   void _launchURL() async {    String url = 'tel:'+leaderPhone;    if (await canLaunch(url)) {      await launch(url);    } else {      throw 'Could not launch $url';    }  } }

这时候就可以打开虚拟机进行调试了,需要说的是,有些虚拟机并出不来拨打电话的效果,如果你的虚拟机出不来这个效果,可以使用真机进行测试。

本节总结 :本节主要学习了使用url_launcher来进行打开网页和拨打电话的设置。希望小伙伴们都有所收获。

#第15节:商品推荐区域制作

简单的部门就适当省略些,中间放图片的步骤就省略点了,这节课学习一下商品推荐这个部分的编写。这个部分是一个横向列表,而且为了避免嵌套,所以要把个个组件进行内部拆分。


#超出边界的处理方法

其实这个操作已经讲过,但是技术胖在编写的时候还是没有进行此步设置,我的锅,我自己背。其实我们只要使用SingleChildScrollView widget就可以了。把这个widget放到我们主build里的Column外边就可以了。

其实这时候我们给自己以后的ListView组件埋了一个坑。

具体代码如下:

return SingleChildScrollView(  child: Column(  children: [      SwiperDiy(swiperDataList:swiperDataList ),   //页面顶部轮播组件      TopNavigator(navigatorList:navigatorList),   //导航组件      AdBanner(advertesPicture:advertesPicture),       LeaderPhone(leaderImage:leaderImage,leaderPhone: leaderPhone),  //广告组件      ], ) , );

#推荐商品类的编写

这个类接收一个List参数,就是推荐商品的列表,这个列表是可以左右滚动的。

//商品推荐 class Recommend extends StatelessWidget {  final List  recommendList;   Recommend({Key key, this.recommendList}) : super(key: key); }

#推荐标题内部方法的编写

因为实际开发中,要尽量减少嵌套,所以我们需要把复杂的组件,单独拿出来一个方法进行编写。这里就把标题单独拿出来进行编写。

 //推荐商品标题  Widget _titleWidget(){     return Container(       alignment: Alignment.centerLeft,       padding: EdgeInsets.fromLTRB(10.0, 2.0, 0,5.0),       decoration: BoxDecoration(         color:Colors.white,         border: Border(           bottom: BorderSide(width:0.5,color:Colors.black12)         )       ),       child:Text(         '商品推荐',         style:TextStyle(color:Colors.pink)         )     );  }

#推荐商品单独项编写

把推荐商品的每一个子项我们也分离出来。每一个子项都使用InkWell,这样为以后的页面导航作准备。里边使用了Column,把内容分成三行。

具体代码:

  Widget _item(index){    return InkWell(      onTap: (){},      child: Container(        height: ScreenUtil().setHeight(330),        width: ScreenUtil().setWidth(250),        padding: EdgeInsets.all(8.0),        decoration:BoxDecoration(          color:Colors.white,          border:Border(            left: BorderSide(width:0.5,color:Colors.black12)          )        ),        child: Column(          children: [            Image.network(recommendList[index]['image']),            Text('¥${recommendList[index]['mallPrice']}'),            Text(              '¥${recommendList[index]['price']}',              style: TextStyle(                decoration: TextDecoration.lineThrough,                color:Colors.grey              ),            )          ],        ),      ),    );  }

#横向列表组件的编写

横向列表组件也进行单独编写,以减少嵌套,这样我们就把每一个重要的部分都进行了分离。这种分离技巧,小伙伴们一定要掌握,这在工作中非常重要。

  Widget _recommedList(){       return Container(        height: ScreenUtil().setHeight(330),        child: ListView.builder(          scrollDirection: Axis.horizontal,          itemCount: recommendList.length,          itemBuilder: (context,index){            return _item(index);          },        ),      );  }

有了这三个基本组件,最后我们在build方法里进行组合,形成商品推荐区域。

 @override  Widget build(BuildContext context) {    return Container(       height: ScreenUtil().setHeight(380),       margin: EdgeInsets.only(top: 10.0),       child: Column(         children: [           _titleWidget(),           _recommedList()         ],       ),    );  } 

整个组件的类代码如下

//商品推荐 class Recommend extends StatelessWidget {  final List  recommendList;   Recommend({Key key, this.recommendList}) : super(key: key);   @override  Widget build(BuildContext context) {    return Container(       height: ScreenUtil().setHeight(380),       margin: EdgeInsets.only(top: 10.0),       child: Column(         children: [           _titleWidget(),           _recommedList()         ],       ),    );  } 
//推荐商品标题  Widget _titleWidget(){     return Container(       alignment: Alignment.centerLeft,       padding: EdgeInsets.fromLTRB(10.0, 2.0, 0,5.0),       decoration: BoxDecoration(         color:Colors.white,         border: Border(           bottom: BorderSide(width:0.5,color:Colors.black12)         )       ),       child:Text(         '商品推荐',         style:TextStyle(color:Colors.pink)         )     );  }   Widget _recommedList(){       return Container(        height: ScreenUtil().setHeight(330),        child: ListView.builder(          scrollDirection: Axis.horizontal,          itemCount: recommendList.length,          itemBuilder: (context,index){            return _item(index);          },        ),      );  }   Widget _item(index){    return InkWell(      onTap: (){},      child: Container(        height: ScreenUtil().setHeight(330),        width: ScreenUtil().setWidth(250),        padding: EdgeInsets.all(8.0),        decoration:BoxDecoration(          color:Colors.white,          border:Border(            left: BorderSide(width:0.5,color:Colors.black12)          )        ),        child: Column(          children: [            Image.network(recommendList[index]['image']),            Text('¥${recommendList[index]['mallPrice']}'),            Text(              '¥${recommendList[index]['price']}',              style: TextStyle(                decoration: TextDecoration.lineThrough,                color:Colors.grey              ),            )          ],        ),      ),    );  } }

#准备数据并进行调用

随着大家越来越熟练的使用,这部分没什么好讲的了。直接上代码:

 List recommendList = (data['data']['recommend'] as List).cast(); // 商品推荐 Recommend(recommendList:recommendList),    

本节总结:这节主要制作了商品推荐区域的制作,知识点可能都是我们以前学过的,但是要重点练习一下如何练习对组件的拆分能力。当你掌握了这种能力后,你会发现Flutter真的很好用,我们只需要Dart这一种语言,就可以编写页面和前台的业务逻辑。不再像使用前端技术时,要回js、html、css还要会框架。 个人感觉使用一种语言来作全部事情,是爽歪歪的。

#第16节:补充_切换后页面状态的保持

这节课算是一个补充,因为这几天一直有小伙伴问我在底部导航栏切换的时候,我作的程序页面并没有保持页面结果,就是每次切换都需要重新加载。这节课我们就来解决一下这个问题。


#上节课遗留的一个而问题

上节课我们虽然做出了效果,但是在模拟器上看是有一些问题的,就是模拟器纵向显示0.5的线支持的不好。所以我们改位1,试一下效果。

改为1,这个问题就应该解决了。

#AutomaticKeepAliveClientMixin

AutomaticKeepAliveClientMixin这个Mixin就是Flutter为了保持页面设置的。哪个页面需要保持页面状态,就在这个页面进行混入。

不过使用使用这个Mixin是有几个先决条件的:

  • 使用的页面必须是StatefulWidget,如果是StatelessWidget是没办法办法使用的。
  • 其实只有两个前置组件才能保持页面状态:PageViewIndexedStack
  • 重写wantKeepAlive方法,如果不重写也是实现不了的。

如果你还不明白什么是混入,可以看技术胖的那个基础文章《20个Flutter实例视频教程 让你轻松上手工作》 有对混入的详细介绍,这里我就不重复讲解了。

#修改index_page.dart

明白基本知识之后,就可以修改index_page.dart,思路就是增加一个IndexedStack包裹在tabBodies外边。

整体代码如下:

import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'home_page.dart'; import 'category_page.dart'; import 'cart_page.dart'; import 'member_page.dart'; 

class IndexPage extends StatefulWidget {  _IndexPageState createState() => _IndexPageState(); }
 class _IndexPageState extends State{    PageController _pageController; 
  final List bottomTabs = [    BottomNavigationBarItem(      icon:Icon(CupertinoIcons.home),      title:Text('首页')    ),    BottomNavigationBarItem(      icon:Icon(CupertinoIcons.search),      title:Text('分类')    ),    BottomNavigationBarItem(      icon:Icon(CupertinoIcons.shopping_cart),      title:Text('购物车')    ),     BottomNavigationBarItem(      icon:Icon(CupertinoIcons.profile_circled),      title:Text('会员中心')    ),  ];  final List tabBodies = [    HomePage(),    CategoryPage(),    CartPage(),    MemberPage()  ];  int currentIndex= 0;  var currentPage ;  @override  void initState() {   currentPage=tabBodies[currentIndex];   _pageController=new PageController()      ..addListener(() {        if (currentPage != _pageController.page.round()) {          setState(() {            currentPage = _pageController.page.round();          });        }  }); 
    super.initState();  }  @override  Widget build(BuildContext context) {    return Scaffold(      backgroundColor: Color.fromRGBO(244, 245, 245, 1.0),      bottomNavigationBar: BottomNavigationBar(        type:BottomNavigationBarType.fixed,        currentIndex: currentIndex,        items:bottomTabs,        onTap: (index){          setState(() {           currentIndex=index;            currentPage =tabBodies[currentIndex];           });                   },      ),      body: IndexedStack(        index: currentIndex,        children: tabBodies      )    );  } }
 

代码虽然很长,但是改动的部分并不多。具体看视频吧,真的不好描述(文笔蹩脚,继续努力)。

#加入Mixin保持页面状态

home_page.dart里加入AutomaticKeepAliveClientMixin混入,加入后需要重写wantKeepAlive方法。主要代码如下:

class _HomePageState extends State with AutomaticKeepAliveClientMixin {    @override  bool get wantKeepAlive =>true; }

为了检验结果,我们在 _HomePageState里增加一个initState,在里边print一些内容,如果内容输出了,证明我们的页面重新加载了,如果没输出,证明我们的页面保持了状态。

@override  void initState() {    super.initState();     print('111111111111111111111111111');  }

本节总结:这节课主要是回答网页在学习中遇到的页面保持状态的问题。

#第17节:首页_楼层区域的编写

这节课主要学习一下楼层区域的编写,楼层目前是有3层的,而且布局都比较特殊,但每个楼层都是一样的,只是商品图片不同,那就可以把每个楼层抽象为一个部件,这样可以减少维护成本。


#编写楼层标题组件

这个组件编写起来非常容易,就是接收一个图片地址,然后显示图片。代码如下:

class FloorTitle extends StatelessWidget {  final String picture_address; // 图片地址  FloorTitle({Key key, this.picture_address}) : super(key: key);   @override  Widget build(BuildContext context) {    return Container(      padding: EdgeInsets.all(8.0),      child: Image.network(picture_address),    );  } }

#楼层商品组件的编写

在编写楼层商品组件时,我们要对它详细的拆分,我们把一个组件拆分成如下内部方法。

  • _goodsItem:每个商品的子项,也算是这个类的最小模块了。
  • _firstRow:前三个商品的组合,是一个Row组件。
  • _otherGoods:其它商品的组合,也是一个Row组件。

总后把这些组件通过Column合起来。总代码如下:

//楼层商品组件 class FloorContent extends StatelessWidget {  final List floorGoodsList;   FloorContent({Key key, this.floorGoodsList}) : super(key: key);   @override  Widget build(BuildContext context) {    return Container(      child: Column(        children: [          _firstRow(),          _otherGoods()        ],      ),    );  }   Widget _firstRow(){    return Row(      children: [        _goodsItem(floorGoodsList[0]),        Column(          children: [           _goodsItem(floorGoodsList[1]),           _goodsItem(floorGoodsList[2]),          ],        )      ],    );  }   Widget _otherGoods(){    return Row(      children: [       _goodsItem(floorGoodsList[3]),       _goodsItem(floorGoodsList[4]),      ],    );  }   Widget _goodsItem(Map goods){     return Container(      width:ScreenUtil().setWidth(375),      child: InkWell(        onTap:(){print('点击了楼层商品');},        child: Image.network(goods['image']),      ),    );  } 
}

#数据的准备

不多说了,一次性全部写出来。

    String floor1Title =data['data']['floor1Pic']['PICTURE_ADDRESS'];//楼层1的标题图片    String floor2Title =data['data']['floor2Pic']['PICTURE_ADDRESS'];//楼层1的标题图片    String floor3Title =data['data']['floor3Pic']['PICTURE_ADDRESS'];//楼层1的标题图片    ist floor1 = (data['data']['floor1'] as List).cast(); //楼层1商品和图片     List floor2 = (data['data']['floor2'] as List).cast(); //楼层1商品和图片     List floor3 = (data['data']['floor3'] as List).cast(); //楼层1商品和图片         return SingleChildScrollView(      child: Column(      children: [          SwiperDiy(swiperDataList:swiperDataList ),   //页面顶部轮播组件          TopNavigator(navigatorList:navigatorList),   //导航组件          AdBanner(advertesPicture:advertesPicture),           LeaderPhone(leaderImage:leaderImage,leaderPhone: leaderPhone),  //广告组件            Recommend(recommendList:recommendList),              FloorTitle(picture_address:floor1Title),          FloorContent(floorGoodsList:floor1),          FloorTitle(picture_address:floor2Title),          FloorContent(floorGoodsList:floor2),          FloorTitle(picture_address:floor3Title),          FloorContent(floorGoodsList:floor3),        ],        ) ,    ); 

本节总结:这节课学习了楼层组件的制作,并进行了复用。

#第18节:首页_火爆专区商品接口制作

这节课我们开始读取火爆专区部分的接口,这个接口制作起来还是稍微有一些麻烦的,比如他里边有上拉加载更多数据这样的操作。


#接口初探

使用Fiddler可以看到火爆专区的商品接口为homePageBelowConten,接收一个page参数,接口类型为post类型。有了这些最进本的信息,就可以先到项目中的接口管理文件lib/config/servic.dart来设置接口。

代码如下:

const servicePath={  'homePageContext': serviceUrl+'wxmini/homePageContent', // 商家首页信息  'homePageBelowConten': serviceUrl+'wxmini/homePageBelowConten', //商城首页热卖商品拉取 };

因为随着项目的制作,接口越来越多,所以一定要做好注释工作。

#获取接口的方法

service/service_method.dart里制作方法。我们先不接收参数,先把接口调通。

//获得火爆专区商品的方法 Future getHomePageBeloConten() async{   try{    print('开始获取下拉列表数据.................');    Response response;    Dio dio = new Dio();    dio.options.contentType=ContentType.parse("application/x-www-form-urlencoded");    int page=1;    response = await dio.post(servicePath['homePageBelowConten'],data:page);    if(response.statusCode==200){      return response.data;    }else{      throw Exception('后端接口出现异常,请检测代码和服务器情况.........');    }  }catch(e){      return print('ERROR:======>${e}');  } 
}

#进行调试接口

接口对接的方法写好了,然后我们进行测试一下接口是否可以读出数据,如果能读出数据,就说明接口已经调通,我们就可以搞事情了。

因为这个新的类是由下拉刷新的,也就是动态的类,所以需要使用StatefulWidget

代码如下:

class HotGoods extends StatefulWidget {  _HotGoodsState createState() => _HotGoodsState(); }
 class _HotGoodsState extends State { 
   void initState() {      super.initState();      getHomePageBeloConten().then((val){         print(val);      });   }      @override  Widget build(BuildContext context) {    return Container(       child:Text('1111'),    );  } }

#精简代码,来个通用接口

在写service_method.dart的时候,你会发现我们大部分的代码都是相同的,甚至复制一个方法后,通过简单的修改几个地方,就可以使用了。那就说明这个地方由优化的必要。让代码更通用更精简。

精简代码如下:

Future request(url,formData)async{    try{      print('开始获取数据...............');      Response response;      Dio dio = new Dio();      dio.options.contentType=ContentType.parse("application/x-www-form-urlencoded");      if(formData==null){          response = await dio.post(servicePath[url]);      }else{          response = await dio.post(servicePath[url],data:formData);      }      if(response.statusCode==200){        return response.data;      }else{          throw Exception('后端接口出现异常,请检测代码和服务器情况.........');      }    }catch(e){        return print('ERROR:======>${e}');    }     } 

使用也是非常简单的,只要传递一个接口名称和相对参数就可以了。

request('homePageBelowConten',1).then((val){         print(val);      });

本节总结:这节主要学习了火爆专区的接口,并进行了调试和优化。主要知识点是对dio方法的优化,这样就可以大大减少代码量。

#第19节:首页_火爆专区界面制作

上节课已经调通了后端接口,这节课我们把火爆专区的页面制作一下,然后再制作上拉加载效果。


#Dart中可选参数的设置

上节课在作通用方法的时候,我们的参数使用了一个必选参数,其实我们可以使用一个可选参数。Dart中的可选参数,直接使用“{}”(大括号)就可以了。可选参数在调用的时候必须使用paramName:value的形式。

我们把上节课的后端接口代码改为如下:

Future request(url,{formData})async{    try{      print('开始获取数据...............');      Response response;      Dio dio = new Dio();      dio.options.contentType=ContentType.parse("application/x-www-form-urlencoded");      if(formData==null){                  response = await dio.post(servicePath[url]);      }else{          response = await dio.post(servicePath[url],data:formData);      }      if(response.statusCode==200){        return response.data;      }else{          throw Exception('后端接口出现异常,请检测代码和服务器情况.........');      }    }catch(e){        return print('ERROR:======>${e}');    }     } 

然后调用的时候,采用的方式是request('homePageBelowConten',formData:formPage),这样就可以实现可选参数了。

#读取火爆专区数据

我们先声明两个变量,一个是火爆专区的商品列表数据,一个是当前的页数。

  int page = 1;  List hotGoodsList=[];

声明好变量后,我们就可以写一个获取数据的方法了。

//火爆商品接口  void _getHotGoods(){     var formPage={'page': page};     request('homePageBelowConten',formData:formPage).then((val){              var data=json.decode(val.toString());       List newGoodsList = (data['data'] as List ).cast();       setState(() {         hotGoodsList.addAll(newGoodsList);         page++;        });                 });  } 

做好方法后,再initState方法里执行,就会得到数据了。

#火爆专区标题编写

火爆专区,我们先采用State的原始方法,来进行制作,因为这也是很多小伙伴要求的,所以我们主要讲解一下StatefulWidget的使用。下次我们写分类页面的时候会用Redux的方法,以为StatefulWidget的方法会让程序耦合性很强,不利于以后程序的维护。

因为首页我们采用StatefulWidget的方法,所以把标题写在内部。这次我们不采用方法返回Widget的方法了,而是采用变量的方法。

代码如下:

//火爆专区标题  Widget hotTitle= Container(        margin: EdgeInsets.only(top: 10.0),                padding:EdgeInsets.all(5.0),        alignment:Alignment.center,        decoration: BoxDecoration(          color: Colors.white,          border:Border(            bottom: BorderSide(width:0.5 ,color:Colors.black12)          )        ),        child: Text('火爆专区'),   );  

#Warp流式布局的使用

当看到下面的火爆商品列表时,很多小伙伴会想到GridView Widget ,其实GridView组件的性能时很低的,毕竟网格的绘制不难么简单,所以这里使用了Warp来进行布局。Warp是一种流式布局。

可以先把火爆专区数据作成List,然后再进行Warp布局。

//火爆专区子项  Widget _wrapList(){     if(hotGoodsList.length!=0){       List listWidget = hotGoodsList.map((val){                    return InkWell(            onTap:(){print('点击了火爆商品');},            child:             Container(              width: ScreenUtil().setWidth(372),              color:Colors.white,              padding: EdgeInsets.all(5.0),              margin:EdgeInsets.only(bottom:3.0),              child: Column(                children: [                  Image.network(val['image'],width: ScreenUtil().setWidth(375),),                  Text(                    val['name'],                    maxLines: 1,                    overflow:TextOverflow.ellipsis ,                    style: TextStyle(color:Colors.pink,fontSize: ScreenUtil().setSp(26)),                  ),                  Row(                    children: [                      Text('¥${val['mallPrice']}'),                      Text(                        '¥${val['price']}',                        style: TextStyle(color:Colors.black26,decoration: TextDecoration.lineThrough),                                              )                    ],                  )                ],              ),             )                     );       }).toList();       return Wrap(        spacing: 2,        children: listWidget,      );    }else{      return Text(' ');    }  } 

有了标题和商品列表组件,我们就可以把这两个组件组合起来了,当然你不组合也是完全可以的。

  //火爆专区组合  Widget _hotGoods(){     return Container(                    child:Column(            children: [              hotTitle,               _wrapList(),            ],          )       );  }

#第20节:首页上拉加载更多功能的制作

这节课学习一下上拉加载效果,其实现在上拉加载的插件有很多,但是还没有一个插件可以说完全一枝独秀,我也找了一个插件,这个插件的优点就是服务比较好,作者能及时回答大家的问题。我觉的选插件也是选人,人对了,插件就对了。


#flutter_easyrefresh简介

flutter_easyrefresh官方简介:

正如名字一样,EasyRefresh很容易就能在Flutter应用上实现下拉刷新以及上拉加载操作,它支持几乎所有的Flutter控件,但前提是需要包裹成ScrollView。它的功能与Android的SmartRefreshLayout很相似,同样也吸取了很多三方库的优点。EasyRefresh中集成了多种风格的Header和Footer,但是它并没有局限性,你可以很轻松的自定义。使用Flutter强大的动画,甚至随便一个简单的控件也可以完成。EasyRefresh的目标是为Flutter打造一个强大,稳定,成熟的下拉刷新框架。

flutter_easyrefresh优点:

  • 能够自定义酷炫的Header和Footer,也就是上拉和下拉的效果。
  • 更新及时,不断在完善,录课截至时已经是v1.2.7版本了。
  • 有一个辅导群,虽然文档不太完善,但是有辅导群和详细的案例。
  • 回掉方法简单,这个具体可以看下面的例子。

引入依赖

直接在pubspec.yaml中的dependencies中进行引入,主要要用最新版本,文章中的版本不一定是最新版本。

dependencies: flutter_easyrefresh: ^1.2.7 

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

import 'package:flutter_easyrefresh/easy_refresh.dart';

#制作上拉加载效果

使用这个插件,要求我们必须是一个ListView,所以我们要改造以前的代码,改造成ListView。

  return EasyRefresh(      child: ListView(        children: [            SwiperDiy(swiperDataList:swiperDataList ),   //页面顶部轮播组件            TopNavigator(navigatorList:navigatorList),   //导航组件            AdBanner(advertesPicture:advertesPicture),             LeaderPhone(leaderImage:leaderImage,leaderPhone: leaderPhone),  //广告组件              Recommend(recommendList:recommendList),                FloorTitle(picture_address:floor1Title),            FloorContent(floorGoodsList:floor1),            FloorTitle(picture_address:floor2Title),            FloorContent(floorGoodsList:floor2),            FloorTitle(picture_address:floor3Title),            FloorContent(floorGoodsList:floor3),            _hotGoods(),                      ],    ) ,    loadMore: ()async{        print('开始加载更多');        var formPage={'page': page};        await  request('homePageBelowConten',formData:formPage).then((val){          var data=json.decode(val.toString());          List newGoodsList = (data['data'] as List ).cast();          setState(() {            hotGoodsList.addAll(newGoodsList);            page++;           });        });      },  );    }else{  return Center(    child: Text('加载中'),      ); }

具体的解释我就在视频中进行了,因为这个还是比较复杂的。

#自定义上拉加载效果

因为它自带的样式是蓝色的,与我们的界面不太相符,所以我们改造一下,它的底部上拉刷新效果。如果你有兴趣做出更炫酷的效果,可以自行查看一下Github,学习一下。

  refreshFooter: ClassicsFooter(        key:_footerKey,        bgColor:Colors.white,        textColor: Colors.pink,        moreInfoColor: Colors.pink,        showMore: true,        noMoreText: '',        moreInfo: '加载中',        loadReadyText:'上拉加载....'       ),

做到这步我们需要进行调试一下,然后看一下我们的效果。

本节总结:这节课主要学习了easy_refresh组件的介绍和使用,并结合项目案例做出了上拉加载的效果。

#第21节:列表页类别数据接口调试

首页的内容我们先告一段落,从这节课开始制作列表页。当然列表页也是这套教程的一个难点。但是小伙伴们也不要为难情绪,我们也会从简到难,逐步讲解。

从这个页面开始,我们的课程也会加大难度,比如数据全部要model和状态要使用bloc来管理。


#禁止滑动的设置

上节课完成了上拉加载,但是小伙伴可能没发现一个小BUG,就是我们的首页导航区域采用了GridView,这个和我们的ListView上拉加载是冲突的,我们的组件没有智能到为我们辨认,所以我们可以直接禁用GridView的滚动。代码如下

  physics: NeverScrollableScrollPhysics(),

#接口文件编写

一个新的接口,需要把这个接口配置放到/config/servvice_url.dart文件中。记得写注释。

 'getCategory': serviceUrl+'wxmini/getCategory', //商品类别信息

添加完成侯,就可以直接在catgoery_page.dart中进行使用了。为什么可以直接使用那?因为已经在/servic/service_method.dart中写了一个通用的方法。

#测试接口的可用性

后台接口部分写完,需要作的第一件事就是测试接口是否可用,因为我也不能保证接口的完全可用。所以我希望大家能掌握这种最简单的测试方法。可用后我们再作后续操作,这样能减少代码调试的难度。

重新改写catgory_page.dart文件,先引入需要的dart文件。

import 'package:flutter/material.dart'; import '../service/service_method.dart'; import 'dart:convert'; 

有了引入后,用快速方法生成一个StatefulWidget,再删除一些无用的代码。代码如下:

class CategoryPage extends StatefulWidget {  _CategoryPageState createState() => _CategoryPageState(); }
 class _CategoryPageState extends State {   @override  Widget build(BuildContext context) {    _getCategory();    return Container(        child: Center(          child: Text('分类页面'),        ),    );  } }

然后在_CategoryPageState中加入一个内部方法,这个内部方法就是为了测试一下接口。(注意这就是一个最简单的方法)

  void _getCategory()async{    await request('getCategory').then((val){          var data = json.decode(val.toString());          print(data);    });  } 

方法写完后,我们在build方法里直接使用就可以了。

  @override  Widget build(BuildContext context) {    _getCategory();    return Container(        child: Center(          child: Text('sssss'),        ),    );  }

课程总结:本节课程内容虽然较少,只是为了调通数据接口,所以也是课程必要环境,希望小伙伴们一定要课后练习。

#第22节:JSON解析与复杂模型转换技巧

其实转换成model类是有好处的,转换后可以减少上线后APP崩溃和出现异常,所以我们从这节课开始,要制作model类模型,然后用model的形式编辑UI界面。在这里我不讨论两种方法的好坏,这就跟你看小电影是喜欢看欧美还是喜欢看岛国的一样,欧美的可能粗狂豪爽一点,岛国的优美婉约一点。


比如现在从后台得到了一串JSON数据:

{"code":"0","message":"success","data":[{"mallCategoryId":"4","mallCategoryName":"白酒","bxMallSubDto":[{"mallSubId":"2c9f6c94621970a801626a35cb4d0175","mallCategoryId":"4","mallSubName":"名酒","comments":""},{"mallSubId":"2c9f6c94621970a801626a363e5a0176","mallCategoryId":"4","mallSubName":"宝丰","comments":""},{"mallSubId":"2c9f6c94621970a801626a3770620177","mallCategoryId":"4","mallSubName":"北京二锅头","comments":""},{"mallSubId":"2c9f6c94679b4fb10167f7cc035c15a8","mallCategoryId":"4","mallSubName":"大明","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7cc2af915a9","mallCategoryId":"4","mallSubName":"杜康","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7cc535115aa","mallCategoryId":"4","mallSubName":"顿丘","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7cc77b215ab","mallCategoryId":"4","mallSubName":"汾酒","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7cca72e15ac","mallCategoryId":"4","mallSubName":"枫林","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7cccae215ad","mallCategoryId":"4","mallSubName":"高粱酒","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7ccf0d915ae","mallCategoryId":"4","mallSubName":"古井","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7cd1d6715af","mallCategoryId":"4","mallSubName":"贵州大曲","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7cd3f2815b0","mallCategoryId":"4","mallSubName":"国池","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7cd5d3015b1","mallCategoryId":"4","mallSubName":"国窖","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7cd7ced15b2","mallCategoryId":"4","mallSubName":"国台","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7cd9b9015b3","mallCategoryId":"4","mallSubName":"汉酱","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7cdbfd215b4","mallCategoryId":"4","mallSubName":"红星","comments":null},{"mallSubId":"2c9f6c946891d16801689474e2a70081","mallCategoryId":"4","mallSubName":"怀庄","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7cdddf815b5","mallCategoryId":"4","mallSubName":"剑南春","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7cdfd4815b6","mallCategoryId":"4","mallSubName":"江小白","comments":null},{"mallSubId":"2c9f6c94679b4fb1016802277c37160e","mallCategoryId":"4","mallSubName":"金沙","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7ce207015b7","mallCategoryId":"4","mallSubName":"京宫","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7ce46d415b8","mallCategoryId":"4","mallSubName":"酒鬼","comments":null},{"mallSubId":"2c9f6c94679b4fb101680226de23160d","mallCategoryId":"4","mallSubName":"口子窖","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7ce705515b9","mallCategoryId":"4","mallSubName":"郎酒","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7ce989e15ba","mallCategoryId":"4","mallSubName":"老口子","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7cec30915bb","mallCategoryId":"4","mallSubName":"龙江家园","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7cef15c15bc","mallCategoryId":"4","mallSubName":"泸州","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7cf156f15bd","mallCategoryId":"4","mallSubName":"鹿邑大曲","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7cf425b15be","mallCategoryId":"4","mallSubName":"毛铺","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7cf9dc915c0","mallCategoryId":"4","mallSubName":"绵竹","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7cfbf1c15c1","mallCategoryId":"4","mallSubName":"难得糊涂","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7cfdd7215c2","mallCategoryId":"4","mallSubName":"牛二爷","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7cf71e715bf","mallCategoryId":"4","mallSubName":"茅台","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7d7eda715c3","mallCategoryId":"4","mallSubName":"绵竹","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7d96e5c15c4","mallCategoryId":"4","mallSubName":"难得糊涂","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7dab93b15c5","mallCategoryId":"4","mallSubName":"牛二爷","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7dae16415c6","mallCategoryId":"4","mallSubName":"牛栏山","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7db11cb15c7","mallCategoryId":"4","mallSubName":"前门","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7db430c15c8","mallCategoryId":"4","mallSubName":"全兴","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7db6cac15c9","mallCategoryId":"4","mallSubName":"舍得","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7db9a4415ca","mallCategoryId":"4","mallSubName":"双沟","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7dc30b815cb","mallCategoryId":"4","mallSubName":"水井坊","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7dc543e15cc","mallCategoryId":"4","mallSubName":"四特","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7dc765c15cd","mallCategoryId":"4","mallSubName":"潭酒","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7dc988a15ce","mallCategoryId":"4","mallSubName":"沱牌","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7dcba5a15cf","mallCategoryId":"4","mallSubName":"五粮液","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7dcd9e815d0","mallCategoryId":"4","mallSubName":"西凤","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7dcf6d715d1","mallCategoryId":"4","mallSubName":"习酒","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7dd11b215d2","mallCategoryId":"4","mallSubName":"小白杨","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7dd2f3c15d3","mallCategoryId":"4","mallSubName":"洋河","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7dd969115d4","mallCategoryId":"4","mallSubName":"伊力特","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7ddb16c15d5","mallCategoryId":"4","mallSubName":"张弓","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7ddd6c715d6","mallCategoryId":"4","mallSubName":"中粮","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7de126815d7","mallCategoryId":"4","mallSubName":"竹叶青","comments":null}],"comments":null,"image":"http://images.baixingliangfan.cn/firstCategoryPicture/20190131/20190131170036_4477.png"},{"mallCategoryId":"1","mallCategoryName":"啤酒","bxMallSubDto":[{"mallSubId":"2c9f6c946016ea9b016016f79c8e0000","mallCategoryId":"1","mallSubName":"百威","comments":""},{"mallSubId":"2c9f6c94608ff843016095163b8c0177","mallCategoryId":"1","mallSubName":"福佳","comments":""},{"mallSubId":"402880e86016d1b5016016db9b290001","mallCategoryId":"1","mallSubName":"哈尔滨","comments":""},{"mallSubId":"402880e86016d1b5016016dbff2f0002","mallCategoryId":"1","mallSubName":"汉德","comments":""},{"mallSubId":"2c9f6c946449ea7e01647cd6830e0022","mallCategoryId":"1","mallSubName":"崂山","comments":""},{"mallSubId":"2c9f6c946449ea7e01647cd706a60023","mallCategoryId":"1","mallSubName":"林德曼","comments":""},{"mallSubId":"2c9f6c94679b4fb10167f7e1411b15d8","mallCategoryId":"1","mallSubName":"青岛","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7e1647215d9","mallCategoryId":"1","mallSubName":"三得利","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7e182e715da","mallCategoryId":"1","mallSubName":"乌苏","comments":null},{"mallSubId":"2c9f6c9468118c9c016811ab16bf0001","mallCategoryId":"1","mallSubName":"雪花","comments":null},{"mallSubId":"2c9f6c9468118c9c016811aa6f440000","mallCategoryId":"1","mallSubName":"燕京","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7e19b8f15db","mallCategoryId":"1","mallSubName":"智美","comments":null}],"comments":null,"image":"http://images.baixingliangfan.cn/firstCategoryPicture/20190131/20190131170044_9165.png"},{"mallCategoryId":"2","mallCategoryName":"葡萄酒","bxMallSubDto":[{"mallSubId":"2c9f6c9460337d540160337fefd60000","mallCategoryId":"2","mallSubName":"澳大利亚","comments":""},{"mallSubId":"402880e86016d1b5016016e083f10010","mallCategoryId":"2","mallSubName":"德国","comments":""},{"mallSubId":"402880e86016d1b5016016df1f92000c","mallCategoryId":"2","mallSubName":"法国","comments":""},{"mallSubId":"2c9f6c94621970a801626a40feac0178","mallCategoryId":"2","mallSubName":"南非","comments":""},{"mallSubId":"2c9f6c94679b4fb10167f7e5c9a115dc","mallCategoryId":"2","mallSubName":"葡萄牙","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7e5e68f15dd","mallCategoryId":"2","mallSubName":"西班牙","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7e609f515de","mallCategoryId":"2","mallSubName":"新西兰","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7e6286a15df","mallCategoryId":"2","mallSubName":"意大利","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7e6486615e0","mallCategoryId":"2","mallSubName":"智利","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7e66c6815e1","mallCategoryId":"2","mallSubName":"中国","comments":null}],"comments":null,"image":"http://images.baixingliangfan.cn/firstCategoryPicture/20190131/20190131170053_878.png"},{"mallCategoryId":"3","mallCategoryName":"清酒洋酒","bxMallSubDto":[{"mallSubId":"402880e86016d1b5016016e135440011","mallCategoryId":"3","mallSubName":"清酒","comments":""},{"mallSubId":"402880e86016d1b5016016e171cc0012","mallCategoryId":"3","mallSubName":"洋酒","comments":""}],"comments":null,"image":"http://images.baixingliangfan.cn/firstCategoryPicture/20190131/20190131170101_6957.png"},{"mallCategoryId":"5","mallCategoryName":"保健酒","bxMallSubDto":[{"mallSubId":"2c9f6c94609a62be0160a02d1dc20021","mallCategoryId":"5","mallSubName":"黄酒","comments":""},{"mallSubId":"2c9f6c94648837980164883ff6980000","mallCategoryId":"5","mallSubName":"药酒","comments":""}],"comments":null,"image":"http://images.baixingliangfan.cn/firstCategoryPicture/20190131/20190131170110_6581.png"},{"mallCategoryId":"2c9f6c946449ea7e01647ccd76a6001b","mallCategoryName":"预调酒","bxMallSubDto":[{"mallSubId":"2c9f6c946449ea7e01647d02f6250026","mallCategoryId":"2c9f6c946449ea7e01647ccd76a6001b","mallSubName":"果酒","comments":""},{"mallSubId":"2c9f6c946449ea7e01647d031bae0027","mallCategoryId":"2c9f6c946449ea7e01647ccd76a6001b","mallSubName":"鸡尾酒","comments":""},{"mallSubId":"2c9f6c946449ea7e01647d03428f0028","mallCategoryId":"2c9f6c946449ea7e01647ccd76a6001b","mallSubName":"米酒","comments":""}],"comments":null,"image":"http://images.baixingliangfan.cn/firstCategoryPicture/20190131/20190131170124_4760.png"},{"mallCategoryId":"2c9f6c946449ea7e01647ccf3b97001d","mallCategoryName":"下酒小菜","bxMallSubDto":[{"mallSubId":"2c9f6c946449ea7e01647dc18e610035","mallCategoryId":"2c9f6c946449ea7e01647ccf3b97001d","mallSubName":"熟食","comments":""},{"mallSubId":"2c9f6c946449ea7e01647dc1d9070036","mallCategoryId":"2c9f6c946449ea7e01647ccf3b97001d","mallSubName":"火腿","comments":""},{"mallSubId":"2c9f6c946449ea7e01647dc1fc3e0037","mallCategoryId":"2c9f6c946449ea7e01647ccf3b97001d","mallSubName":"速冻食品","comments":""}],"comments":null,"image":"http://images.baixingliangfan.cn/firstCategoryPicture/20190131/20190131170133_4419.png"},{"mallCategoryId":"2c9f6c946449ea7e01647ccdb0e0001c","mallCategoryName":"饮料","bxMallSubDto":[{"mallSubId":"2c9f6c946449ea7e01647d09cbf6002d","mallCategoryId":"2c9f6c946449ea7e01647ccdb0e0001c","mallSubName":"茶饮","comments":""},{"mallSubId":"2c9f6c946449ea7e01647d09f7e8002e","mallCategoryId":"2c9f6c946449ea7e01647ccdb0e0001c","mallSubName":"水饮","comments":""},{"mallSubId":"2c9f6c946449ea7e01647d0a27e1002f","mallCategoryId":"2c9f6c946449ea7e01647ccdb0e0001c","mallSubName":"功能饮料","comments":""},{"mallSubId":"2c9f6c946449ea7e01647d0b1d4d0030","mallCategoryId":"2c9f6c946449ea7e01647ccdb0e0001c","mallSubName":"果汁","comments":""},{"mallSubId":"2c9f6c946449ea7e01647d14192b0031","mallCategoryId":"2c9f6c946449ea7e01647ccdb0e0001c","mallSubName":"含乳饮料","comments":""},{"mallSubId":"2c9f6c946449ea7e01647d24d9600032","mallCategoryId":"2c9f6c946449ea7e01647ccdb0e0001c","mallSubName":"碳酸饮料","comments":""}],"comments":null,"image":"http://images.baixingliangfan.cn/firstCategoryPicture/20190131/20190131170143_361.png"},{"mallCategoryId":"2c9f6c946449ea7e01647cd108b60020","mallCategoryName":"乳制品","bxMallSubDto":[{"mallSubId":"2c9f6c946449ea7e01647dd4ac8c0048","mallCategoryId":"2c9f6c946449ea7e01647cd108b60020","mallSubName":"常温纯奶","comments":""},{"mallSubId":"2c9f6c946449ea7e01647dd4f6a40049","mallCategoryId":"2c9f6c946449ea7e01647cd108b60020","mallSubName":"常温酸奶","comments":""},{"mallSubId":"2c9f6c946449ea7e01647dd51ab7004a","mallCategoryId":"2c9f6c946449ea7e01647cd108b60020","mallSubName":"低温奶","comments":""}],"comments":null,"image":"http://images.baixingliangfan.cn/firstCategoryPicture/20190131/20190131170151_9234.png"},{"mallCategoryId":"2c9f6c946449ea7e01647ccfddb3001e","mallCategoryName":"休闲零食","bxMallSubDto":[{"mallSubId":"2c9f6c946449ea7e01647dc51d93003c","mallCategoryId":"2c9f6c946449ea7e01647ccfddb3001e","mallSubName":"方便食品","comments":""},{"mallSubId":"2c9f6c946449ea7e01647dd204dc0040","mallCategoryId":"2c9f6c946449ea7e01647ccfddb3001e","mallSubName":"面包糕点","comments":""},{"mallSubId":"2c9f6c946449ea7e01647dd22f760041","mallCategoryId":"2c9f6c946449ea7e01647ccfddb3001e","mallSubName":"糖果巧克力","comments":""},{"mallSubId":"2c9f6c946449ea7e01647dd254530042","mallCategoryId":"2c9f6c946449ea7e01647ccfddb3001e","mallSubName":"膨化食品","comments":""},{"mallSubId":"2c9f6c94679b4fb10167f7fa132b15e7","mallCategoryId":"2c9f6c946449ea7e01647ccfddb3001e","mallSubName":"坚果炒货","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7f4bfc415e2","mallCategoryId":"2c9f6c946449ea7e01647ccfddb3001e","mallSubName":"肉干豆干","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7f5027a15e3","mallCategoryId":"2c9f6c946449ea7e01647ccfddb3001e","mallSubName":"饼干","comments":null},{"mallSubId":"2c9f6c94679b4fb10167f7f530fd15e4","mallCategoryId":"2c9f6c946449ea7e01647ccfddb3001e","mallSubName":"冲调","comments":null},{"mallSubId":"2c9f6c94683a6b0d016846b49436003b","mallCategoryId":"2c9f6c946449ea7e01647ccfddb3001e","mallSubName":"休闲水果","comments":null}],"comments":null,"image":"http://images.baixingliangfan.cn/firstCategoryPicture/20190131/20190131170200_7553.png"},{"mallCategoryId":"2c9f6c946449ea7e01647cd08369001f","mallCategoryName":"粮油调味","bxMallSubDto":[{"mallSubId":"2c9f6c946449ea7e01647dd2e8270043","mallCategoryId":"2c9f6c946449ea7e01647cd08369001f","mallSubName":"油/粮食","comments":""},{"mallSubId":"2c9f6c946449ea7e01647dd31bca0044","mallCategoryId":"2c9f6c946449ea7e01647cd08369001f","mallSubName":"调味品","comments":""},{"mallSubId":"2c9f6c946449ea7e01647dd35a980045","mallCategoryId":"2c9f6c946449ea7e01647cd08369001f","mallSubName":"酱菜罐头","comments":""}],"comments":null,"image":"http://images.baixingliangfan.cn/firstCategoryPicture/20181212/20181212115842_9733.png"},{"mallCategoryId":"2c9f6c9468a85aef016925444ddb271b","mallCategoryName":"积分商品","bxMallSubDto":[],"comments":null,"image":"http://images.baixingliangfan.cn/firstCategoryPicture/20190225/20190225232703_9950.png"}]} 

我们可以使用这个网站格式化一下JSON数据,然后简单分析一下。

http://www.bejson.com/

视频中我会带着你简单的分析一下这个接口的数据。

#模型层的建立

把模型层单独放到一个文件夹里,然后建立一个category.dart文件。这个文件就是要结合json文件,形成的modle文件。文件里大量使用了dart中的 factory语法。

工厂构造函数

factory 关键字的功能,当实现构造函数但是不想每次都创建该类的一个实例的时候使用。

工厂模式是我们最常用的实例化对象模式了,是用工厂方法代替new操作的一种模式。用简单明了的方式解释,模式上类似于面向对象的多态,用起来和静态方法差不多。高雅和低俗的结合,相当于听着贝多芬的交响乐《命运》,看着波多野结衣的岛国小电影,只要你爽,什么都可以。

我们先制作了一个大分类的Class,代码如下:

class CategoryBigModel {  String mallCategoryId;    //类别编号  String mallCategoryName;  //类别名称  List bxMallSubDto;        //小类列表  String image;             //类别图片  Null comments;          //列表描述   //构造函数  CategoryBigModel({    this.mallCategoryId,    this.mallCategoryName,    this.comments,    this.image,    this.bxMallSubDto  });   //工厂模式-用这种模式可以省略New关键字  factory CategoryBigModel.fromJson(dynamic json){     return CategoryBigModel(      mallCategoryId:json['mallCategoryId'],      mallCategoryName:json['mallCategoryName'],      comments:json['comments'],      image:json['image'],      bxMallSubDto:json['bxMallSubDto']    );   }  }

这个只是单个的一个大类信息的模型,但我们是一个列表,这时候就需要制作一个列表的模型,而这个List里边是我们定义的CategoryBigModel模型。简单理解就是先定义一个单项模型,然后再定义个列表的模型。

代码如下:

class CategoryBigListModel {   List data;  CategoryBigListModel(this.data);  factory CategoryBigListModel.formJson(List json){    return CategoryBigListModel(      json.map((i)=>CategoryBigModel.fromJson((i))).toList()    );  }  } 

这样就建立好了一个模型,其实这个模型还可以继续建立,以后的课程中也会逐渐深入。这里到这里,相信大家都掌握了建立模型的方法。

#数据模型的使用

使用数据模型就简单很多了。直接声明变量,调用formJson方法就可以了。直接在_getCategory()方法里。记得先引入数据模型类,然后用.的形式进行输出了。

import '../model/category.dart';
void _getCategory()async{  await request('getCategory').then((val){        var data = json.decode(val.toString());                CategoryBigListModel list = CategoryBigListModel.formJson(data['data']);                list.data.forEach((item)=>print(item.mallCategoryName));          }); }

写完这些,你就可以在控制台看到结果了。如果是第一次接触数据模型,可能还是稍微有些绕的。

#json_to_dart的使用

如果我们得到一个特别复杂的JSON,有时候会无从下手开始写Model,这时候就可以使用一些辅助工具。我认为json_to_dart是比较好用的一个。它可以直接把json转换成dart类,然后进行一定的修改,就可以快乐的使用了。工作中我拿到一个json,都是先操作一下,然后再改的。算是一个小窍门吧。

请记住网址:

https://javiercbk.github.io/json_to_dart/

本节总结:本节主要对分类页面的分类中的大类进行了分析,然后又学习了json转数据模型的方法,最后讲解了如何使用json_to_dart转换复杂模型的方法。

#第23节:列表页大类展示效果制作

上节课我们学习了数据模型的建立,这节学习一下如何把建立好的数据模型展示在UI界面上,特别是这种List形式的数据模型。


#自动生成数据模型

上节课课再最后我讲了一个快速生成的方法,但是很多小伙伴都问我,生成后如何使用。所以就在这节详细讲一下平时自动生成Modle的使用方法。

首先我们到下面网址,自动生成model模型。

https://javiercbk.github.io/json_to_dart/

然后一定根据自己的需要改一下名字,比如这里是类别Model,我们就改名为CategoryModel

如果以后内容很多,记得不要类的名字重复,否则到时候不好找到问题。

#左侧动态菜单的建立

这里使用类的形式建立一个动态菜单,所以用快捷键stful,快速建立了一个名字为LeftCategoryNavStatefulWidget。并声明了一个List数据,起名就叫list。具体代码如下:

//左侧导航菜单 class LeftCategoryNav extends StatefulWidget {    _LeftCategoryNavState createState() => _LeftCategoryNavState(); }
 class _LeftCategoryNavState extends State {   List list=[];     @override  Widget build(BuildContext context) {    return Container();  } }
 

把上节课的调用后台类别的方法拷贝到这里,并进行改写。注意这里我们使用了setState来改变lsit 的状态,这样我们从后台获取数据后,页面就会有数据。

void _getCategory()async{  await request('getCategory').then((val){        var data = json.decode(val.toString());                    CategoryModel category= CategoryModel.fromJson(data);                setState(() {          list =category.data;         });  }); }

#编写大类子项

把大类里的子项分成一个单独的方法,这样可以起到复用的作用。主要知识点是用模型的形式展示数据。

Widget _leftInkWel(int index){    return InkWell(      onTap: (){},      child: Container(        height: ScreenUtil().setHeight(100),        padding:EdgeInsets.only(left:10,top:20),        decoration: BoxDecoration(          color: Colors.white,          border:Border(            bottom:BorderSide(width: 1,color:Colors.black12)          )        ),        child: Text(list[index].mallCategoryName,style: TextStyle(fontSize:ScreenUtil().setSp(28)),),      ),    ); }

#完善build方法

当子类别写好后,可以对build方法进行完善,build方法我们采用动态的ListView来写,代码如下:

@override Widget build(BuildContext context) {  return Container(        width: ScreenUtil().setWidth(180),        decoration: BoxDecoration(          border: Border(            right: BorderSide(width: 1,color:Colors.black12)          )        ),        child: ListView.builder(          itemCount:list.length,          itemBuilder: (context,index){            return _leftInkWel(index);          },        ),              ); }

我们希望获取数据只在Widget初始化的时候进行,所以再重写一个initState方法。

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

写完这步,就可以进行预览了,如果一切正常的话,在分类页面也该已经展示出了大类的一个类别列表。

#第24节:Provide状态管理基础

项目的商品类别页面将大量的出现类和类中间的状态变化,这就需要状态管理。现在Flutter的状态管理方案很多,redux、bloc、state、Provide。

  • Scoped Model : 最早的状态管理方案,我刚学Flutter的时候就使用的这个,虽然还有公司在用,但是大部分已经选用其它方案了。

  • Redux:现在国内用的最多,因为咸鱼团队一直在用,还出了自己fish redux

  • bloc:个人觉的比Redux简单,而且好用,特别是一个页面里的状态管理,用起来很爽。

  • state:我们首页里已经简单接触,缺点是耦合太强,如果是大型应用,管理起来非常混乱。

  • Provide:是在Google的Github下的一个项目,刚出现不久,所以可以推测他是Google的亲儿子,用起来也是相当的爽。

所以个人觉的Flutter_provide是目前最好的状态管理方案,那我们就采用这种方案来制作项目。


#flutter_Provide简介

Provide是Google官方推出的状态管理模式。官方地址为:https://github.com/google/flutter-provide

A simple framework for state management in Flutter

个人看来Provide被设计为ScopedModel的代替品,并且允许我们更加灵活地处理数据类型和数据。

#使用Provide

这节课就简单用flutter_provide进行一个简单的小实例,例子是这样的,我们在一个页面上增加了Text和一个RaisedButton.并且故意使用了StatelessWidget作了两个类。也就是估计作了一个不可变的页面,并且用两个类隔离了。然后我们要点击按钮,增加数字数量,也就是把状态打通。

制作最基本的页面

快速写一个最基本的页面,并且全部使用了StatelessWidget进行。

import 'package:flutter/material.dart'; 

class CartPage extends StatelessWidget {  @override  Widget build(BuildContext context) {    return Scaffold(      body:Center(        child: Column(         children: [           Number(),           MyButton()         ],        ),      )    );  } }
 
class Number extends StatelessWidget {    @override  Widget build(BuildContext context) {    return Container(      margin: EdgeInsets.only(top:200),      child:Text('0')    );  } }
 
class MyButton extends StatelessWidget {    @override  Widget build(BuildContext context) {    return Container(      child:RaisedButton(        onPressed: (){},        child: Text('递增'),      )    );  } }

添加依赖

pubspec.yaml中添加Provide的依赖。请使用最新版本。

dependencies:    provide: ^1.0.2

创建Provide

这个类似于创建一个state,但是为了跟State区分,我们叫创建Provide。新建一个provide文件夹,然后再里边新建一个counter.dart 文件.代码如下:

import 'package:flutter/material.dart'; 
class Counter with ChangeNotifier {  int value =0 ;   increment(){    value++;    notifyListeners();  } }

这里混入了ChangeNotifier,意思是可以不用管理听众。现在你可以看到数和操作数据的方法都在Provide中,很清晰的把业务分离出来了。通过notifyListeners可以通知听众刷新。

将状态放入顶层

先引入providecounter

import 'package:provide/provide.dart'; import './provide/counter.dart';

然后进行将providecounter引入程序顶层。

void main(){  var counter =Counter();  var providers  =Providers();  providers    ..provide(Provider.value(counter));  runApp(ProviderNode(child:MyApp(),providers:providers)); }

ProviderNode封装了InheritWidget,并且提供了 一个providers容器用于放置状态。

获取状态

使用Provide Widget的形式就可以获取状态,比如现在获取数字的状态,代码如下。

class Number extends StatelessWidget {    @override  Widget build(BuildContext context) {    return Container(      margin: EdgeInsets.only(top:200),      child: Provide(        builder: (context,child,counter){          return Text(            '${counter.value}',             style: Theme.of(context).textTheme.display1,          );        },      ),    );  } }

修改状态

直接编写按钮的单击事件,并调用provide里的方法,代码修改如下。

class MyButton extends StatelessWidget {    @override  Widget build(BuildContext context) {    return Container(      child:RaisedButton(        onPressed: (){          Provide.value(context).increment();        },        child: Text('递增'),      )    );  } }

#其它页面读取状态

为了更进一步说明状态是共享的,在“会员中心”页面,我们也显示出这个数字,代码如下:

import 'package:flutter/material.dart'; import 'package:provide/provide.dart'; import '../provide/counter.dart'; 
class MemberPage extends StatelessWidget {  @override  Widget build(BuildContext context) {    return Scaffold(      body:Center(        child: Provide(        builder: (context,child,counter){          return Text(            '${counter.value}',             style: Theme.of(context).textTheme.display1,          );        },      ),      )    );  } }

本节总结:通过本节终结,可以掌握flutter_provide的使用方法,并作了一个最简单的案例。如果你以前使用过其它状态管理方案,你就会知道provide到底有多爽了。所以建议小伙伴使用Provide来进行管理状态。

#第25节:列表页_使用Provide控制子类-1

上节课已经学习了基础的flutter_provide用法,也作了一个最基本的案例。这节课我们就把学到的知识用到实战案例当中,点击列表页的大类,改变小类的效果,当然这个程序还是稍微有点复杂,所以我们分两节课来讲。这里建议,如果你对上节的知识还没有完全掌握,那你需要多看几遍上节课的视频。并做出课程中的效果。


#编写二级分类UI

学到现在,编写任何UI应该都非常容易了,我这里就先给出代码了。具体的介绍就在视频中解释了。值得说的是,我们故意重新写了一个类,让我们的代码解耦,形成一个独立的小部件。

 //右侧小类类别 
class RightCategoryNav extends StatefulWidget {  _RightCategoryNavState createState() => _RightCategoryNavState(); }
 class _RightCategoryNavState extends State {   List list = ['名酒','宝丰','北京二锅头'];    @override  Widget build(BuildContext context) {        return Container(              child:Container(            height: ScreenUtil().setHeight(80),            width: ScreenUtil().setWidth(570),            decoration: BoxDecoration(              color: Colors.white,              border: Border(                bottom: BorderSide(width: 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: [          LeftCategoryNav(),          Column(            children: [              RightCategoryNav()            ],          )        ],      ),    ),  ); }

编写完后,我们就应该能看到效果,但是现在数据都是写死的,还没有实现状态的控制,但是我也不想把视频录制的太长,所以这节课程就做到这里。 我也建议你跟着视频中的效果制作,然后马上继续下一节。

#第26节:列表页_使用Provide控制子类-2

上节课已经进行了二级分类的UI布局,并且已经显示到了页面上。但是并没有实现交互效果,那这节课我们就通过Provide管理全局app的状态,实现二级分类和一级分类的交互效果吧。


#编写二级分类的Provide文件

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

import 'package:flutter/material.dart'; import '../model/category.dart'; 
//ChangeNotifier的混入是不用管理听众 class ChildCategory with ChangeNotifier{     List 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.value(counter))    ..provide(Provider.value(childCategory)); }

#修改二级分类状态

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

onTap: () {    var childList = list[index].bxMallSubDto;      Provide.value(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,我们需要在Container外边加入一个Provide组件,注意这里使用了泛型。

Widget build(BuildContext context) {    return Container(    // child: Text('${childCategory.childCategoryList.length}'),      child: Provide(      builder: (context,child,childCategory){        return Container(          height: ScreenUtil().setHeight(80),          width: ScreenUtil().setWidth(570),          decoration: BoxDecoration(            color: Colors.white,            border: Border(              bottom: BorderSide(width: 1,color: Colors.black12)            )          ),          child:ListView.builder(            scrollDirection: Axis.horizontal,            itemCount: childCategory.childCategoryList.length,            itemBuilder: (context,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,

全部代码如下:

import 'package:flutter/material.dart'; import '../service/service_method.dart'; import 'dart:convert'; import 'package:flutter_easyrefresh/easy_refresh.dart'; import '../model/category.dart'; import 'package:provide/provide.dart'; import '../provide/child_category.dart'; 
import 'package:flutter_screenutil/flutter_screenutil.dart'; 
class CategoryPage extends StatefulWidget {  _CategoryPageState createState() => _CategoryPageState(); }
 class _CategoryPageState extends State {  // CategoryBigListModel listCategory = CategoryBigListModel([]);   @override Widget build(BuildContext context) {  return Scaffold(    appBar: AppBar(      title: Text('商品分类'),    ),    body: Container(      child: Row(        children: [          LeftCategoryNav(),          Column(            children: [              RightCategoryNav()            ],          )        ],      ),    ),  ); }
} 
//左侧导航菜单 class LeftCategoryNav extends StatefulWidget {  _LeftCategoryNavState createState() => _LeftCategoryNavState(); }
 class _LeftCategoryNavState extends State {  List list = [];  var listIndex = 0; //索引   @override  void initState() {    _getCategory();    super.initState();  }   @override  Widget build(BuildContext context) {    return Container(      width: ScreenUtil().setWidth(180),      decoration: BoxDecoration(          border: Border(right: BorderSide(width: 1, color: Colors.black12))),      child: ListView.builder(        itemCount: list.length,        itemBuilder: (context, index) {          return _leftInkWel(index);        },      ),    );  }   Widget _leftInkWel(int index) {    bool isClick=false;    isClick=(index==listIndex)?true:false;     return InkWell(      onTap: () {          setState(() {           listIndex=index;         });         var childList = list[index].bxMallSubDto;                 Provide.value(context).getChildCategory(childList);      },      child: Container(        height: ScreenUtil().setHeight(100),        padding: EdgeInsets.only(left: 10, top: 20),        decoration: BoxDecoration(            color: isClick?Colors.black26:Colors.white,            border:                Border(bottom: BorderSide(width: 1, color: Colors.black12))),        child: Text(          list[index].mallCategoryName,          style: TextStyle(fontSize: ScreenUtil().setSp(28)),        ),      ),    );  }   //得到后台大类数据  void _getCategory() async {    await request('getCategory').then((val) {      var data = json.decode(val.toString());       CategoryModel category = CategoryModel.fromJson(data);       setState(() {        list = category.data;      });        Provide.value(context).getChildCategory( list[0].bxMallSubDto);       print(list[0].bxMallSubDto);       list[0].bxMallSubDto.forEach((item) => print(item.mallSubName));    });  } }
 //右侧小类类别 
class RightCategoryNav extends StatefulWidget {  _RightCategoryNavState createState() => _RightCategoryNavState(); }
 class _RightCategoryNavState extends State {       @override Widget build(BuildContext context) {    return Container(    // child: Text('${childCategory.childCategoryList.length}'),      child: Provide(      builder: (context,child,childCategory){        return Container(          height: ScreenUtil().setHeight(80),          width: ScreenUtil().setWidth(570),          decoration: BoxDecoration(            color: Colors.white,            border: Border(              bottom: BorderSide(width: 1,color: Colors.black12)            )          ),          child:ListView.builder(            scrollDirection: Axis.horizontal,            itemCount: childCategory.childCategoryList.length,            itemBuilder: (context,index){              return _rightInkWell(childCategory.childCategoryList[index]);            },          )        );      },    )  ); }
  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)),        ),      ),    );  } }
 

课程总结:

通过三节课的学习,你应该能基本掌握状态管理和界面交互效果改变的用法了,需要说明的是,状态管理在工作中有很高的作用,所以必须要掌握好,如果你还不能自己写出视频中的效果,我建议多练习几遍。这是Flutter技术的一个瓶颈,所以必须要掌握好。

#第27节:列表页现有Bug的完善

这节先解决上节课遗留的小问题,作为一个有工匠精神的老司机,写程序一定要尽善尽美,所以把现有程序的Bug解决一下。


#进入分类页面后小类无数据BUG

修改刚进入页面没有子类数据的方案非常简单,只要在进入页面后的_getCategory里在等到大类数据后,把第一个小类的数据同时进行状态修改。

代码如下:

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

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

这个直接使用Flutter里的RGBO模式就可以了,当然你也完全可以使用Colors.black12,但是为了让小伙伴见到更多的代码,我们这里采用RGBO的模式。在_leftInkWellContainer里设置颜色。代码如下:

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

全部代码如下:

child: Container(  height: ScreenUtil().setHeight(100),  padding: EdgeInsets.only(left: 10, top: 20),  decoration: BoxDecoration(      color: isClick?Color.fromRGBO(236, 238, 239, 1.0):Colors.white,      border:          Border(bottom: BorderSide(width: 1, color: Colors.black12))),  child: Text(    list[index].mallCategoryName,    style: TextStyle(fontSize: ScreenUtil().setSp(28)),      ), ), 

#添加子类“全部”按钮

我们可以看到,小程序上在二级分类上是有“全部”字样的,但我们作的这里并没有。其实加上这个全部也非常简单,只要我们在状态管理,改变状态的方法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 childCategoryList = []; 

    getChildCategory(List list){      BxMallSubDto all=  BxMallSubDto();      all.mallSubId='00';      all.mallCategoryId='00';      all.mallSubName = '全部';      all.comments = 'null';      childCategoryList=[all];      childCategoryList.addAll(list);         notifyListeners();    } }

这时候就可以使用了,把基本的Bug已经解决掉了。下节课我们开始作商品分类的列表页。

#第28节:列表页_商品列表接口调试

这节课的主要内容就是调通商品分类页里的商品列表接口,这个接口是这套视频中最复杂也最重要的接口。接口包括上拉加载、大类切换和小类切换的互动,虽然复杂,小伙伴们也不要担心,我们会尽量讲的细致和简单,让每个伙伴都可以学会。


#配置URL路径

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

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

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

配置好后,保存文件。

#测试大类商品列表接口

因为在前面的课程中的lib\service\service_method.dart文件中写了一个统一的方法,所以这里直接调试就可以了。在lib\pages\category_page.dart文件里,新建一个CategoryGoodsList类,这个类我们也将用状态管理的放心进行管理,所以这个类并没有什么其它的耦合,不接收任何参数。

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

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

  void _getGoodList()async {    var data={      'categoryId':'4',      'categorySubId':"",      'page':1    };    await request('getMallGoods',formData:data ).then((val){        var data = json.decode(val.toString());        print('分类商品列表:>>>>>>>>>>>>>${data}');    });   } 

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

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

#调式代码如下

为了方便小伙伴学习,这里给出全部代码:

//商品列表,可以上拉加载 
class CategoryGoodsList extends StatefulWidget {  @override  _CategoryGoodsListState createState() => _CategoryGoodsListState(); }
 class _CategoryGoodsListState extends State {   @override  void initState() {    _getGoodList();    super.initState();  }   @override  Widget build(BuildContext context) {    return Container(      child: Text('商品列表'),    );  }   void _getGoodList()async {    var data={      'categoryId':'4',      'categorySubId':"",      'page':1    };    await request('getMallGoods',formData:data ).then((val){        var data = json.decode(val.toString());        print('分类商品列表:>>>>>>>>>>>>>${data}');    });   } }

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

#第29节: 列表页_商品列表数据模型的建立

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


#商品列表页数据模型

这里还是使用快速生成的方法,利用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"}]} 

先复制上边的JSON,然后把复制的代码粘贴到https://javiercbk.github.io/json_to_dart/中,得到快速生成的Model类,在model文件夹下,新建一个文件categoryGoodsList.dart,这时候我们需要修改一下代码,防止产生冲突。修改完成的代码如下:

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

#修改_getGoodList方法

我们Model类做好后,需要在lib\pages\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界面制作一下,为了小伙伴们看着更方便,所以拆成了两节。

#第30接:列表页_商品列表UI布局

接上节课,其实我觉的小伙伴们对布局一定是没有问题了,所以我把布局这节课单独拿出来了,小伙伴完全可以不看这节课的内容,自己写出一个自己喜欢的布局效果。但是为了保证课程的完整性,所以这节必须进行录制,防止有些小伙伴做不出来这个效果。

我们在首页的时候已经使用Wrap的布局方式制作火爆专区列表,这节课如果还用Wrap的形式就没有什么意思了,所以这里使用ListView的形式,可能跟模仿的小程序稍微有些不同,但我们的目标是学知识。

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004003962519

#商品图片方法编写

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

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

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

#商品名称方法编写

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

  Widget _goodsName(index){    return Container(       padding: EdgeInsets.all(5.0),      width: 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),      width: ScreenUtil().setWidth(370),      child:Row(        children: [            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(width: 1.0,color: Colors.black12)          )        ),                child: Row(          children: [            _goodsImage(index)           ,            Column(              children: [                _goodsName(index),                _goodsPrice(index)              ],            )          ],        ),      )    );   }

#ListView的构建

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

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

构建好后,就可以进行测试了。然后再根据你想要的效果进行微调。需要注意的是,你完全可以根据你自己的喜好做出更漂亮的页面。

#第31节:列表页_商品列表交互效果制作

现在页面布局已经基本完成,接下来就要作商品分类页的各种交互效果了,当我们熟练掌握了Provide的状态管理后,这些交互页变的相当容易。但为了实现交互效果,还是需要把页面代码进行重新规划一下的,让页面符合状态管理的规范的。

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004014751875

#制作商品列表的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 goodsList = [];      //点击大类时更换商品列表    getGoodsList(List 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.value(childCategory))    ..provide(Provider.value(categoryGoodsListProvide))    ..provide(Provider.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('getMallGoods',formData:data ).then((val){        var  data = json.decode(val.toString());        CategoryGoodsListModel goodsList=  CategoryGoodsListModel.fromJson(data);        Provide.value(context).getGoodsList(goodsList.data);           });  } 

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

3.使用_getGoodList方法

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

  onTap: () {       setState(() {        listIndex=index;      });      var childList = list[index].bxMallSubDto;      var categoryId= list[index].mallCategoryId;          Provide.value(context).getChildCategory(childList);      _getGoodList(categoryId:categoryId );  },

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

4.修改商品列表代码

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

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

修改后的代码如下:

class CategoryGoodsList extends StatefulWidget {  @override  _CategoryGoodsListState createState() => _CategoryGoodsListState(); }
 class _CategoryGoodsListState extends State {     @override  Widget build(BuildContext context) {             return Provide(        builder: (context,child,data){          return Container(            width: ScreenUtil().setWidth(570) ,            height: ScreenUtil().setHeight(1000),            child:ListView.builder(                itemCount: data.goodsList.length,                itemBuilder: (context,index){                  return _ListWidget(data.goodsList,index);                },              ) ,          );        },                      );  }    Widget _ListWidget(List newList,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(width: 1.0,color: Colors.black12)          )        ),                child: Row(          children: [            _goodsImage(newList,index)           ,            Column(              children: [                _goodsName(newList,index),                _goodsPrice(newList,index)              ],            )          ],        ),      )    );   }   Widget _goodsImage(List newList,int index){     return  Container(      width: ScreenUtil().setWidth(200),      child: Image.network(newList[index].image),    );   }   Widget _goodsName(List newList,int index){    return Container(       padding: EdgeInsets.all(5.0),      width: ScreenUtil().setWidth(370),      child: Text(        newList[index].goodsName,        maxLines: 2,        overflow: TextOverflow.ellipsis,        style: TextStyle(fontSize: ScreenUtil().setSp(28)),        ),      );  }   Widget _goodsPrice(List newList,int index){    return  Container(       margin: EdgeInsets.only(top:20.0),      width: ScreenUtil().setWidth(370),      child:Row(        children: [            Text(              '价格:¥${newList[index].presentPrice}',              style: TextStyle(color:Colors.pink,fontSize:ScreenUtil().setSp(30)),              ),            Text(              '¥${newList[index].oriPrice}',              style: TextStyle(                color: Colors.black26,                decoration: TextDecoration.lineThrough              ),            )        ]      )    );  } 

} 

总结:这节课算是Provide的高级应用了,如果这个状态管理小伙伴都很熟练了,至少Flutter的状态管理这个知识点是没有问题了。我们下节课要晚上子类和商品列表的互动,当然也是使用状态管理了。

#第32节:列表页_小类高亮交互效果制作

这节课主要学习小类高亮交互效果的实现,通过几节课的练习,应该对状态管理有了比较深刻的理解。我建议小伙伴们可以先不看视频自己作一下,检验一下自己的学习能力。

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004034244833

#Expanded Widget的使用

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

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

 @override  Widget build(BuildContext context) {    return Provide(        builder: (context,child,data){          return Expanded(            child:Container(              width: 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 childCategoryList = [];    int childIndex = 0;       //点击大类时更换    getChildCategory(List list){            childIndex=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)
  1. 定义是否高亮变量,再根据状态进行赋值
   bool isCheck = false;   isCheck =(index==Provide.value(context).childIndex)?true:false;

3.点击时修改状态

onTap: (){    Provide.value(context).changeChildIndex(index); },

4.用isCheck判断是否高亮

color:isCheck?Colors.pink:Colors.black ),

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

#第33节:列表页_子类和商品列表切换

其实点击大类切换商品列表效果如果你会了,那点击小类切换商品列表效果几乎是一样。只有很小的改动。

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004034340673

#修改Provide类

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

import 'package:flutter/material.dart'; import '../model/category.dart'; 

//ChangeNotifier的混入是不用管理听众 class ChildCategory with ChangeNotifier{     List childCategoryList = [];    int childIndex = 0;    String categoryId = '4';       //点击大类时更换    getChildCategory(List list,String id){      categoryId=id;      childIndex=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();    } }

#修改调用getChildCategory放

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

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

#增加getGoodsList方法

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

   //得到商品列表数据   void _getGoodList(String categorySubId) {         var data={      'categoryId':Provide.value(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(context).getGoodsList(goodsList.data);        Provide.value(context).getGoodsList(goodsList.data);           });  } 

#调用方法改版列表

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

onTap: (){    Provide.value(context).changeChildIndex(index);    _getGoodList(item.mallSubId); }, 

#第34节:列表页_小Bug的修复

在列表页还是有小Bug的,这节课我们就利用几分钟,进行修复一下.

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004048538961

#子类没有商品时报错

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

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

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

  //得到商品列表数据   void _getGoodList(String categorySubId) {         var data={      'categoryId':Provide.value(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(context).getGoodsList(goodsList.data);        if(goodsList.data==null){         Provide.value(context).getGoodsList([]);        }else{          Provide.value(context).getGoodsList(goodsList.data);                  }    });  } 

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

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

  @override  Widget build(BuildContext context) {    return Provide(        builder: (context,child,data){          if(data.goodsList.length>0){             return Expanded(                child:Container(                  width: 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 childCategoryList = []; //商品列表    int childIndex = 0; //子类索引值    String categoryId = '4'; //大类ID    String subId =''; //小类ID        //点击大类时更换    getChildCategory(List 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();    } }

这就为以后我们作上拉加载效果打下了基础。这节学完,你应该对Proive的有了深刻的理解,并且达到工作水平。

#第35节:列表页_上拉加载功能的制作

这节主要制作一下列表页的上拉加载更多功能,因为在首页的视频中,已经讲解了上拉加载更多的效果,所以我们不会再着重讲解语法,而重点会放在上拉加载和Provide结合的方法。小伙伴们学习的侧重点也应该是状态管理的应用。

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004087266325

#增加page和noMoreText到Provide里

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

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

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

//点击大类时更换    getChildCategory(List 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       noMoreText='';       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进行包裹,然后加上各种需要的参数,这个部分已经在前几节课讲过了,这里就不作过多的讲解了。

@override  Widget build(BuildContext context) {    return Provide(        builder: (context,child,data){                              if(data.goodsList.length>0){             return Expanded(                child:Container(                  width: ScreenUtil().setWidth(570) ,                  child:EasyRefresh(                    refreshFooter: ClassicsFooter(                      key:_footerKey,                      bgColor:Colors.white,                      textColor:Colors.pink,                      moreInfoColor: Colors.pink,                      showMore:true,                      noMoreText:Provide.value(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(context).addPage();     var data={      'categoryId':Provide.value(context).categoryId,      'categorySubId':Provide.value(context).subId,      'page':Provide.value(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(context).changeNoMore('没有更多了');        }else{                     Provide.value(context).addGoodsList(goodsList.data);                  }    }); 
  } 

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

#切换类别返回顶部

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

try{  if(Provide.value(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);  }, ) ,

这时候再进行测试,应该就可以了。这节课就到这里,虽然还有些小Bug,但是总体效果已经制作完成了。

#第36节:Fluttertoast组件的介绍

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

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004097684445

#Fluttertoast 组件简介

这是一个第三方组件,目前版本是3.0.1,当你学习的时候可以到Github上查找最新版本。讲课时此插件又200Star。

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

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

#如何使用Fluttertoast

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

fluttertoast: ^3.0.1

引入后在需要使用的页面使用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 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(); }

这节课主要学习了FlutterToast组件的使用。这个组件虽然很简单,但是在开发中少不了。所以在这里给小伙伴进行了一个详细的讲解。

#第37节:路由_fluro引入和商品详细页建立

Flutter本身提供了路由机制,作个人的小型项目,完全足够了。但是如果你要作企业级开发,可能就会把入口文件变得臃肿不堪。而再Flutter问世之初,就已经了企业级路由方案fluro。

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004113923565

#flutter_fluro简介

fluro简化了Flutter的路由开发,也是目前Flutter生态中最成熟的路由框架。

GitHub地址:https://github.com/theyakka/fluro

它出现的比较早啊,是目前用户最多的Flutter路由解决方案,目前Github上有将近1000Star,可以说是相当了不起了。

#建立商品详情页面

在学习Fluro之前,我们先建立一个商品详情页面,当然我们只是为了调通路由代码,所以尽量简化代码。在page文件夹下,建立一个details_page. dart文件,然后写入下面的代码:

import 'package:flutter/material.dart'; 
class DetailsPage extends StatelessWidget {  final String goodsId;  DetailsPage(this.goodsId);   @override  Widget build(BuildContext context) {    return Container(      child:Text('商品ID为:${goodsId}')          );  } }

这里使用了静态组件,测试也没必要使用动态组件,然后组件接收一个goodsId参数,接收参数我们使用了构造方法,因为新版的Flutter已经不在要求key值,所以没必要再写了。

#引入fluro

pubspec.yaml文件里,直接注册版本依赖,代码如下。

dependencies: fluro: "^1.4.0" 

如果你这个版本下载不下来,你也可以使用git的方式注册依赖,这样页是可以下载包的(这也是小伙伴提的一个问题),代码如下:

dependencies: fluro:   git: git://github.com/theyakka/fluro.git

在项目的入口文件,也就是main.dart中引入,代码如下:

import 'package:fluro/fluro.dart';

通过上面的三步,就算把Fluro引入到项目中了,下面就可以开心的使用了。这就好比,衣服脱了,剩下就看你怎么玩了。

总结:我们把路由flutter_fluro分4节课来讲,这样调理更清晰,虽然每节课程的代码不多,但是很好理解。

#第38节:路由_fluro中Handler文件编写

handler就是每个路由的规则,编写handler就是配置路由规则,比如我们要传递参数,参数的值是什么,这些都需要在Handler中完成。

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004113726401

#初始化Fluro

现在可以进行使用了,使用时需要先在Build方法里进行初始化,其实就是把对象new出来。

final router = Router(); 

#编写rotuer_handler

handler相当于一个路由的规则,比如我们要到详细页面,这时候就需要传递商品的ID,那就要写一个handler。这次我按照大型企业级真实项目开发来部署项目目录和文件,把路由全部分开,Handler单独写成一个文件。 新建一个routers文件夹,然后新建router_handler.dart文件

import 'package:flutter/material.dart'; import 'package:fluro/fluro.dart'; import '../pages/details_page.dart'; 

Handler detailsHanderl =Handler(  handlerFunc: (BuildContext context,Map> params){    String goodsId = params['id'].first;    print('index>details goodsID is ${goodsId}');    return DetailsPage(goodsId);   } ); 

这样一个Handler就写完了。Hanlder的编写是路由中最重要的一个环境,知识点也是比较多的,这里我们学的只是最简单的一个Handler编写,以后会随着课程的增加,我们会再继续深入讲解Handler的编写方法。

#第39节:路由_fluro的路由配置和静态化

Hanlder只是对每个路由的独立配置文件,fluro当然还需要一个总体配置文件。这节课就来学习一下fluro总体配置文件的编写。这样配置好后,我们还需要一个静态化文件,方便我们在UI页面进行使用。

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004113726682

#配置路由

我们还需要对路由有一个总体的配置,比如跟目录,出现不存在的路径如何显示,工作中我们经常把这个文件单独写一个文件。在routes.dart里,新建一个routes.dart文件。

代码如下:

import 'package:flutter/material.dart'; import './router_handler.dart'; import 'package:fluro/fluro.dart'; 
class Routes{  static String root='/';  static String detailsPage = '/detail';  static void configureRoutes(Router router){    router.notFoundHandler= new Handler(      handlerFunc: (BuildContext context,Map> params){        print('ERROR====>ROUTE WAS NOT FONUND!!!');      }    );     router.define(detailsPage,handler:detailsHandler);  } 
} 

这段代码在视频中有详细的解释,这里就作过多的文字介绍了。

#把Fluro的Router静态化

这一步就是为了使用方便,直接把Router进行静态化,这样在任何一个页面都可以直接进行使用了。代码如下:

import 'package:fluro/fluro.dart'; 
class Application{  static Router router; }

总结:这节课完成后,我们基本就把Fluro的路由配置好了,这样的配置虽然稍显复杂,但是跟层次和条理化,扩展性也很强。所以小伙伴们也要练习一下。

#第40节:路由_fluro的全局注入和使用

通过3节课的学习,已经把路由配置好了,但是如果想正常使用,还需要在main.dart文件里进行全局注入。注入后就可以爽快的使用了,配置好后的使用方法也是非常简单的。

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004114118125

#把路由注册到顶层

打开main.dart文件,首页引入routes.dartapplication.dart文件,代码如下:

import './routers/routes.dart'; import './routers/application.dart'; 

引入后需要进行赋值,进行注入程序。这里展示主要build代码。

class MyApp extends StatelessWidget {  @override  Widget build(BuildContext context) {    //-------------------主要代码start    final router = Router();    Routes.configureRoutes(router);    Application.router=router;    //-------------------主要代码end        return Container(            child: MaterialApp(        title:'百姓生活+',        debugShowCheckedModeBanner: false,        //----------------主要代码start        onGenerateRoute: Application.router.generator,        //----------------主要代码end        theme: ThemeData(          primaryColor:Colors.pink,        ),        home:IndexPage()      ),    );  } }

上面代码就是注入整个程序,让我们在任何页面直接引入application.dart就可以使用。

#在首页使用

前戏终于完成,现在就可以痛痛快快大干一场了。现在要在首页里使用路由,直接在首页打开商品详细页面。

先引入application.dart文件:

import './routers/application.dart';

然后再火爆专区的列表中使用配置好的路由,打开商品详细页面details_page.dart

打开home_page.dart文件,找到火爆专区列表里的ontap事件,然后在ontap事件中直接使用application进行跳转,代码如下:

 Application.router.navigateTo(context,"/detail?id=${val['goodsId']}");

这时候可以测试一下,如果一切正常,应该可以打开商品详细页面了,当然这时候的商品详细页面实在是太丑了。

#第41节:详细页_后台数据接口调试

开始作商品详细页,这节课主要是调通商品信息页的后端接口和制作数据模型。我们完全安装真实项目的开发目录接口和文件组织来进行开发。

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004124061536

#建立商品详细模型

我们还是用快速生成的方式建立一下商品详细页的接口模型,有这样一段从后端获取的JSON,直接用快速生成的方式,把这段JSON生成模型,然后进行必要的修改。

JSON如下:

{"code":"0","message":"success","data":{"goodInfo":{"image5":"","amount":10000,"image3":"","image4":"","goodsId":"ed675dda49e0445fa769f3d8020ab5e9","isOnline":"yes","image1":"http://images.baixingliangfan.cn/shopGoodsImg/20190116/20190116162618_2924.jpg","image2":"","goodsSerialNumber":"6928804011173","oriPrice":3.00,"presentPrice":2.70,"comPic":"http://images.baixingliangfan.cn/compressedPic/20190116162618_2924.jpg","state":1,"shopId":"402880e860166f3c0160167897d60002","goodsName":"可口可乐500ml/瓶","goodsDetail":"\"\"\"\"\"\"\"\"\"\"\"\""},"goodComments":[{"SCORE":5,"comments":"果断卸载,2.5个小时才送到","userName":"157******27","discussTime":1539491266336}],"advertesPicture":{"PICTURE_ADDRESS":"http://images.baixingliangfan.cn/advertesPicture/20190113/20190113134955_5825.jpg","TO_PLACE":"1"}}}

复制上面的的代码,代开下面的地址,利用JSON代码,快速生成MOdel模型。

https://javiercbk.github.io/json_to_dart/

lib/model文件夹下新建立details.dart文件,然后把生成的代码拷贝到下面。

class DetailsModel {  String code;  String message;  DetailsGoodsData data;   DetailsModel({this.code, this.message, this.data});   DetailsModel.fromJson(Map json) {    code = json['code'];    message = json['message'];    data = json['data'] != null ? new DetailsGoodsData.fromJson(json['data']) : null;  }   Map toJson() {    final Map data = new Map();    data['code'] = this.code;    data['message'] = this.message;    if (this.data != null) {      data['data'] = this.data.toJson();    }    return data;  } }
 class DetailsGoodsData {  GoodInfo goodInfo;  List goodComments;  AdvertesPicture advertesPicture;   DetailsGoodsData({this.goodInfo, this.goodComments, this.advertesPicture});   DetailsGoodsData.fromJson(Map json) {    goodInfo = json['goodInfo'] != null        ? new GoodInfo.fromJson(json['goodInfo'])        : null;    if (json['goodComments'] != null) {      goodComments = new List();      json['goodComments'].forEach((v) {        goodComments.add(new GoodComments.fromJson(v));      });    }    advertesPicture = json['advertesPicture'] != null        ? new AdvertesPicture.fromJson(json['advertesPicture'])        : null;  }   Map toJson() {    final Map data = new Map();    if (this.goodInfo != null) {      data['goodInfo'] = this.goodInfo.toJson();    }    if (this.goodComments != null) {      data['goodComments'] = this.goodComments.map((v) => v.toJson()).toList();    }    if (this.advertesPicture != null) {      data['advertesPicture'] = this.advertesPicture.toJson();    }    return data;  } }
 class GoodInfo {  String image5;  int amount;  String image3;  String image4;  String goodsId;  String isOnline;  String image1;  String image2;  String goodsSerialNumber;  double oriPrice;  double presentPrice;  String comPic;  int state;  String shopId;  String goodsName;  String goodsDetail;   GoodInfo(      {this.image5,      this.amount,      this.image3,      this.image4,      this.goodsId,      this.isOnline,      this.image1,      this.image2,      this.goodsSerialNumber,      this.oriPrice,      this.presentPrice,      this.comPic,      this.state,      this.shopId,      this.goodsName,      this.goodsDetail});   GoodInfo.fromJson(Map json) {    image5 = json['image5'];    amount = json['amount'];    image3 = json['image3'];    image4 = json['image4'];    goodsId = json['goodsId'];    isOnline = json['isOnline'];    image1 = json['image1'];    image2 = json['image2'];    goodsSerialNumber = json['goodsSerialNumber'];    oriPrice = json['oriPrice'];    presentPrice = json['presentPrice'];    comPic = json['comPic'];    state = json['state'];    shopId = json['shopId'];    goodsName = json['goodsName'];    goodsDetail = json['goodsDetail'];  }   Map toJson() {    final Map data = new Map();    data['image5'] = this.image5;    data['amount'] = this.amount;    data['image3'] = this.image3;    data['image4'] = this.image4;    data['goodsId'] = this.goodsId;    data['isOnline'] = this.isOnline;    data['image1'] = this.image1;    data['image2'] = this.image2;    data['goodsSerialNumber'] = this.goodsSerialNumber;    data['oriPrice'] = this.oriPrice;    data['presentPrice'] = this.presentPrice;    data['comPic'] = this.comPic;    data['state'] = this.state;    data['shopId'] = this.shopId;    data['goodsName'] = this.goodsName;    data['goodsDetail'] = this.goodsDetail;    return data;  } }
 class GoodComments {  int sCORE;  String comments;  String userName;  int discussTime;   GoodComments({this.sCORE, this.comments, this.userName, this.discussTime});   GoodComments.fromJson(Map json) {    sCORE = json['SCORE'];    comments = json['comments'];    userName = json['userName'];    discussTime = json['discussTime'];  }   Map toJson() {    final Map data = new Map();    data['SCORE'] = this.sCORE;    data['comments'] = this.comments;    data['userName'] = this.userName;    data['discussTime'] = this.discussTime;    return data;  } }
 class AdvertesPicture {  String pICTUREADDRESS;  String tOPLACE;   AdvertesPicture({this.pICTUREADDRESS, this.tOPLACE});   AdvertesPicture.fromJson(Map json) {    pICTUREADDRESS = json['PICTURE_ADDRESS'];    tOPLACE = json['TO_PLACE'];  }   Map toJson() {    final Map data = new Map();    data['PICTURE_ADDRESS'] = this.pICTUREADDRESS;    data['TO_PLACE'] = this.tOPLACE;    return data;  } }
 

#Provide建立

在实际开发中,我们是将业务逻辑和UI表现分开的,所以线建立一个Provide文件,所有业务逻辑将写在Provide里,然后pages文件夹里只写UI层面的东西。这样就把业务逻辑和UI进行了分离。

lib/provide/文件夹下新建立一个details_info.dart文件,这个文件就是写商品详细页相关的业务逻辑的。

import 'package:flutter/material.dart'; import '../model/details.dart'; import '../service/service_method.dart'; import 'dart:convert'; 
class DetailsInfoProvide with ChangeNotifier{     DetailsModel goodsInfo =null;   //从后台获取商品信息   getGoodsInfo(String id ){    var formData = { 'goodId':id, };        request('getGoodDetailById',formData:formData).then((val){      var responseData= json.decode(val.toString());      print(responseData);      goodsInfo=DetailsModel.fromJson(responseData);            notifyListeners();    });     } 
} 

先引入刚建立好的Model,然后引入service_method.dart文件。声明DetailsInfoProvidel类,在类里边声明一个DetailsModel类型的 goodsInfo变量,初始值甚至成null,然后写一个从后台获取数据的方法,命名为getGoodsInfo

#在UI调试接口

直接在pages文件夹的details_page.dart文件里,写一个_getBackInfo方法,然后在build方法里使用一下。 如果控制台打印出商品详细的数据,说明接口已经调通。

  void _getBackInfo(BuildContext context )async{      await  Provide.value(context).getGoodsInfo(goodsId);      print('加载完成............');  } 

总结:从这节课开始你的重点不应该放到Flutter语法生,要把重点放在项目的组织和分离上。

#第42节:详细页UI主页面架构搭建

上节课已经把详细页大体的业务结构和跟后台的数据接口调通了,这节课开始搭建页面的UI。会把一个详细页分为6个主要部分来编写,也就是说把一个页面拆成六个大组件,并在不同的页面中。

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004136605913

#details_page页面的编写

这个页面已经建立好了,在lib/pages/目录下,我们主要修改build方法。代码如下,视频中我会一行行进行解释。

import 'package:flutter/material.dart'; import 'package:provide/provide.dart'; import '../provide/details_info.dart'; 

 class DetailsPage extends StatelessWidget {  final String goodsId;  DetailsPage(this.goodsId);   @override  Widget build(BuildContext context) {       return Scaffold(         appBar: AppBar(            leading: IconButton(              icon:Icon(Icons.arrow_back),              onPressed: (){                print('返回上一页');                Navigator.pop(context);              },              ),            title: Text('商品详细页'),          ),          body:FutureBuilder(            future: _getBackInfo(context) ,            builder: (context,snapshot){              if(snapshot.hasData){                  return Container(                    child:Column(                          children: [                                                      ],                    )                  );              }else{                  return Text('加载中........');              }            }          )       );  } 
} 

在body区域,使用了FutureBuilder Widget ,可以实现异步建在的效果。并且在可以判断snapshot.hasData进行判断是否在加载还是在加载中。

#_getBackInfo方法的修改

在build方法里使用了FutureBuilder部件,所以使用的后台得到数据的方法,也要相应的做出修改,要最后返回一个Future 部件。代码如下:

 Future _getBackInfo(BuildContext context )async{      await  Provide.value(context).getGoodsInfo(goodsId);      return '完成加载';  } 

在这里给出所有代码方便你学习:

import 'package:flutter/material.dart'; import 'package:provide/provide.dart'; import '../provide/details_info.dart'; 

 class DetailsPage extends StatelessWidget {  final String goodsId;  DetailsPage(this.goodsId);   @override  Widget build(BuildContext context) {           return Scaffold(         appBar: AppBar(            leading: IconButton(              icon:Icon(Icons.arrow_back),              onPressed: (){                print('返回上一页');                Navigator.pop(context);              },              ),            title: Text('商品详细页'),          ),          body:FutureBuilder(            future: _getBackInfo(context) ,            builder: (context,snapshot){              if(snapshot.hasData){                  return Container(                    child:Row(                          children: [                                                      ],                    )                  );              }else{                  return Text('加载中........');              }            }          )       );              }   Future _getBackInfo(BuildContext context )async{      await  Provide.value(context).getGoodsInfo(goodsId);      return '完成加载';  } }

总结:这节课主要是把商品详细页的首页制作好,制作好以后会把商品详细页进行拆分,拆分成不同的组件到不同的文件中,虽然这很绕,但是在公司中的开发就是这样的。细致的差分适合于大型项目多人开发。最后由组长组合成一个页面。

#第43节:路由_补充首页跳转到详细页

前几节课只把首页的“火爆专区”加了跳转,这节课内容正好不多,就把其它需要加跳转到详细页的位置都加上跳转。需要注意的是,这些都需要加入context,上下文文件。

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004136702600

#轮播图加入跳转

直接打开home_page.dart找到轮播图组件,在ontap里,加入下面的代码。

 Application.router.navigateTo(context,"/detail?id=${swiperDataList[index]['goodsId']}"); 

#商品推荐加入跳转

同样在商品推荐的_item内部方法里的onTap中加入下面代码。

Application.router.navigateTo(context,"/detail?id=${recommendList[index]['goodsId']}"); 

#楼层加入跳转

在楼层方法的_goodsItem中的onTap方法中加入下面的代码.

 Application.router.navigateTo(context, "/detail?id=${goods['goodsId']}");

总结:我本来觉的这个小伙伴可以自己加入进来,但是还是有很多小伙伴遇到了麻烦,那为了能让每个人都做出视频中的效果,这节课作为一个补充。

#第44节:详细页_首屏自定义Widget编写

这节课把详细页首屏独立出来,这样业务逻辑更具体,以后也会降低维护成本。最主要的是主UI文件不会变的臃肿不堪。

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004163770688

#建立文件和引入资源

/lib/pages/文件夹下面,新建一个文件夹,命名为details_page,然后进入文件夹,新建立文件details_top_area.dart。意思是商品详细页的顶部区域。

然后用import引入如下文件:

import 'package:flutter/material.dart'; import 'package:provide/provide.dart'; import '../../provide/details_info.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; 

然后用快速生成的方法,新建一个StatelessWidget的类。

class DetailsTopArea extends StatelessWidget {    } 

先不管build方法,通过分析,我们把这个首屏页面进行一个组件方法的拆分。

#商品图片方法

直接写一个内部方法,然后返回一个商品图片就可以了,代码如下:

  //商品图片  Widget _goodsImage(url){    return  Image.network(        url,        width:ScreenUtil().setWidth(740)     );   } 

#商品名称方法

  //商品名称  Widget _goodsName(name){       return Container(        width: ScreenUtil().setWidth(730),        padding: EdgeInsets.only(left:15.0),        child: Text(          name,          maxLines: 1,          style: TextStyle(            fontSize: ScreenUtil().setSp(30)          ),        ),      );  }

#编号方法

  Widget _goodsNum(num){    return  Container(      width: ScreenUtil().setWidth(730),      padding: EdgeInsets.only(left:15.0),      margin: EdgeInsets.only(top:8.0),      child: Text(        '编号:${num}',        style: TextStyle(          color: Colors.black26        ),      ),          );  } 

#Build方法编写

再build方法的最外层,使用了Provde Widget,目的就是当状态发生变化时页面也进行变化。在Provide的构造器里,声明了一个goodsInfo变量,再通过Provide得到变量。然后进行UI的组合编写。

代码如下:

  Widget build(BuildContext context) {    return Provide(       builder:(context,child,val){        var goodsInfo=Provide.value(context).goodsInfo.data.goodInfo;         if(goodsInfo != null){            return Container(                color: Colors.white,                padding: EdgeInsets.all(2.0),                child: Column(                  children: [                      _goodsImage( goodsInfo.image1),                      _goodsName( goodsInfo.goodsName ),                        _goodsNum(goodsInfo.goodsSerialNumber),                      _goodsPrice(goodsInfo.presentPrice,goodsInfo.oriPrice)                  ],                ),              );         }else{          return Text('正在加载中......');        }      }    );  } 

为了方便学习,现在给出总体代码:

import 'package:flutter/material.dart'; import 'package:provide/provide.dart'; import '../../provide/details_info.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; 
//商品详情页的首屏区域,包括图片、商品名称,商品价格,商品编号的UI展示 class DetailsTopArea extends StatelessWidget {  @override  Widget build(BuildContext context) {    return Provide(       builder:(context,child,val){        var goodsInfo=Provide.value(context).goodsInfo.data.goodInfo;         if(goodsInfo != null){            return Container(                color: Colors.white,                padding: EdgeInsets.all(2.0),                child: Column(                  children: [                      _goodsImage( goodsInfo.image1),                      _goodsName( goodsInfo.goodsName ),                        _goodsNum(goodsInfo.goodsSerialNumber),                      _goodsPrice(goodsInfo.presentPrice,goodsInfo.oriPrice)                  ],                ),              );         }else{          return Text('正在加载中......');        }      }    );  }   //商品图片  Widget _goodsImage(url){    return  Image.network(        url,        width:ScreenUtil().setWidth(740)     );   }   //商品名称  Widget _goodsName(name){       return Container(        width: ScreenUtil().setWidth(730),        padding: EdgeInsets.only(left:15.0),        child: Text(          name,          maxLines: 1,          style: TextStyle(            fontSize: ScreenUtil().setSp(30)          ),        ),      );  }   //商品编号   Widget _goodsNum(num){    return  Container(      width: ScreenUtil().setWidth(730),      padding: EdgeInsets.only(left:15.0),      margin: EdgeInsets.only(top:8.0),      child: Text(        '编号:${num}',        style: TextStyle(          color: Colors.black26        ),      ),          );  }   //商品价格方法   Widget _goodsPrice(presentPrice,oriPrice){     return  Container(      width: ScreenUtil().setWidth(730),      padding: EdgeInsets.only(left:15.0),      margin: EdgeInsets.only(top:8.0),      child: Row(        children: [          Text(            '¥${presentPrice}',            style: TextStyle(              color:Colors.pinkAccent,              fontSize: ScreenUtil().setSp(40),             ),           ),          Text(            '市场价:¥${oriPrice}',            style: TextStyle(              color: Colors.black26,              decoration: TextDecoration.lineThrough            ),                                        )        ],      ),    );   }   }

#加入到UI当中

现在这个首屏组件算是编写好,就可以在主UI文件中lib/pages/details_page.dart中进行引入,并展现出来了。

import './details_page/details_top_area.dart';

引入后,在build方法里的column部件中进行加入下面的代码.

body:FutureBuilder(  future: _getBackInfo(context) ,  builder: (context,snapshot){    if(snapshot.hasData){        return Container(          child:Column(                children: [                    //关键代码------start                    DetailsTopArea(),                    //关键代码------end                ],          )        );    }else{        return Text('加载中........');    }  } )

总结:本节课的内容比较多,都是些Flutter页面制作的实战方法,希望小伙伴们动手制作,都能实现出完美的效果。

#第45节:详细页_说明区域UI编写

这节先把说明区域给制作出来,当然这部分也单独的独立出来。然后再自己学一个tabBar Widget。对!你没有听错,就是自己写,不用官方自带的。学习吗,就是要变态的折磨自己,现在不是流行盘吗。那我们也要有盘的心态,赏玩Flutter。

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004177447745

#说明区域制作

首先在lib/pages/details_page文件夹下,建立details_explain文件。建立好后,先引入所需要的文件,代码如下:

import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';

然后生成一个StatelessWidget,然后就是编写UI样式了,整体艾玛如下。

import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; 

class DetailsExplain extends StatelessWidget {  @override  Widget build(BuildContext context) {    return Container(       color:Colors.white,       margin: EdgeInsets.only(top: 10),       width: ScreenUtil().setWidth(750),       padding: EdgeInsets.all(10.0),       child: Text(         '说明:> 急速送达 > 正品保证',         style: TextStyle(           color:Colors.red,           fontSize:ScreenUtil().setSp(30) ),      )    );  } }

编写好以后,可以到details_page.dart里进行引用和使用,先进行引用。

import './details_page/details_explain.dart';

然后在build方法body区域的Column中引用,代码如下,关注关键代码即可。

body:FutureBuilder(  future: _getBackInfo(context) ,  builder: (context,snapshot){    if(snapshot.hasData){        return Container(          child:Column(                children: [                    DetailsTopArea(),                    //关键代码----------start                    DetailsExplain(),                    //关键代码----------end                ],          )        );    }else{        return Text('加载中........');    }  } )

这步完成后就可以进行预览效果了,看看效果是不是自己想要的。

总结:这节课内容很少,但绝对不是混集数,原计划的60集如果不够,我会把集数调多,保证把规划的知识点都讲了。

#第46节:详细页_自建TabBar Widget

这节课自己建一个tabBar Widget,而不用Flutter自带的tabBar widget。对!你没有听错,就是自己写,不用官方自带的。学习吗,就是要变态的折磨自己,现在不是流行盘吗。那我们也要有盘的心态,赏玩Flutter。这几天我也花了60大洋买了一个文玩核桃,准备学着盘完一下,磨一下放浪不羁的心性。

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004184492944

#tabBar编写技巧

lib/pages/details_page文件夹下,新建一个details_tabbar.dart文件。

这个文件主要是写bar区域的UI和交互效果,就算这样简单的业务逻辑,也进行了分离。

先打开provide文件夹下的details_info.dart文件,进行修改。需要增加两个变量,用来控制那个Tab被选中。

   bool isLeft = true;   bool isRight = false;

然后在文件的最下方加入一个方法,用来改变选中的值,这个方法先这样写,以后会随着业务的增加而继续补充和改变.

  //改变tabBar的状态  changeLeftAndRight(String changeState){    if(changeState=='left'){      isLeft=true;      isRight=false;    }else{      isLeft=false;      isRight=true;    }     notifyListeners();   } 

Provide文件编写好以后,就可以打开刚才建立好的details_tabbar.dart文件进行编写了。

先把所需要的文件进行引入:

mport 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:provide/provide.dart'; import '../../provide/details_info.dart'; 

然后用快捷方法生成一个StatelessWidget,在build方法的下方,写入一个返回Widget的方法,代码如下:

  Widget _myTabBarLeft(BuildContext context,bool isLeft){    return InkWell(      onTap: (){              Provide.value(context).changeLeftAndRight('left');      },      child: Container(               padding:EdgeInsets.all(10.0),        alignment: Alignment.center,        width: ScreenUtil().setWidth(375),        decoration: BoxDecoration(          color: Colors.white,          border: Border(            bottom: BorderSide(              width: 1.0,              color: isLeft?Colors.pink:Colors.black12             )          )        ),        child: Text(          '详细',          style: TextStyle(            color:isLeft?Colors.pink:Colors.black           ),        ),      ),    );  } 

这个方法就是详细的bar,然后再复制这段代码,修改成右边的bar。

Widget _myTabBarRight(BuildContext context,bool isRight){    return InkWell(      onTap: (){              Provide.value(context).changeLeftAndRight('right');      },      child: Container(                 padding:EdgeInsets.all(10.0),        alignment: Alignment.center,        width: ScreenUtil().setWidth(375),        decoration: BoxDecoration(          color: Colors.white,          border: Border(            bottom: BorderSide(              width: 1.0,              color: isRight?Colors.pink:Colors.black12             )          )        ),        child: Text(          '评论',          style: TextStyle(            color:isRight?Colors.pink:Colors.black           ),        ),      ),    );  } 

两个方法当然是一个合并成一个方法的,这样会放到所有代码实现之后,我们进行代码的优化。现在要作的是把build方法写好。代码如下:

Widget build(BuildContext context) { return Provide(  builder: (context,child,val){    var isLeft= Provide.value(context).isLeft;    var isRight =Provide.value(context).isRight;        return Container(      margin: EdgeInsets.only(top: 15.0),      child: Column(        children: [          Row(            children: [              _myTabBarLeft(context,isLeft),              _myTabBarRight(context,isRight)            ],          ),        ], 
      ),          ) ;  },  ); } 

为了方便你学习,这里给出所有的details_tabbar.dart文件,代码如下:

import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:provide/provide.dart'; import '../../provide/details_info.dart'; 
class DetailsTabBar extends StatelessWidget {      Widget build(BuildContext context) {    return Provide(      builder: (context,child,val){        var isLeft= Provide.value(context).isLeft;        var isRight =Provide.value(context).isRight;               return Container(          margin: EdgeInsets.only(top: 15.0),          child: Column(            children: [              Row(                children: [                  _myTabBarLeft(context,isLeft),                  _myTabBarRight(context,isRight)                ],              ),            ], 
          ),                  ) ;      },          );   }   Widget _myTabBarLeft(BuildContext context,bool isLeft){    return InkWell(      onTap: (){              Provide.value(context).changeLeftAndRight('left');      },      child: Container(               padding:EdgeInsets.all(10.0),        alignment: Alignment.center,        width: ScreenUtil().setWidth(375),        decoration: BoxDecoration(          color: Colors.white,          border: Border(            bottom: BorderSide(              width: 1.0,              color: isLeft?Colors.pink:Colors.black12             )          )        ),        child: Text(          '详细',          style: TextStyle(            color:isLeft?Colors.pink:Colors.black           ),        ),      ),    );  }  Widget _myTabBarRight(BuildContext context,bool isRight){    return InkWell(      onTap: (){              Provide.value(context).changeLeftAndRight('right');      },      child: Container(                 padding:EdgeInsets.all(10.0),        alignment: Alignment.center,        width: ScreenUtil().setWidth(375),        decoration: BoxDecoration(          color: Colors.white,          border: Border(            bottom: BorderSide(              width: 1.0,              color: isRight?Colors.pink:Colors.black12             )          )        ),        child: Text(          '评论',          style: TextStyle(            color:isRight?Colors.pink:Colors.black           ),        ),      ),    );  } 
} 

#把TabBar引入项目

打开details_page.dart文件,然后把detals_tabbar.dart文件进行引入。

import './details_page/details_tabBar.dart';

然后再coloumn部分加入就可以了

child:Column(      children: [          DetailsTopArea(),          DetailsExplain(),          DetailsTabBar()      ], )

总结:这节的内容还是比较多的,重点是如何不用Flutter自带UI自己实现页面交互效果。希望小伙伴们多多练习。

#第47节:详细页_Flutter_html插件的使用

在详细页里的商品详细部分,是由图片和HTML组成的。但是Flutter本身是不支持Html的解析的,所以需要找个轮子,我之前用的是flutter_webView_plugin,但是效果不太好。经过大神网友推荐,最终选择了flutter_html.

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004194782674

#首次进入详细页Bug处理

在第一次进入进入详细页的时候,会有错误出现,页面也会变成一篇红色,当然这只是一瞬间。所以很多小伙伴没有看出来,但是如果你注意控制台,就会看出这个错误提示。

这个问题的主要原因是没有使用异步方法,所以在Provide里使用一下异步就可以解决。代码如下:

  //从后台获取商品数据  getGoodsInfo(String id) async{    var formData = {'goodId':id};    await request('getGoodDetailById',formData:formData).then((val){      var responseData= json.decode(val.toString());      goodsInfo = DetailsModle.fromJson(responseData);      notifyListeners();     });   }

#flutter_html介绍

flutter_html是一个可以解析静态html标签的Flutter Widget,现在支持超过70种不同的标签。

github地址:https://github.com/Sub6Resources/flutter_html

也算是目前支持html标签比较多的插件了,先进行插件的依赖注册,打开pubspec.yaml文件。在dependencies里边,加入下面的代码:

flutter_html: ^0.9.6

如果你不是跟着教程走的,你需要到github上看一下最新的版本,然后使用最新的版本。

#代码的编写

当依赖和包下载好以后,直接在lib/pages/details_page文件夹下建立一个detals_web.dart文件。

建立好后,先引入依赖包。

import 'package:flutter/material.dart'; import 'package:provide/provide.dart'; import '../../provide/details_info.dart'; import 'package:flutter_html/flutter_html.dart'; 

然后写一个StatelessWidget,在他的build方法里,声明一个变量goodsDetail,然后用Provide的获得值。有了值之后直接使用Html Widget 就可以显示出来了。

import 'package:flutter/material.dart'; import 'package:provide/provide.dart'; import '../../provide/details_info.dart'; import 'package:flutter_html/flutter_html.dart'; 
class DetailsWeb extends StatelessWidget {    @override  Widget build(BuildContext context) {    var goodsDetail=Provide.value(context).goodsInfo.data.goodInfo.goodsDetail;    return Container(        child: Html(          data:goodsDetail        ),              );  } }

这节课我们先不写什么业务逻辑,只是学习一下这个组件就可以。下节课我们在完善具体的业务逻辑。

#加入到details_page.dart

先引入刚才编写的details_web.dart文件。

import './details_page/details_web.dart';

然后在columnchildren数组中加入DetailsWeb()

children: [    DetailsTopArea(),    DetailsExplain(),    DetailsTabBar(),    //关键代码-------------start    DetailsWeb()    //关键代码-------------end ],

如果出现溢出问题,那直接把Column换成ListView就可以了。

这些都做完了,就可以简单看一下效果了,应该还是很完美的。那需要注意的是,这只是为了讲课每节课都有一个节点,以后还会改动UI代码和业务逻辑增加。

#第48节:详细页_详情和评论切换效果制作

这节主要制作一下商品详情和评论页面的切换交互效果,思路是利用Provide进行业务处理,然后根据状态进行判断返回不同的Widget。

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004208877794

#嵌套Provide组件

在build返回里,的return部分,嵌套一个Provide组件。然后在builder里取得isLieft的值,如果值为true,那说明点击了商品详情,如果是false,那说明点击了评论的tabBar

全部代码如下:

import 'package:flutter/material.dart'; import 'package:provide/provide.dart'; import '../../provide/details_info.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; 
class DetailsWeb extends StatelessWidget {    @override  Widget build(BuildContext context) {    var goodsDetail=Provide.value(context).goodsInfo.data.goodInfo.goodsDetail;             return  Provide(                builder: (context,child,val){           var isLeft = Provide.value(context).isLeft;           if(isLeft){             return  Container(                  child: Html(                    data:goodsDetail                  ),              );           }else{            return Container(              width: ScreenUtil().setWidth(750),              padding: EdgeInsets.all(10),              alignment: Alignment.center,              child:Text('暂时没有数据')            );           }        },      );        } }

我看了小程序中,大部分都是没有商品评论的,而且商品评论的代码也没有什么新的知识点,所以这里就写成固定的内容。如果感兴趣的小伙伴可以自己完成此部分的编写。

总结,到目前位置,详细页面的主要制作已经完成。只是还缺少一个底部的购买按钮。

#第49节:详细页页_Stack作底部操作栏

在详细页面底部是有一个操作栏一直在底部的,主要用于进行加入购物车、直接购买商品和进入购物车页面。制作这个只要需要使用Stack组件就可以了。

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004224296454

#Stack组件介绍

Stack组件是层叠组件,里边的每一个子控件都是定位或者不定位,定位的子控件是被Positioned Widget进行包裹的。

比如现在改写之前的details_page.dart文件,在ListView的外边包裹Stack Widget。修改的代码如下。

import 'package:flutter/material.dart'; import 'package:provide/provide.dart'; import '../provide/details_info.dart'; import './details_page/details_top_area.dart'; import './details_page/details_explain.dart'; import './details_page/details_tabBar.dart'; import './details_page/details_web.dart'; 

 class DetailsPage extends StatelessWidget {  final String goodsId;  DetailsPage(this.goodsId);     @override  Widget build(BuildContext context) {           return Scaffold(         appBar: AppBar(            leading: IconButton(              icon:Icon(Icons.arrow_back),              onPressed: (){                print('返回上一页');                Navigator.pop(context);              },              ),            title: Text('商品详细页'),          ),          body:FutureBuilder(            future: _getBackInfo(context) ,            builder: (context,snapshot){              if(snapshot.hasData){              //关键代码-----------start                  return Stack(                    children: [                      ListView(                        children: [                            DetailsTopArea(),                            DetailsExplain(),                            DetailsTabBar(),                            DetailsWeb(),                                                      ],                        ),                      Positioned(                        bottom: 0,                        left: 0,                        child: Text('测试')                      )                    ],                  );            //关键代码---------------end              }else{                  return Text('加载中........');              }              }            )       );  }   Future _getBackInfo(BuildContext context )async{      await  Provide.value(context).getGoodsInfo(goodsId);      return '完成加载';  } 
} 

修改完成后,就可以看一下效果了。是不是已经实现了层叠效果了。

#制作底部工具栏

这个工具栏我们使用Flutter自带的bottomNavBar是没办法实现的,所以,我们才用了Stack,把他固定在页面底部。然后我们还需要新建立一个页面,在lib/pages/details_page文件夹下,新建立一个details_bottom.dart文件。

在这个文件中,我们才用了Row布局,然后使用Containter进行了精准的控制,最终实现了想要的结果。代码如下:

import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; 

class DetailsBottom extends StatelessWidget {  @override  Widget build(BuildContext context) {    return Container(       width:ScreenUtil().setWidth(750),       color: Colors.white,       height: ScreenUtil().setHeight(80),       child: Row(         children: [           InkWell(             onTap: (){},             child: Container(                width: ScreenUtil().setWidth(110) ,                alignment: Alignment.center,                child:Icon(                      Icons.shopping_cart,                      size: 35,                      color: Colors.red,                    ),               ) ,           ),           InkWell(             onTap: (){},             child: Container(               alignment: Alignment.center,               width: ScreenUtil().setWidth(320),               height: ScreenUtil().setHeight(80),               color: Colors.green,               child: Text(                 '加入购物车',                 style: TextStyle(color: Colors.white,fontSize: ScreenUtil().setSp(28)),               ),             ) ,           ),           InkWell(             onTap: (){},             child: Container(               alignment: Alignment.center,               width: ScreenUtil().setWidth(320),               height: ScreenUtil().setHeight(80),               color: Colors.red,               child: Text(                 '马上购买',                 style: TextStyle(color: Colors.white,fontSize: ScreenUtil().setSp(28)),               ),             ) ,           ),         ],       ),    );  } }

#加入到页面中

写完这个Widget后,需要在商品详细页里先用import引入。

import './details_page/details_bottom.dart';

然后把组件放到Positioned里,代码如下:

Positioned(  bottom: 0,  left: 0,  child: DetailsBottom() )

总结:这节课完成后,我们商品详细页的大部分交互效果就已经完成了,下节课开始,我们要制作购物车的效果了。希望小伙伴们能耐心的把商品详细页的代码完成。

#第50节:持久化_shared_preferences基础1

购物车中的一项功能是持久化,就是我们关掉APP,下次进入后,还是可以显示出我们放入购物车的商品。但是这些商品不和后台进行数据交互,前台如果使用sqflite又显得太重,还要懂SQL知识。所以在购物车页面我们采用shared_preferences来进行持久化,它是简单的键-值的操作。

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004252820576

#认识shared_preferences

shared_preferences是一个Flutter官方出的插件,它的主要作用就是可以key-value的形式来进行APP可客户端的持久化。

GitHub地址:https://github.com/flutter/plugins/tree/master/packages/shared_preferences

项目包依赖设置

既然是插件,使用前需要在pubspec.yaml里进行依赖设置,直接在dependencies里加入下面的代码:

shared_preferences: ^0.5.1 

课程编写是0.5.1是最新版本,你学习时请使用最新版本。写完以来后,需要进行下载package

#shared_preferences 增加方法

先来看看shared_preferences如何进行增加所存储的key-value值。删除购物车页面以前的代码,在这个页面进行新知识的学习。

先引入几个必要的包,使用shared_preferences前是要用import进行引入的。

import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart';

然后用快速生成的方法stful,生成一个StatefulWidget类,起类名叫CartPage。在类里声明一个变量testList

  List testList =[];

此时代码如下:

import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; 

class CartPage extends StatefulWidget {  @override  _CartPageState createState() => _CartPageState(); }
 class _CartPageState extends State {   List testList =[];  @override  Widget build(BuildContext context) {    return Container(     );  } }

#编写增加方法

我们在类里声明一个内部方法add,代码如下:

  void _add() async {      SharedPreferences prefs = await SharedPreferences.getInstance();      String temp="技术胖是最胖的!";      testList.add(temp);      prefs.setStringList('testInfo', testList);      _show();  } 

#编写显示方法

  void _show() async{    SharedPreferences prefs = await SharedPreferences.getInstance();    setState(() {        if(prefs.getStringList('testInfo')!=null){            testList=prefs.getStringList('testInfo');        }           });  }

#编写删除方法

 void _clear() async{    SharedPreferences prefs = await SharedPreferences.getInstance();    //prefs.clear(); //全部清空    prefs.remove('testInfo'); //删除key键    setState((){      testList=[];    });  }

#build方法编写

有了这些方法,我们只要在build里加入一个ListView再加上两个按钮就可以了。

@override  Widget build(BuildContext context) {    _show();  //每次进入前进行显示    return Container(       child:Column(        children: [          Container(            height: 500.0,            child: ListView.builder(                itemCount:testList.length ,                itemBuilder: (context,index){                  return ListTile(                    title: Text(testList[index]),                  );                },              ) ,          ),                   RaisedButton(            onPressed: (){_add();},            child: Text('增加'),          ),          RaisedButton(            onPressed: (){_clear();},            child: Text('清空'),          ),        ],      )           );  }

这样就完成了所有代码的编写,但这节课并不是为了做出什么效果,而是学会shared_preferences的增删改查操作。

#第51节:购物车_添加商品

从这节课开始,就正式开始制作购物车部分的内容了。这也算是本套视频最复杂的一个章节,也是我们基本掌握Flutter实战技巧关键的一个章节,当然我会还是采用UI代码和业务逻辑完全分开的形式,让代码完全解耦。

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004265575653

#Provide的建立

因为要UI和业务进行分离,所以还是需要先建立一个Provide文件,在lib/provide/文件夹下,建立一个cart.dart文件。

先引入下面三个文件和包:

import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'dart:convert';

引进后建立一个类,并在里边写一个字符串变量(后期会换成对象)。代码如下:

import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'dart:convert'; 
class CartProvide with ChangeNotifier{   String cartString="[]"; 
} 

#添加商品到购物车

先来制作把商品添加到购物车的方法。思路是这样的,利用shared_preferences可以保存字符串的特点,我们先把List传换成字符串,然后操作的时候,我们再转换回来。说简单点就是持久化的只是一串字符串,然后需要操作的时候,我们变成List,操作List的每一项就可以了。

  save(goodsId,goodsName,count,price,images) async{    //初始化SharedPreferences    SharedPreferences prefs = await  SharedPreferences.getInstance();    cartString=prefs.getString('cartInfo');  //获取持久化存储的值    //判断cartString是否为空,为空说明是第一次添加,或者被key被清除了。    //如果有值进行decode操作    var temp=cartString==null?[]:json.decode(cartString.toString());    //把获得值转变成List    List tempList= (temp as List).cast();    //声明变量,用于判断购物车中是否已经存在此商品ID    var isHave= false;  //默认为没有    int ival=0; //用于进行循环的索引使用    tempList.forEach((item){//进行循环,找出是否已经存在该商品      //如果存在,数量进行+1操作      if(item['goodsId']==goodsId){        tempList[ival]['count']=item['count']+1;        isHave=true;      }      ival++;    });    //  如果没有,进行增加    if(!isHave){      tempList.add({        'goodsId':goodsId,        'goodsName':goodsName,        'count':count,        'price':price,        'images':images      });    }    //把字符串进行encode操作,    cartString= json.encode(tempList).toString();    print(cartString);    prefs.setString('cartInfo', cartString);//进行持久化     }

#清空购物车

为了测试方便,再顺手写一个清空购物车的方法,这个还没有谨慎思考,只是为了测试使用。

  remove() async{    SharedPreferences prefs = await SharedPreferences.getInstance();    //prefs.clear();//清空键值对    prefs.remove('cartInfo');    print('清空完成-----------------');    notifyListeners();  }

#注册全局依赖

main.dart文件中注册全局依赖,先引入cart.dart文件.

import './provide/cart.dart';

然后在main区域进行声明

var cartProvide = CartProvide();

进行注入:

..provide(Provider.value(cartProvide))

#业务逻辑加入到UI

details_bottom.dart文件里,加入Provide,先进行引入。

import 'package:provide/provide.dart'; import '../../provide/cart.dart'; import '../../provide/details_info.dart';

然后声明provide的save方法中需要的参数变量。

var goodsInfo = Provide.value(context).goodsInfo.data.goodInfo; var goodsId= goodsInfo.goodsId; var goodsName =goodsInfo.goodsName; var count =1; var price =goodsInfo.presentPrice; var images= goodsInfo.image1; 

然后在加入购物车的按钮的onTap方法中,加入下面代码.

onTap: ()async {  await Provide.value(context).save(goodsID,goodsName,count,price,images);  },

先暂时把“马上结账”按钮方式清除购物车的方法,方便我们测试。

onTap: ()async{  await Provide.value(context).remove(); },

做完这个写,我们就要查看一下效果了,看看是否可以真的持久化。

#第52节:购物车_建立数据模型

上节课使用了字符串进行持久化,然后输出的时候都是Map,但是在真实工作中为了减少异常的发生,都要进行模型化处理,就是把Map转变为对象。

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004278100281

#建立模型文件

得到的购物车数据,如下:

{"goodsId":"2171c20d77c340729d5d7ebc2039c08d","goodsName":"五粮液52°500ml","count":1,"price":830.0,"images":"http://images.baixingliangfan.cn/shopGoodsImg/20181229/20181229211422_8507.jpg"}

拷贝到自动生成mode的页面上,网址是:

https://javiercbk.github.io/json_to_dart/

生成后,在model文件夹下,建立一个新文件cartInfo.dart,然后把生成的mode文件进行改写,代码如下:

class CartInfoMode {  String goodsId;  String goodsName;  int count;  double price;  String images;   CartInfoMode(      {this.goodsId, this.goodsName, this.count, this.price, this.images});   CartInfoMode.fromJson(Map json) {    goodsId = json['goodsId'];    goodsName = json['goodsName'];    count = json['count'];    price = json['price'];    images = json['images'];  }   Map toJson() {    final Map data = new Map();    data['goodsId'] = this.goodsId;    data['goodsName'] = this.goodsName;    data['count'] = this.count;    data['price'] = this.price;    data['images'] = this.images;    return data;  } }

这个相对于以前其它Model文件简单很多。其实你完全可以手写练习一下。

#在provide里使用模型

有了模型文件之后,需要先引入provide里,然后进行改造。引入刚刚写好的模型层文件。

import '../model/cartInfo.dart';

provide类的最上部新声明一个List变量,这就是购物车页面用于显示的购物车列表了.

List cartList=[];

然后改造save方法,让他支持模型类,但是要注意,原来的字符串不要改变,因为shared_preferences不持支对象的持久化。

  save(goodsId,goodsName,count,price,images) async{    //初始化SharedPreferences    SharedPreferences prefs = await  SharedPreferences.getInstance();    cartString=prefs.getString('cartInfo');  //获取持久化存储的值    //判断cartString是否为空,为空说明是第一次添加,或者被key被清除了。    //如果有值进行decode操作    var temp=cartString==null?[]:json.decode(cartString.toString());    //把获得值转变成List    List tempList= (temp as List).cast();    //声明变量,用于判断购物车中是否已经存在此商品ID    var isHave= false;  //默认为没有    int ival=0; //用于进行循环的索引使用    tempList.forEach((item){//进行循环,找出是否已经存在该商品      //如果存在,数量进行+1操作      if(item['goodsId']==goodsId){        tempList[ival]['count']=item['count']+1;         //关键代码-----------------start        cartList[ival].count++;         //关键代码-----------------end        isHave=true;      }      ival++;    });    //  如果没有,进行增加    if(!isHave){       //关键代码-----------------start          Map newGoods={             'goodsId':goodsId,            'goodsName':goodsName,            'count':count,            'price':price,            'images':images          };          tempList.add(newGoods);          cartList.add(new CartInfoMode.fromJson(newGoods));       //关键代码-----------------end    }    //把字符串进行encode操作,    cartString= json.encode(tempList).toString();    print(cartString);    print(cartList.toString());    prefs.setString('cartInfo', cartString);//进行持久化    notifyListeners();  } 

#得到购物车中商品方法

有了增加方法,我们还需要写一个得到购物车中的方法,现在就学习一下结合Model如何得到持久化的数据。

  //得到购物车中的商品  getCartInfo() async {     SharedPreferences prefs = await SharedPreferences.getInstance();     //获得购物车中的商品,这时候是一个字符串     cartString=prefs.getString('cartInfo');      //把cartList进行初始化,防止数据混乱      cartList=[];     //判断得到的字符串是否有值,如果不判断会报错     if(cartString==null){       cartList=[];     }else{       List tempList= (json.decode(cartString.toString()) as List).cast();       tempList.forEach((item){          cartList.add(new CartInfoMode.fromJson(item));       });      }      notifyListeners();  } 

有了这个方法,下节课就可以开心的布局页面了,再也不用在终端里看结果了。

#第53节:购物车_大体结构布局

这节课终于可以不再忍受终端中查看结果的苦恼了,开始制作页面。其实在实际开发中也有很多这样的情况。就是先得到数据,再调试页面。

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004294912896

#页面基本结构搭建

先建立页面的基本接口,还是使用脚手架组件Scaffold来进行操作。代码如下:

import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:provide/provide.dart'; import '../provide/cart.dart'; 

class CartPage extends StatelessWidget {   @override  Widget build(BuildContext context) {    return Scaffold(      appBar: AppBar(        title: Text('购物车'),      ),      body:Text('测试')    );  } }

再body区域我们使用Future Widget,因为就算是本地持久化,还是有一个时间的,当然这个时间可能你肉眼看不见。不过这样控制台可能会把错误信息返回回来。

  body: FutureBuilder(    future:_getCartInfo(context),    builder: (context,snapshot){      List cartList=Provide.value(context).cartList;      if(snapshot.hasData){             }else{        return Text('正在加载');      }    },  ),  );  } 

#Future方法编写

使用了Future组件,自然需要一个返回Future的方法了,在这个方法里,我们使用Provide取出本地持久化的数据,然后进行变化。

  Future _getCartInfo(BuildContext context) async{     await Provide.value(context).getCartInfo();     return 'end';  } 

#用ListView简单输出

return ListView.builder(  itemCount: cartList.length,  itemBuilder: (context,index){    return ListTile(      title:Text(cartList[index].goodsName)    );  }, );

到这步后,就可以简单的进行预览,当然页面还是很丑的,下节课会继续进行美化。会把列表的子项单独拿出一个文件,这样会降低以后的维护成本。

#第54节:购物车_商品列表子项组件编写

上节课已经把购物车页面的大体结构编写好,并且也可以获得购物车中的商品列表信息了,但是页面依然丑陋,这节课继续上节课完成子项的UI美化.

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004295310344

#编写购物车单项方法

为了以后维护方便,我们还是采用单独编写的方式,把购物车里边的每一个子项统一作一个组件出来。

现在lib\pages下建立一个新文件夹cart_page,然后在新文件夹下面家里一个cart_item.dart文件。先引入几个必要的文件.

import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../../model/cartInfo.dart'; 

然后声明一个stateLessWidget 类,名字叫CartItem并设置接收参数,这里的接收参数就是cartInfo对象,也就是每个购物车商品的子项。代码如下:

import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../../model/cartInfo.dart'; 
class CartItem extends StatelessWidget {  final CartInfoMode item;  CartItem(this.item);   @override  Widget build(BuildContext context) {    print(item);    return Container(        margin: EdgeInsets.fromLTRB(5.0,2.0,5.0,2.0),        padding: EdgeInsets.fromLTRB(5.0,10.0,5.0,10.0),        decoration: BoxDecoration(          color: Colors.white,          border: Border(            bottom: BorderSide(width:1,color:Colors.black12)          )        ),        child: Row(          children: [                    ],        ),      );  } 

 }

#编写多选按钮方法

//多选按钮  Widget _cartCheckBt(item){    return Container(      child: Checkbox(        value: true,        activeColor:Colors.pink,        onChanged: (bool val){},      ),    );  } 

#编写商品图片方法

//商品图片   Widget _cartImage(item){        return Container(      width: ScreenUtil().setWidth(150),      padding: EdgeInsets.all(3.0),      decoration: BoxDecoration(        border: Border.all(width: 1,color:Colors.black12)      ),      child: Image.network(item.images),    );  }

#编写商品名称方法

//商品名称  Widget _cartGoodsName(item){    return Container(      width: ScreenUtil().setWidth(300),      padding: EdgeInsets.all(10),      alignment: Alignment.topLeft,      child: Column(        children: [          Text(item.goodsName)        ],      ),    );  }

#编写商品价格方法

//商品价格  Widget _cartPrice(item){     return Container(        width:ScreenUtil().setWidth(150) ,        alignment: Alignment.centerRight,                child: Column(          children: [            Text('¥${item.price}'),            Container(              child: InkWell(                onTap: (){},                child: Icon(                  Icons.delete_forever,                  color: Colors.black26,                  size: 30,                ),              ),            )          ],        ),      );  }

#进行整合

这些组件写好以后,我们可以进行一个整合。

child: Row(  children: [    _cartCheckBt(item),    _cartImage(item),    _cartGoodsName(item),    _cartPrice(item)  ], ),

为了方便学习,全部代码如下:

import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../../model/cartInfo.dart'; 
class CartItem extends StatelessWidget {  final CartInfoMode item;  CartItem(this.item);   @override  Widget build(BuildContext context) {    print(item);    return Container(        margin: EdgeInsets.fromLTRB(5.0,2.0,5.0,2.0),        padding: EdgeInsets.fromLTRB(5.0,10.0,5.0,10.0),        decoration: BoxDecoration(          color: Colors.white,          border: Border(            bottom: BorderSide(width:1,color:Colors.black12)          )        ),        child: Row(          children: [            _cartCheckBt(item),            _cartImage(item),            _cartGoodsName(item),            _cartPrice(item)          ],        ),      );  }  //多选按钮  Widget _cartCheckBt(item){    return Container(      child: Checkbox(        value: true,        activeColor:Colors.pink,        onChanged: (bool val){},      ),    );  }  //商品图片   Widget _cartImage(item){        return Container(      width: ScreenUtil().setWidth(150),      padding: EdgeInsets.all(3.0),      decoration: BoxDecoration(        border: Border.all(width: 1,color:Colors.black12)      ),      child: Image.network(item.images),    );  }  //商品名称  Widget _cartGoodsName(item){    return Container(      width: ScreenUtil().setWidth(300),      padding: EdgeInsets.all(10),      alignment: Alignment.topLeft,      child: Column(        children: [          Text(item.goodsName)        ],      ),    );  }   //商品价格  Widget _cartPrice(item){     return Container(        width:ScreenUtil().setWidth(150) ,        alignment: Alignment.centerRight,                child: Column(          children: [            Text('¥${item.price}'),            Container(              child: InkWell(                onTap: (){},                child: Icon(                  Icons.delete_forever,                  color: Colors.black26,                  size: 30,                ),              ),            )          ],        ),      );  } 
} 

#第55节:购物车_制作底部结算栏的UI

这节课主要布局一下底部操作栏。这个使用了Stack Widget,由于以前视频中学过,所以做起来也就没那么难了,但是还是有很多样式需要我们书写,以保证完成一个美观的购物车页面的。

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004307739660

#建立底部结算栏页面

lib/pages/cart_page文件夹下,新建一个cart_bottom.dart文件。文件建立好以后,先引入下面的基础package

import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';

引入完成后,用快捷的方式建立一个StatelessWidget,建立后,我们使用Row来进行总体布局,并给Container一些必要的修饰.代码如下:

class CartBottom extends StatelessWidget {  @override  Widget build(BuildContext context) {    return Container(      margin: EdgeInsets.all(5.0),      color: Colors.white,      width: ScreenUtil().setWidth(750),      child: Row(        children: [                ],      ),    );  } }

这就完成了一个底部结算栏的大体结构确定,大体结构完成后,我们还是把里边的细节,拆分成不同的方法返回对象的组件。

#全选按钮方法

先来制作全选按钮方法,这个外边采用Container,里边使用了一个Row,这样能很好的完成横向布局的需求.

  //全选按钮  Widget selectAllBtn(){    return Container(      child: Row(        children: [          Checkbox(            value: true,            activeColor: Colors.pink,            onChanged: (bool val){},          ),          Text('全选')        ],      ),    );  } 

#合计区域方法

合计区域由于布局对齐方式比较复杂,所以这段代码虽然很简单,但是代码设计的样式比较多,需要你有很好的样式编写能力.代码如下:

  // 合计区域  Widget allPriceArea(){     return Container(      width: ScreenUtil().setWidth(430),      alignment: Alignment.centerRight,      child: Column(        children: [          Row(            children: [              Container(                alignment: Alignment.centerRight,                width: ScreenUtil().setWidth(280),                child: Text(                  '合计:',                  style:TextStyle(                    fontSize: ScreenUtil().setSp(36)                  )                ),               ),              Container(                 alignment: Alignment.centerLeft,                width: ScreenUtil().setWidth(150),                child: Text(                  '¥1922',                  style:TextStyle(                    fontSize: ScreenUtil().setSp(36),                    color: Colors.red,                  )                ),                              )                                       ],          ),          Container(            width: ScreenUtil().setWidth(430),            alignment: Alignment.centerRight,            child: Text(              '满10元免配送费,预购免配送费',              style: TextStyle(                color: Colors.black38,                fontSize: ScreenUtil().setSp(22)              ),            ),          )                  ],      ),    );   }

#结算按钮方法

这个方法里边的按钮,我们并没有使用Flutter Button Widget 而是使用InkWell自己制作一个组件。这样作能很好的控制按钮的形状,还可以解决水波纹的问题,一举两得。代码如下:

//结算按钮  Widget goButton(){        return Container(      width: ScreenUtil().setWidth(160),      padding: EdgeInsets.only(left: 10),      child:InkWell(        onTap: (){},        child: Container(          padding: EdgeInsets.all(10.0),          alignment: Alignment.center,          decoration: BoxDecoration(             color: Colors.red,             borderRadius: BorderRadius.circular(3.0)          ),          child: Text(            '结算(6)',            style: TextStyle(              color: Colors.white            ),          ),        ),      ) ,    );          }

#加入到页面中

组件样式基本都各自完成后,接下来就是组合和加入到页面中了,我们先把个个方法组合到底部结算区域,也就是放到build方法里。

  Widget build(BuildContext context) {    return Container(      margin: EdgeInsets.all(5.0),      color: Colors.white,      width: ScreenUtil().setWidth(750),      child: Row(        children: [          selectAllBtn(),          allPriceArea(),          goButton()        ],      ),    );  }

这步完成后就是到lib/pages/cart_page.dart文件中,加入底部结算栏的操作了,这里我们需要使用Stack Widget组件。

首先需要引入cart_bottom.dart

import './cart_page/cart_bottom.dart'; 

然后改写FutureBuilder Widget里边的builder方法,这时候返回的是一个Stack Widget。代码如下:

import 'package:flutter/material.dart'; import 'package:provide/provide.dart'; import '../provide/cart.dart'; import './cart_page/cart_item.dart'; import './cart_page/cart_bottom.dart'; 

 class CartPage extends StatelessWidget {   @override  Widget build(BuildContext context) {    return Scaffold(      appBar: AppBar(        title: Text('购物车'),      ),      body: FutureBuilder(        future:_getCartInfo(context),        builder: (context,snapshot){          List cartList=Provide.value(context).cartList;          if(snapshot.hasData && cartList!=null){            //关键代码-------------------start            return Stack(              children: [                ListView.builder(                  itemCount: cartList.length,                  itemBuilder: (context,index){                    return CartItem(cartList[index]);                  },                ),                Positioned(                  bottom:0,                  left:0,                  child: CartBottom(),                )              ],            );            //关键代码-----------------end                       }else{            return Text('正在加载');          }        },      ),    );  }   Future _getCartInfo(BuildContext context) async{     await Provide.value(context).getCartInfo();     return 'end';  }   }

这步做完之后,就可以进行预览了。相信小伙伴们都可以得到满意的效果,其实学到这里,你应该有自己布局任何页面的能力,你可以试着把这个页面布局成自己想要的样子。下节课制作我们的数量加减组件。

#第56节:购物车_制作数量加减按钮UI

购物车的UI界面已经基本完成了,只差最后一个数量加载的部分没有进行布局,这节课就用几分钟时间,把这个部分的布局制作完成。

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004346946982

#建立组件和基本结构

lib/pages/cart_page/文件夹下,建立一个新的文件cart_count.dart。先引入两个布局使用的基本文件。

import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';

然后开始写基本结构,我们这里使用ContainerRow的形式。

  Widget build(BuildContext context) {    return Container(      width: ScreenUtil().setWidth(165),      margin: EdgeInsets.only(top:5.0),      decoration: BoxDecoration(        border:Border.all(width: 1 , color:Colors.black12)      ),      child: Row(        children: [        ],      ),          );  }

写完这个,我们再把Row里边的每个子元素进行拆分.

#减少按钮UI编写

  // 减少按钮  Widget _reduceBtn(){    return InkWell(      onTap: (){},      child: Container(        width: ScreenUtil().setWidth(45),        height: ScreenUtil().setHeight(45),        alignment: Alignment.center,               decoration: BoxDecoration(          color: Colors.white,          border:Border(            right:BorderSide(width:1,color:Colors.black12)          )        ),        child: Text('-'),      ),    );  }

#添加按钮UI编写

  //添加按钮  Widget _addBtn(){    return InkWell(      onTap: (){},      child: Container(        width: ScreenUtil().setWidth(45),        height: ScreenUtil().setHeight(45),        alignment: Alignment.center,                decoration: BoxDecoration(          color: Colors.white,          border:Border(            left:BorderSide(width:1,color:Colors.black12)          )        ),        child: Text('+'),      ),    );  }

#数量区域UI编写

  //中间数量显示区域  Widget _countArea(){    return Container(      width: ScreenUtil().setWidth(70),      height: ScreenUtil().setHeight(45),      alignment: Alignment.center,      color: Colors.white,       child: Text('1'),    );  }

#进行组合

组件都写好后,要进行组合和加入到页面中的操作。

组合:直接在build区域的Row数组中进行组合。

  Widget build(BuildContext context) {    return Container(      width: ScreenUtil().setWidth(165),      margin: EdgeInsets.only(top:5.0),      decoration: BoxDecoration(        border:Border.all(width: 1 , color:Colors.black12)      ),      child: Row(        //关键代码----------------start        children: [          _reduceBtn(),          _countArea(),          _addBtn(),        ],        //关键代码----------------end      ),          );  } 

这个不完成后,再到同级目录下的cart_item.dart,引入和使用。先进行文件的引入.

import './cart_count.dart';

引入后,再商品名称的方法中直接引入就。

 //商品名称  Widget _cartGoodsName(item){    return Container(      width: ScreenUtil().setWidth(300),      padding: EdgeInsets.all(10),      alignment: Alignment.topLeft,      child: Column(        children: [          Text(item.goodsName),          //关键代码---------start          CartCount()          //关键代码---------end        ],      ),    );  } 

完成后就可以进行预览了。通过几节课的制作,终于算是完成了购物车UI界面的编写。下节课开始编写购物车的业务逻辑。

#第57节:购物车_在Model中增加选中字段

通过布局,我们可以看到是有选中和多选操作的,但是在设计购物车模型时并没有涉及这个操作,所以这节课利用几分钟时间,把坑填补一下。

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004358384650

#修改Model文件

首先我们打开lib/model/cartInfo.dart文件,增加一个新的变量isCheck

class CartInfoMode {  String goodsId;  String goodsName;  int count;  double price;  String images;  //------新添加代码----start  bool isCheck;  //------新添加代码----end   CartInfoMode(      //需要修改---------start-----      {this.goodsId, this.goodsName, this.count, this.price, this.images,this.isCheck});      //修改需改--------end------   CartInfoMode.fromJson(Map json) {    goodsId = json['goodsId'];    goodsName = json['goodsName'];    count = json['count'];    price = json['price'];    images = json['images'];    //------新添加代码----start    isCheck = json['isCheck'];    //------新添加代码----end  }   Map toJson() {    final Map data = new Map();    data['goodsId'] = this.goodsId;    data['goodsName'] = this.goodsName;    data['count'] = this.count;    data['price'] = this.price;    data['images'] = this.images;    //------新添加代码----start    data['isCheck']= this.isCheck;    /------新添加代码----end    return data;  } }

#在增加时加入isCheck

打开lib/provide/cart.dart文件,找到添加购物车商品的方法save,修改增加的部分代码。

Map newGoods={  'goodsId':goodsId,  'goodsName':goodsName,  'count':count,  'price':price,  'images':images,  //-----新添加代码-----start  'isCheck': true  //是否已经选择  //-----新添加代码-----end };

#修改UI的值

之前UI中多选按钮的值,我们是写死的,现在就可以使用这个动态的值了。打开lib/pages/cart_page/cart_item.dart文件,找到多选按钮的部分,修改val的值.

Widget _cartCheckBt(context,item){  return Container(    child: Checkbox(      //修改部分--------start----      value: item.isCheck,      //修改部分--------end------      activeColor:Colors.pink,      onChanged: (bool val){      },    ),  ); }

记得修改完成后,要把原来的持久化字符串删除掉,删除掉后再次填入新的商品到购物车,就可以正常显示了。

#第58节:购物车_删除单个商品功能制作

页面终于制作完成了,剩下来就是逐步完善购物车中的各项功能,这部分的视频可能拆分的比较细致。这节课主要讲一下如何实现购物车中的删除功能。

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004359459591

#编写删除方法

直接在provide中的cart.dart文件里,增加一个deleteOneGoods方法。编写思路是这样的,先从持久化数据里得到数据,然后把纯字符串转换成字List,转换之后进行循环,如果goodsId,相同,说明就是要删除的项,把索引进行记录,记录之后用removeAt方法进行删除,删除后再次进行持久化,并重新获得数据。 主要代码如下:

  //删除单个购物车商品  deleteOneGoods(String goodsId) async{     SharedPreferences prefs = await SharedPreferences.getInstance();     cartString=prefs.getString('cartInfo');      List tempList= (json.decode(cartString.toString()) as List).cast();        int tempIndex =0;     int delIndex=0;     tempList.forEach((item){                  if(item['goodsId']==goodsId){          delIndex=tempIndex;                 }         tempIndex++;     });      tempList.removeAt(delIndex);      cartString= json.encode(tempList).toString();      prefs.setString('cartInfo', cartString);//      await getCartInfo();       }

这个部分需要注意的是,为什么循环时不进行删除,因为dart语言不支持迭代时进行修改,这样可以保证在循环时不出错。

#修改UI界面,实现效果

UI界面主要时增加Proivde组件,就是当值法伤变化时,界面也随着变化。打开cart_page.dart文件,主要修改build里的ListView区域,代码如下:

import 'package:flutter/material.dart'; import 'package:provide/provide.dart'; import '../provide/cart.dart'; import './cart_page/cart_item.dart'; import './cart_page/cart_bottom.dart'; 

 
class CartPage extends StatelessWidget {   @override  Widget build(BuildContext context) {    return Scaffold(      appBar: AppBar(        title: Text('购物车'),      ),      body: FutureBuilder(        future:_getCartInfo(context),        builder: (context,snapshot){          List cartList=Provide.value(context).cartList;          if(snapshot.hasData && cartList!=null){              return Stack(                children: [                  //主要代码--------------------start--------                  Provide(                                       builder: (context,child,childCategory){                       cartList= Provide.value(context).cartList;                      print(cartList);                      return ListView.builder(                        itemCount: cartList.length,                        itemBuilder: (context,index){                          return CartItem(cartList[index]);                        },                      );                    }                  ),                   //主要代码------------------end---------                  Positioned(                    bottom:0,                    left:0,                    child: CartBottom(),                  )                ],              );                  }else{            return Text('正在加载');          }        },      ),    );  }   Future _getCartInfo(BuildContext context) async{     await Provide.value(context).getCartInfo();     return 'end';  }   }

#增加删除响应事件

cart_item.dart文件中,增加删除响应事件,由于所有业务逻辑都在Provide中,所以需要引入下面两个文件。

import 'package:provide/provide.dart'; import '../../provide/cart.dart';

有了这两个文件后,可以修改对应的方法_cartPrice。首先要加入context选项,然后修改里边的onTap方法。具体代码如下:

  //商品价格  Widget _cartPrice(context,item){     return Container(        width:ScreenUtil().setWidth(150) ,        alignment: Alignment.centerRight,                child: Column(          children: [            Text('¥${item.price}'),            Container(              child: InkWell(                onTap: (){                  //主要代码---------------start----------                  Provide.value(context).deleteOneGoods(item.goodsId);                  //主要代码--------------end-----------                },                child: Icon(                  Icons.delete_forever,                  color: Colors.black26,                  size: 30,                ),              ),            )          ],        ),      );  } 

这步做完,已经有了删除功能,可以进行测试了.

#第59节:购物车_计算商品价格和数量

购物车中都有自动计算商品价格和商品数量的功能,这节课我们就把这两个小功能实现一下。

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004358188569

#增加Provide变量

lib/provide/cart.dart文件的类头部,增加总价格allPrice和总商品数量allGoodsCount两个变量.

class CartProvide with ChangeNotifier{   String cartString="[]";  List cartList=[]; //商品列表对象  //新代码----------start  double allPrice =0 ;   //总价格  int allGoodsCount =0;  //商品总数量

#修改getCartInfo()方法

主要是在循环是累计增加数量和价格,这里给出全部增加的代码,并标注了修改部分。

getCartInfo() async {     SharedPreferences prefs = await SharedPreferences.getInstance();     //获得购物车中的商品,这时候是一个字符串     cartString=prefs.getString('cartInfo');           //把cartList进行初始化,防止数据混乱      cartList=[];     //判断得到的字符串是否有值,如果不判断会报错     if(cartString==null){       cartList=[];     }else{       List tempList= (json.decode(cartString.toString()) as List).cast();        //---------修改代码------start-------------       allPrice=0;       allGoodsCount=0;        //---------修改代码------end-------------       tempList.forEach((item){           //---------修改代码------start-------------          if(item['isCheck']){             allPrice+=(item['count']*item['price']);             allGoodsCount+=item['count'];          }           //---------修改代码------end-------------                   cartList.add(new CartInfoMode.fromJson(item));        });      }      notifyListeners();  } 

#修改UI界面 显示结果

有了业务逻辑,就应该可以正常的显示出界面效果了。但是需要把原来我们写死的值,都改成动态的。

打开lib/pages/cart_page/cart_bottom.dart文件,先用import引入provide package

import 'package:provide/provide.dart'; import '../../provide/cart.dart'; 

然后把底部的三个区域方法都加上context上下文参数,因为Provide的使用,必须有上下文参数。

  Widget build(BuildContext context) {    return Container(      margin: EdgeInsets.all(5.0),      color: Colors.white,      width: ScreenUtil().setWidth(750),      child: Provide(        builder: (context,child,childCategory){          return  Row(            children: [              //修改部分--------start----------              selectAllBtn(context),              allPriceArea(context),              goButton(context)              //修改部分--------end-----------            ],          );        },      )    );  }

然后在两个方法中都从Provide里动态获取变量,就可以实现效果了。

合计区域的方法代码:

  // 合计区域  Widget allPriceArea(context){    //修改代码---------------start------------    double allPrice = Provide.value(context).allPrice;    //修改代码---------------end------------    return Container(      width: ScreenUtil().setWidth(430),      alignment: Alignment.centerRight,      child: Column(        children: [          Row(            children: [              Container(                alignment: Alignment.centerRight,                width: ScreenUtil().setWidth(280),                child: Text(                  '合计:',                  style:TextStyle(                    fontSize: ScreenUtil().setSp(36)                  )                ),               ),              Container(                 alignment: Alignment.centerLeft,                width: ScreenUtil().setWidth(150),                 //修改代码---------------start------------                child: Text(                  '¥${allPrice}',                  style:TextStyle(                    fontSize: ScreenUtil().setSp(36),                    color: Colors.red,                  )                ),                 //修改代码---------------end------------                              )                                       ],          ),          Container(            width: ScreenUtil().setWidth(430),            alignment: Alignment.centerRight,            child: Text(              '满10元免配送费,预购免配送费',              style: TextStyle(                color: Colors.black38,                fontSize: ScreenUtil().setSp(22)              ),            ),          )                  ],      ),    );   }

结算按钮区域

//结算按钮  Widget goButton(context){    //修改代码---------------start------------    int allGoodsCount =  Provide.value(context).allGoodsCount;    //修改代码---------------end--------------    return Container(      width: ScreenUtil().setWidth(160),      padding: EdgeInsets.only(left: 10),      child:InkWell(        onTap: (){},        child: Container(          padding: EdgeInsets.all(10.0),          alignment: Alignment.center,          decoration: BoxDecoration(             color: Colors.red,             borderRadius: BorderRadius.circular(3.0)          ),          //修改代码---------------start------------          child: Text(            '结算(${allGoodsCount})',            style: TextStyle(              color: Colors.white            ),          ),          //修改代码---------------end------------        ),      ) ,    );        } 

这步完成后,就应该可以正常动态显示购物车中的商品数量和商品价格了。

#第60节:购物车_商品选中功能制作

在购物车里是有选择和取消选择,还有全选的功能按钮的。当我们选择时,价格和数量都是跟着自动计算的,列表也是跟着刷新的。这节课主要完成单选和全选按钮的交互效果。

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004371854914

#制作商品单选按钮的交效果

这些业务逻辑代码,当然需要写到Provide中,打开lib/provide/cart.dart文件。新建一个changeCheckState方法:

  changeCheckState(CartInfoMode cartItem) async{     SharedPreferences prefs = await SharedPreferences.getInstance();     cartString=prefs.getString('cartInfo');  //得到持久化的字符串     List tempList= (json.decode(cartString.toString()) as List).cast(); //声明临时List,用于循环,找到修改项的索引     int tempIndex =0;  //循环使用索引     int changeIndex=0; //需要修改的索引     tempList.forEach((item){                  if(item['goodsId']==cartItem.goodsId){          //找到索引进行复制          changeIndex=tempIndex;         }         tempIndex++;     });     tempList[changeIndex]=cartItem.toJson(); //把对象变成Map值     cartString= json.encode(tempList).toString(); //变成字符串     prefs.setString('cartInfo', cartString);//进行持久化     await getCartInfo();  //重新读取列表      } 

业务逻辑写完后到到UI层进行修改,打开lib/pages/cart_page/cart_item.dart文件,修改多选按钮的onTap方法。

//多选按钮  Widget _cartCheckBt(context,item){    return Container(      child: Checkbox(        value: item.isCheck,        activeColor:Colors.pink,        //-------新增代码--------start---------        onChanged: (bool val){          item.isCheck=val;          Provide.value(context).changeCheckState(item);        },        //-------新增代码--------end---------      ),    );  } 

修改完成后,可以点击测试一下效果,如果一切正常,就可以进行选中和取消的交互了。

#全选按钮交互效果制作

声明一个状态变量isAllCheck,然后在读取购物车商品数据时进行更改。

  bool isAllCheck= true; //是否全选

修改getCartInfo方法,就是获取购物车列表的方法.

 //得到购物车中的商品  getCartInfo() async {     SharedPreferences prefs = await SharedPreferences.getInstance();     //获得购物车中的商品,这时候是一个字符串     cartString=prefs.getString('cartInfo');           //把cartList进行初始化,防止数据混乱      cartList=[];     //判断得到的字符串是否有值,如果不判断会报错     if(cartString==null){       cartList=[];     }else{       List tempList= (json.decode(cartString.toString()) as List).cast();       allPrice=0;       allGoodsCount=0;       //--------新增代码----------start--------       isAllCheck=true;       //--------新增代码----------end--------       tempList.forEach((item){           //--------新增代码----------start--------          if(item['isCheck']){             allPrice+=(item['count']*item['price']);             allGoodsCount+=item['count'];          }else{            isAllCheck=false;          }          //--------新增代码----------end--------                   cartList.add(new CartInfoMode.fromJson(item));        });      }      notifyListeners();  } 

全选按钮的方法和当个商品很类似,也是在Provide中,新建一个changeAllCheckBtnState方法,写入下面的代码.

  //点击全选按钮操作  changeAllCheckBtnState(bool isCheck) async{    SharedPreferences prefs = await SharedPreferences.getInstance();    cartString=prefs.getString('cartInfo');     List tempList= (json.decode(cartString.toString()) as List).cast();     List newList=[]; //新建一个List,用于组成新的持久化数据。    for(var item in tempList ){      var newItem = item; //复制新的变量,因为Dart不让循环时修改原值      newItem['isCheck']=isCheck; //改变选中状态      newList.add(newItem);    }         cartString= json.encode(newList).toString();//形成字符串     prefs.setString('cartInfo', cartString);//进行持久化     await getCartInfo();   } 

完成后,到UI界面加入交互效果,打开lib/pages/cart_page/cart_bottom.dart文件,修改selectAllBtn(context)方法。

  //全选按钮  Widget selectAllBtn(context){    //--------新增代码----------start--------    bool isAllCheck = Provide.value(context).isAllCheck;    //--------新增代码----------end--------    return Container(      child: Row(        children: [          Checkbox(            value: isAllCheck,            activeColor: Colors.pink,            //--------新增代码----------start--------            onChanged: (bool val){              Provide.value(context).changeAllCheckBtnState(val);            },            //--------新增代码----------end--------          ),          Text('全选')        ],      ),    );  } 

做完这步,就可以测试一下交互效果了。这的代码比较零散,所以修改的时候要特别注意,防止犯错。

#第61节:购物车_商品数量的加减操作

现在基本购物车页面只差一个商品数量的加减操作了,通过几节课的学习,应该大部分小伙i版已经掌握了编写业务逻辑和持久化的方法。你可以先自己试着能不能做出这个效果。

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004371757854

#编写业务逻辑方法

直接在lib/provide/cart.dart文件中,新建立一个方法addOrReduceAction()方法。方法接收两个参数.

  • cartItem:要修改的项.
  • todo: 是加还是减。

代码如下:

  addOrReduceAction(var cartItem, String todo )async{    SharedPreferences prefs = await SharedPreferences.getInstance();    cartString=prefs.getString('cartInfo');     List tempList= (json.decode(cartString.toString()) as List).cast();    int tempIndex =0;    int changeIndex=0;    tempList.forEach((item){         if(item['goodsId']==cartItem.goodsId){          changeIndex=tempIndex;          }         tempIndex++;     });     if(todo=='add'){       cartItem.count++;     }else if(cartItem.count>1){       cartItem.count--;     }     tempList[changeIndex]=cartItem.toJson();     cartString= json.encode(tempList).toString();     prefs.setString('cartInfo', cartString);//     await getCartInfo();   } 

方法写完后,就可以修改UI部分了,让其有交互效果.

#UI交互效果的修改

现在页面中引入Provide相关的文件.

import 'package:provide/provide.dart'; import '../../provide/cart.dart';

然后设置接收参数,接收item就可以了

  var item;  CartCount(this.item);

然后把组件的内部方法都加入参数context,这里直接给出所有代码,方便你学习。

import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:provide/provide.dart'; import '../../provide/cart.dart'; 
class CartCount extends StatelessWidget {  //--------------新增加代码------------start--------  var item;  CartCount(this.item);  //--------------新增加代码------------end-------- 

  @override  Widget build(BuildContext context) {    return Container(      width: ScreenUtil().setWidth(165),      margin: EdgeInsets.only(top:5.0),      decoration: BoxDecoration(        border:Border.all(width: 1 , color:Colors.black12)      ),      child: Row(        children: [          //--------------新增加代码------------start--------          _reduceBtn(context),          _countArea(),          _addBtn(context),          //--------------新增加代码------------end--------        ],      ),          );  }  // 减少按钮  Widget _reduceBtn(context){    return InkWell(      onTap: (){        //--------------新增加代码------------start--------        Provide.value(context).addOrReduceAction(item,'reduce');        //--------------新增加代码------------end--------      },      child: Container(        width: ScreenUtil().setWidth(45),        height: ScreenUtil().setHeight(45),        alignment: Alignment.center,               decoration: BoxDecoration(          //--------------新增加代码------------start--------          color: item.count>1?Colors.white:Colors.black12,          //--------------新增加代码------------end--------          border:Border(            right:BorderSide(width:1,color:Colors.black12)          )        ),        //--------------新增加代码------------start--------        child:item.count>1? Text('-'):Text(' '),        //--------------新增加代码------------end--------      ),    );  }   //添加按钮  Widget _addBtn(context){    return InkWell(      onTap: (){       //--------------新增加代码------------start--------        Provide.value(context).addOrReduceAction(item,'add');        //--------------新增加代码------------end--------      },      child: Container(        width: ScreenUtil().setWidth(45),        height: ScreenUtil().setHeight(45),        alignment: Alignment.center,                decoration: BoxDecoration(          color: Colors.white,          border:Border(            left:BorderSide(width:1,color:Colors.black12)          )        ),        child: Text('+'),      ),    );  }   //中间数量显示区域  Widget _countArea(){    return Container(      width: ScreenUtil().setWidth(70),      height: ScreenUtil().setHeight(45),      alignment: Alignment.center,      color: Colors.white,      //--------------新增加代码------------start--------       child: Text('${item.count}'),      //--------------新增加代码------------end--------    );  } 
}

全部改完后,还需要到cart_item.dart里的_cartGoodsName里的调用组件的方法。

  //商品名称  Widget _cartGoodsName(item){    return Container(      width: ScreenUtil().setWidth(300),      padding: EdgeInsets.all(10),      alignment: Alignment.topLeft,      child: Column(        children: [          Text(item.goodsName),          //-----------修改关键代码------start-------          CartCount(item)          //-----------修改关键代码------end-------        ],      ),    );  } 

这步完成后,就应该可以实现商品数量的加减交互了。

#第62节:购物车_首页Provide化 让跳转随心所欲

在开始学习教程时,由于为了教学效果,所以底部导航跳转并没有使用Provide,而是使用了简单的变量,这样作的结果就是其它页面没办法控制首页底部导航的跳转,让项目的跳转非常笨拙,缺乏灵活性。这节课就通过我们小小的改造,把首页index_page.dart,加入Provide控制。

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004370328408

#编写Provide文件

先在lib/provide文件夹下面,新建一个currentIndex.dart文件,然后声明一个索引变量,这个变量就是控制底部导航和页面跳转的。也就是说我们只要把这个索引进行状态管理,那所以的页面可以轻松的控制首页的跳转了。代码如下:

import 'package:flutter/material.dart'; 
class CurrentIndexProvide with ChangeNotifier{  int currentIndex=0;    changeIndex(int newIndex){    currentIndex=newIndex;    notifyListeners();  } 
} 

#重新编写首页

现在就要改造首页了,这次改动的地方比较多,所以干脆先注释掉所有代码,然后重新进行编写。

import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'home_page.dart'; import 'category_page.dart'; import 'cart_page.dart'; import 'member_page.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:provide/provide.dart'; import '../provide/currentIndex.dart'; 

class IndexPage extends StatelessWidget {  final List bottomTabs = [    BottomNavigationBarItem(      icon:Icon(CupertinoIcons.home),      title:Text('首页')    ),    BottomNavigationBarItem(      icon:Icon(CupertinoIcons.search),      title:Text('分类')    ),    BottomNavigationBarItem(      icon:Icon(CupertinoIcons.shopping_cart),      title:Text('购物车')    ),     BottomNavigationBarItem(      icon:Icon(CupertinoIcons.profile_circled),      title:Text('会员中心')    ),  ];    final List tabBodies = [      HomePage(),      CategoryPage(),      CartPage(),      MemberPage()   ];   @override  Widget build(BuildContext context) {       ScreenUtil.instance = ScreenUtil(width: 750, height: 1334)..init(context);      return Provide(       builder: (context,child,val){        //------------关键代码----------start---------        int currentIndex= Provide.value(context).currentIndex;        // ----------关键代码-----------end ----------        return Scaffold(            backgroundColor: Color.fromRGBO(244, 245, 245, 1.0),            bottomNavigationBar: BottomNavigationBar(              type:BottomNavigationBarType.fixed,              currentIndex: currentIndex,              items:bottomTabs,              onTap: (index){                //------------关键代码----------start---------                Provide.value(context).changeIndex(index);                // ----------关键代码-----------end ----------              },            ),             body: IndexedStack(                    index: currentIndex,                    children: tabBodies                  ),        );       }    );       } }

修改思路是这样的,把原来的statfulWidget换成静态的statelessWeidget然后进行主要修改build方法里。加入Provide Widget,然后再每次变化时得到索引,点击下边导航时改变索引.

#修改商品详细页,实现跳转

打开/lib/pages/details_page/details_bottom.dart文件,先引入curretnIndex.dart文件.

import '../../provide/currentIndex.dart';

然后修改build方法里的购物车图标区域.在图标的onTap方法里,加入下面的代码.

InkWell(  onTap: (){      //--------------关键代码----------start-----------      Provide.value(context).changeIndex(2);      Navigator.pop(context);      //-------------关键代码-----------end--------  },  child: Container(      width: ScreenUtil().setWidth(110) ,      alignment: Alignment.center,      child:Icon(            Icons.shopping_cart,            size: 35,            color: Colors.red,          ),     ) , ),

这步做完,可以试着测试一下了,看看是不是可以从详细页直接跳转到购物车页面了。

#第63节:购物车_详细页显示购物车商品数量

现在购物车的基本功能都已经做完了,但是商品详细页面还有一个小功能没有完成,就是在商品详细页添加商品到购物车时,购物车的图标要动态显示出此时购物车的数量。这节课就利用点时间完成这个功能。

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004370328408

https://m.qlchat.com/topic/details?topicId=2000004370328408

#修改文件结构

打开/lib/pages/details_page/details_bottom.dart文件,修改图片区域,增加层叠组件Stack Widget,然后在右上角加入购物车现有商品数量。

 children: [          //关键代码--------------------start--------------           Stack(             children: [               InkWell(                  onTap: (){                      Provide.value(context).changeIndex(2);                      Navigator.pop(context);                  },                  child: Container(                      width: ScreenUtil().setWidth(110) ,                      alignment: Alignment.center,                      child:Icon(                            Icons.shopping_cart,                            size: 35,                            color: Colors.red,                          ),                     ) ,                ),                Provide(                  builder: (context,child,val){                    int  goodsCount = Provide.value(context).allGoodsCount;                    return  Positioned(                        top:0,                        right: 10,                        child: Container(                          padding:EdgeInsets.fromLTRB(6, 3, 6, 3),                          decoration: BoxDecoration(                            color:Colors.pink,                            border:Border.all(width: 2,color: Colors.white),                            borderRadius: BorderRadius.circular(12.0)                          ),                          child: Text(                            '${goodsCount}',                            style: TextStyle(                              color: Colors.white,                              fontSize: ScreenUtil().setSp(22)                            ),                          ),                        ),                      ) ;                  },                )                           ],           ),                      //关键代码--------------------end----------------

#修改provide/cart.dart文件

因为我们要实现动态展示,所以在添加购物车商品时,应该也有数量的变化,所以需要修改cart.dart文件里的save()方法。

  save(goodsId,goodsName,count,price,images) async{    //初始化SharedPreferences    SharedPreferences prefs = await  SharedPreferences.getInstance();    cartString=prefs.getString('cartInfo');  //获取持久化存储的值    var temp=cartString==null?[]:json.decode(cartString.toString());    //把获得值转变成List    List tempList= (temp as List).cast();    //声明变量,用于判断购物车中是否已经存在此商品ID    var isHave= false;  //默认为没有    int ival=0; //用于进行循环的索引使用    //-----------------关键代码---------start---------    allPrice=0;     allGoodsCount=0;  //把商品总数量设置为0    //-----------------关键代码---------end---------    tempList.forEach((item){//进行循环,找出是否已经存在该商品      //如果存在,数量进行+1操作      if(item['goodsId']==goodsId){        tempList[ival]['count']=item['count']+1;        cartList[ival].count++;        isHave=true;      }      //-----------------关键代码---------start---------      if(item['isCheck']){         allPrice+= (cartList[ival].price* cartList[ival].count);         allGoodsCount+= cartList[ival].count;      }      //-----------------关键代码---------end---------                ival++;    });    //  如果没有,进行增加    if(!isHave){      Map newGoods={        'goodsId':goodsId,        'goodsName':goodsName,        'count':count,        'price':price,        'images':images,        'isCheck': true  //是否已经选择      };      tempList.add(newGoods);      cartList.add(new CartInfoMode.fromJson(newGoods));      //-----------------关键代码---------start---------      allPrice+= (count * price);      allGoodsCount+=count;      //-----------------关键代码---------end---------    }    //把字符串进行encode操作,    cartString= json.encode(tempList).toString();     prefs.setString('cartInfo', cartString);//进行持久化    notifyListeners();  } 

完成后,就可以实现商品详细页购物车中商品数量的动态展示了。也算我们购物车区域所有功能都已经完成了。

#第64节:会员中心_首页头部布局

这节课开始布局会员中心的UI,如果你前边的课程都认真听了,并且也跟着作了,那这部分的内容对你来说就比较简单了。你可以作为一个练习来作。

#页面大体架构的编写

打开以前建立的/lib/pages/member_page.dart文件,先删除里边的代码,然后引入我们需要的package代码。

import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; 

引入package后,就可以编写一个StatelessWidget,代码如下:

import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; 

class MemberPage extends StatelessWidget {  @override  Widget build(BuildContext context) {     } }

然后返回一个Scaffold,在body区域里加入一个ListView。

import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; 

class MemberPage extends StatelessWidget {  @override  Widget build(BuildContext context) {    return Scaffold(     appBar: AppBar(       title: Text('会员中心'),     ),     body:ListView(       children: [       ],     ) ,   );  } }

这样大体结构就已经编写完成了,编写完成后我们把ListView的进行分离出来,编写成不同的方法。

#顶部头像区域编写

头像区域我们外边套一层Container,然后里边放入Column,圆形头像这个部分,我们使用ClipOval Widget。代码我直接放在下面了。

  Widget _topHeader(){     return Container(      width: ScreenUtil().setWidth(750),      padding: EdgeInsets.all(20),      color: Colors.pinkAccent,      child: Column(        children: [          Container(            margin: EdgeInsets.only(top: 30),             child: ClipOval(                            child:Image.network('http://blogimages.jspang.com/blogtouxiang1.jpg')            ),          ),          Container(            margin: EdgeInsets.only(top: 10),            child: Text(              '技术胖',              style: TextStyle(                fontSize: ScreenUtil().setSp(36),                color:Colors.white,               ),            ),          )        ],      ),    );   } 

写完后把这个组件加入到build的ListView里就可以了。然后就可以进行一个预览了。

#第65节:会员中心_订单区域UI编写

头部区域编写好后,我们就可以编写订单区域了,这部分我们简单分成两个方法来进行编写。

#订单标题区域

直接上代码了。

//我的订单顶部  Widget _orderTitle(){     return Container(      margin: EdgeInsets.only(top:10),      decoration: BoxDecoration(        color: Colors.white,        border: Border(          bottom:BorderSide(width: 1,color:Colors.black12)        )      ),      child: ListTile(        leading: Icon(Icons.list),        title:Text('我的订单'),        trailing: Icon(Icons.arrow_right),      ),    );   } 

#订单列表区域

直接上代码

  Widget _orderType(){     return Container(      margin: EdgeInsets.only(top:5),      width: ScreenUtil().setWidth(750),      height: ScreenUtil().setHeight(150),      padding: EdgeInsets.only(top:20),      color: Colors.white,      child: Row(        children: [          Container(            width: ScreenUtil().setWidth(187),            child: Column(              children: [                Icon(                  Icons.party_mode,                  size: 30,                ),                Text('待付款'),              ],            ),          ),          //-----------------          Container(            width: ScreenUtil().setWidth(187),            child: Column(              children: [                Icon(                  Icons.query_builder,                  size: 30,                ),                Text('待发货'),              ],            ),          ),           //-----------------          Container(            width: ScreenUtil().setWidth(187),            child: Column(              children: [                Icon(                  Icons.directions_car,                   size: 30,                ),                Text('待收货'),              ],            ),          ),          Container(            width: ScreenUtil().setWidth(187),            child: Column(              children: [                Icon(                  Icons.content_paste,                   size: 30,                ),                Text('待评价'),              ],            ),          ),        ],      ),    );   } 

这两个方法写完后,直接加到Build里就可以了。

#第66节:会员中心_编写ListTile的通用方法

这节课我们就把会员中心的剩下UI做完,可以看到,订单下面就全部都是类似List的形式了。那我们可以编写一个通用的方法,然后传递不同的值,来快速布局出下面的部分。

#ListTile通用方法

我们利用方法传递参数的形式,创建一个可以通用的方法,只要传递不同的参数,就可以形成不同的组件。代码如下

 Widget _myListTile(String title){     return Container(       decoration: BoxDecoration(        color: Colors.white,        border: Border(          bottom:BorderSide(width: 1,color:Colors.black12)        )      ),      child: ListTile(        leading: Icon(Icons.blur_circular),        title: Text(title),        trailing: Icon(Icons.arrow_right),      ),    );  } 

#组合List布局

有了通用的方法后,我们就可以进行组合List布局,代码如下:

  Widget _actionList(){    return Container(      margin: EdgeInsets.only(top: 10),      child: Column(        children: [            _myListTile('领取优惠券'),            _myListTile('已领取优惠券'),            _myListTile('地址管理'),            _myListTile('客服电话'),            _myListTile('关于我们'),        ],      ),    );  }

这个组件编写完成后,可以组合到Build方法里面。这步完成后,就形成了一个完成的会员中心页面。

总结:这节课结束后,我原计划的所有知识点就已经讲完了。但是课程并没有结束,我后边还会不断的更新课程,我管这个叫做加餐。

  • 优化现有程序:我会不断优化现有程序和存在的Bug,有重大优化时,就会更新课程。
  • 对小伙伴期望的知识点作补充讲解:这个要10人以上提出的共性知识点作补充讲解。
  • 后续功能升级:如果后期后台API有重点变化,影响学习,我会录课补充修改。

#第67课:加餐_高德地图插件的使用

这是一个加餐课,很多小伙伴都给我留言说,需要这个功能,经过两天的摸索,总算是可以使用了,当然这个插件的坑也是巨多的。使用的插件叫amap_base_flutter,也是国内用的最多的地图一个插件。此节课收到了很多小伙伴的帮助,特别感谢"鲁隽彧(网名)"

视频链接地址:https://m.qlchat.com/topic/details?topicId=2000004451659358

#1.注册和建立高德API应用

这个需要到高德的网站进行,网站地址为:https://lbs.amap.com/

你需要先注册一个账号,这个过程我就不演示了。这个你自己再弄不明白,那么接下来我就不带你去找小姐姐了。

有了账号之后到控制台-应用管理-创建应用(这个我就再视频中演示了)

#2.获得SHA1

在创建应用的时候,需要填入SHA1,这个必须需要在Android Studio里进行,VS Code里还没有摸清如何获得,如果你知道如何获得,可以文章下方给技术胖留言。(获得方式,在视频中进行演示)

#3.获得PackageName

这个的获得比较简单,打开/android/app/build.gradle文件,然后找到applicationId,这个就是packageName,比如我的项目的packageName就是com.example.amap_test

把这两项填写好后,我们就可以开心的编写程序了。

#4.配置AndoridManifest.xml文件

这个文件在/android/app/src/main/AndroidManifest.xml,然后在标签里,加入下面的代码:

 

#5.编写代码

需要先进入根目录的pubspec.yaml文件,进行依赖注册,这个package下载还是需要挺长时间的,我反正用了将近15分钟。

amap_base: ^0.3.5

写完后点击右上角的packages get,剩下的就是耐心等待。

进入lib/main.dart文件,写入下面代码。

进的要用import引入amap_base.dart文件。

import 'package:flutter/material.dart'; import 'package:amap_base/amap_base.dart'; 

 void main()async{  runApp(MyApp()); 
} 
class MyApp extends StatelessWidget {  // This widget is the root of your application. 
  @override  Widget build(BuildContext context) {    return MaterialApp(      title: 'Flutter Demo',      theme: ThemeData(         primarySwatch: Colors.blue,      ),      home: MyHomePage(title: '高德地图测试'),    );  } }
 

 class MyHomePage extends StatefulWidget {  MyHomePage({Key key, this.title}) : super(key: key);  final String title;   @override  _MyHomePageState createState() => _MyHomePageState(); }
 class _MyHomePageState extends State {   AMapController _controller;   @override  Widget build(BuildContext context) {     return Scaffold(      appBar: AppBar(         title: Text(widget.title),      ),      body:AMapView(              onAMapViewCreated: (controller) {                _controller = controller;              },              amapOptions: AMapOptions(                compassEnabled: false,                zoomControlsEnabled: true,                logoPosition: LOGO_POSITION_BOTTOM_CENTER,                camera: CameraPosition(                  target: LatLng(41.851827, 112.801637),                  zoom: 4,                ),              ),               );  } 
} 

写完代码后,你要记得不要使用虚拟机进行测试,我在学习的时候,就是使用虚拟机测试,一直是黑屏,后来采用了真机测试,才能出现效果。

这就是我在集成高德地图插件时遇到的几个坑,希望小伙伴们都能别走弯路。

#第68节:加餐_极光推送插件使用-1

现在每个app都需要有推送功能,这也是一个app的价值所在,和你的顾客产生联系。极光推送是中国很出色的推送服务提供商,有着很好的口碑和稳定性,送达率也是国内领先的。Flutter1.0版本发布后,极光也很及时的退出了Flutter插件。这节课就带着小伙伴了解一下极光推送的使用。

#申请极光账号和建立应用

极光推送的官方网址为:https://www.jiguang.cn/

注册的过程这里我依然省略了,有劳小伙伴们自己辛苦一下。

注册好后,进入'服务中心',然后再进入'开发者平台',点击创建应用。这时候会出现新页面,让你填写“应用名称”和上传“应用图标”。 创建完成,极光平台就会给我们两个key。

  • appKey : 移动客户端使用的key
  • Master Secret : 服务端使用的key

我们这里只做移动端不做服务端,所以只需要appKey。得到这个Key也算是极光平台操作完了。

#加入dependencies依赖

github网址:https://github.com/jpush/jpush-flutter-plugin

要使用极光推送插件必须先下载包,要下载包就需要先添加依赖,直接把下面的代码加入pubspec.yaml文件中。

jpush_flutter: 0.0.11

需要注意的是,使用最新版本,这里使用的只是我录课时的最新版本。

写完代码后,选择Android Studio右上角的Packages get进行下载,下载完成后进行操作。

#build.gradle添加可以和cpu型号代码

打开android/app/src/build.gradle文件,加入如下代码:

    defaultConfig {       ...
         ndk {            //选择要添加的对应 cpu 类型的 .so 库。            abiFilters 'armeabi', 'armeabi-v7a', 'x86', 'x86_64', 'mips', 'mips64'// 'arm64-v8a',            // 还可以添加        }
        manifestPlaceholders = [                JPUSH_PKGNAME: applicationId,                JPUSH_APPKEY : "这里写入你自己申请的Key哦", // NOTE: JPush 上注册的包名对应的 Appkey.                JPUSH_CHANNEL: "developer-default", //暂时填写默认值即可.        ]
     }

到这里你的第一步工作算是完成了,你已经可以开发推送功能了。这部分如果对于移动开发者来说,可能很容易。所以单独拿出一课来,这样有移动开发经验的可以跳过这节。

#第69节:加餐_极光推送插件使用-2

这节课继续讲解一下极光推送的使用,由于技术胖也是作前端的,PHP也有3年没有碰过了,所以这里讲一下极光推送的本地推送,服务器端代码就不在编写了。工作中应该也不用你编写,这是后端的事情。

#先引入主要文件

打开代码lib/main.dart文件,先引入需要使用的主要文件

import 'package:flutter/material.dart'; import 'dart:async'; 
import 'package:flutter/services.dart'; import 'package:jpush_flutter/jpush_flutter.dart';

#主要方法编写

void main() => runApp(new MyApp());

class MyApp extends StatefulWidget {  @override  _MyAppState createState() => new _MyAppState();
}

class _MyAppState extends State {   @override  void initState() {    super.initState();  }
// 编写视图  @override  Widget build(BuildContext context) {    return new MaterialApp(      home: new Scaffold(        appBar: new AppBar(          title: const Text('极光推送'),        ),        body: new Center(          child:Text('临时的.........')         ),      ),    );  }
}

#编写initPlatformState方法

在使用极光推送之前,我们需要初始化一下,初始化时的主要任务就是写一下监听响应方法。在写主要方法之前,需要声明两个变量。

 String debugLable = 'Unknown';   //错误信息  final JPush jpush = new JPush();  //初始化极光插件

然后编写initPlatformState方法

  Future initPlatformState() async {    String platformVersion;     try {      //监听响应方法的编写      jpush.addEventHandler(        onReceiveNotification: (Map message) async {          print(">>>>>>>>>>>>>>>>>flutter 接收到推送: $message");          setState(() {            debugLable = "接收到推送: $message";          });        }      );     } on PlatformException {      platformVersion = '平台版本获取失败,请检查!';    } 
    if (!mounted) return;     setState(() {      debugLable = platformVersion;    });  } 

#编写build的视图

  Widget build(BuildContext context) {    return new MaterialApp(      home: new Scaffold(        appBar: new AppBar(          title: const Text('极光推送'),        ),        body: new Center(            child: new Column(                children:[                  new Text('结果: $debugLable\n'),                  new FlatButton(                      child: new Text('发送推送消息\n'),                      onPressed: () {                        // 三秒后出发本地推送                        var fireDate = DateTime.fromMillisecondsSinceEpoch(DateTime.now().millisecondsSinceEpoch + 3000);                        var localNotification = LocalNotification(                            id: 234,                            title: '技术胖的飞鸽传说',                            buildId: 1,                            content: '看到了说明已经成功了',                            fireTime: fireDate,                            subtitle: '一个测试',                        );                        jpush.sendLocalNotification(localNotification).then((res) {                          setState(() {                            debugLable = res;                          });                        });                       }),                 ]            )         ),      ),    );

这里的详细意思,在视频中解释吧,写注释还是挺累的。为了你能达到很好的学习效果,这里给出全部代码。

import 'package:flutter/material.dart';
import 'dart:async';

import 'package:flutter/services.dart';
import 'package:jpush_flutter/jpush_flutter.dart';

void main() => runApp(new MyApp());

class MyApp extends StatefulWidget {  @override  _MyAppState createState() => new _MyAppState();
}

class _MyAppState extends State {  String debugLable = 'Unknown';   //错误信息  final JPush jpush = new JPush();  //初始化极光插件  @override  void initState() {    super.initState();    initPlatformState();  //极光插件平台初始化  }
   Future initPlatformState() async {    String platformVersion;
    try {      //监听响应方法的编写      jpush.addEventHandler(        onReceiveNotification: (Map message) async {          print(">>>>>>>>>>>>>>>>>flutter 接收到推送: $message");          setState(() {            debugLable = "接收到推送: $message";          });        }      );
    } on PlatformException {      platformVersion = '平台版本获取失败,请检查!';    }
     if (!mounted) return;
    setState(() {      debugLable = platformVersion;    });  }
 

// 编写视图  @override  Widget build(BuildContext context) {    return new MaterialApp(      home: new Scaffold(        appBar: new AppBar(          title: const Text('极光推送'),        ),        body: new Center(            child: new Column(                children:[                  new Text('结果: $debugLable\n'),                  new FlatButton(                      child: new Text('发送推送消息\n'),                      onPressed: () {                        // 三秒后出发本地推送                        var fireDate = DateTime.fromMillisecondsSinceEpoch(DateTime.now().millisecondsSinceEpoch + 3000);                        var localNotification = LocalNotification(                            id: 234,                            title: '技术胖的飞鸽传说',                            buildId: 1,                            content: '看到了说明已经成功了',                            fireTime: fireDate,                            subtitle: '一个测试',                        );                        jpush.sendLocalNotification(localNotification).then((res) {                          setState(() {                            debugLable = res;                          });                        });
                      }),
                ]            )
        ),      ),    );  }
}
 

这里就完成了,现在可以打开虚拟机来测试一下效果了,看看推送是不是可以成功实现。

后期更多免费Flutter视频,到https://jspang.com进行观看。


#后端接口API文档

URL地址是不断变化的,所以不会提供准确的地址给你们。

#商城首页基本信息

说明:调用此接口,可获取首页所有的基本信息,包括导航,推荐商品,楼层商品。

参数:lon,lat 接口地址:wxmini/homePageContent

返回参数:

  • advertesPicture:首页中部广告条。
  • category:首页UI分类信息
  • floor1:楼层1的商品信息和图片
  • floor2:楼层2的商品详细和图片
  • floor3:楼层3的商品详细和图片
  • recommend:商品推荐的信息
  • slides:滑动图片和对应的商品编号
  • shopInfo:根据定位获得的门店图片和店长电话

#火爆专区商品列表

参数:page

接口地址:wxmini/homePageBelowConten

返回参数:

  • image :商品图片地址,可以直接使用。
  • name: 商品名称
  • mallPrice:商品商城价格
  • price: 商品价格,指市场价格

#商品类别信息

接口地址:wxmini/getCategory

返回参数:

  • mallCategoryId : 类别ID,用于控制子类别和商品列表。
  • mallCategoryName : 类别名称,例如“白酒”
  • bxMallSubDto:二级类别,是个数组
  • comments:类别描述,目前全是null
  • image:类别图片,可能是以后扩展使用的。

#商品分类页中的商品列表

接口地址:wxmini/getMallGoods

参数:

  • categoryId:大类ID,字符串类型
  • categorySubId : 子类ID,字符串类型,如果没有可以填写空字符串,例如''
  • page: 分页的页数,int类型

返回参数 - goodsId:商品的Id,用于进入商品页时,查询商品详情。 - goodsName: 商品名称 - image: 商品的图片 - oriPrice: 市场价格(贵的价格) - presentPrice:商城价格(便宜的价格)

I BUILT MY SITE FOR FREE USING