目标:已录制 359 集免费视频,目标 1000 集免费视频。 [视频目录(359/1000)]

Flutter-shop

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

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

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

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

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

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

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

学习讨论QQ群:806799257

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

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

第01节:课程介绍

购买地址

点击购买

购买后可以直接微信搜索“千聊”,或手机下载千聊APP,当然也可以收藏本页,在当前页面用web进行观看。

图片地址

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节:建立项目和编写入口文件

点击链接看视频:https://m.qlchat.com/topic/details?topicId=2000003619529754

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

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

  • 第一季Flutter视频教程地址:http://www.slsbk.com/posts/2019/01/20/flutter-base.html
  • 第二季Flutter视频教程地址:http://www.slsbk.com/posts/2019/01/21/flutter-base2.html
  • 第三季Flutter视频教程地址:http://www.slsbk.com/posts/2019/01/28/flutter-base3.html
  • 第四季Flutter视频教程地址:http://www.slsbk.com/posts/2019/02/01/flutter-base4.html
  • 20个Flutter实例视频教程:http://www.slsbk.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实战 全网首发。

点击链接看视频:https://m.qlchat.com/topic/details?topicId=2000003624991418

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

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<IndexPage> {
  @override
  Widget build(BuildContext context) {
    return Container(
       child: child,
    );
  }
}

bottomTabs List

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

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

代码如下:

final List<BottomNavigationBarItem> 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就算完事,目的是让底部导航栏可以使用和在页面之间进行切换。

点击链接看视频:https://m.qlchat.com/topic/details?topicId=2000003625092387

新建四个基本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<IndexPage> {
  final List<BottomNavigationBarItem> 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请求库。

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

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?我当时并没有回答,因为这个不是用文字很好表达清楚的。不回答并不代表我置之不理,而是我准备了一晚上,今天给大家用视频的形式进行演示(为了回答小伙伴们的问题,我在原有课程知识点中提高了难度,融入了大家的问题)。

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

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

案例说明

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

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

一图顶千言

http://blogimages.www.slsbk.com/flutter_meihaorenjian.png

生成动态组件

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

class HomePage extends StatefulWidget {
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @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: <Widget>[
                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<HomePage> {

  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: <Widget>[
                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比较好。

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

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: <Widget>[
                  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基础_伪造请求头获取数据

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

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

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

查看极客时间的数据端口

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

首先在浏览器端打开掘金网站(我用的是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<HomePage> {
  String showText='还没有请求数据';
  @override
  Widget build(BuildContext context) {
    return Container(
       child: Scaffold(
         appBar: AppBar(title: Text('请求远程数据'),),
         body: SingleChildScrollView(
           child: Column(
             children: <Widget>[
               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<HomePage> {
  String showText='还没有请求数据';
  @override
  Widget build(BuildContext context) {
    return Container(
       child: Scaffold(
         appBar: AppBar(title: Text('请求远程数据'),),
         body: SingleChildScrollView(
           child: Column(
             children: <Widget>[
               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还有一些比较高级的用法,这些用法就不单独拿出来讲了,在项目中遇到后再详细讲解。从这节开始,我们来制作商城的首页,那制作商城的首页第一步还是需要从后端接口获取需要使用的记录。

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

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<HomePage> {
  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都懒得看,那就泡杯茶,看视频吧。

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

引入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<Map> swiperDataList = (data['data']['slides'] as List).cast(); // 顶部轮播组件数
             return Column(
               children: <Widget>[
                   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<HomePage> {
  
  


  @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<Map> swiperDataList = (data['data']['slides'] as List).cast(); // 顶部轮播组件数
             return Column(
               children: <Widget>[
                   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妹子也会经常,甜甜的问我们:“哥,设计用啥尺寸的?” 作为一个公司的技术和颜值担当,你一定要很轻松的回答这个问题。你回答后会不会心里胆怯,不用怕,学完今天这节课,你就可以轻松的回答这个问题。

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

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<HomePage> {
  
  


  @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<Map> swiperDataList = (data['data']['slides'] as List).cast(); // 顶部轮播组件数
             return Column(
               children: <Widget>[
                   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 小部件进行制作,当然制作中我们也会讲到一些布局技巧。

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

导航单元素的编写

从外部看,导航是一个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: <Widget>[
          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<Map> 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条就可以了。

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

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来制作拨打电话的效果。

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

编写店长电话模块

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

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节:商品推荐区域制作

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

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

超出边界的处理方法

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

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

具体代码如下:

return SingleChildScrollView(
  child: Column(
  children: <Widget>[
      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: <Widget>[
            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: <Widget>[
           _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: <Widget>[
           _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: <Widget>[
            Image.network(recommendList[index]['image']),
            Text('¥${recommendList[index]['mallPrice']}'),
            Text(
              '¥${recommendList[index]['price']}',
              style: TextStyle(
                decoration: TextDecoration.lineThrough,
                color:Colors.grey
              ),
            )
          ],
        ),
      ),
    );
  }
}

准备数据并进行调用

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

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

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

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

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

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

上节课遗留的一个而问题

上节课我们虽然做出了效果,但是在模拟器上看是有一些问题的,就是模拟器纵向显示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<IndexPage>{

   PageController _pageController;


  final List<BottomNavigationBarItem> 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<Widget> 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<HomePage> with AutomaticKeepAliveClientMixin {
  
  @override
  bool get wantKeepAlive =>true;
}

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

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

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

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

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

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

编写楼层标题组件

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

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: <Widget>[
          _firstRow(),
          _otherGoods()
        ],
      ),
    );
  }

  Widget _firstRow(){
    return Row(
      children: <Widget>[
        _goodsItem(floorGoodsList[0]),
        Column(
          children: <Widget>[
           _goodsItem(floorGoodsList[1]),
           _goodsItem(floorGoodsList[2]),
          ],
        )
      ],
    );
  }

  Widget _otherGoods(){
    return Row(
      children: <Widget>[
       _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<Map> floor1 = (data['data']['floor1'] as List).cast(); //楼层1商品和图片 
    List<Map> floor2 = (data['data']['floor2'] as List).cast(); //楼层1商品和图片 
    List<Map> floor3 = (data['data']['floor3'] as List).cast(); //楼层1商品和图片 
    
    return SingleChildScrollView(
      child: Column(
      children: <Widget>[
          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节:首页_火爆专区商品接口制作

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

视频链接地址:https://m.qlchat.com/live/channel/channelPage/2000003595896347.htm

接口初探

使用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<HotGoods> {


   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节:首页_火爆专区界面制作

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

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

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<Map> hotGoodsList=[];

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

//火爆商品接口
  void _getHotGoods(){
     var formPage={'page': page};
     request('homePageBelowConten',formData:formPage).then((val){
       
       var data=json.decode(val.toString());
       List<Map> 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<Widget>,然后再进行Warp布局。

//火爆专区子项
  Widget _wrapList(){

    if(hotGoodsList.length!=0){
       List<Widget> 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: <Widget>[
                  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: <Widget>[
                      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: <Widget>[
              hotTitle,
               _wrapList(),
            ],
          )   
    );
  }

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

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

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

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: <Widget>[
            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<Map> 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来管理。

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

禁止滑动的设置

上节课完成了上拉加载,但是小伙伴可能没发现一个小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<CategoryPage> {

  @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界面。在这里我不讨论两种方法的好坏,这就跟你看小电影是喜欢看欧美还是喜欢看岛国的一样,欧美的可能粗狂豪爽一点,岛国的优美婉约一点。

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

类别json的分析

比如现在从后台得到了一串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<dynamic> 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<CategoryBigModel> 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形式的数据模型。

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

自动生成数据模型

上节课课再最后我讲了一个快速生成的方法,但是很多小伙伴都问我,生成后如何使用。所以就在这节详细讲一下平时自动生成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<LeftCategoryNav> {
   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是目前最好的状态管理方案,那我们就采用这种方案来制作项目。

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

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: <Widget>[
           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<Counter>.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<Counter>(
        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<Counter>(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<Counter>(
        builder: (context,child,counter){
          return Text(
            '${counter.value}',
             style: Theme.of(context).textTheme.display1,
          );
        },
      ),
      )
    );
  }
}

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

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

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

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

编写二级分类UI

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



//右侧小类类别

class RightCategoryNav extends StatefulWidget {
  _RightCategoryNavState createState() => _RightCategoryNavState();
}

class _RightCategoryNavState extends State<RightCategoryNav> {

  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: <Widget>[
          LeftCategoryNav(),
          Column(
            children: <Widget>[
              RightCategoryNav()
            ],
          )
        ],
      ),
    ),
  );
}

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

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

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

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

编写二级分类的Provide文件

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

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

//ChangeNotifier的混入是不用管理听众
class ChildCategory with ChangeNotifier{

    List<BxMallSubDto> childCategoryList = [];

    getChildCategory(List list){
      childCategoryList=list;
      notifyListeners();
    }
}

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

main里进行引入

import './provide/child_category.dart';

void main(){
    var childCategory=ChildCategory();
    providers
    ..provide(Provider<Counter>.value(counter))
    ..provide(Provider<ChildCategory>.value(childCategory));
}

修改二级分类状态

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

onTap: () {
    var childList = list[index].bxMallSubDto;
  
    Provide.value<ChildCategory>(context).getChildCategory(childList);
},

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

二级分类展现

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

 Widget _rightInkWell(BxMallSubDto item){

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


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

Widget build(BuildContext context) {
  
  return Container(
    // child: Text('${childCategory.childCategoryList.length}'),
  
    child: Provide<ChildCategory>(
      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<CategoryPage> {
  // CategoryBigListModel listCategory = CategoryBigListModel([]);

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

//左侧导航菜单
class LeftCategoryNav extends StatefulWidget {
  _LeftCategoryNavState createState() => _LeftCategoryNavState();
}

class _LeftCategoryNavState extends State<LeftCategoryNav> {
  List list = [];
  var listIndex = 0; //索引

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

  @override
  Widget build(BuildContext context) {
    return Container(
      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<ChildCategory>(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<ChildCategory>(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<RightCategoryNav> {

  
  
  @override
Widget build(BuildContext context) {
  
  return Container(
    // child: Text('${childCategory.childCategoryList.length}'),
  
    child: Provide<ChildCategory>(
      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解决一下。

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

进入分类页面后小类无数据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<ChildCategory>(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<BxMallSubDto> childCategoryList = [];



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

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

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

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

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

配置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<CategoryGoodsList> {

  @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<CategoryGoodsList> {

  @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://m.qlchat.com/topic/details?topicId=2000004001883543

商品列表页数据模型

这里还是使用快速生成的方法,利用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<CategoryListData> data;

  CategoryGoodsListModel({this.code, this.message, this.data});

  CategoryGoodsListModel.fromJson(Map<String, dynamic> json) {
    code = json['code'];
    message = json['message'];
    if (json['data'] != null) {
      data = new List<CategoryListData>();
      json['data'].forEach((v) {
        data.add(new CategoryListData.fromJson(v));
      });
    }
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['code'] = this.code;
    data['message'] = this.message;
    if (this.data != null) {
      data['data'] = this.data.map((v) => v.toJson()).toList();
    }
    return data;
  }
}

class CategoryListData {
  String image;
  double oriPrice;
  double presentPrice;
  String goodsName;
  String goodsId;

  CategoryListData(
      {this.image,
      this.oriPrice,
      this.presentPrice,
      this.goodsName,
      this.goodsId});

  CategoryListData.fromJson(Map<String, dynamic> json) {
    image = json['image'];
    oriPrice = json['oriPrice'];
    presentPrice = json['presentPrice'];
    goodsName = json['goodsName'];
    goodsId = json['goodsId'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['image'] = this.image;
    data['oriPrice'] = this.oriPrice;
    data['presentPrice'] = this.presentPrice;
    data['goodsName'] = this.goodsName;
    data['goodsId'] = this.goodsId;
    return data;
  }
}

修改_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: <Widget>[
            Text(
              '价格:¥${list[index].presentPrice}',
              style: TextStyle(color:Colors.pink,fontSize:ScreenUtil().setSp(30)),
              ),
            Text(
              '¥${list[index].oriPrice}',
              style: TextStyle(
                color: Colors.black26,
                decoration: TextDecoration.lineThrough
              ),
            )
        ]
      )
    );
  }

把方法进行组合

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

  Widget _ListWidget(int index){

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

  }

ListView的构建

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

 @override
  Widget build(BuildContext context) {
    return Container(
      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<CategoryListData> goodsList = [];
  
    //点击大类时更换商品列表
    getGoodsList(List<CategoryListData> list){
           
      goodsList=list;   
      notifyListeners();
    }
}

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

将状态放入顶层

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

void main(){
  var childCategory= ChildCategory();
  var categoryGoodsListProvide= CategoryGoodsListProvide();

  var counter =Counter();
  var providers  =Providers();
  providers
    ..provide(Provider<ChildCategory>.value(childCategory))
    ..provide(Provider<CategoryGoodsListProvide>.value(categoryGoodsListProvide))
    ..provide(Provider<Counter>.value(counter));

  runApp(ProviderNode(child:MyApp(),providers:providers));
}

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

修改category_page.dart页面

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

1.引入provide文件

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

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

2.修改_getGoodsList方法

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

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

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

3.使用_getGoodList方法

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

  onTap: () {

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

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

4.修改商品列表代码

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

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

修改后的代码如下:

class CategoryGoodsList extends StatefulWidget {
  @override
  _CategoryGoodsListState createState() => _CategoryGoodsListState();
}

class _CategoryGoodsListState extends State<CategoryGoodsList> {

  

  @override
  Widget build(BuildContext context) {

     
   
    return Provide<CategoryGoodsListProvide>(
        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: <Widget>[
            _goodsImage(newList,index)
           ,
            Column(
              children: <Widget>[
                _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: <Widget>[
            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<CategoryGoodsListProvide>(
        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<BxMallSubDto> childCategoryList = [];
    int childIndex = 0;
  


    //点击大类时更换
    getChildCategory(List<BxMallSubDto> 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<ChildCategory>(context).childIndex)?true:false;

3.点击时修改状态

onTap: (){
    Provide.value<ChildCategory>(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<BxMallSubDto> childCategoryList = [];
    int childIndex = 0;
    String categoryId = '4';
  


    //点击大类时更换
    getChildCategory(List<BxMallSubDto> 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<ChildCategory>(context).getChildCategory(childList,categoryId);
Provide.value<ChildCategory>(context).getChildCategory(list[0].bxMallSubDto,list[0].mallCategoryId);

增加getGoodsList方法

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

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

调用方法改版列表

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

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

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

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

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

子类没有商品时报错

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

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

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

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

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

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

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


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

这就为以后我们作上拉加载效果打下了基础。这节学完,你应该对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<BxMallSubDto> list,String id){
      isNewCategory=true;
      categoryId=id;
      childIndex=0;
      //------------------关键代码start
      page=1;
      noMoreText = ''; 
      //------------------关键代码end
      subId=''; //点击大类时,把子类ID清空
      noMoreText='';
      BxMallSubDto all=  BxMallSubDto();
      all.mallSubId='00';
      all.mallCategoryId='00';
      all.mallSubName = '全部';
      all.comments = 'null';
      childCategoryList=[all];
      childCategoryList.addAll(list);   
      notifyListeners();
    }
    //改变子类索引 ,
    changeChildIndex(int index,String id){
      isNewCategory=true;
      //传递两个参数,使用新传递的参数给状态赋值
       childIndex=index;
       subId=id;
       //------------------关键代码start
       page=1;
       noMoreText = ''; //显示更多的表示
       //------------------关键代码end
       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<CategoryGoodsListProvide>(
        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<ChildCategory>(context).noMoreText,
                      moreInfo:'加载中',
                      loadReadyText:'上拉加载'
                    ),
                    child:ListView.builder(
                      itemCount: data.goodsList.length,
                      itemBuilder: (context,index){
                        return _ListWidget(data.goodsList,index);
                      },
                    ) ,
                    loadMore: ()async{
                        print('没有更多了.......');
                    },
                  )
                  
                ) ,
              ); 
          }else{
            return  Text('暂时没有数据');
          }
       },

    );
  }

修改请求数据的方法

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

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


  }

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

切换类别返回顶部

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

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

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

var scrollController=new ScrollController();

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

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

这时候再进行测试,应该就可以了。这节课就到这里,虽然还有些小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<BxMallSubDto> list,String id){
  isNewCategory=true;
  categoryId=id;
  childIndex=0;
  page=1;
  subId=''; //点击大类时,把子类ID清空
  noMoreText='';
  BxMallSubDto all=  BxMallSubDto();
  //--------修改的关键代码start
  all.mallSubId='';
  //--------修改的关键代码end
  all.mallCategoryId='00';
  all.mallSubName = '全部';
  all.comments = 'null';
  childCategoryList=[all];
  childCategoryList.addAll(list);   
  notifyListeners();
}

这节课主要学习了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<String,List<String>> 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<String,List<String>> 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":"<img src=\"http://images.baixingliangfan.cn/shopGoodsDetailImg/20171224/20171224081109_5060.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.baixingliangfan.cn/shopGoodsDetailImg/20171224/20171224081109_1063.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.baixingliangfan.cn/shopGoodsDetailImg/20171224/20171224081110_8029.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.baixingliangfan.cn/shopGoodsDetailImg/20171224/20171224081110_1074.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.baixingliangfan.cn/shopGoodsDetailImg/20171224/20171224081110_8439.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.baixingliangfan.cn/shopGoodsDetailImg/20171224/20171224081110_6800.jpg\" width=\"100%\" height=\"auto\" alt=\"\" />"},"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<String, dynamic> json) {
    code = json['code'];
    message = json['message'];
    data = json['data'] != null ? new DetailsGoodsData.fromJson(json['data']) : null;
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['code'] = this.code;
    data['message'] = this.message;
    if (this.data != null) {
      data['data'] = this.data.toJson();
    }
    return data;
  }
}

class DetailsGoodsData {
  GoodInfo goodInfo;
  List<GoodComments> goodComments;
  AdvertesPicture advertesPicture;

  DetailsGoodsData({this.goodInfo, this.goodComments, this.advertesPicture});

  DetailsGoodsData.fromJson(Map<String, dynamic> json) {
    goodInfo = json['goodInfo'] != null
        ? new GoodInfo.fromJson(json['goodInfo'])
        : null;
    if (json['goodComments'] != null) {
      goodComments = new List<GoodComments>();
      json['goodComments'].forEach((v) {
        goodComments.add(new GoodComments.fromJson(v));
      });
    }
    advertesPicture = json['advertesPicture'] != null
        ? new AdvertesPicture.fromJson(json['advertesPicture'])
        : null;
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    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<String, dynamic> 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<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    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<String, dynamic> json) {
    sCORE = json['SCORE'];
    comments = json['comments'];
    userName = json['userName'];
    discussTime = json['discussTime'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    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<String, dynamic> json) {
    pICTUREADDRESS = json['PICTURE_ADDRESS'];
    tOPLACE = json['TO_PLACE'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    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<DetailsInfoProvide>(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: <Widget>[
                            
                          ],
                    )
                  );
              }else{
                  return Text('加载中........');
              }
            }
          )
       );
  }

}

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

_getBackInfo方法的修改

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

 Future _getBackInfo(BuildContext context )async{
      await  Provide.value<DetailsInfoProvide>(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: <Widget>[
                            
                          ],
                    )
                  );
              }else{
                  return Text('加载中........');
              }
            }
          )
       );

       
    
  }

  Future _getBackInfo(BuildContext context )async{
      await  Provide.value<DetailsInfoProvide>(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<DetailsInfoProvide>(

      builder:(context,child,val){
        var goodsInfo=Provide.value<DetailsInfoProvide>(context).goodsInfo.data.goodInfo;

        if(goodsInfo != null){

           return Container(
                color: Colors.white,
                padding: EdgeInsets.all(2.0),
                child: Column(
                  children: <Widget>[
                      _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<DetailsInfoProvide>(

      builder:(context,child,val){
        var goodsInfo=Provide.value<DetailsInfoProvide>(context).goodsInfo.data.goodInfo;

        if(goodsInfo != null){

           return Container(
                color: Colors.white,
                padding: EdgeInsets.all(2.0),
                child: Column(
                  children: <Widget>[
                      _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: <Widget>[
          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: <Widget>[
                    //关键代码------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: <Widget>[
                    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<DetailsInfoProvide>(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<DetailsInfoProvide>(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<DetailsInfoProvide>(
  builder: (context,child,val){
    var isLeft= Provide.value<DetailsInfoProvide>(context).isLeft;
    var isRight =Provide.value<DetailsInfoProvide>(context).isRight;
    
    return Container(
      margin: EdgeInsets.only(top: 15.0),
      child: Column(
        children: <Widget>[
          Row(
            children: <Widget>[
              _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<DetailsInfoProvide>(
      builder: (context,child,val){
        var isLeft= Provide.value<DetailsInfoProvide>(context).isLeft;
        var isRight =Provide.value<DetailsInfoProvide>(context).isRight;
       
        return Container(
          margin: EdgeInsets.only(top: 15.0),
          child: Column(
            children: <Widget>[
              Row(
                children: <Widget>[
                  _myTabBarLeft(context,isLeft),
                  _myTabBarRight(context,isRight)
                ],
              ),
            ],


          ),
          
        ) ;
      },
      
    ); 
  }

  Widget _myTabBarLeft(BuildContext context,bool isLeft){
    return InkWell(
      onTap: (){
      
        Provide.value<DetailsInfoProvide>(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<DetailsInfoProvide>(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: <Widget>[
          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<DetailsInfoProvide>(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: <Widget>[
    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<DetailsInfoProvide>(context).goodsInfo.data.goodInfo.goodsDetail;
    
   
      return  Provide<DetailsInfoProvide>(
        
        builder: (context,child,val){
           var isLeft = Provide.value<DetailsInfoProvide>(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: <Widget>[
                      ListView(
                        children: <Widget>[
                            DetailsTopArea(),
                            DetailsExplain(),
                            DetailsTabBar(),
                            DetailsWeb(),
                            

                          ],
                        ),
                      Positioned(
                        bottom: 0,
                        left: 0,
                        child: Text('测试')
                      )
                    ],
                  );
            //关键代码---------------end
              }else{
                  return Text('加载中........');
              }
              }
            )
       );
  }

  Future _getBackInfo(BuildContext context )async{
      await  Provide.value<DetailsInfoProvide>(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: <Widget>[
           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<String> 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<CartPage> {

  List<String> 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: <Widget>[
          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<Map>传换成字符串,然后操作的时候,我们再转换回来。说简单点就是持久化的只是一串字符串,然后需要操作的时候,我们变成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<Map> 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<CartProvide>.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<DetailsInfoProvide>(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<CartProvide>(context).save(goodsID,goodsName,count,price,images);
  },

先暂时把“马上结账”按钮方式清除购物车的方法,方便我们测试。

onTap: ()async{
  await Provide.value<CartProvide>(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<String, dynamic> json) {
    goodsId = json['goodsId'];
    goodsName = json['goodsName'];
    count = json['count'];
    price = json['price'];
    images = json['images'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    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<CartInfoMode> 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<Map> 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<String, dynamic> 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<Map> 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<CartProvide>(context).cartList;
      if(snapshot.hasData){
       
      }else{
        return Text('正在加载');
      }
    },
  ),
  );
  }

Future方法编写

使用了Future组件,自然需要一个返回Future的方法了,在这个方法里,我们使用Provide取出本地持久化的数据,然后进行变化。


  Future<String> _getCartInfo(BuildContext context) async{
     await Provide.value<CartProvide>(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>[
          
          ],
        ),
      );
  }



}

编写多选按钮方法

//多选按钮
  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: <Widget>[
          Text(item.goodsName)
        ],
      ),
    );
  }

编写商品价格方法

//商品价格
  Widget _cartPrice(item){

    return Container(
        width:ScreenUtil().setWidth(150) ,
        alignment: Alignment.centerRight,
        
        child: Column(
          children: <Widget>[
            Text('¥${item.price}'),
            Container(
              child: InkWell(
                onTap: (){},
                child: Icon(
                  Icons.delete_forever,
                  color: Colors.black26,
                  size: 30,
                ),
              ),
            )
          ],
        ),
      );
  }

进行整合

这些组件写好以后,我们可以进行一个整合。

child: Row(
  children: <Widget>[
    _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: <Widget>[
            _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: <Widget>[
          Text(item.goodsName)
        ],
      ),
    );
  }

  //商品价格
  Widget _cartPrice(item){

    return Container(
        width:ScreenUtil().setWidth(150) ,
        alignment: Alignment.centerRight,
        
        child: Column(
          children: <Widget>[
            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: <Widget>[
        
        ],
      ),
    );
  }
}

这就完成了一个底部结算栏的大体结构确定,大体结构完成后,我们还是把里边的细节,拆分成不同的方法返回对象的组件。

全选按钮方法

先来制作全选按钮方法,这个外边采用Container,里边使用了一个Row,这样能很好的完成横向布局的需求.

  //全选按钮
  Widget selectAllBtn(){
    return Container(
      child: Row(
        children: <Widget>[
          Checkbox(
            value: true,
            activeColor: Colors.pink,
            onChanged: (bool val){},
          ),
          Text('全选')
        ],
      ),
    );
  }

合计区域方法

合计区域由于布局对齐方式比较复杂,所以这段代码虽然很简单,但是代码设计的样式比较多,需要你有很好的样式编写能力.代码如下:

  // 合计区域
  Widget allPriceArea(){

    return Container(
      width: ScreenUtil().setWidth(430),
      alignment: Alignment.centerRight,
      child: Column(
        children: <Widget>[
          Row(
            children: <Widget>[
              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: <Widget>[
          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<CartProvide>(context).cartList;
          if(snapshot.hasData && cartList!=null){
            //关键代码-------------------start
            return Stack(
              children: <Widget>[
                ListView.builder(
                  itemCount: cartList.length,
                  itemBuilder: (context,index){
                    return CartItem(cartList[index]);
                  },
                ),
                Positioned(
                  bottom:0,
                  left:0,
                  child: CartBottom(),
                )
              ],
            );
            //关键代码-----------------end

            
          }else{
            return Text('正在加载');
          }
        },
      ),
    );
  }

  Future<String> _getCartInfo(BuildContext context) async{
     await Provide.value<CartProvide>(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: <Widget>[
        ],
      ),
      
    );
  }

写完这个,我们再把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: <Widget>[
          _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: <Widget>[
          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<String, dynamic> json) {
    goodsId = json['goodsId'];
    goodsName = json['goodsName'];
    count = json['count'];
    price = json['price'];
    images = json['images'];
    //------新添加代码----start
    isCheck = json['isCheck'];
    //------新添加代码----end
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    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<String, dynamic> 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<Map> 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<CartProvide>(context).cartList;
          if(snapshot.hasData && cartList!=null){
              return Stack(
                children: <Widget>[
                  //主要代码--------------------start--------
                  Provide<CartProvide>(
                   
                    builder: (context,child,childCategory){
                       cartList= Provide.value<CartProvide>(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<String> _getCartInfo(BuildContext context) async{
     await Provide.value<CartProvide>(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: <Widget>[
            Text('¥${item.price}'),
            Container(
              child: InkWell(
                onTap: (){
                  //主要代码---------------start----------
                  Provide.value<CartProvide>(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<CartInfoMode> 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<Map> 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<CartProvide>(
        builder: (context,child,childCategory){
          return  Row(
            children: <Widget>[
              //修改部分--------start----------
              selectAllBtn(context),
              allPriceArea(context),
              goButton(context)
              //修改部分--------end-----------
            ],
          );
        },
      )
    );
  }

然后在两个方法中都从Provide里动态获取变量,就可以实现效果了。

合计区域的方法代码:

  // 合计区域
  Widget allPriceArea(context){
    //修改代码---------------start------------
    double allPrice = Provide.value<CartProvide>(context).allPrice;
    //修改代码---------------end------------
    return Container(
      width: ScreenUtil().setWidth(430),
      alignment: Alignment.centerRight,
      child: Column(
        children: <Widget>[
          Row(
            children: <Widget>[
              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<CartProvide>(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<Map> 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<CartProvide>(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<Map> 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<Map> tempList= (json.decode(cartString.toString()) as List).cast(); 
    List<Map> 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<CartProvide>(context).isAllCheck;
    //--------新增代码----------end--------
    return Container(
      child: Row(
        children: <Widget>[
          Checkbox(
            value: isAllCheck,
            activeColor: Colors.pink,
            //--------新增代码----------start--------
            onChanged: (bool val){
              Provide.value<CartProvide>(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<Map> 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: <Widget>[
          //--------------新增加代码------------start--------
          _reduceBtn(context),
          _countArea(),
          _addBtn(context),
          //--------------新增加代码------------end--------
        ],
      ),
      
    );
  }
  // 减少按钮
  Widget _reduceBtn(context){
    return InkWell(
      onTap: (){
        //--------------新增加代码------------start--------
        Provide.value<CartProvide>(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<CartProvide>(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: <Widget>[
          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<BottomNavigationBarItem> 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<Widget> tabBodies = [
      HomePage(),
      CategoryPage(),
      CartPage(),
      MemberPage()
   ];

  @override
  Widget build(BuildContext context) {
   
    ScreenUtil.instance = ScreenUtil(width: 750, height: 1334)..init(context);
  
    return Provide<CurrentIndexProvide>(

      builder: (context,child,val){
        //------------关键代码----------start---------
        int currentIndex= Provide.value<CurrentIndexProvide>(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<CurrentIndexProvide>(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<CurrentIndexProvide>(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: <Widget>[
          //关键代码--------------------start--------------
           Stack(
             children: <Widget>[
               InkWell(
                  onTap: (){
                      Provide.value<CurrentIndexProvide>(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<CartProvide>(
                  builder: (context,child,val){
                    int  goodsCount = Provide.value<CartProvide>(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<Map> 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<String, dynamic> 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: <Widget>[
       ],
     ) ,
   );
  }
}

这样大体结构就已经编写完成了,编写完成后我们把ListView的进行分离出来,编写成不同的方法。

顶部头像区域编写

头像区域我们外边套一层Container,然后里边放入Column,圆形头像这个部分,我们使用ClipOval Widget。代码我直接放在下面了。

  Widget _topHeader(){

    return Container(
      width: ScreenUtil().setWidth(750),
      padding: EdgeInsets.all(20),
      color: Colors.pinkAccent,
      child: Column(
        children: <Widget>[
          Container(
            margin: EdgeInsets.only(top: 30), 
            child: ClipOval(
              
              child:Image.network('http://blogimages.www.slsbk.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: <Widget>[
          Container(
            width: ScreenUtil().setWidth(187),
            child: Column(
              children: <Widget>[
                Icon(
                  Icons.party_mode,
                  size: 30,
                ),
                Text('待付款'),
              ],
            ),
          ),
          //-----------------
          Container(
            width: ScreenUtil().setWidth(187),
            child: Column(
              children: <Widget>[
                Icon(
                  Icons.query_builder,
                  size: 30,
                ),
                Text('待发货'),
              ],
            ),
          ),
           //-----------------
          Container(
            width: ScreenUtil().setWidth(187),
            child: Column(
              children: <Widget>[
                Icon(
                  Icons.directions_car,
                   size: 30,
                ),
                Text('待收货'),
              ],
            ),
          ),
          Container(
            width: ScreenUtil().setWidth(187),
            child: Column(
              children: <Widget>[
                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: <Widget>[
            _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,然后在<activity>标签里,加入下面的代码:

<meta-data
  android:name="com.amap.api.v2.apikey"
  android:value="自己的key" />

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<MyHomePage> {

  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 {
  
  _MyAppState createState() => new _MyAppState();
}

class _MyAppState extends State<MyApp> {
 
  
  void initState() {
    super.initState();
  }
// 编写视图
  
  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<void> initPlatformState() async {
    String platformVersion;

    try {
      //监听响应方法的编写
      jpush.addEventHandler(
        onReceiveNotification: (Map<String, dynamic> 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 {
  
  _MyAppState createState() => new _MyAppState();
}

class _MyAppState extends State<MyApp> {
  String debugLable = 'Unknown';   //错误信息
  final JPush jpush = new JPush();  //初始化极光插件
  
  void initState() {
    super.initState();
    initPlatformState();  //极光插件平台初始化
  }


  Future<void> initPlatformState() async {
    String platformVersion;

    try {
      //监听响应方法的编写
      jpush.addEventHandler(
        onReceiveNotification: (Map<String, dynamic> message) async {
          print(">>>>>>>>>>>>>>>>>flutter 接收到推送: $message");
          setState(() {
            debugLable = "接收到推送: $message";
          });
        }
      );

    } on PlatformException {
      platformVersion = '平台版本获取失败,请检查!';
    }


    if (!mounted) return;

    setState(() {
      debugLable = platformVersion;
    });
  }



// 编写视图
  
  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视频,到http://www.slsbk.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:商城价格(便宜的价格)

黑龙江快乐十分