如何使用Flutter+SpringBoot+Mysql开发一个简易的抽奖APP(Android)教学

3 篇文章 3 订阅
3 篇文章 0 订阅

如果你需要项目资源,欢迎来到我的个人博客: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创建:


  1. 打开项目路径,输入cmd进入到命令行,输入 flutter create weat 进行flutter项目创建。

  1. 打开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);
​
    /**
     * 菜品服务接口
     */
    @Autowired
    privateIvDishesServicedishesService;
​
    /***
     * 获取菜品列表
     *
     * @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
     */
    @Override
    publicIPage<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 d
        ORDER BY RAND()
    </select>

</mapper>
application.yaml配置:
server:
  port: 9999
spring:
  datasource:
    druid:
      driver-class-name: com.mysql.jdbc.Driver
      url: jdbc:mysql://127.0.0.1:3306/iv-user-services?createDatabaseIfNotExist=true&useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
      username: 'root'
      password: 'root'
      initial-size: 10
      max-active: 100
      min-idle: 10
      max-wait: 6000
      pool-prepared-statements: true
      max-pool-prepared-statement-per-connection-size: 20
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 300000
      #Oracle需要打开注释
      #      validation-query: SELECT 1 FROM DUAL
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      stat-view-servlet:
        enabled: true
        url-pattern: /druid/*
        #login-username: admin
        #login-password: admin
        #达梦数据库,需要注释掉,其他数据库可以打开
      filter:
        stat:
          log-slow-sql: true
          slow-sql-millis: 1000
          merge-sql: false
        wall:
          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
  #easyloading
  flutter_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;

  @override
  Widget 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), // border
              borderRadius:
                  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;
  @override
  State<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;
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this);
    _controller?.addListener(() {
      setState(() {
        _offset = _animation!.value;
      });
    });
  }

  @override
  void 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();
  }

  @override
  Widget 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_constructors

import '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_whitespace

import '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);

  @override
  State<IndexView> createState() => _IndexViewState();
}

class _IndexViewState extends State<IndexView> {
  @override
  Widget build(BuildContext context) {
    return const LotterView();
  }
}

/*
 * 轮播抽奖页面
 */
class LotterView extends StatefulWidget {
  const LotterView({Key? key}) : super(key: key);

  @override
  State<LotterView> createState() => _LotterViewState();
}

class _LotterViewState extends State<LotterView> {
  //抽奖控制器
  final SimpleLotteryController _simpleLotteryController =
      SimpleLotteryController();

  int page = 1;
  int size = 8;

  //奖品列表
  List dishesList = [];

  //奖品loding
  bool 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(() {})
        });
  }

  @override
  void initState() {
    super.initState();
    _initPrizeList();
  }

  @override
  void dispose() {
    _simpleLotteryController.dispose();
    super.dispose();
  }

  @override
  Widget 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], //chan
                                        onPrimary: Colors
                                            .white, //change text color of button
                                        shape: 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], //chan
                                        onPrimary: Colors
                                            .white, //change text color of button
                                        shape: 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);
  }

  @override
  String toString() {
    return "target : $target , isPlaying : $isPlaying , isFinish : $isFinish";
  }
}

//抽奖控制器
class SimpleLotteryController extends ValueNotifier {
  SimpleLotteryController() : super(SimpleLotteryValue());

  /// 开启抽奖
  ///
  /// [target] 中奖目标
  void start(int target) {
    // 九宫格抽奖里范围为0~8
    assert(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);

  @override
  State<SimpleLotteryWidget> createState() => _SimpleLotteryWidgetState();
}

class _SimpleLotteryWidgetState extends State<SimpleLotteryWidget>
    with TickerProviderStateMixin {
  Future<int>? future; // 标识

  @override
  Widget 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));

  @override
  void 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(() {});
    });
  }

  @override
  void deactivate() {
    widget.simpleLotteryController.removeListener(_listener!);
    super.deactivate();
  }

  @override
  void 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;
  @override
  State<DishesDetailView> createState() => _DishesDetailViewState();
}

class _DishesDetailViewState extends State<DishesDetailView> {
  List steplist = [];
  @override
  void initState() {
    super.initState();
    //分割步骤
    steplist = widget.dishes['dishesStep'].toString().split('\r\n');
  }

  @override
  Widget 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;

  @override
  Widget 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 {
  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  void initState() {
    super.initState();
    _init();
  }

//全局路由key
  final 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,
      ],
    );
  }

  @override
  Widget 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 打包到真机运行

最终呈现出我们想要的效果:


抽奖中:

中奖:

详情:

本期教程到此结束,欢迎你的观看~

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
实验目的: 1. 掌握Flutter应用程序开发的基本流程和技巧。 2. 熟悉Flutter和Dart语言的基本语法和应用。 3. 了解Flutter的UI组件和布局,以及它们在应用程序中的实际应用。 4. 学习如何使用Flutter和Dart来调用摄像头和文件系统API,实现拍照功能。 5. 学习如何使用Flutter和Dart来处理图像数据,以及如何在应用程序中显示和保存图像。 实验内容: 本次实验的主要内容是使用Flutter和Dart语言开发一个拍照应用程序,程序的主要功能包括: 1. 调用摄像头API,实现拍照功能。 2. 显示拍摄的照片,并支持手势缩放和旋转。 3. 保存拍摄的照片到本地文件系统中。 以下是实验的具体步骤: 1. 创建一个新的Flutter项目,使用Flutter提供的UI组件和布局来实现应用程序的UI设计。 2. 在应用程序中添加一个按钮,用于触发拍照功能。 3. 在应用程序中调用摄像头API,实现拍照功能,并将拍摄的照片数据保存到内存中。 4. 在应用程序中显示拍摄的照片,并支持手势缩放和旋转。您可以使用Flutter提供的GestureDetector和Transform组件来实现这些功能。 5. 在应用程序中将拍摄的照片保存到本地文件系统中。您可以使用Flutter提供的dart:io库来实现这个功能。 在完成上述步骤后,您可以在模拟器或实际设备上运行应用程序,并测试它的功能。如果需要,您还可以对应用程序进行进一步的优化和改进。 总之,本次实验将帮助您掌握Flutter和Dart语言的基本知识和技巧,以及如何使用它们来开发实际应用程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员Brath

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值