更新日期 | 更新内容 |
2022年5月13日之前 | 豆瓣主页界面 |
2022年5月13日 | 1、添加了豆瓣电影界面代码地址;2、美食广场App项目创建 |
2022年5月14日 | 美食广场【1、初始化项目;2、路由初步配置;3、创建若干界面;4、主页界面至2.26】 |
2022年5月15日 | 美食广场【1、内容从2.27开始至2.8.4;2、代码更新至github】 |
2022年5月16日 | 美食广场【1、内容至2.9.4;2、代码更新至github】 |
2022年5月17日 | 美食广场【1、APP国际化;2、flutter测试】 |
1、项目一:豆瓣电影界面
代码地址
点击进入源码
1.1 评分组件
import 'package:flutter/material.dart';
main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue, splashColor: Colors.transparent),
home: HYStarRating(
rating: 5,
count: 5,
));
}
}
class HYStarRating extends StatefulWidget {
final double rating; //分数
final double maxRating; //最大分数
final int count; //星星的棵树
final double size; //星星的大小
final Color unselectedColor; //未选中的星星颜色
final Color selectedColor; //选中的星星颜色
final Widget selectedImage; //选中后的图片
final Widget unselectedImage; //未选中时的图片
//构造函数
HYStarRating({
required this.rating,
this.maxRating = 10,
this.count = 5,
this.size = 30,
this.unselectedColor = const Color(0xffbbbbbb),
this.selectedColor = const Color(0xffff0000),
selectedImage,
unselectedImage,
//初始化列表
}): unselectedImage = unselectedImage ?? Icon(Icons.star_border, color: unselectedColor, size: size),
selectedImage = selectedImage ?? Icon(Icons.star, color: selectedColor, size: size);
@override
State<HYStarRating> createState() => _HYStarRatingState();
}
class _HYStarRatingState extends State<HYStarRating> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(""),
),
body: Center(
//未选中图标叠上一定数量的选中状态的图标,使用stack
child: Stack(
children: [
Row(
//设置row为最短长度,从而居中
mainAxisSize: MainAxisSize.min,
children: buildUnselectedStar(),
),
Row(
mainAxisSize: MainAxisSize.min,
children: buildSelectedStar(),
)
],
),
));
}
//生成指定个数的图标(未选中时的星星)
List<Widget> buildUnselectedStar() {
return List.generate(widget.count, (index) {
return widget.unselectedImage;
});
}
List<Widget> buildSelectedStar() {
final star = widget.selectedImage;
List<Widget> stars = [];
double oneValue = widget.maxRating / widget.count; //计算一个星星的分值
int entireCount = (widget.rating / oneValue).floor(); //floor向下取整;ceil向上取整
//完整星星
for (var i = 0; i < entireCount; i++) {
stars.add(star);
}
//剩余部分多大
double leftWidth = ((widget.rating / oneValue) - entireCount) * widget.size;
final halStar = ClipRect(
clipper: HYStarClipper(leftWidth),//裁剪星星
child: star,
);
stars.add(halStar);
//超过最大判断
if(stars.length > widget.count) {
return stars.sublist(0, widget.count);
}
return stars;
}
}
class HYStarClipper extends CustomClipper<Rect> { //以矩形传入对象进行裁剪
double width;
//构造函数
HYStarClipper(this.width);
@override
Rect getClip(Size size) {
//从左侧0开始到距离20,从顶部0到距离整个大小都保留,剩余部分裁剪
return Rect.fromLTRB(0, 0, width, size.height);
}
//重新剪裁
@override
bool shouldReclip(HYStarClipper oldClipper) {
return oldClipper.width != width;
}
}
1.2 虚线组件
import 'package:flutter/material.dart';
main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue, splashColor: Colors.transparent),
home: Scaffold(
appBar: AppBar(
title: Text("Demo"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 200,
child: HYDashLine(
dashWidth: 5,
count: 20,
axis: Axis.horizontal,
),
),
Container(
height: 200,
child: HYDashLine(
dashHeight: 5,
count: 20,
axis: Axis.vertical,
),
),
],
),
),
));
}
}
class HYDashLine extends StatelessWidget {
final Axis axis;
final double dashWidth;
final double dashHeight;
final int count;
final Color color;
HYDashLine(
{this.axis = Axis.horizontal,
this.dashWidth = 1,
this.dashHeight = 1,
this.count = 10,
this.color = Colors.red});
@override
Widget build(BuildContext context) {
return Flex(
direction: axis,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(count, (index) {
return SizedBox(
width: dashWidth,
height: dashHeight,
child: DecoratedBox(
decoration: BoxDecoration(color: color),
),
);
}),
);
}
}
1.3 项目地址
点击进入项目地址github 界面截图(在chrom上运行项目)
1.4 项目目录结构
说明:assets文件夹中存放底部图标、“想看”按钮图标等静态图片;
model包放json数据转换来的对象、实体;pages包存放页面,包括group、home、main、mall、profile、subject。以home为例,与home相关的dart文件都存放在此处;main下面存放底部导航栏图标的bottom_bar_item.dart、初始化数据的initialize_items.dart;Widgets包里面是上文提到的虚线组件HYDashLineWidget和评分组件HYStarRatingWidget。最后是service包,存放网络请求数据的封装好的类。程序从main.dart开始加载。
1.5 界面介绍(以主页为例)
从main.dart开始加载
对APP主题颜色进行设置,进入HYMainPage。先设置底部的导航栏,
这里要用到IndexedStack,stack本身可以使得多个widget重叠在一起,而indexStack多了个index来指定显示在最上面的是编号为index的widget。
因为使得代码更加公正好看,就将pages数组放到initialize_item.dart中,而这个initialize_item.dart是用于初始化数据。
主页内容为一个列表,采用ListView.builder构建
对于里面的每一项,在home_movie_item.dart中编写。
在此之前,需要用到封装好的HttpRequest类,调用request方法获取电影资源。
import 'package:dio/dio.dart';
import 'package:flutter_learn/service/config.dart';
class HttpRequest {
static final BaseOptions baseOption = BaseOptions(
baseUrl: HttpConfig.baseURL, connectTimeout: HttpConfig.timeout);
static final Dio dio = Dio(baseOption);
static Future<T> request<T>(String url, {method, params, inter}) async {
//创建单独配置
final options = Options(method: method);
//全局拦截器
//创建默认的拦截器
Interceptor dInter = InterceptorsWrapper(onRequest: (options, handler) {
print("请求拦截");
return handler.next(options);
}, onError: (error, handle) {
print("错误拦截");
return handle.next(error);
}, onResponse: (response, handler) {
print("响应拦截");
handler.next(response);
});
List<Interceptor> inters = [dInter];
//请求单独拦截器
if(inter != null){
inters.add(inter);
}
//统一添加
dio.interceptors.addAll(inters);
//发送网络请求
try {
Response response =
await dio.request(url, queryParameters: params, options: options);
//返回数据
return response.data;
} on DioError catch (e) {
//返回错误
return Future.error(e);
}
}
}
可以看到大的布局是Column,分别是排名、每个Item的info,再是原名;对于Item的Info,是row布局,从左至右,但是中间电影信息要嵌套expanded,使得长度可以随着两侧宽度变化,有多大占据多大。再是虚线和“想看”按钮。最后是电影信息,包裹一个Column,分别是电影标题、评分、体裁
由此,结构清晰
【界面完善】底部图标会有闪烁,gaplessPlayback属性设置为true,表示无缝加载Image
【打印日志工具】
import 'package:stack_trace/stack_trace.dart';
class HyCustomTrace {
final StackTrace _trace;
final Object message;
HyCustomTrace(this.message, this._trace){
_parseTrace();
}
void _parseTrace() {
final chain = Chain.forTrace(_trace);
// 拿出其中一条信息
final frames = chain.toTrace().frames;
final frame = frames[1];
// 打印
print("所在文件:${frame.uri} 所在行 ${frame.line} 所在列 ${frame.column};打印信息:$message");
}
}
构建一个类来专门打印Log
2、项目二:美食广场
2.1 代码地址
点击进入源码
2.2 项目创建
flutter create XXXXX
2.3 初始化项目
- Icon
- appId
- 启动图
Android - App名称
- ios
2.3.1项目目录
主目录包括UI和Core,子目录
2.3.2 主题
import 'package:flutter/material.dart';
class HYAppTheme {
//共有属性
static const double xSmallFontSize = 14;
static const double smallFontSize = 16;
static const double normalFontSize = 22;
static const double largeFontSize = 24;
static const double xLargeFontSize = 24;
//普通模式
static const Color norTextColors = Colors.red;
static final ThemeData norTheme = ThemeData(
primarySwatch: Colors.amber, //包含大部分颜色设置
canvasColor: Color.fromRGBO(255, 254, 222, 1), //APP背景颜色
textTheme: const TextTheme(
bodySmall: TextStyle(fontSize: xSmallFontSize),
displaySmall: TextStyle(fontSize: smallFontSize),
displayMedium: TextStyle(fontSize: normalFontSize),
displayLarge: TextStyle(fontSize: largeFontSize),
),
);
//暗黑模式
static const Color darkTextColors = Colors.green;
static final ThemeData darkTheme = ThemeData(
primarySwatch: Colors.grey,
textTheme: const TextTheme(
bodyText1: TextStyle(
fontSize: normalFontSize,
color: darkTextColors,
),
),
);
}
包括通常模式下的主题和暗黑模式下的主题,暗黑模式之后设置
2.3.3 屏幕适配
之前学习笔记有提到屏幕适配,这里就加上工具
import '../../ui/shared/size_fit.dart';
extension DoubleFit on double{
double get px {
return HYSizeFit.setPx(this);
}
double get rpx {
return HYSizeFit.setRpx(this);
}
}
import '../../ui/shared/size_fit.dart';
extension IntFit on int {
double get px {
return HYSizeFit.setPx(this.toDouble());
}
double get rpx {
return HYSizeFit.setRpx(this.toDouble());
}
}
用法就很简单,比如FontSize:20,就在20后面加上".px"
2.4 路由初步设置
import 'package:favorcate/ui/pages/main/main.dart';
import 'package:flutter/material.dart';
class HYRouter {
static final String initialRoute = HYMainScreen.routeName; //main路由
static final Map<String, WidgetBuilder> routes = {
HYMainScreen.routeName: (ctx) => HYMainScreen(),
};
static final RouteFactory generateRoute = (setting) {
return null;
};
static final RouteFactory unKnownRoute = (setting) {
return null;
};
}
在main.dart中添加initialRoute、routes、generateRoute、unKnownRoute
import 'package:favorcate/core/router/route.dart';
import 'package:favorcate/ui/shared/app_theme.dart';
import 'package:flutter/material.dart';
main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '美食广场',
//主题
theme: HYAppTheme.norTheme,
//路由
initialRoute: HYRouter.initialRoute,
routes: HYRouter.routes,
onGenerateRoute: HYRouter.generateRoute,
onUnknownRoute: HYRouter.unKnownRoute,
);
}
}
2.5 创建若干界面,并添加路由映射
2.5.1 main.dart
设置底部导航栏BottomNavigationBar,和点击切换页面的效果
import 'package:favorcate/ui/pages/main/initialize_items.dart';
import 'package:flutter/material.dart';
class HYMainScreen extends StatefulWidget {
static const String routeName = "/";
@override
State<HYMainScreen> createState() => _HYMainScreenState();
}
class _HYMainScreenState extends State<HYMainScreen> {
int _currentIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _currentIndex,
children: pages,
),
bottomNavigationBar: BottomNavigationBar(
selectedFontSize: 14,
unselectedFontSize: 14,
currentIndex: _currentIndex,
items: items,
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
),
);
}
}
对pages和items做一个抽取,pages放要显示的页面,items则是底部的BottomNavigationBarItem
import 'package:favorcate/ui/pages/favor/favor.dart';
import 'package:favorcate/ui/pages/home/home.dart';
import 'package:flutter/material.dart';
final List<Widget> pages = [
HYHomeScreen(),
HYFavorScreen()
];
final List<BottomNavigationBarItem> items = [
BottomNavigationBarItem(
label: "首页",
icon: Icon(Icons.home),
),
BottomNavigationBarItem(
label: "收藏",
icon: Icon(Icons.star),
),
];
2.5.2 home.dart
import 'package:flutter/material.dart';
class HYHomeScreen extends StatelessWidget {
static const String routeName = "/home";
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("美食广场"),
),
body: Center(
child: Text(
"美食广场",
),
),
);
}
}
2.5.3 favor.dart
import 'package:flutter/material.dart';
class HYFavorScreen extends StatelessWidget {
static const String routeName = "/favor";
const HYFavorScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("我的收藏"),
),
body: Center(
child: Text("我的收藏"),
),
);
}
}
运行看一下效果
2.6 主页
主页需要用到一些数据
2.6.1 json数据
category.json
点击获取json数据meal.json
点击获取json数据解析json数据,需要用到rootBundle,json数据解析成一个一个的对象HYCategoryModel,存储在List中。
2.6.2 category_model
import 'package:flutter/cupertino.dart';
class HYCategoryModel {
String id = "";
String title = "";
String color = "";
Color changedColor = const Color.fromARGB(255, 255, 255, 255);
HYCategoryModel({required this.id, required this.title, required this.color});
HYCategoryModel.fromJson(Map<String, dynamic> json) {
id = json['id'];
title = json['title'];
color = json['color'];
/**
* 1、将color转成十六进制数字
* 2、透明度加上(或运算符,留下后六位,前两位FF拼上)
*/
final colorInt = int.parse(color, radix: 16);
changedColor = Color(colorInt | 0xFF000000);
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['id'] = this.id;
data['title'] = this.title;
data['color'] = this.color;
return data;
}
}
利用工具生成模型代码
颜色有些特殊,因为传入的是字符串,这里做了转换
2.6.3 解析json
import 'dart:convert';
import 'package:flutter/services.dart';
import '../model/category_model.dart';
class JsonParse {
static Future<List<HYCategoryModel>> getCategoryData() async{
//1、加载json文件,loadString返回的类型是Future,所以是异步操作
final jsonString = await rootBundle.loadString("assets/json/category.json");
/**
* decode解析,即将json转成map或list;
* encode则反过来
*/
//2、将jsonString转成Map/List
final result = json.decode(jsonString);
//3、将Map中的内哦荣转成一个个的对象
final resultList = result["category"];
List<HYCategoryModel> categories = [];
for (var json in resultList) {
categories.add(HYCategoryModel.fromJson(json));
}
return categories;
}
}
2.6.4 主页界面
import 'package:flutter/material.dart';
import '../../../core/model/category_model.dart';
import 'package:favorcate/core/services/json_parse.dart';
import '../../../core/extension/int_extension.dart';
import '../../../core/extension/double_extension.dart';
class HYHomeContent extends StatefulWidget {
@override
State<HYHomeContent> createState() => _HYHomeContentState();
}
class _HYHomeContentState extends State<HYHomeContent> {
List<HYCategoryModel> _categories = [];
@override
void initState() {
super.initState();
//加载json数据
JsonParse.getCategoryData().then((value) {
setState(() {
_categories = value;
});
});
}
@override
Widget build(BuildContext context) {
return GridView.builder(
padding: EdgeInsets.all(20.px),
itemCount: _categories.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( //固定个数
crossAxisCount: 2,
crossAxisSpacing: 20.px,
mainAxisSpacing: 20.px,
childAspectRatio: 1.5),
itemBuilder: (ctx, index) {
final bgColor = _categories[index].changedColor;
return Container(
decoration: BoxDecoration( //圆角
color: bgColor,
borderRadius: BorderRadius.circular(12),
gradient: LinearGradient( //渐变色
colors: [
bgColor.withOpacity(.5), //半透明到全透明
bgColor
]
)
),
alignment: Alignment.center,
child: Text(
_categories[index].title,
style: Theme.of(context).textTheme.displaySmall?.copyWith( //copyWith:在原有基础上添加属性
fontWeight: FontWeight.bold
),
),
);
},
);
}
}
2.6.5 界面展示
这里用浏览器展示
2.6.6 HYHomeContent代码优化
将statefulWidgets转换为statelessWidget,可以使用FutureBuilder来完成这项功能,但是不能用于频繁刷新的界面
home_content.dart
import 'package:favorcate/core/model/category_model.dart';
import 'package:flutter/material.dart';
import 'package:favorcate/core/services/json_parse.dart';
import '../../../core/extension/int_extension.dart';
import 'home_category_item.dart';
class HYHomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FutureBuilder<List<HYCategoryModel>>( //泛型类
future: HYJsonParse.getCategoryData(),
builder: (ctx, snapshot) {
if (!snapshot.hasData) return Center(child: CircularProgressIndicator());
final categories = snapshot.data;
return GridView.builder(
padding: EdgeInsets.all(20.px),
itemCount: categories?.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( //固定个数
crossAxisCount: 2,
crossAxisSpacing: 20.px,
mainAxisSpacing: 20.px,
childAspectRatio: 1.5),
itemBuilder: (ctx, index) {
return HYHomeCategoryItem(categories![index]);
},
);
}
);
}
}
home_category_item.dart
import 'package:flutter/material.dart';
import '../../../core/model/category_model.dart';
class HYHomeCategoryItem extends StatelessWidget {
final HYCategoryModel _category;
HYHomeCategoryItem(this._category);
@override
Widget build(BuildContext context) {
final bgColor = _category.changedColor;
return Container(
decoration: BoxDecoration( //圆角
color: bgColor,
borderRadius: BorderRadius.circular(12),
gradient: LinearGradient( //渐变色
colors: [
bgColor.withOpacity(.5), //半透明到全透明
bgColor
]
)
),
alignment: Alignment.center,
child: Text(
_category.title,
style: Theme.of(context).textTheme.displaySmall?.copyWith( //copyWith:在原有基础上添加属性
fontWeight: FontWeight.bold
),
),
);
}
}
2.7 菜品页面
2.7.1 网络请求数据
category.json数据采用了解析本地的json数据,那么meal.json数据就采用网络请求的方式获取
配置dio
这里用到了之前学习案例里面的两个dart文件
http_request.dart
import 'package:dio/dio.dart';
import 'config.dart';
class HttpRequest {
static final BaseOptions baseOption = BaseOptions(
baseUrl: HttpConfig.baseURL, connectTimeout: HttpConfig.timeout);
static final Dio dio = Dio(baseOption);
static Future<T> request<T>(String url, {method, params, inter}) async {
//创建单独配置
final options = Options(method: method);
//全局拦截器
//创建默认的拦截器
Interceptor dInter = InterceptorsWrapper(onRequest: (options, handler) {
print("请求拦截");
return handler.next(options);
}, onError: (error, handle) {
print("错误拦截");
return handle.next(error);
}, onResponse: (response, handler) {
print("响应拦截");
handler.next(response);
});
List<Interceptor> inters = [dInter];
//请求单独拦截器
if(inter != null){
inters.add(inter);
}
//统一添加
dio.interceptors.addAll(inters);
//发送网络请求
try {
Response response =
await dio.request(url, queryParameters: params, options: options);
//返回数据
return response.data;
} on DioError catch (e) {
//返回错误
return Future.error(e);
}
}
}
config.dart
class HttpConfig {
static const String baseURL = "http://123.207.32.32:8001/api";
static const int timeout = 10000;
}
meal_request.dart
请求meals数据
import 'package:favorcate/core/model/meal_model.dart';
import 'http_request.dart';
class HYMealRequest {
static Future<List<HYMealModel>> getMealData() async{
// 1、发送网络请求
const url = "/meal";
final result = await HttpRequest.request(url);
//2、json转modal
final mealArray = result["meal"];
List<HYMealModel> meals = [];
for (var json in mealArray) {
meals.add(HYMealModel.fromJson(json));
}
return meals;
}
}
2.7.2 生成MealModel
meal_request.dart
import 'package:favorcate/core/model/meal_model.dart';
import 'http_request.dart';
class HYMealRequest {
static Future<List<HYMealModel>> getMealData() async{
// 1、发送网络请求
final url = "/meal";
final result = await HttpRequest.request(url);
//2、json转modal
final mealArray = result["meal"];
List<HYMealModel> meals = [];
for (var json in mealArray) {
meals.add(HYMealModel.fromJson(json));
}
return meals;
}
}
2.7.3 异步数据共享(provider)
共享meals数据,在viewmodel中创建meal_view_model.dart
meal_view_model.dart
import 'package:favorcate/core/model/meal_model.dart';
import 'package:favorcate/core/services/meal_request.dart';
import 'package:flutter/cupertino.dart';
class HYMealViewModel extends ChangeNotifier {
List<HYMealModel> _meals = [];
List<HYMealModel> get meals => _meals;
HYMealViewModel() {
HYMealRequest.getMealData().then((value) {
_meals = value;
notifyListeners();
});
}
}
接下来在main.dart,也就是一开始加载App处,添加懒加载
main() {
runApp(
ChangeNotifierProvider(
create: (ctx) => HYMealViewModel(),
child: MyApp(),
)
);
}
2.7.4 初步构建meal界面
meal.dart
import 'package:favorcate/core/model/category_model.dart';
import 'package:favorcate/ui/pages/meal/meal_content.dart';
import 'package:flutter/material.dart';
class HYMealScreen extends StatelessWidget {
static const String routeName = "/meal";
@override
Widget build(BuildContext context) {
//获取参数
final category = ModalRoute.of(context)?.settings.arguments as HYCategoryModel;
return Scaffold(
appBar: AppBar(
title: Text(category.title),
),
body: Center(
child: HYMealContent(),
),
);
}
}
meal_content.dart
import '../../../core/model/category_model.dart';
class HYMealContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
//全局路由查找种类
final category =
ModalRoute.of(context)?.settings.arguments as HYCategoryModel;
//Consumer获取
return Consumer<HYMealViewModel>(builder: (ctx, mealVM, child) {
//where表示过滤,如果包含当前种类的就保留
final meals = mealVM.meals
.where((meal) => meal.categories.contains(category.id))
.toList();
return ListView.builder(
itemCount: meals.length,
itemBuilder: (ctx, index) {
return ListTile(
title: Text(meals[index].title),
);
});
});
}
}
2.7.5 界面展示
目前可以获取到数据,并在各个不同的分类中
2.7.6 菜品界面布局
可以采用consumer的方式获取数据,也可以采用selector
meal_content.dart
import 'package:favorcate/core/model/meal_model.dart';
import 'package:favorcate/core/viewmodel/meal_view_model.dart';
import 'package:favorcate/ui/pages/meal/meal_item.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:collection/collection.dart';
import '../../../core/model/category_model.dart';
class HYMealContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
//全局路由查找种类
final category =
ModalRoute.of(context)?.settings.arguments as HYCategoryModel;
return Selector<HYMealViewModel, List<HYMealModel>>(
shouldRebuild: (prev, next) {
//列表不同需要重新执行build,相同相同需要重新build
return !const ListEquality().equals(prev, next);
},
selector: (ctx, mealVM) => mealVM.meals
.where((meal) => meal.categories.contains(category.id))
.toList(),
builder: (ctx, meals, child) {
return ListView.builder(
itemCount: meals.length,
itemBuilder: (ctx, index) {
return HYMealItem(meals[index]);
},
);
},
);
// //Consumer获取
// return Consumer<HYMealViewModel>(builder: (ctx, mealVM, child) {
// //where表示过滤,如果包含当前种类的就保留
// final meals = mealVM.meals
// .where((meal) => meal.categories.contains(category.id))
// .toList();
// return ListView.builder(
// itemCount: meals.length,
// itemBuilder: (ctx, index) {
// return ListTile(
// title: Text(meals[index].title),
// );
// });
// });
}
}
传入一个个的meal,每一个item的布局在meal_item.dart中构造
meal_item.dart
import 'package:favorcate/core/model/meal_model.dart';
import 'package:favorcate/ui/widgets/operation_item.dart';
import 'package:flutter/material.dart';
import 'package:favorcate/core/extension/int_extension.dart';
final cardRadius = 12.px;
class HYMealItem extends StatelessWidget {
final HYMealModel _meal;
HYMealItem(this._meal);
@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.all(10.px),
elevation: 4, //阴影
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.px)),
child: Column(children: [buildBasicInfo(context),buildOperationInfo()]),
);
}
Widget buildBasicInfo(BuildContext context) {
return Stack(
children: [
ClipRRect(
//只裁剪上边两个角
borderRadius: BorderRadius.only(
topLeft: Radius.circular(cardRadius),
topRight: Radius.circular(cardRadius),
),
child: Image.network(
_meal.imageUrl,
width: double.infinity,
height: 250.px,
fit: BoxFit.cover,
),
),
Positioned(
right: 10.px,
bottom: 10.px,
child: Container(
width: 300.px,
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(6.px),
),
padding: EdgeInsets.symmetric(
horizontal: 10.px,
vertical: 5.px,
),
child: Text(
_meal.title,
style: Theme.of(context)
.textTheme
.displayMedium
?.copyWith(color: Colors.white),
),
),
)
],
);
}
Widget buildOperationInfo() {
return Padding(
padding: EdgeInsets.all(16.px),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
HYOperationItem(Icon(Icons.schedule), "${_meal.duration}分钟"),
HYOperationItem(Icon(Icons.restaurant), _meal.complexityStr),
HYOperationItem(Icon(Icons.favorite), "未收藏")
],
),
);
}
}
难度字段
2.7.7 界面展示
2.8 菜品详情界面
2.8.1 路由配置
2.8.2 界面布局及数据如何获取
要构建如下界面,主体是Column,里面包含若干个Widget。
在detail.dart中通过全局查询来获取meal菜品的数据
final meal = ModalRoute.of(context)?.settings.arguments as HYMealModel;
detail.dart
import 'package:favorcate/core/model/meal_model.dart';
import 'package:favorcate/core/viewmodel/favor_view_model.dart';
import 'package:favorcate/ui/pages/detail/detail_floating_button.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'detail_content.dart';
class HYDetailScreen extends StatelessWidget {
static const String routeName = "/detail";
@override
Widget build(BuildContext context) {
final meal = ModalRoute.of(context)?.settings.arguments as HYMealModel;
return Scaffold(
appBar: AppBar(
title: Text(meal.title),
),
body: HYDetailContent(meal),
floatingActionButton: HYDetailFloatingButton(meal),
);
}
}
detail_content.dart
import 'package:favorcate/core/model/meal_model.dart';
import 'package:flutter/material.dart';
import 'package:favorcate/core/extension/int_extension.dart';
class HYDetailContent extends StatelessWidget {
final HYMealModel _meal;
HYDetailContent(this._meal);
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
children: [
buildBannerImage(),
buildMakeTitle(context, "制作材料"),
buildMakeMaterial(context),
buildMakeTitle(context, "制作步骤"),
buildMakeSteps(context),
],
),
);
}
//1、横幅图片
Widget buildBannerImage() {
return Container(
width: double.infinity, //这里属性必须设置,如果图片未加载,那么Column对齐
margin: EdgeInsets.all(5.px),
child: ClipRRect(
borderRadius: BorderRadius.circular(12.px),
child: Image.network(_meal.imageUrl),
),
);
}
Widget buildMakeMaterial(BuildContext context) {
return buildMakeContent(
context: context,
child: ListView.builder(
/**
* 1、shrinkWrap:
* true->内容多大就占据多大的高度,false->尽可能占据高度
* 2、column嵌套LListView,Column需要ListView给出一个指定高度,
* 这里不是局部滚动,所以不设置Height,而设置shrinkWrap
*/
shrinkWrap: true,
//禁止滚动
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
itemCount: _meal.ingredients.length,
itemBuilder: (ctx, index) {
return Card(
color: Theme.of(context).colorScheme.secondary,
child: Padding(
padding: EdgeInsets.symmetric(vertical: 5.px, horizontal: 10.px),
child: Text(_meal.ingredients[index]),
),
);
},
),
);
}
Widget buildMakeSteps(BuildContext context) {
return buildMakeContent(
context: context,
child: ListView.separated(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (ctx, index) {
return ListTile(
leading: CircleAvatar(
child: Text("#${index + 1}"),
),
title: Text(_meal.steps[index]),
);
},
separatorBuilder: (cttx, index) {
return Divider();
},
itemCount: _meal.steps.length));
}
Widget buildMakeTitle(BuildContext context, String title) {
return Container(
padding: EdgeInsets.symmetric(vertical: 10.px),
child: Text(
title,
style: Theme.of(context)
.textTheme
.displayLarge
?.copyWith(fontWeight: FontWeight.bold, color: Colors.black),
),
);
}
Widget buildMakeContent(
{required BuildContext context, required Widget child}) {
return Container(
padding: EdgeInsets.all(8.px),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5.px),
border: Border.all(color: Colors.grey), //边框
color: Colors.white,
),
width: MediaQuery.of(context).size.width - 30.px, //媒体查询,距离左右15px
child: child);
}
}
2.8.3 收藏功能
首先创建共享数据favor,即用户收藏的菜品
favor_view_model.dart
import 'package:flutter/material.dart';
import 'package:favorcate/core/model/meal_model.dart';
class HYFavorViewModel extends ChangeNotifier {
List<HYMealModel> _favorMeals = [];
void addMeal(HYMealModel meal) {
_favorMeals.add(meal);
notifyListeners();
}
void removeMeal(HYMealModel meal) {
_favorMeals.remove(meal);
notifyListeners();
}
List<HYMealModel> get favorMeals => _favorMeals;
//判断是否收藏
bool isFavor(HYMealModel meal) {
return _favorMeals.contains(meal);
}
void handleMeal(HYMealModel meal) {
if(isFavor(meal)) {
removeMeal(meal);
} else {
addMeal(meal);
}
}
}
还需要在main.dart去添加Provider
main() {
runApp(MultiProvider(
providers: [
ChangeNotifierProvider(
create: (ctx) => HYMealViewModel(),
),
ChangeNotifierProvider(
create: (ctx) => HYFavorViewModel(),
),
],
child: MyApp(),
));
}
收藏按钮点击效果在detail_floating_button.dart中
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../../core/model/meal_model.dart';
import '../../../core/viewmodel/favor_view_model.dart';
class HYDetailFloatingButton extends StatelessWidget {
final HYMealModel meal;
HYDetailFloatingButton(this.meal);
@override
Widget build(BuildContext context) {
return Consumer<HYFavorViewModel>(
builder: (ctx, favorVM, child) {
final iconData =
favorVM.isFavor(meal) ? Icons.favorite : Icons.favorite_border;
final iconColor = favorVM.isFavor(meal) ? Colors.red : Colors.black;
return FloatingActionButton(
child: Icon(
iconData,
color: iconColor,
),
onPressed: () {
favorVM.handleMeal(meal);
},
);
},
);
}
}
以及meal界面的点击收藏按钮
Widget buildFavorItem() {
return Consumer<HYFavorViewModel>(
builder: (ctx, favorVM, child) {
final iconData =
favorVM.isFavor(_meal) ? Icons.favorite : Icons.favorite_border;
final favorColor = favorVM.isFavor(_meal) ? Colors.red : Colors.black;
final title = favorVM.isFavor(_meal) ? "收藏" : "未收藏";
return GestureDetector(
child: HYOperationItem(
Icon(
iconData,
color: favorColor,
),
title,
textColor: favorColor,
),
onTap:() {
favorVM.handleMeal(_meal);
},
);
},
);
}
对于这里的布局,封装成一个Widget
operation_item.dart
import 'package:flutter/material.dart';
import 'package:favorcate/core/extension/int_extension.dart';
class HYOperationItem extends StatelessWidget {
final Widget _icon;
final String _title;
final Color textColor;
//可选参数不能以下划线开头
HYOperationItem(this._icon, this._title, {this.textColor = Colors.black});
@override
Widget build(BuildContext context) {
return Container(
width: 80.px,
padding: EdgeInsets.symmetric(vertical: 12.px),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_icon,
SizedBox(
width: 3.px,
),
Text(
_title,
style: TextStyle(color: textColor),
),
],
),
);
}
}
2.9 收藏界面
favor.dart
import 'package:favorcate/ui/pages/favor/favor_content.dart';
import 'package:flutter/material.dart';
class HYFavorScreen extends StatelessWidget {
static const String routeName = "/favor";
const HYFavorScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("我的收藏"),
),
body: HYFavorContent()
);
}
}
favor_content.dart
import 'package:favorcate/core/viewmodel/favor_view_model.dart';
import 'package:favorcate/ui/pages/meal/meal_item.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class HYFavorContent extends StatelessWidget {
const HYFavorContent({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Consumer<HYFavorViewModel>(builder: (ctx, favorVM, child) {
return ListView.builder(
itemBuilder: (itemCtx, index) {
if(favorVM.favorMeals.length == 0) {
return Center(
child: Text("未收藏美食"),
);
}
return HYMealItem(favorVM.favorMeals[index]);
},
itemCount: favorVM.favorMeals.length,
);
});
}
}
2.9.1 侧边栏
侧边栏包括点餐按钮和过滤按钮,在main.dart中添加drawer
home_drawer.dart
import 'package:favorcate/ui/pages/filter/filter.dart';
import 'package:flutter/material.dart';
import 'package:favorcate/core/extension/int_extension.dart';
class HYHomeDrawer extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
child: Drawer(
child: Column(
children: [
buildHeaderView(context),
buildListTile(
context,
Icon(Icons.restaurant),
"进餐",
() {
Navigator.of(context).pop();
},
),
buildListTile(context, Icon(Icons.settings), "过滤", () {
Navigator.of(context).pushNamed(HYFilterScreen.routeName);
}),
],
),
),
width: 250.px,
);
}
Widget buildHeaderView(BuildContext context) {
return Container(
margin: EdgeInsets.only(bottom: 20.px),
color: Colors.orange,
alignment: const Alignment(0, 0.5),
width: double.infinity,
height: 100.px,
child: Text(
"开始动手",
style: Theme.of(context)
.textTheme
.displayLarge
?.copyWith(fontWeight: FontWeight.bold, color: Colors.black),
),
);
}
Widget buildListTile(
BuildContext context, Widget icon, String title, Function handler) {
return ListTile(
leading: icon,
title: Text(
title,
style: Theme.of(context).textTheme.displaySmall,
),
onTap: handler as Function(), //这里指明类型Function
);
}
}
appBar的leading属性可以设置点击事件,弹出侧边栏
home_app_bar.dart
import 'package:flutter/material.dart';
class HYHomeAppBar extends AppBar {
HYHomeAppBar(BuildContext context)
: super(
title: const Text("美食广场"),
leading: IconButton(
icon: const Icon(Icons.build),
onPressed: () {
Scaffold.of(context).openDrawer();
},
),
);
}
在route.dart中注册路由
//过滤界面
static final RouteFactory generateRoute = (setting) {
if (setting.name == HYFilterScreen.routeName) {
return MaterialPageRoute(
builder: (ctx) {
return HYFilterScreen();
},
fullscreenDialog: true);
}
return null;
};
2.9.2 过滤界面布局
import 'package:favorcate/core/viewmodel/filter_view_model.dart';
import 'package:flutter/material.dart';
import 'package:favorcate/core/extension/int_extension.dart';
import 'package:provider/provider.dart';
class HYFilterContent extends StatelessWidget {
const HYFilterContent({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
children: [buildChoiceText(context), buildChoiceSelect(context)],
);
}
Widget buildChoiceText(BuildContext context) {
return Container(
padding: EdgeInsets.all(20.px),
alignment: Alignment.center,
child: Text(
"展示你的选择",
style: Theme.of(context)
.textTheme
.displaySmall
?.copyWith(fontWeight: FontWeight.bold),
),
);
}
Widget buildChoiceSelect(BuildContext context) {
return Expanded(
child: Consumer<HYFilterViewModel>(
builder: (ctx, filterVM, child) {
return ListView(
children: [
buildListTile(
"无谷蛋白",
"展示无谷蛋白食物",
filterVM.isGlutenFree,
(value) {
filterVM.isGlutenFree = value;
},
),
buildListTile(
"不含乳糖",
"展示不含乳糖食物",
filterVM.isLactoseFree,
(value) {
filterVM.isLactoseFree = value;
},
),
buildListTile(
"普通素食者",
"展示普通素食者食物",
filterVM.isVegetarian,
(value) {
filterVM.isVegetarian = value;
},
),
buildListTile(
"严格素食者",
"展示严格素食者食物",
filterVM.isVegan,
(value) {
filterVM.isVegan = value;
},
),
],
);
},
),
);
}
Widget buildListTile(
String title, String subtitle, bool value, Function(bool) onChange) {
return ListTile(
title: Text(title),
subtitle: Text(subtitle),
trailing: Switch(
value: value,
onChanged: onChange, //指明类型
),
);
}
}
2.9.3 共享数据(过滤条件共享、收藏菜品、所有菜品)
这两个共享的数据有类似的代码段,可以考虑创建一个父类,并让这两个子类继承它
在总的main.dart中配置Provider
main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(
create: (ctx) => HYFilterViewModel(),
),
//HYMealViewModel 依赖 HYFilterViewModel
ChangeNotifierProxyProvider<HYFilterViewModel, HYMealViewModel>(
create: (ctx) => HYMealViewModel(),
update: (ctx, filterVM, mealVM) {
mealVM?.updateFilters(filterVM);
return mealVM as HYMealViewModel;
},
),
ChangeNotifierProxyProvider<HYFilterViewModel, HYFavorViewModel>(
create: (ctx) => HYFavorViewModel(),
update: (ctx, filterVM, favorVM) {
favorVM?.updateFilters(filterVM);
return favorVM as HYFavorViewModel;
},
),
],
child: MyApp(),
),
);
}
BaseMealViewModel.dart
import 'package:flutter/cupertino.dart';
import '../model/meal_model.dart';
import 'filter_view_model.dart';
class BaseMealViewModel extends ChangeNotifier {
List<HYMealModel> _meals = [];
HYFilterViewModel _filterVM = HYFilterViewModel();
void updateFilters(HYFilterViewModel filterVM) {
_filterVM = filterVM;
notifyListeners(); //这里加上更新界面
}
//筛选符合条件的菜品
List<HYMealModel> get meals {
return _meals.where((meal) {
if (_filterVM.isGlutenFree && !meal.isGlutenFree) return false;
if (_filterVM.isLactoseFree && !meal.isLactoseFree) return false;
if (_filterVM.isVegetarian && !meal.isVegetarian) return false;
if (_filterVM.isVegan && !meal.isVegan) return false;
return true;
}).toList();
}
List<HYMealModel> get originalMeals {
return _meals;
}
set meals(List<HYMealModel> value) {
_meals = value;
notifyListeners();
}
}
2.9.4 界面展示
3、项目三:i18n_demo(国际化开发)
如果APP是面向国际用户,那么需要设置哪些呢?先创建新项目,用AndroidStudio打开
3.1.1 添加依赖库
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
3.1.2 添加支持的语言和delegate
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
//支持的语言
supportedLocales: [
Locale("zh"),
Locale("en"),
],
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
HYLocalizationsDelegate.delegate
],
home: HYHomePage());
}
}
localizations_delegate.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:i18n_demo/i18n/localizations.dart';
class HYLocalizationsDelegate extends LocalizationsDelegate<HYLocalizations> {
static HYLocalizationsDelegate delegate = HYLocalizationsDelegate();
//判断当前语言是否为英文或者中文
@override
bool isSupported(Locale locale) {
return ["zh", "en"].contains(locale.languageCode);
}
@override
Future<HYLocalizations> load(Locale locale) {
return SynchronousFuture(HYLocalizations(locale));
}
//当数据发生改变时,是否需要重新build
@override
bool shouldReload(covariant LocalizationsDelegate<HYLocalizations> old) {
return false;
}
}
构建本地对应的文本Map,如中文title字段就表示“首页local”
localizations.dart
import 'package:flutter/cupertino.dart';
class HYLocalizations {
final Locale locale;
HYLocalizations(this.locale);
static HYLocalizations of(BuildContext context) {
return Localizations.of(context, HYLocalizations);
}
static Map<String, Map<String, String>> _localizaValues = {
"en": {
"title": "Home",
"hello": "Hello~",
"pickTime": "Pick a Time~"
},
"zh": {
"title": "首页local",
"hello": "您好local",
"pickTime": "选择一个时间local"
}
};
String? get title {
return _localizaValues[locale.languageCode]!["title"];
}
String? get hello {
return _localizaValues[locale.languageCode]!["hello"];
}
String? get pickTime {
return _localizaValues[locale.languageCode]!["pickTime"];
}
}
测试
class HYHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// //不能在MyApp里面的build使用,可以拿到语言
// HYLocalizations(Localizations.localeOf(context));
return Scaffold(
appBar: AppBar(
title: Text(HYLocalizations.of(context).title as String),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(HYLocalizations.of(context).hello as String),
RaisedButton(
child: Text(HYLocalizations.of(context).pickTime as String),
onPressed: () {
showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime(3000));
})
],
),
),
);
}
}
3.1.3 运行截图
当改变语言时,里面控件的文本对应发生了改变
3.1.4 异步加载json数据
新建一个assets文件夹,存储json数据
注释掉之前的json数据,将json数据存储在json文件中,修改localization.dart
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
class HYLocalizations {
final Locale locale;
HYLocalizations(this.locale);
static HYLocalizations of(BuildContext context) {
return Localizations.of(context, HYLocalizations);
}
static Map<String, Map<String, String>> _localizaValues = {
// "en": {
// "title": "Home",
// "hello": "Hello~",
// "pickTime": "Pick a Time~"
// },
// "zh": {
// "title": "首页local",
// "hello": "您好local",
// "pickTime": "选择一个时间local"
// }
};
Future loadJson() async{
//加载json文件
final jsonString = await rootBundle.loadString("assets/json/i18n.json");
//对json进行解析
Map<String, dynamic> map = json.decode(jsonString);
_localizaValues = map.map((key, value) {
return MapEntry(key, value.cast<String, String>());
});
}
String? get title {
return _localizaValues[locale.languageCode]!["title"];
}
String? get hello {
return _localizaValues[locale.languageCode]!["hello"];
}
String? get pickTime {
return _localizaValues[locale.languageCode]!["pickTime"];
}
}
重写localizations_delegate.dart中的load方法
@override
Future<HYLocalizations> load(Locale locale) async{
final localizations = HYLocalizations(locale);
await localizations.loadJson();
return localizations;
}
也能实现上面的效果
3.1.5 工具 flutter Intl
初始化文件
在生成的intl_en.arb文件中添加json数据
接着在main.dart中修改代码
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
//修改此处
supportedLocales: S.delegate.supportedLocales,
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
HYLocalizationsDelegate.delegate,
S.delegate //添加delegate
],
home: HYHomePage());
}
}
使用插件
添加中文的local
添加对应的json数据
当语言为中文时,显示的是中文文本,英文时,则显示英文的文本
4、案例:flutter测试
4.1 单元测试
import 'package:flutter_test/flutter_test.dart';
import 'package:test_demo/unit/math_utils.dart';
void main() {
group("test math util fime", () {
test("math utils file sum test", () {
final result = sum(20,30);
expect(result, 50);
});
test("math utils file mul test", () {
final result = mul(20, 30);
expect(result, 600);
});
});
}
4.2 widget 测试
import 'package:flutter_test/flutter_test.dart';
import 'package:test_demo/widget/contacts.dart';
void main() {
testWidgets("Test Contacts Widget", (WidgetTester tester) async {
/**
* 注:scaffold需要包裹MaterialApp,在MaterialApp中初始化
*/
//注入需要测试的Widget
await tester.pumpWidget(
MaterialApp(home: HYContacts(["abc", "cba", "nma", "cab"])));
//在HYContacts中查找Widget/Text
final abcText = find.text("abc");
final cbaText = find.text("cba");
final icons = find.byIcon(Icons.people);
//断言
expect(abcText, findsOneWidget);
expect(cbaText, findsOneWidget);
expect(icons, findsNWidgets(4));
});
}
5、混合开发
5.1 概念
1、Flutter想要调用一些原生的能力
- 相册、相机、位置信息、Map
- pub.dev第三方插件
- 获取原生的能力、但没有对应的插件(Dart调用原生的代码)
2、原有的APP产品(IOS、Android)
- 原有的APP产品(IOS、Android)
- 新功能(模块)-》Flutter开发
- 原有模块(重构)-》Flutter开发
5.2 访问相册
添加依赖
image_picker: ^0.8.5+3
测试代码,当点击选择图片时,从相册选择一张图片显示
import 'dart:io';
import 'package:image_picker/image_picker.dart';
import 'package:flutter/material.dart';
main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue, splashColor: Colors.transparent
),
home: HYHomePage()
);
}
}
class HYHomePage extends StatefulWidget {
@override
State<HYHomePage> createState() => _HYHomePageState();
}
class _HYHomePageState extends State<HYHomePage> {
File? _imageFile;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(""),
),
body: Center(
child: Column(
children: [
RaisedButton(
onPressed: _pickImage,
child: Text("选择照片"),
),
_imageFile == null ? Image.asset("assets/image/test.jpg") : Image.file(_imageFile!),
],
),
),
);
}
void _pickImage() async{
final ImagePicker _picker = ImagePicker();
final XFile? file = (await _picker.pickImage(source: ImageSource.gallery));
setState(() {
_imageFile = File(file?.path as String);
});
}
}