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视频教程-小部件
比如服务器返回的数据比较复杂、也就是嵌套的层比较多
使用 第三方JSONToDart 可能会出现创建类的情况 会使用系统类。比如List
建议使用 app.quicktype.io 支持多种语言并且不会转成系统类和出错继续完善我们上一篇的内容
- 我们之前的首页Item的是放到home_content里面的。我们这里做一个抽取
- 并且将home_content 的 StatefulWidget 改成State
因为_YHHomeContentState
里面有很多冗余的代码。我们来精简以下
YHHomeContent
改造成 StatelessWidget
我们之前讲过的将一个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),));
}
}
YHHomeContent
改造成 StatelessWidget
之前的
YHHomeContent
里面
- 数组的创建
List _categorys = [];- initState的加载数据
YHJsonParse.getCatgoryData().then((res){
setState(() {
_categorys = res;
});
// print(res);
});
因为数据时异步加载的。
我们这里换一个Widget ->FutureBuilder
FutureBuilder
有两个属性future
、builder
future
是异步请求的数据返回、
builder
是需要构建的Widget ,builder
会返回2个参数
ctx
、snapshot
快照 用来操作异步请求返回的数据
FutureBuilder
无法处理 上拉下拉更新
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]);
});
},
);
}
}
接下来继续搞我们的首页点击跳转到首页详情列表里面去
之前首页的数据 是加载本地JSON数据
详情数据 使用Dio加载
可以使用我的接口加载 http://101.35.169.76:8800/msApi/meal.json
请求工具类配置
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;
}
请求工具类
// 封装建议
// 命名最好使用 单词使用下划线分开
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解析类
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;
}
}
首页详情列表请求
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
共享数据处理
第三方的库安装
- pubspec.yaml
provider: ^6.0.2
- 封装一个ViewModel类
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();
});
}
}
YHMealViewModel class
的使用 runApp(ChangeNotifierProvider(
// YHMealViewModel 是懒加载数据 是用到时才去加载
create: (cxt) => YHMealViewModel(),
child: MyApp(),));
- 添加详情页面
- 添加首页列表的点击事件 点击跳转到详情页面
- 首页详情页面 通过路由获取参数
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"),
),
);
}
}
使用
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);
},
);
}
}
final category = ModalRoute.of(context).settings.arguments as YHCategoryModel;
初始化
main() {
// 只加载一次数据 可以使用共享数据
// 使用provider
// provider -> ViewModel/provider/Consumer selector
// YHMealRequest.getMealData().then((res){
// print("getMealData ${res}");
// });
// runApp(MyApp());
runApp(ChangeNotifierProvider(
// YHMealViewModel 是懒加载数据 是用到时才去加载
create: (cxt) => YHMealViewModel(),
child: MyApp(),));
}
首页详情列表内容数据
- 不推荐使用 会重复构建
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),);
});
}
);
}
}
首页详情列表内容数据
- 推荐使用 不会重复构建
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
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),"未收藏"),
],
),
);
}
}
YHMealItem
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),
],
);
}
}
YHOperationItem
YHDetailScreen
继续添加美食详情页面
- 要配置路由映射
在YHRoute.dart
加入路由映射YHDetailScreen.routeName : (ctx) => YHDetailScreen(),
- 创建美食详情页面
details.dart
用来展示 步骤和材料的使用
具体布局UI 下一篇文章再继续讲解
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),
);
}
}