【Flutter从入门到实战】⑲Flutter的项目实战-美食广场-2、widget改成Stateless、Provider共享数据 、Consumer-Selector的使用、抽取重用的widget

Flutter从入门到实战
一共分为23个系列
①(Flutter、Dart环境搭建篇) 共3个内容 已更新
②(Dart语法1 篇) 共4个内容 已更新
③(Dart语法2 篇) 共2个内容 已更新
④(Flutter案例开发篇) 共4个内容 已更新
⑤(Flutter的StatelessWidget 共3个内容 已更新
⑥(Flutter的基础Widget篇) 共2个内容 已更新
⑦(布局Widget篇) 共1个内容 已更新
⑧(Flex、Row、Column以及Flexible、Stack篇) 共1个内容 已更新
⑨(滚动的Widget篇) 共4个内容 已更新
⑩(Dart的Future和网络篇) 共3个内容 已更新
⑪(豆瓣案例-1篇) 共3个内容 已更新
⑫(豆瓣案例-2篇) 共3个内容 已更新
⑬(Flutter渲染流程篇) 共3个内容 已更新
⑭(状态管理篇) 共3个内容 已更新
⑮(Flutter事件监听-以及路由使用篇) 共2个内容 已更新
⑯(Flutter的动画篇) 共4个内容 已更新
⑰(Flutter的主题、屏幕适配、测试篇) 共4个内容 已更新
⑱(Flutter的项目实战-美食广场篇) 共8个内容 已更新
⑲(Flutter的项目实战-美食广场-2篇) 共3个内容 已更新

官方文档说明

官方视频教程
Flutter的YouTube视频教程-小部件

【Flutter从入门到实战】⑲Flutter的项目实战-美食广场-2、widget改成Stateless、Provider共享数据 、Consumer-Selector的使用、抽取重用的widget_第1张图片


⑲Flutter的项目实战-美食广场-2篇

    • 小贴士 - 复杂的json数据解析
    • ①、Item的抽取、`YHHomeContent` 改造成 `StatelessWidget`
      • 1、Item的抽取
      • 2、`YHHomeContent` 改造成 `StatelessWidget`
      • 改造 - YHHomeContent.dart
      • 效果图 - 改造 - YHHomeContent.dart 和之前的效果是一样的
      • 3、使用网络请求加载首页详情列表数据
      • config.dart `请求工具类配置`
      • http_request.dart `请求工具类`
      • 首页的JSON解析 - json_parse.dart `json解析类`
      • meal_request.dart `首页详情列表请求`
    • ②、⭐️使用Provider共享数据 、Consumer的使用、Selector的使用
      • meal_view_model.dart `YHMealViewModel class`
      • meal_view_model.dart `YHMealViewModel class`的使用
      • 1、添加首页详情列表页面 以及给首页列表添加点击跳转到首页详情列表页面
      • 1.1、详情列表页面
      • 1.2、首页列表添加点击事件
      • 1.3、详情页面获取路由参数
      • 2、Provider的共享数据
      • 3、Provider的Consumer的使用 `首页详情列表内容数据` - 不推荐使用 `会重复构建`
      • 4、⭐️Provider的Selector的使用 `首页详情列表内容数据` - 推荐使用 `不会重复构建`
    • ③、抽取重用的`widget`
      • 首页列表Item的抽取 `YHMealItem`
      • 效果图 - 首页列表Item的抽取 `YHMealItem`
      • 首页列表Item的抽取 `YHOperationItem`
      • 效果图 - 首页列表Item的抽取 `YHOperationItem`
      • 添加美食详情页面 `YHDetailScreen`
      • detail.dart

小贴士 - 复杂的json数据解析

比如服务器返回的数据比较复杂、也就是嵌套的层比较多
使用 第三方JSONToDart 可能会出现创建类的情况 会使用系统类。比如List
建议使用 app.quicktype.io 支持多种语言并且不会转成系统类和出错

继续完善我们上一篇的内容

  1. 我们之前的首页Item的是放到home_content里面的。我们这里做一个抽取
  2. 并且将home_content 的 StatefulWidget 改成State
    因为_YHHomeContentState 里面有很多冗余的代码。我们来精简以下

①、Item的抽取、YHHomeContent 改造成 StatelessWidget

1、Item的抽取

我们之前讲过的将一个Widget快速抽取一个Widget,但是这个系统生成的会把一些参数都会抽取出来。但是可能不是我们想要的效果
所以 我们还是自己抽取

如果是系统抽取出来 会单独抽取_categorys 数组 和 BgColor
而我们的item 只需要_categorys的某一项数据即可


// 抽取分类的item
import 'package:favorcate/core/extension/int_extension.dart';
import 'package:flutter/material.dart';

import '../../../core/model/category_model.dart';

class YHCategoryItemWidget extends StatelessWidget {
  final YHCategoryModel _category;

  YHCategoryItemWidget(this._category);

  @override
  Widget build(BuildContext context) {
    final bgColor = _category.cColor;
    return Container(
        decoration: BoxDecoration(
            color:bgColor,
            borderRadius: BorderRadius.circular(12.px),
            // 渐变颜色
            gradient: LinearGradient(
                colors: [
                  bgColor.withOpacity(.5),
                  bgColor
                ]

            )
        ),
        alignment: Alignment.center,
        child: Text(_category.title!,style: TextStyle(fontWeight: FontWeight.bold),));
  }
}

2、YHHomeContent 改造成 StatelessWidget

之前的YHHomeContent 里面

  1. 数组的创建
    List _categorys = [];
  2. initState的加载数据
    YHJsonParse.getCatgoryData().then((res){
    setState(() {
    _categorys = res;
    });
    // print(res);
    });

因为数据时异步加载的。
我们这里换一个Widget -> FutureBuilder
FutureBuilder 有两个属性 futurebuilder
future是异步请求的数据返回、
builder是需要构建的Widget ,builder会返回2个参数

  1. ctx
  2. snapshot 快照 用来操作异步请求返回的数据
    FutureBuilder 无法处理 上拉下拉更新

改造 - YHHomeContent.dart

import 'package:favorcate/core/extension/int_extension.dart';
import 'package:flutter/material.dart';

import '../../../core/model/category_model.dart';
import '../../../core/services/json_parse.dart';
import 'home_category_item.dart';

class YHHomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FutureBuilder<List<YHCategoryModel>> (
      future: YHJsonParse.getCatgoryData(),
        builder: (ctx,snapshot) {
        // 判断是否有数据 如果没有数据 一直转圈
        if (!snapshot.hasData) return Center(child: CircularProgressIndicator());
        // 如果请求失败 就返回错误信息
        if (snapshot.error != null) return Center(child:Text("请求失败"));
        // 返回成功 拿到数据
        final categorys = snapshot.data!;
        return GridView.builder(
            padding: EdgeInsets.all(20.px),
            itemCount: categorys.length,
            gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 2,
              crossAxisSpacing: 20.px,
              mainAxisSpacing: 20.px,
              childAspectRatio: 1.5,// 宽高比
            ), itemBuilder: (ctx,index){
          final bgColor = categorys[index].cColor;
          return YHCategoryItemWidget(categorys[index]);
        });
        },
        );
  }
}



效果图 - 改造 - YHHomeContent.dart 和之前的效果是一样的

【Flutter从入门到实战】⑲Flutter的项目实战-美食广场-2、widget改成Stateless、Provider共享数据 、Consumer-Selector的使用、抽取重用的widget_第2张图片

3、使用网络请求加载首页详情列表数据

接下来继续搞我们的首页点击跳转到首页详情列表里面去
之前首页的数据 是加载本地JSON数据
详情数据 使用Dio加载
可以使用我的接口加载 http://101.35.169.76:8800/msApi/meal.json

config.dart 请求工具类配置

class HttpConfig{
  // static const String baseUrl = "https://httpbin.org";
  static const String baseUrl = "http://101.35.169.76:8800/msApi";
  static const int timeout = 10000;

}

http_request.dart 请求工具类

// 封装建议
// 命名最好使用 单词使用下划线分开

import 'package:dio/dio.dart';

import 'config.dart';
class HttpRequest {
  // Dio 有基本的配置
  static final BaseOptions baseOptions = BaseOptions(
      baseUrl: HttpConfig.baseUrl,connectTimeout: HttpConfig.timeout
  );
  static final Dio dio = Dio(baseOptions);
  // static void request(String url,
  // 由于异步请求 所以使用 Future
  // 其中T代表泛型 因为不知道服务器返回什么具体的类型 所以使用泛型代表
  static Future<T> request<T>(String url,
                      {String method="get",
                        Map<String,dynamic> params,
                        Interceptor inter}) async
      {
        // 1. 创建单独配置
        final option = Options(method: method);
        // 全局拦截器
        // 创建默认的全局拦截器

        Interceptor dInter = InterceptorsWrapper(
            onRequest: (request,handle) {
              print("请求拦截");
              return handle.next(request);
            },
            onResponse: (response,handle) {
              print("响应拦截");
              return handle.next(response);
            },
            onError: (e, handler) async {
              print("错误拦截");
              return handler.next(e);
            }
        );
        List<Interceptor> inters = [dInter];
        // 请求单独拦截器
        if (inter != null) {
          inters.add(inter);
        }
        // 统一添加到拦截器中
        dio.interceptors.addAll(inters);

        // 2. 发送网络请求 url 参数 配置
        // 请求可能发生错误 所以使用try 捕捉异常
        try{
          Response response = await dio.request(url,queryParameters: params,options: option);
          // 3. 返回响应的数据
          return response.data;

        } on DioError catch(e) {
          // 请求失败情况
          return Future.error(e);
        }



      }
}

首页的JSON解析 - json_parse.dart json解析类

import 'dart:convert';

import 'package:favorcate/core/model/category_model.dart';
import 'package:flutter/services.dart';
class YHJsonParse{
  // static void getCatgoryData() async{
  static Future<List<YHCategoryModel>> getCatgoryData() async{
    // 1. 加载json文件
    final jsonString = await rootBundle.loadString("assets/json/category.json");

    // 2. 将JsonString 转成 Map/List
    // json.decode(source) // 将字符串转成Map或者list
    // json.encode(value) // 将json对象也就是Map或者List 转成 json字符串

    // 因为转换是异步操作 所以要使用 async
    final result = json.decode(jsonString);

    // 3. 将Map中的内容转换成一个个对象
    // 转换工具 https://javiercbk.github.io/json_to_dart/
    final resultList = result["category"];
    List<YHCategoryModel> categorys = [];
    for (var json in resultList)
      {
        categorys.add(YHCategoryModel.fromJson(json));
      }
    return categorys;
  }
}

meal_request.dart 首页详情列表请求

import 'package:favorcate/core/services/http_request.dart';

import '../model/detail_model.dart';

class YHMealRequest {
  static Future<List<YHMealModel>> getMealData() async {
    // 1. 发送网络请求
    final url = "/meal.json";
    final result = await HttpRequest.request(url);
    // print(result);

    // 2. json转model
    final mealArray = result["meal"];

    List<YHMealModel> meals = [];
    for (var json in mealArray){
      // print(json);
      meals.add(YHMealModel.fromJson(json));
    }
    return meals;
  }
}

详情列表数据获取的使用
YHMealRequest.getMealData().then((res){
// … do something
}

②、⭐️使用Provider共享数据 、Consumer的使用、Selector的使用

因为详情列表数据 可能只会加载一次 所以使用Provider共享数据处理
第三方的库安装

  1. pubspec.yaml provider: ^6.0.2
  2. 封装一个ViewModel类 meal_view_model.dart

meal_view_model.dart YHMealViewModel class

import 'package:favorcate/core/model/detail_model.dart';
import 'package:flutter/material.dart';

import '../services/meal_request.dart';

class YHMealViewModel extends ChangeNotifier {
  List<YHMealModel> _meals  = [];
  List<YHMealModel> get meals {
    return _meals;
  }

  YHMealViewModel(){
    YHMealRequest.getMealData().then((res){
      _meals = res;
      // print("getMealData ${res}");
      // notifyListeners 所有依赖当前ViewModel的 都会进行通知重新构建.
      notifyListeners();
    });
  }
}

meal_view_model.dart YHMealViewModel class的使用

 runApp(ChangeNotifierProvider(
    // YHMealViewModel 是懒加载数据 是用到时才去加载
    create: (cxt) => YHMealViewModel(),
    child: MyApp(),));

1、添加首页详情列表页面 以及给首页列表添加点击跳转到首页详情列表页面

  1. 添加详情页面
  2. 添加首页列表的点击事件 点击跳转到详情页面
  3. 首页详情页面 通过路由获取参数

1.1、详情列表页面

import 'package:favorcate/core/model/category_model.dart';
import 'package:favorcate/core/model/detail_model.dart';
import 'package:flutter/material.dart';

class YHMealScreen extends StatelessWidget {

  // 命名路由
  static const String routeName = "/meal";

  // const YHMealScreen({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // 获取上一个页面传递过来的参数
    // 并且转换成 YHMealModel
    final category = ModalRoute.of(context).settings.arguments as   YHCategoryModel;


    return Scaffold(
      appBar: AppBar(
        title: Text(category.title),

      ),
      body: Center(
        child: Text("meal list"),
      ),
    );
  }
}

1.2、首页列表添加点击事件

使用 GestureDetector 进行包裹
并且实现 OnTap 回调 跳转到详情页面


// 抽取分类的item
import 'package:favorcate/core/extension/int_extension.dart';
import 'package:favorcate/ui/pages/meal/meal.dart';
import 'package:flutter/material.dart';

import '../../../core/model/category_model.dart';

class YHCategoryItemWidget extends StatelessWidget {
  final YHCategoryModel _category;

  YHCategoryItemWidget(this._category);

  @override
  Widget build(BuildContext context) {
    final bgColor = _category.cColor;
    // 监听点击 使用GestureDetector
    return GestureDetector(
      child: Container(
          decoration: BoxDecoration(
              color:bgColor,
              borderRadius: BorderRadius.circular(12.px),
              // 渐变颜色
              gradient: LinearGradient(
                  colors: [
                    bgColor.withOpacity(.5),
                    bgColor
                  ]

              )
          ),
          alignment: Alignment.center,
          child: Text(_category.title,style: TextStyle(fontWeight: FontWeight.bold),)),
      onTap: (){
        // 跳转详情 并且把Item的数据 传递给详情页面
        Navigator.of(context).pushNamed(YHMealScreen.routeName,arguments: _category);
      },
    );
  }
}

1.3、详情页面获取路由参数

final category = ModalRoute.of(context).settings.arguments as   YHCategoryModel;

2、Provider的共享数据

初始化

main() {

  // 只加载一次数据 可以使用共享数据
  // 使用provider
  //  provider -> ViewModel/provider/Consumer selector
  // YHMealRequest.getMealData().then((res){
  //   print("getMealData ${res}");
  // });
  // runApp(MyApp());
  runApp(ChangeNotifierProvider(
    // YHMealViewModel 是懒加载数据 是用到时才去加载
    create: (cxt) => YHMealViewModel(),
    child: MyApp(),));
}

3、Provider的Consumer的使用 首页详情列表内容数据 - 不推荐使用 会重复构建

 Consumer 数据发生改变会重新构建 不推荐使用
class YHMealContent extends StatelessWidget {
  // const YHMealContent({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // 永远都是获取顶层的数据
    final category = ModalRoute.of(context).settings.arguments as   YHCategoryModel;

    // 获取数据
    // Consumer 数据发生改变会重新构建 不推荐使用
    return Consumer<YHMealViewModel>(
      builder :  (ctx,mealVM,chind){
        // where 过滤数据
        // toList 说明拿到的是数组
        final meals = mealVM.meals.where((meal) => meal.categories.contains(category.id)).toList();
        // print(mealVM.meals);
        // return Text("${mealVM.meals.length}");
        return ListView.builder(
          itemCount: meals.length,
            itemBuilder: (ctx,index){
              return ListTile(title: Text(meals[index].title),);
        });

      }
    );
  }
}

4、⭐️Provider的Selector的使用 首页详情列表内容数据 - 推荐使用 不会重复构建

class YHMealContent extends StatelessWidget {
  // const YHMealContent({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // 永远都是获取顶层的数据
    final category = ModalRoute.of(context).settings.arguments as   YHCategoryModel;


    // 获取数据
    // 使用Selector 构建数据
    // Selector(,List<>) 表示希望 YHMealViewModel 转换成YHMealModel类型
    return Selector<YHMealViewModel,List<YHMealModel>>(
      //  mealVM就是 YHMealViewModel
      //  希望 mealVM的meals 转换成YHMealModel
      // 并且进行过滤
      // 第一种写法
      // selector: (ctx, mealVM) => mealVM.meals.where((meal) => meal.categories.contains(category.id)).toList(),
      // 第二种写法
      selector: (cxt,mealVM){
        return mealVM.meals.where((meal) {
          // 是否包含id 如果包含id 就返回到mealVM.meals里面
          return meal.categories.contains(category.id);
        }).toList();
      },
      // shouldRebuild 是否需要重新build的
      //  判断两个数据相同 如果相同不需要刷新
      // 使用API 可以对比两个列表是否相同
      shouldRebuild: (prev,next) => !ListEquality().equals(prev,next),
      builder: (ctx,meals,child){
        return ListView.builder(
          itemCount: meals.length,
            itemBuilder: (ctx,index){
            return YHMealItem(meals[index]); // item的抽取
            });
      },
    );
  }
}

③、抽取重用的widget

  1. 商品详情列表的item 重用
    在首页列表使用
    在美食商品详情使用
  2. 首页列表详情 操作 的重用
    【Flutter从入门到实战】⑲Flutter的项目实战-美食广场-2、widget改成Stateless、Provider共享数据 、Consumer-Selector的使用、抽取重用的widget_第3张图片

首页列表Item的抽取 YHMealItem

import 'package:favorcate/core/extension/int_extension.dart';
import 'package:favorcate/core/model/detail_model.dart';
import 'package:favorcate/ui/pages/detail/detail.dart';
import 'package:favorcate/ui/pages/meal/meal.dart';
import 'package:favorcate/ui/widgets/operation_item.dart';
import 'package:flutter/material.dart';
/**
 * 布局流程
 * Card
 *  Column
 *   Stack
 *   Row 
 *    image text
 * */

final cardRadius = 12.px;

class YHMealItem extends StatelessWidget {

  final YHMealModel _meal;
  YHMealItem(this._meal);
  @override
  Widget build(BuildContext context) {


    return GestureDetector(
      child: Card(
        margin: EdgeInsets.all(10.px),
        // 阴影
        elevation: 5,
        // 矩形圆角
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.px)),
        child: Column(
          children: [
            buildBasicInfo(),
            buildOperationInfo(),
          ],
        ),


      ),
      onTap: (){
        Navigator.of(context).pushNamed(YHDetailScreen.routeName,arguments:_meal );
      },
    );
  }


  // 基本信息
  Widget buildBasicInfo(){
    return Stack(
      children: [
        // 宽度尽可能大 double.infinity
        // 图片的裁剪 上边圆角
        ClipRRect(borderRadius: BorderRadius.only(
          topLeft: Radius.circular(cardRadius),
          topRight: Radius.circular(cardRadius),
        ), child: Image.network(_meal.imageUrl,width: double.infinity,height: 250,fit: BoxFit.cover,)),
        // 绝对定位
        Positioned(
          right: 10.px,
          bottom: 10.px,
          child: Container(
            width: 300.px,
            padding: EdgeInsets.symmetric(horizontal: 10,vertical: 5),
            decoration: BoxDecoration(
              color: Colors.black54,
              borderRadius: BorderRadius.circular(6.px),
            ),
            child: Text(_meal.title,style: TextStyle(color: Colors.white),),
          ),
        )
      ],
    );
  }


  Widget buildOperationInfo(){
      return Padding(
        padding:  EdgeInsets.all(16.px),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            YHOperationItem(Icon(Icons.schedule),"${_meal.duration}分钟"),
            YHOperationItem(Icon(Icons.restaurant),"${_meal.complexStr}"),
            YHOperationItem(Icon(Icons.favorite),"未收藏"),

          ],
        ),
      );
  }
}

效果图 - 首页列表Item的抽取 YHMealItem

【Flutter从入门到实战】⑲Flutter的项目实战-美食广场-2、widget改成Stateless、Provider共享数据 、Consumer-Selector的使用、抽取重用的widget_第4张图片

首页列表Item的抽取 YHOperationItem

import 'package:favorcate/core/extension/int_extension.dart';
import 'package:flutter/material.dart';

class YHOperationItem extends StatelessWidget {
  final Widget _icon;
  final String _title;

  YHOperationItem(this._icon,this._title);
  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        _icon,
        SizedBox(width: 3.px,),
        Text(_title),
      ],
    );
  }
}

效果图 - 首页列表Item的抽取 YHOperationItem

在这里插入图片描述

添加美食详情页面 YHDetailScreen

继续添加美食详情页面

  1. 要配置路由映射
    YHRoute.dart 加入路由映射 YHDetailScreen.routeName : (ctx) => YHDetailScreen(),
  2. 创建美食详情页面 details.dart
    用来展示 步骤和材料的使用
    具体布局UI 下一篇文章再继续讲解

detail.dart

class YHDetailScreen extends StatelessWidget {
  static const String routeName = "/detail";


  @override
  Widget build(BuildContext context) {
    final meal = ModalRoute.of(context).settings.arguments as YHMealModel;

    return Scaffold(
      appBar: AppBar(
        title: Text(meal.title),
      ),
      body: Text(meal.title),
    );
  }
}

【Flutter从入门到实战】⑲Flutter的项目实战-美食广场-2、widget改成Stateless、Provider共享数据 、Consumer-Selector的使用、抽取重用的widget_第5张图片

你可能感兴趣的:(Flutter,Flutter,Flutter项目实战,Provider,抽取重用widget,Dio网络请求)