如果你需要项目资源,欢迎来到我的个人博客:https://brath.top/2023/02/24/%E3%80%90Flutter&SpringBoot%E3%80%91%E5%A6%82%E4%BD%95%E7%94%A8Flutter%E5%92%8CJava%E6%9D%A5%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA%E6%8A%BD%E5%A5%96APP/
APP体验地址:http://interviewnote.online:8080/weat.apk
前言:
Weat 中文译为:吃啥
吃啥来自于女朋友的一个问题,问我可不可以做个抽奖的APP,奖品都是菜,抽中那个今天就做那个菜吃,我灵机一动,使用InterviewCoder公众号里面的chatGPT小程序编辑了下面的文案:
吃啥是一款新奇有趣的应用,旨在帮助你找到今天吃什么菜。它通过精心的菜谱抽奖模式,为你提供多样而有色彩的食物,让你的用餐经历变得更加美好。不仅如此,它还可以添加朋友和家人的偏好,为大家带来更多惊喜。加入吃啥,让你的用餐经历变得更加完美!
架构方面:
移动端:Flutter
后台服务:SpringBoot、Mysql
没有安装dart、flutter、Android Studio、vscode的同学可以看看以前的教程,这里就不 一 一 介绍了
本文将详细介绍weatapp的开发流程,前后端代码编写阶段,以及后台代码部署。
一、APP创建:
打开项目路径,输入cmd进入到命令行,输入 flutter create weat 进行flutter项目创建。
打开Android Studio整理项目,修改仓库配置
maven { url'https://maven.aliyun.com/repository/google' }
maven { url'https://maven.aliyun.com/repository/jcenter' }
maven { url'http://maven.aliyun.com/nexus/content/groups/public' }
3.点击Open for Editing in Android Studio 进入安卓视图,拉取gradle库
4.当Android主文件不暴红,说明配置完毕了,gradle库已经拉下来了
二、APP启动:
1.打开一个安卓模拟器,有条件的同学可以直接使用真机,老师这里为了方便就直接用模拟器了
2.使用vscode打开项目并启动flutter项目
至此,一个flutter项目,创建完成,并启动了!
三、APP图标配置
需要打开两个网址:
https://logo.aliyun.com/logo#/name 阿里云LOGO服务
https://icon.wuruihong.com/ 图标工厂
1.打开阿里云LOGO 输入APP名字,吃啥
2.生成图标:
购买一个你喜欢的图标,如果不购买的话,自己在网上找一个也行,但是一定要有商业授权!
得到图标后,进入图标工厂,生成一套ios和一套Android的图标文件:
进入安卓文件夹,将该目录的文件复制到你的项目下面 \android\app\src\main\res
成效:
至此,APP图标设置完成!
四、APP后台开发(SpringBoot+Mysql)
为什么先开发APP部分?,这个纯属个人习惯,我比较喜欢先有数据和接口,直接开发APP的感觉会更轻松!
整理需求:
每次进入app随机获取食物列表
点击开始抽奖 随机跳转 点击停止 按钮回显食物名称 点击查看菜谱 弹窗提示做法
根据如上需求,我明确了客户到底想要什么
1.这是一个服务类型的APP,服务于不知道吃什么的客户
2.每次进入抽奖页面需要获取不同的菜谱奖品列表
3.这个页面需要有个按钮,可以来重置奖品,并且要限制次数
4.点击抽奖按钮,开始抽奖,再次点击,或者超过限制时间,停止选择,并将按钮置为:查看食谱
5.点击查看食谱,进入食谱详情,展示过程
根据我的聪明思考,画出了如下UI:
开玩笑,画的很烂,但是就是这个意思!。
那么,开始开发后台数据,为我们的APP提供一个接口~!
经过一系列操作,得到以下数据:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS =0;
-- ----------------------------
-- Table structure for iv_dishes
-- ----------------------------
DROPTABLEIFEXISTS`iv_dishes`;
CREATETABLE`iv_dishes` (`id`int(50)NOTNULLAUTO_INCREMENTCOMMENT'唯一ID',`dishes_name`varchar(255)CHARACTERSET utf8 COLLATE utf8_general_ci NULLDEFAULTNULLCOMMENT'菜品名字',`dishes_step`varchar(255)CHARACTERSET utf8 COLLATE utf8_general_ci NULLDEFAULTNULLCOMMENT'菜品做法',`dishes_url`varchar(255)CHARACTERSET utf8 COLLATE utf8_general_ci NULLDEFAULTNULLCOMMENT'菜品图片地址',PRIMARYKEY(`id`)USINGBTREE
)ENGINE=InnoDBAUTO_INCREMENT=9CHARACTERSET= utf8 COLLATE= utf8_general_ci COMMENT='菜品表'ROW_FORMAT= Compact;
-- ----------------------------
-- Records of iv_dishes
-- ----------------------------
INSERTINTO`iv_dishes`VALUES(1,'凉拌黄瓜虾仁','1.小米辣 白芝麻蒜未辣椒面 淋上少许热油\r\n2.生抽2勺 油 醋各1代糖半勺搅匀备用\r\n3.虾煮熟去壳 木耳焯水捞出 黄瓜拍块\r\n4.淋上酱汁拌匀即可','https://brath.oss-cn-shanghai.aliyuncs.com/pigo/image-20230224081935481.png');
INSERTINTO`iv_dishes`VALUES(2,'低卡酱油鸡蛋','1.生抽4勺 醋2香油1勺一把葱花\r\n2.鸡蛋冷水下锅煮8分钟盖盖焖1分钟后泡冷水\r\n3.温开水4勺搅匀\r\n4.鸡蛋剥壳切对半淋上酱汁拌匀即可','https://brath.oss-cn-shanghai.aliyuncs.com/pigo/image-20230224082035768.png');
INSERTINTO`iv_dishes`VALUES(3,'低卡葱香鸡腿','1.蒜末 葱花 白艺麻小米辣淋上少许热油\r\n2.生抽1勺 许盐搅匀\r\n3.鸡腿熟捞出微凉后撕成小块\r\n4.淋上酱汁拌匀即可','https://brath.oss-cn-shanghai.aliyuncs.com/pigo/image-20230224082101091.png');
INSERTINTO`iv_dishes`VALUES(4,'低卡平菇炒蛋','1.鸡蛋炒熟盛出\r\n2.油热下从蒜炒香\r\n3.下平菇炒出汁,倒入炒好的鸡蛋\r\n4.生抽 油 辣椒粉各1勺 少许盐炒匀即可','https://brath.oss-cn-shanghai.aliyuncs.com/pigo/image-20230224082137439.png');
INSERTINTO`iv_dishes`VALUES(5,'凉拌虾仁西兰花','1.蒜未 葱花 辣椒面白芝麻淋上少许热油\r\n2.生抽2勺 醋各1勺搅拌均匀\r\n3.西兰花焯水捞出,虾焯水去壳鸡蛋白切块\r\n4.淋上酱汁拌匀即可','https://brath.oss-cn-shanghai.aliyuncs.com/pigo/image-20230224082204322.png');
INSERTINTO`iv_dishes`VALUES(6,'凉拌黄瓜木耳鸡蛋','1.木耳鸡蛋各煮熟捞出 黄瓜拍块\r\n2.生抽2勺醋香油各11勺温开水拌匀即可开吃','https://brath.oss-cn-shanghai.aliyuncs.com/pigo/image-20230224082230282.png');
INSERTINTO`iv_dishes`VALUES(7,'凉拌黄瓜豆腐','1.白芝麻辣椒面小米辣,蒜未 葱花 淋少许热油\r\n2.生抽2勺 醋油各1勺 少许盐代糖拌匀\r\n3.黄瓜拍块去籽豆腐煮熟捞出\r\n4.撒上香葱淋上酱汁拌匀即可','https://brath.oss-cn-shanghai.aliyuncs.com/pigo/image-20230224082404878.png');
INSERTINTO`iv_dishes`VALUES(8,'木耳炒鸡蛋','1.鸡蛋炒熟盛出\r\n2.蒜未和胡萝人炒软倒木耳炒熟再倒鸡蛋\r\n3.耗油生抽各1勺少许盐-小半碗淀粉水\r\n4.炒匀下葱段即可','https://brath.oss-cn-shanghai.aliyuncs.com/pigo/image-20230224082512562.png');
SET FOREIGN_KEY_CHECKS =1;
接下来创建一个SpringBoot项目,并写出entity、 controller、service、impl、mapper、xml等需要的代码
entity:
/*** <p>* 菜品表* </p>** @author Brath* @since 2023-02-23*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("iv_dishes")
@ApiModel(value="IvDishes对象", description="菜品表")
public class IvDishes implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "唯一ID")@TableId(value = "id", type = IdType.AUTO)private Integer id;
@ApiModelProperty(value = "菜品名字")private String dishesName;
@ApiModelProperty(value = "菜品做法")private String dishesStep;
@ApiModelProperty(value = "菜品图片地址")private String dishesUrl;
}
controller:
/**
* <p>
* 菜品表 前端控制器
* </p>
*
* @author Brath
* @since 2023-02-23
*/
@RestController
@RequestMapping("/dishes")
publicclassIvDishesController {
/**** SLF4J日志*/privateLoggerlogger=LoggerFactory.getLogger(IvDishesController.class);
/*** 菜品服务接口*/@AutowiredprivateIvDishesServicedishesService;
/**** 获取菜品列表** @param page* @param size* @return*/@GetMapping("/getDishes")publicObjectgetDishes(@RequestParam(value="page", defaultValue="1") Integerpage, @RequestParam(value="size", defaultValue="8") Integersize) {logger.info("【用户服务】获取菜品列表,开始");Map<Object, Object>result=newHashMap<>();IPage<IvDishes>prizeRecords=dishesService.getDishes(page, size);if (CollectionUtils.isEmpty(prizeRecords.getRecords())) {result.put("fail", ResponseCode.DATA_DOES_NOT_EXIST);logger.error("【用户服务】获取菜品列表,服务错误:{}", ResponseCode.DATA_DOES_NOT_EXIST);}result.put("prizeRecords", prizeRecords.getRecords());logger.info("【用户服务】获取菜品列表,完毕");returnResponseUtil.ok(result);}
}
service:
/**
* <p>
* 菜品表 服务类
* </p>
*
* @author Brath
* @since 2023-02-23
*/
publicinterfaceIvDishesServiceextendsIService<IvDishes> {
/*** 获取菜品列表** @param page* @param size* @return*/IPage<IvDishes>getDishes(Integerpage, Integersize);
}
impl:
/**
* <p>
* 菜品表 服务实现类
* </p>
*
* @author Brath
* @since 2023-02-23
*/
@Service
publicclassIvDishesServiceImplextendsServiceImpl<IvDishesMapper, IvDishes>implementsIvDishesService {
/*** 获取菜品列表** @param page* @param size* @return*/@OverridepublicIPage<IvDishes>getDishes(Integerpage, Integersize) {returnbaseMapper.getDishes(newPage<>(page, size), newQueryWrapper<>());}
}
mapper:
/**
* <p>
* 菜品表 Mapper 接口
* </p>
*
* @author Brath
* @since 2023-02-23
*/
@Mapper
publicinterfaceIvDishesMapperextendsBaseMapper<IvDishes> {
/*** 获取菜品列表** @param objectPage* @param objectQueryWrapper* @return*/IPage<IvDishes>getDishes(Page<Object>objectPage, QueryWrapper<Object>objectQueryWrapper);
}
xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.brath.mapper.IvDishesMapper"><!-- 通用查询映射结果 --><resultMap id="DishesMap" type="cn.brath.entity.IvDishes"><id column="id" property="id"/><result column="dishes_name" property="dishesName"/><result column="dishes_step" property="dishesStep"/><result column="dishes_url" property="dishesUrl"/></resultMap><select id="getDishes" resultType="cn.brath.entity.IvDishes">select d.*from iv_dishes dORDER BY RAND()</select></mapper>
application.yaml配置:
server:port: 9999
spring:datasource:druid:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://127.0.0.1:3306/iv-user-services?createDatabaseIfNotExist=true&useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=trueusername: 'root'password: 'root'initial-size: 10max-active: 100min-idle: 10max-wait: 6000pool-prepared-statements: truemax-pool-prepared-statement-per-connection-size: 20time-between-eviction-runs-millis: 60000min-evictable-idle-time-millis: 300000#Oracle需要打开注释# validation-query: SELECT 1 FROM DUALtest-while-idle: truetest-on-borrow: falsetest-on-return: falsestat-view-servlet:enabled: trueurl-pattern: /druid/*#login-username: admin#login-password: admin#达梦数据库,需要注释掉,其他数据库可以打开filter:stat:log-slow-sql: trueslow-sql-millis: 1000merge-sql: falsewall:config:multi-statement-allow: true
经过如上一系列配置,我们项目启动成功并可以访问到接口:
接下来只要把程序部署上线就搞定了!
部署后台程序:
将我们的Dockerfile、运行脚本、jar包传入服务器
Dockerfile:
#java8环境
FROM openkbs/jdk11-mvn-py3#root用户
USER root#设置时区
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone#Auth
MAINTAINER Brath#设置工作目录集
WORKDIR /root/weatWork#复制jars和命令
ADD *.jar /root/weatWork/
ADD run.sh /root/weatWork/run.sh#脚本权限设置
RUN chmod +x /root/weatWork/run.sh#暴露端口
EXPOSE 9999
1.Dockerfile打包镜像
1.进入工作目录
2.docker build -t weat .
2.运行容器
docker run -dit -p 9999:9999 --privileged=true -P --name weat weat /bin/bash -c "tail -f /dev/null" -g "daemon off;"
3.启动Jar包
#进入容器
docker exec -it weat bash
#运行脚本
sh run.sh
#查看日志
tail -100f weatlog.log
4.联调接口
五、APP移动端开发(Flutter)
weatApp架构设计大概如下:
lib:
common 通用层
core 核心层
routers 路由曾
utils 工具层
viewmodel 视图模型层
views 视图层
需要安装的依赖:
#网络请求dio: ^4.0.6#getx get: ^4.6.5#透明弹出框fluttertoast: ^8.0.8#屏幕适配flutter_screenutil: ^5.5.3+2#全局状态管理provider: ^6.0.1#轮播图flutter_swiper_plus: ^2.0.4# GET WIDGET UI库getwidget: ^2.0.5#图片缓存cached_network_image: ^3.2.0#图片放大缩小photo_view: ^0.13.0#权限申请permission_handler: ^9.2.0#贝壳组件库bruno: ^2.2.0#easyloadingflutter_easyloading: ^3.0.3
代码如下:
common.dart
import 'dart:io';
import 'dart:math';import 'package:bruno/bruno.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:getwidget/getwidget.dart';
import 'package:provider/provider.dart';
import 'package:weat/main.dart';/** Common组件库* @Auth: Brath*//** 获取title的AppBar*/
AppBar getAppBar(String title, {required context}) {return AppBar(toolbarHeight: 40.0.h,centerTitle: true,shadowColor: Color.fromARGB(255, 59, 82, 76),backgroundColor: Colors.transparent,leading: null,leadingWidth: 30.w,elevation: 0.0,actions: [],title: (Text(title,style: TextStyle(fontSize: 16.sp,color: Colors.black,fontFamily: '',fontWeight: FontWeight.bold),)));
}/** 获取卡片圆形背景容器*/
class getCardContainer extends StatelessWidget {getCardContainer({Key? key,this.isBorder,this.BorderCircular,this.width,this.height,this.widget,this.margin,}) : super(key: key);bool? isBorder;double? BorderCircular;double? height;double? width;Widget? widget;EdgeInsetsGeometry? margin;@overrideWidget build(BuildContext context) {return Container(margin: margin,decoration: isBorder == true? BoxDecoration(color: Colors.white,shape: BoxShape.rectangle,boxShadow: [BoxShadow(color: Colors.grey[200]!,offset: Offset(1.w, 1.h),blurRadius: 1.r,spreadRadius: 1.r,),],border: Border.all(color: Colors.white, width: 1.w), // borderborderRadius:BorderRadius.circular((BorderCircular ?? 15.r)), // 圆角): null,width: width,height: height,child: widget,);}
}
ZoomImage.dart
import 'dart:ui';import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';class ZoomImage extends StatefulWidget {ZoomImage({Key? key, this.url}) : super(key: key);String? url;@overrideState<StatefulWidget> createState() {return _ZoomImage();}
}class _ZoomImage extends State<ZoomImage> with SingleTickerProviderStateMixin {AnimationController? _controller;Animation<Offset>? _animation;Offset _offset = Offset.zero;double _scale = 1.0;Offset? _normalizedOffset;double? _previousScale;final double _kMinFlingVelocity = 600.0;bool _isEnlarge = false;bool _isHideTitleBar = false;@overridevoid initState() {super.initState();_controller = AnimationController(vsync: this);_controller?.addListener(() {setState(() {_offset = _animation!.value;});});}@overridevoid dispose() {super.dispose();_controller!.dispose();}Offset _clampOffset(Offset offset) {final Size size = context.size!;// widget的屏幕宽度final Offset minOffset = Offset(size.width, size.height) * (1.0 - _scale);// 限制他的最小尺寸return Offset(offset.dx.clamp(minOffset.dx, 0.0), offset.dy.clamp(minOffset.dy, 0.0));}_handleOnScaleStart(ScaleStartDetails details) {setState(() {_isHideTitleBar = true;_previousScale = _scale;_normalizedOffset = (details.focalPoint - _offset) / _scale;// 计算图片放大后的位置_controller!.stop();});}_handleOnScaleUpdate(ScaleUpdateDetails details) {setState(() {_scale = (_previousScale! * details.scale).clamp(1.0, 3.0);// 限制放大倍数 1~3倍_offset = _clampOffset(details.focalPoint - _normalizedOffset! * _scale);// 更新当前位置});}_handleOnScaleEnd(ScaleEndDetails details) {_setSystemUi();final double magnitude = details.velocity.pixelsPerSecond.distanceSquared;if (magnitude < _kMinFlingVelocity) return;final Offset direction = details.velocity.pixelsPerSecond / magnitude;// 计算当前的方向final double distance = (Offset.zero & context.size!).shortestSide;// 计算放大倍速,并相应的放大宽和高,比如原来是600*480的图片,放大后倍数为1.25倍时,宽和高是同时变化的_animation = _controller!.drive(Tween<Offset>(begin: _offset, end: _clampOffset(_offset + direction * distance)));_controller!..value = 0.0..fling(velocity: magnitude / 1000.0);}_onDoubleTap() {_isHideTitleBar = true;_setSystemUi();Size size = context.size!;_isEnlarge = !_isEnlarge;setState(() {if (!_isEnlarge) {_scale = 2.0;_offset = Offset(-(size.width / 2), -(size.height / 2));} else {_scale = 1.0;_offset = Offset.zero;}});}_onTap() {setState(() {_isHideTitleBar = !_isHideTitleBar;});_setSystemUi();}@overrideWidget build(BuildContext context) {return Stack(children: [_bodyView(), _titleBar()],);}_bodyView() {return GestureDetector(onScaleStart: _handleOnScaleStart,onScaleUpdate: _handleOnScaleUpdate,onScaleEnd: _handleOnScaleEnd,onDoubleTap: _onDoubleTap,onTap: _onTap,child: Container(color: _isHideTitleBar ? Colors.black : Colors.white,child: SizedBox.expand(child: ClipRect(child: Transform(transform: Matrix4.identity()..translate(_offset.dx, _offset.dy)..scale(_scale),child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [SizedBox(width: 700.0.w,height: 600.0.h,child: Image.network(widget.url!)),Container()],),),),),),);}_titleBar() {return Offstage(child: Container(alignment: Alignment.centerLeft,padding: EdgeInsets.only(top: MediaQueryData.fromWindow(window).padding.top,left: ScreenUtil().setWidth(24)),color: const Color.fromARGB(255, 32, 32, 32),height: MediaQuery.of(context).size.height * 0.1,width: MediaQuery.of(context).size.width,child: GestureDetector(child: Icon(Icons.arrow_back,size: 30.0.w,color: Colors.white,),onTap: () {Navigator.pop(context);},),),offstage: _isHideTitleBar,);}_setSystemUi() {if (_isHideTitleBar) {SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);} else {SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual);}}
}
Global.dart
import 'package:dio/dio.dart';class Global {static String BaseUrl = 'http://127.0.0.1:9999/';/*请求dio对象 */late Dio dio;/*通用超时 */int timeOut = 50000;/*请求单例 */static Global? _instance;/*获取实例 */static Global? getInstance() {if (_instance == null) _instance = Global();return _instance;}Global() {dio = Dio();dio.options = BaseOptions(baseUrl: BaseUrl,connectTimeout: timeOut,sendTimeout: timeOut,receiveTimeout: timeOut,contentType: Headers.jsonContentType,headers: {"Access-Control-Allow-Origin": "*",});// 请求拦截器 and 响应拦截机 and 错误处理dio.interceptors.add(InterceptorsWrapper(onRequest: (options, handler) {print("\n================== 请求数据 ==========================");print("url = ${options.uri.toString()}");print("headers = ${options.headers}");print("params = ${options.data}");print("\n================== 请求数据 ==========================");return handler.next(options);}, onResponse: (response, handler) {print("\n================== 响应数据 ==========================");print("code = ${response.statusCode}");print("data = ${response.data}");print("\n================== 响应数据 ==========================");handler.next(response);}, onError: (DioError e, handler) {print("\n================== 错误响应数据 ======================");print("type = ${e.type}");print("message = ${e.message}");print("\n================== 错误响应数据 ======================");return handler.next(e);}));}
}
routes.dart
// ignore_for_file: prefer_const_constructorsimport 'package:flutter/cupertino.dart';
import 'package:weat/views/index/IndexView.dart';Map<String, WidgetBuilder> routes = {"/": (context) => IndexView(),
};/** 渐隐跳转路由*/
void opcityPush(BuildContext context, Widget view, {int? milliseconds}) {Navigator.push(context,PageRouteBuilder(transitionDuration:Duration(milliseconds: milliseconds ?? 300), //动画时间为300毫秒pageBuilder: (BuildContext context, Animation<double> animation,Animation secondaryAnimation) {return FadeTransition(//使用渐隐渐入过渡,opacity: animation,child: view, //路由B);},),);
}/** 带路由树跳转*/
void Push(BuildContext context, Widget view) {Navigator.push(context,CupertinoPageRoute(builder: (context) => view,));
}void NavigatorPop(BuildContext context) {Navigator.of(context).pop();
}/** 返回上一级路由树,没有上级会黑屏*/
void Pop(BuildContext context, Widget view) {Navigator.pop(context,CupertinoPageRoute(builder: (context) => view,));
}
showmessage_util.dart
// ignore_for_file: prefer_equal_for_default_values, sized_box_for_whitespaceimport 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:fluttertoast/fluttertoast.dart';/// @author brath
/// @创建时间:2022/5/10
/// 封装自定义弹框
class DialogUtils {/// 显示普通消息static showMessage(String msg,{toastLength: Toast.LENGTH_LONG,gravity: ToastGravity.CENTER,timeInSecForIosWeb: 2,textColor: Colors.black,backgroundColor: Colors.grey,fontSize: 16.0}) {// 先关闭弹框再显示对应弹框Fluttertoast.cancel();Fluttertoast.showToast(msg: msg,toastLength: toastLength,webShowClose: true,gravity: gravity,textColor: textColor,timeInSecForIosWeb: timeInSecForIosWeb,backgroundColor: Colors.grey[50],fontSize: 13.0.sp,);}
index_viewmodel.dart
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';class IndexViewModel extends ChangeNotifier {bool? isLotte = false;Map? currentDishes = {};bool? get getIsLotte {return isLotte;}void setIsLotte(bool isLotte) {this.isLotte = isLotte;notifyListeners();}Map? get getCurrentDishes {return currentDishes;}void setCurrentDishes(Map currentDishes) {this.currentDishes = currentDishes;notifyListeners();}
}
IndexView.dart
import 'dart:math';import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:getwidget/getwidget.dart';
import 'package:provider/provider.dart';
import 'package:weat/common/ZoomImage.dart';
import 'package:weat/core/Global.dart';
import 'package:weat/routers/routes.dart';
import 'package:weat/utils/showmessage_util.dart';
import 'package:weat/viewmodel/index_viewmodel.dart';
import 'package:weat/views/index/DishesDetail.dart';/** 首页视图*/
class IndexView extends StatefulWidget {const IndexView({Key? key}) : super(key: key);@overrideState<IndexView> createState() => _IndexViewState();
}class _IndexViewState extends State<IndexView> {@overrideWidget build(BuildContext context) {return const LotterView();}
}/** 轮播抽奖页面*/
class LotterView extends StatefulWidget {const LotterView({Key? key}) : super(key: key);@overrideState<LotterView> createState() => _LotterViewState();
}class _LotterViewState extends State<LotterView> {//抽奖控制器final SimpleLotteryController _simpleLotteryController =SimpleLotteryController();int page = 1;int size = 8;//奖品列表List dishesList = [];//奖品lodingbool load = false;//初始化奖品列表_initPrizeList() async {Global.getInstance()!.dio.get("dishes/getDishes", queryParameters: {'page': 1,'size': 8,}).then((value) => {dishesList = value.data['data']['prizeRecords'],load = true,setState(() {})});}@overridevoid initState() {super.initState();_initPrizeList();}@overridevoid dispose() {_simpleLotteryController.dispose();super.dispose();}@overrideWidget build(BuildContext context) {return Scaffold(body: MediaQuery.removePadding(removeTop: true,context: context,child: ListView(children: [Container(width: MediaQuery.of(context).size.width,height: MediaQuery.of(context).size.height,decoration: BoxDecoration(gradient: LinearGradient(begin: Alignment.topLeft, //左中end: Alignment.bottomRight, //右中colors: [Theme.of(context).primaryColor,const Color.fromRGBO(180, 0, 0, 0.1),const Color.fromRGBO(187, 0, 0, 0.1),Theme.of(context).primaryColor]),),child: Column(children: [Container(padding: EdgeInsets.only(top: 50.h),child: Wrap(children: [Text('今日菜品',style: TextStyle(fontSize: 32.sp,fontWeight: FontWeight.w600,fontFamily: 'jinbu',color: Colors.white,)),Text('大放送',style: TextStyle(fontSize: 32.sp,fontWeight: FontWeight.w600,fontFamily: 'jinbu',color: const Color.fromARGB(255, 252, 217, 61)),)],),),Container(margin: EdgeInsets.only(top: 10.h),alignment: Alignment.center,child: Text('猜猜今天是什么菜系?',style: TextStyle(fontSize: 12.sp,color: Colors.white,fontFamily: 'jinbu',),),),SizedBox(height: 10.h,),Padding(padding: EdgeInsets.all(12.0.sp),child: Container(height: 400.h,padding: EdgeInsets.all(14.sp),decoration: BoxDecoration(gradient: LinearGradient(begin: Alignment.topLeft, //左中end: Alignment.bottomRight, //右中colors: [Theme.of(context).primaryColor,const Color.fromRGBO(180, 0, 0, 0.1),const Color.fromRGBO(187, 0, 0, 0.1),Theme.of(context).primaryColor]),// color: const Color.fromARGB(255, 205, 221, 235),border: Border.all(color: Colors.transparent, width: 14.w),borderRadius:BorderRadius.all(Radius.elliptical(20.r, 20.r))),child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [load? SimpleLotteryWidget(dishesList: dishesList,simpleLotteryController:_simpleLotteryController): SizedBox(width: double.infinity,height: 260.h,child: const GFLoader(type: GFLoaderType.ios,loaderColorOne: Colors.blue,loaderColorTwo: Colors.blue,loaderColorThree: Colors.blue,duration: Duration(milliseconds: 300)),),//抽奖按钮AnimatedSwitcher(duration: const Duration(milliseconds: 400),child: Provider.of<IndexViewModel>(context,listen: true).getIsLotte!? SizedBox(height: 34.h,width: MediaQuery.of(context).size.width -120.w,child: ElevatedButton(style: ElevatedButton.styleFrom(primary: Colors.red[200], //chanonPrimary: Colors.white, //change text color of buttonshape: RoundedRectangleBorder(borderRadius:BorderRadius.circular(30.r),),elevation: 3.0.h,),onPressed: () {//跳转详情页面Push(context,DishesDetailView(dishes:Provider.of<IndexViewModel>(context,listen: false).getCurrentDishes));//设置状态Provider.of<IndexViewModel>(context,listen: false).setIsLotte(false);},child: Row(mainAxisAlignment:MainAxisAlignment.center,children: [SizedBox(width: 4.w,),Text('点击查看:${Provider.of<IndexViewModel>(context, listen: false).getCurrentDishes!['dishesName']}',style: TextStyle(fontWeight: FontWeight.w500,fontFamily: '',color: Colors.white,fontSize: 14.0.sp),),],),)): SizedBox(height: 34.h,width: MediaQuery.of(context).size.width -220.w,child: ElevatedButton(style: ElevatedButton.styleFrom(primary: Colors.red[200], //chanonPrimary: Colors.white, //change text color of buttonshape: RoundedRectangleBorder(borderRadius:BorderRadius.circular(30.r),),elevation: 3.0.h,),onPressed: () {//开始抽奖_simpleLotteryController.start(Random.secure().nextInt(2)); //开启setState(() {});},child: Row(mainAxisAlignment:MainAxisAlignment.center,children: [SizedBox(width: 4.w,),Text('吃啥',style: TextStyle(fontWeight: FontWeight.w500,fontFamily: '',color: Colors.white,fontSize: 14.0.sp),),],),)),)],),),),],),),],),),);}
}//奖品参数
class SimpleLotteryValue {SimpleLotteryValue({this.target = 0, this.isFinish = false, this.isPlaying = false});/// 中奖目标int target = 0;bool isPlaying = false;bool isFinish = false;SimpleLotteryValue copyWith({int target = 0,bool isPlaying = false,bool isFinish = false,}) {return SimpleLotteryValue(target: target, isFinish: isFinish, isPlaying: isPlaying);}@overrideString toString() {return "target : $target , isPlaying : $isPlaying , isFinish : $isFinish";}
}//抽奖控制器
class SimpleLotteryController extends ValueNotifier {SimpleLotteryController() : super(SimpleLotteryValue());/// 开启抽奖////// [target] 中奖目标void start(int target) {// 九宫格抽奖里范围为0~8assert(target >= 0 && target <= 8);if (value.isPlaying) {return;}value = value.copyWith(target: target, isPlaying: true);}void finish() {value = value.copyWith(isFinish: true);}
}/** 奖品列表容器 */
class SimpleLotteryWidget extends StatefulWidget {final SimpleLotteryController simpleLotteryController;final List dishesList;const SimpleLotteryWidget({Key? key,required this.dishesList,required this.simpleLotteryController}): super(key: key);@overrideState<SimpleLotteryWidget> createState() => _SimpleLotteryWidgetState();
}class _SimpleLotteryWidgetState extends State<SimpleLotteryWidget>with TickerProviderStateMixin {Future<int>? future; // 标识@overrideWidget build(BuildContext context) {return FutureBuilder(future: future,builder: (context, snapshot) {return Container(margin: EdgeInsets.all(5.h),width: double.infinity,height: 280.h,child: GridView.builder(physics: const NeverScrollableScrollPhysics(),itemCount: 9,gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3,crossAxisSpacing: 8.w,mainAxisSpacing: 8.h),itemBuilder: (context, index) {if (index != 4) {return commodity(index);}return Image.network('https://brath.cloud/app_log.png',fit: BoxFit.cover,);}));},);}// 奖品列表Widget commodity(int index) {final int toIndex;toIndex = _deserializeMap[index];return GestureDetector(onTap: () {opcityPush(context,ZoomImage(url: '${widget.dishesList[toIndex]['dishesUrl']}'));},child: Stack(children: [Container(decoration: BoxDecoration(borderRadius: BorderRadius.all(Radius.circular(10.r)),color: widget.dishesList[toIndex]['id']! > 5? Colors.red[300]!.withAlpha(32): widget.dishesList[toIndex]['id']! >= 4 &&widget.dishesList[toIndex]['id'] <= 5? Colors.amber[300]!.withAlpha(32): Colors.blue[300]!.withAlpha(32),),child: SizedBox(width: double.infinity,height: double.infinity,child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [Image.network('${widget.dishesList[toIndex]['dishesUrl']}',fit: BoxFit.cover,width: 60.w,height: 60.w,),SizedBox(height: 5.h,),Text('${widget.dishesList[toIndex]['dishesName']}',maxLines: 1,overflow: TextOverflow.ellipsis,style: TextStyle(fontFamily: 'jinbu',fontSize: 9.sp,color: Theme.of(context).primaryColor,),),],),),),Container(decoration: BoxDecoration(color: index == _currentSelect? Colors.yellow.withOpacity(0.5): Colors.transparent,borderRadius: BorderRadius.all(Radius.circular(10.r)),)),],),);}Animation? _selectedIndexTween;AnimationController? _startAnimateController;int _currentSelect = -1;int _target = 0;/// 旋转的圈数final int repeatRound = 4;VoidCallback? _listener;/// 选中下标的映射final Map _selectMap = {0: 0, 1: 3, 2: 6, 3: 7, 4: 8, 5: 5, 6: 2, 7: 1};//反下标的映射final Map _deserializeMap = {0: 0,3: 1,4: 8,6: 2,7: 3,8: 4,5: 5,2: 6,1: 7};simpleLotteryWidgetState() {_listener = () {// 开启抽奖动画if (widget.simpleLotteryController.value.isPlaying) {_startAnimateController?.reset();_target = widget.simpleLotteryController.value.target;_selectedIndexTween = _initSelectIndexTween(_target);_startAnimateController?.forward();}};}/// 初始化tween////// [target] 中奖的目标Animation _initSelectIndexTween(int target) =>StepTween(begin: 0, end: repeatRound * 8 + target).animate(CurvedAnimation(parent: _startAnimateController!, curve: Curves.easeOutQuart));@overridevoid initState() {super.initState();future = Future.value(42);_startAnimateController =AnimationController(vsync: this, duration: const Duration(seconds: 5));_selectedIndexTween = _initSelectIndexTween(_target);//开启动画simpleLotteryWidgetState();// 控制监听widget.simpleLotteryController.addListener(_listener!);// 动画监听_startAnimateController?.addListener(() {// 更新选中的下标_currentSelect = _selectMap[_selectedIndexTween?.value % 8];if (_startAnimateController!.isCompleted) {widget.simpleLotteryController.finish();_currentSelect = -1;dynamic dishes = widget.dishesList[_target];Provider.of<IndexViewModel>(context, listen: false).setIsLotte(true);Provider.of<IndexViewModel>(context, listen: false).setCurrentDishes(dishes);DialogUtils.showMessage('恭喜你,今天做:${dishes['dishesName']},点击按钮查看菜品做法!');}setState(() {});});}@overridevoid deactivate() {widget.simpleLotteryController.removeListener(_listener!);super.deactivate();}@overridevoid dispose() {_startAnimateController?.dispose();super.dispose();}
}//获取当前奖品出现的次数
getNumberTimesCurrentPrizeAppears(String? id, List<dynamic> arr) {//定义集合Map obj = {};List idList = [];for (var i = 0; i < arr.length; i++) {idList.add(arr[i]['id']);}for (var i = 0; i < idList.length; i++) {if (obj.containsValue([idList[i]])) {obj[idList[i]]++;} else {obj[idList[i]] = 1;}}return obj[int.parse(id!)];
}int next(int min, int max) {int res = min + Random().nextInt(max - min + 1);return res;
}
DishesDetail.dart
import 'package:bruno/bruno.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_swiper_plus/flutter_swiper_plus.dart';
import 'package:weat/common/common.dart';class DishesDetailView extends StatefulWidget {DishesDetailView({Key? key,required this.dishes}) : super(key: key);var dishes;@overrideState<DishesDetailView> createState() => _DishesDetailViewState();
}class _DishesDetailViewState extends State<DishesDetailView> {List steplist = [];@overridevoid initState() {super.initState();//分割步骤steplist = widget.dishes['dishesStep'].toString().split('\r\n');}@overrideWidget build(BuildContext context) {return WillPopScope(onWillPop: () {BrnDialogManager.showConfirmDialog(context,showIcon: false,barrierDismissible: false,title: "你正在做菜,要退出吗~",confirm: "退出",cancel: "不", onConfirm: () {Navigator.of(context).pop();Navigator.of(context).pop();}, onCancel: () {Navigator.of(context).pop();});return Future.value(false);},child: Scaffold(appBar: getAppBar(widget.dishes['dishesName'], context: context),body: ListView(children: [Padding(padding: const EdgeInsets.all(8.0),child: ImageFulWidget(imageList: [widget.dishes['dishesUrl']]),),getCardContainer(margin: EdgeInsets.only(top: 10.h, bottom: 10.h, left: 10.w, right: 10.w),height: 220.0.h,isBorder: true,BorderCircular: 5.r,width: double.infinity,widget: Column(crossAxisAlignment: CrossAxisAlignment.start,children: [Padding(padding: EdgeInsets.only(left: 10.w, top: 10.h),child: BrnCSS2Text.toTextView('步骤:',maxLines: 1,textOverflow: TextOverflow.ellipsis,defaultStyle:TextStyle(fontSize: 20.sp, fontFamily: 'jinbu'),),),Padding(padding: EdgeInsets.only(left: 10.w, top: 10.h),child: Column(crossAxisAlignment: CrossAxisAlignment.start,children: steplist.map((e) {return Container(margin: EdgeInsets.only(top: 5.h),child: BrnCSS2Text.toTextView('$e',maxLines: 1,textOverflow: TextOverflow.ellipsis,defaultStyle: TextStyle(fontSize: 13.sp, fontFamily: 'jinbu'),),);}).toList(),),),],),)],)),);}
}/** 顶部轮播图Widget*/
class ImageFulWidget extends StatelessWidget {ImageFulWidget({Key? key,required this.imageList,}) : super(key: key);List<String>? imageList;@overrideWidget build(BuildContext context) {return ClipRRect(borderRadius: BorderRadius.vertical(top: Radius.circular(20.h), bottom: Radius.circular(20.h)),child: SizedBox(height: MediaQuery.of(context).size.height * 0.25,width: MediaQuery.of(context).size.width - 60.0.w,child: Swiper(autoplay: false,duration: 2000,curve: Curves.linearToEaseOut,itemBuilder: (BuildContext context, int index) {return Image.network(imageList![index],height: MediaQuery.of(context).size.height * 0.25,width: MediaQuery.of(context).size.width - 60.0.w,fit: BoxFit.cover,);},itemCount: imageList!.length,pagination: SwiperPagination(builder: DotSwiperPaginationBuilder(size: 8.sp, // 设置未选中的小点大小activeSize: 10.sp, // 设置选中的小点大小color: const Color.fromARGB(255, 219, 219, 219), // 设置为未选中的小点颜色activeColor: Colors.blue, // 设置选中的小点颜色),),control: const SwiperControl(color: Colors.transparent),),),);}
}
main.dart
import 'dart:async';
import 'dart:io';
import 'dart:math';import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
import 'package:flutter/services.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get_navigation/src/root/get_material_app.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:provider/provider.dart';
import 'package:weat/routers/routes.dart';
import 'package:weat/viewmodel/index_viewmodel.dart';//全局路由key
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();/** 主函数*/
void main() async {initializeDateFormatting().then((_) => runApp(MultiProvider(providers: [ChangeNotifierProvider(create: (context) => IndexViewModel()),],child: MyApp(),)));//安卓屏蔽顶部阴影if (Platform.isAndroid) {SystemUiOverlayStyle systemUiOverlayStyle =SystemUiOverlayStyle(statusBarColor: Colors.transparent);SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle);}
}//根节点
class MyApp extends StatefulWidget {@overrideState<MyApp> createState() => _MyAppState();
}class _MyAppState extends State<MyApp> {@overridevoid initState() {super.initState();_init();}//全局路由keyfinal GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();/// 程序初始化_init() async {await EasyLoading.init();EasyLoading.instance..displayDuration = const Duration(milliseconds: 2000)..indicatorType = EasyLoadingIndicatorType.fadingCircle..loadingStyle = EasyLoadingStyle.dark..indicatorSize = 45.0..radius = 10.0..progressColor = Colors.yellow..backgroundColor = Colors.green..indicatorColor = Colors.yellow..textColor = Colors.yellow..maskColor = Colors.blue.withOpacity(0.5)..userInteractions = true..dismissOnTap = false;await WidgetsFlutterBinding.ensureInitialized();await SystemChrome.setPreferredOrientations([// 竖屏 Portrait 模式DeviceOrientation.portraitUp,DeviceOrientation.portraitDown,],);}@overrideWidget build(BuildContext context) {return ScreenUtilInit(designSize: Size(375, 667),minTextAdapt: true,splitScreenMode: true,builder: (context, child) {return GetMaterialApp(theme: ThemeData(primarySwatch: Colors.blueGrey, //全局主题颜色splashColor: Colors.transparent, // 点击时的高亮效果设置为透明// backgroundColor: Color.fromARGB(255, 255, 255, 255), //系统背景主题颜色backgroundColor: Color(0xFFF3F4F6), //系统背景主题颜色highlightColor: Colors.transparent, // 长按时的扩散效果设置为透明primaryColor: Color.fromARGB(188, 0, 0, 97), //系统原色focusColor: Color.fromARGB(235, 73, 74, 116), //焦点主题颜色hoverColor: Color.fromARGB(235, 103, 104, 172), //悬停主题颜色disabledColor: Color.fromARGB(235, 105, 106, 133), //禁用主题颜色primaryColorLight: Colors.white, // 白色主题颜色primaryColorDark: Color.fromARGB(235, 73, 74, 116), //黑色主题颜色selectedRowColor: Color.fromARGB(255, 104, 80, 145), //选中效果颜色appBarTheme: AppBarTheme(backgroundColor: Colors.white),buttonTheme: ButtonThemeData(focusColor: Color.fromARGB(235, 73, 74, 116),hoverColor: Color.fromARGB(235, 103, 104, 172),),tabBarTheme: TabBarTheme(labelColor: Color.fromARGB(235, 103, 104, 172),),progressIndicatorTheme: ProgressIndicatorThemeData(color: Color.fromARGB(235, 101, 102, 158),),),navigatorKey: navigatorKey,routes: routes,debugShowCheckedModeBanner: false,initialRoute: "/", //初始化进入加载页面builder: (context, widget) {return MediaQuery(data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),child: widget!,);},);});}
}
代码编写完毕后 flutter build apk 打包到真机运行
最终呈现出我们想要的效果:
抽奖中:
中奖:
详情: