赞
踩
更新日期 | 更新内容 |
---|---|
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测试】 |
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; } }
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), ), ); }), ); } }
点击进入项目地址github
界面截图(在chrom上运行项目)
说明: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开始加载。
从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
flutter create XXXXX
Icon
appId
启动图
Android
App名称
ios
主目录包括UI和Core,子目录
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, ), ), ); }
包括通常模式下的主题和暗黑模式下的主题,暗黑模式之后设置
之前学习笔记有提到屏幕适配,这里就加上工具
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"
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, ); } }
设置底部导航栏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), ), ];
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( "美食广场", ), ), ); } }
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("我的收藏"), ), ); } }
运行看一下效果
主页需要用到一些数据
category.json
点击获取json数据
meal.json
点击获取json数据
解析json数据,需要用到rootBundle,json数据解析成一个一个的对象HYCategoryModel,存储在List中。
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; } }
利用工具生成模型代码
颜色有些特殊,因为传入的是字符串,这里做了转换
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; } }
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 ), ), ); }, ); } }
这里用浏览器展示
将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 ), ), ); } }
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;
}
请求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; } }
工具
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; } }
共享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(),
)
);
}
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), ); }); }); } }
目前可以获取到数据,并在各个不同的分类中
可以采用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), "未收藏") ], ), ); } }
难度字段
要构建如下界面,主体是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); } }
首先创建共享数据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), ), ], ), ); } }
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, ); }); } }
侧边栏包括点餐按钮和过滤按钮,在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;
};
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, //指明类型 ), ); } }
这两个共享的数据有类似的代码段,可以考虑创建一个父类,并让这两个子类继承它
在总的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(); } }
如果APP是面向国际用户,那么需要设置哪些呢?先创建新项目,用AndroidStudio打开
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
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)); }) ], ), ), ); } }
当改变语言时,里面控件的文本对应发生了改变
新建一个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;
}
也能实现上面的效果
在生成的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()); } }
添加对应的json数据
当语言为中文时,显示的是中文文本,英文时,则显示英文的文本
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);
});
});
}
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)); }); }
1、Flutter想要调用一些原生的能力
2、原有的APP产品(IOS、Android)
添加依赖
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); }); } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。