创作初衷

这篇文章创作的初衷,只是为了写一个有关日历类的软件供自己使用,考虑到自己从来还没有使用flutter正式创作一个app,因此磨刀霍霍想试一试。

至于为什么要做一款日历软件,因为发现市面上的关于万年历的软件都有很多广告,想着自己也能做,就做个给自己用。同时里面包含了额外的模块,包括万年历、天气以及小常识等等。。。



创作过程

由于自己是flutter小白,对Dart语言也是一知半解,因此想在快速的时间内去完成一款app,就可能得翻破flutter官网相关的文档,效率不见得很高,因此主要结合chatGPT给我做知识扫盲以及方案选型建议。

比如我让chatGPT给我生成一段日历的核心逻辑:

然后不断加以修正,比如可以支持从星期日开始:

虽然不是很熟悉dart语法,但是并不是很影响我读懂代码。一般做过React或者声明式语言(android compose/swift)语言的人,上手flutter会相当快。

chatGPT在问答的过程中,也会说一些胡话,比如我在做天气模块的时候,需要实现一个向上滚动,标题部分自动缩小,并保证滚动条在标题下方滚动的功能,但是chatGPT并不能给我正确的回答,准确的说,它能给我回答,但是大多都是它胡诌的。

所以在使用这类AI工具的时候,需要自己识别它给出的到底是不是一个正确的答案,可以不断去试错,切不可一条路走到黑,无脑去相信。

关于如何精准使用chatGPT做问答、搜索、创作,以及源码解析,我司邮件每天都有讨论,欢迎加入探讨。

说(遇)说(到)重(的)点(坑)

几个重要的库或选择

至于为什么这么选择?我都是在chatGPT中问出来的,毕竟小白首先得知道方向在哪里,然后根据给出的提示去官方文档进行比较。

比如在选择使用哪个天气时,我首先从chatGPT给我的推荐中去官网查看,看是否能够满足我的需求

  • 免费API (或者说调用次数在多少次内免费)
  • 是否提供当天的详细天气情况
  • 是否提供一天24小时的天气走势
  • 是否提供7天之内的详细情况

经过比较之后,我发现上述的都不是特别合适,基本上提供7天以上的就不能免费订阅了,所以在此基础上,我就会再加上一些关键词,比如 "免费API", "7天天气详情"等等。

底部导航栏动画

原本采用的是flutter默认提供的导航栏,后来想想怎么也的折腾一番。但是这一折腾不打紧,导致我后面路由的设计全改变了。

页面有4个导航tab,所以我最开始采用了4个路由,分别对应4个tab

class Routes {
static String calendar = "/calendar";
static String weather = "/weather";
static String sense = "/sense";
static String settings = "/settings";
// ...
}

这样安于现状老老实实切换是木有问题的,但是我想在切换的时候加点动画,类似与这样的,就不work了:

原因是这个组件在路由切换的时候,都会重新渲染一份,所以动画肯定是没有的,无奈之下,就提取了一个公共页,采用分支逻辑hide/show,来做tab页面的切换

Scaffold(
appBar: getAppBar(selectedIndex, context),
body: getBody(selectedIndex, senseState),
bottomNavigationBar: renderBottomNavigationBar(
context,
selectedIndex,
(index) {
setState(() {
selectedIndex = index;
});
},
),
floatingActionButton: getFloatingActionButton(selectedIndex, homeState),
); Widget getBody(int index, SenseState senseState) {
switch (index) {
case 0:
return const Calendar();
case 1:
return const Weather();
case 2:
return CommonSense(senseState: senseState);
case 3:
return const Settings();
default:
return const Calendar();
}
}

数据预加载

我做的这个demo里面,由于需要展示天气信息,所以在显示日历的时候,就可以进行天气信息的预加载了。

我的具体做法是在main.dart中,在weatherState初始化后就立即将天气信息获取然后塞入state中,这样在我切换到天气页面的时候,就可以获取到详细的数据了。【可能有更加好的办法】

// main.dart
final position = await _determinePosition();
final weatherState = WeatherState(position);
weatherState.getWeatherInfo(); // weather_state.dart
Future<void> getWeatherInfo() async {
final location = "${position.longitude},${position.latitude}";
final responses = await loadAllWeatherData(location); if (responses.isNotEmpty && responses.length == 4) {
final weatherLocation = responses[0] as WeatherLocation;
final weatherNow = responses[1] as WeatherNow;
final weatherHourly = responses[2] as WeatherTwentyFourHours;
final weatherDaily = responses[3] as WeatherSevenDays;
setWeatherInfo(
weatherLocation.location[0],
weatherNow.now,
weatherHourly.hourly,
weatherDaily.daily,
);
}
}

日历月份切换

采用了flutter_swiper这个组件来做左右日历的滑动,但是要想很丝滑(当滑动下一个月的时候,能够立马看到数据),就需要把提前将下一个月的日历详情全部生成出来,最开始想直接生成几年的数据,想想还是太粗暴了,所以只是生成了前一个月以及后一个月的数据。

var list = [prevCalendarDates, calendarDates, nextCalendarDates];

Swiper(
index: 1,
loop: false,
duration: 1,
itemCount: list.length,
onIndexChanged: (int index) {},
itemBuilder: (BuildContext context, int index) {}
)

可以看到,我默认在swiper中显示的索引是1,这样显示的就是当前月份的日历信息。但是这样也有一个问题,由于这个swiper组件自带从左到右的动画,滑到上个月还好,但是滑到下一个月,就会有一个先向左再向右的动画突兀,所以我将duration的值改为了1,就是避免使用swiper的动画。

关于本地存储

最开始其实没有打算用到服务器来进行api请求,毕竟最开始的打算只是做一个简简单单的万年历,所以所有的事件、提醒信息都打算存储在本地,采用sqlite关系型数据库来解决。

后来需求膨胀(加了常识模块),发现这玩意就不好使了,因为常识模块需要添加的字段比较多,并不像日历部分只需要加几个简单的字段,而且也不会特别多,所以不得已又迫使搞出个后台来。

其间纠结了很久,要不要就统一使用本地数据库呢?常识这块搞一个本地后台管理就好了,连接到august.db文件,然后进行增删查改也不是不能接受,后来发现有点虚,毕竟我是想在自己的手机上run的,难道每次同步还得把自己电脑后台服务打开,想想都有点麻烦。

所以后来还是把常识这块部署到了生产环境,日历事件部分采用的本地数据库,这样会快一点进行每天日历事件的初始化。所以整个一块的改动也是反反复复的。

日历事件采用本地sqlite

class DatabaseProvider {
// ... Future<Database> _initDatabase() async {
final databasesPath = await getDatabasesPath();
final path = join(databasesPath, 'august.db');
Logger.d("database path: $path"); return await openDatabase(
path,
version: 1,
onCreate: (db, version) async {
await db.execute('''
CREATE TABLE IF NOT EXISTS ${CalendarDB.calendarEvent} (
id TEXT,
dateId TEXT,
title TEXT,
content TEXT,
date INTEGER,
lunarDate TEXT,
isCycle INTEGER,
cycleBy INTEGER,
createTime INTEGER,
modifyTime INTEGER,
deleted INTEGER
)
''');
},
);
}
}

常识部分调远端api

final baseUrl = "${dotenv.env['SENSE_BACKEND_URL']}/api/senses";

Future<List<CommonSense>> getCommonSenseByPage(
{int page = 1, int pageSize = 20}) async {
final response = await Http.get(
"$baseUrl/",
params: {'page': page, 'pageSize': pageSize},
); return SenseResponse.fromJson(response.data).data;
}

然后至于本地的事件提醒数据,打算定期备份,即把本地的数据库文件上传至服务器。【TODO】

天气滑动动画

为了实现上面的动画,chatGPT多少是在这块犯浑了,尽管给我指引了采用sliverAppBar来实现此功能;

但是当向上滑动时,滚动条默认会从屏幕的最顶端开始滑动,这就导致了滑动的内容会透过缩小后的文字 [贴图中 -> 旧金山 多云 13°C]显示在下面,再次询问如何解决时,给我的总是错误的答案,看来还是不能轻信啊

后来google了解决办法,采用了CustomClipper,这里贴一下:

import 'package:flutter/material.dart';
import 'dart:math' as math; class CustomClipperContainer extends StatelessWidget {
final Widget child; const CustomClipperContainer({super.key, required this.child}); @override
Widget build(BuildContext context) {
return ClipRect(
clipper: MyCustomClipper(
clipHeight: MediaQuery.of(context).size.height - 220,
),
child: child,
);
}
} class MyCustomClipper extends CustomClipper<Rect> {
final double clipHeight; MyCustomClipper({required this.clipHeight}); @override
getClip(Size size) {
double top = math.max(size.height - clipHeight, 0);
Rect rect = Rect.fromLTRB(0.0, top, size.width, size.height);
return rect;
} @override
bool shouldReclip(CustomClipper oldClipper) {
return false;
}
} // 使用
CustomClipperContainer(
child: ListView(
padding: EdgeInsets.zero,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
children: const [
HourlyForecast(),
SevenDayForecast(),
CurrentDetail(),
],
),
)

天气背景映射

由于天气背景我采用了flutter_weather_bg这个库,里面包括了一系列的天气背景动画,比如下雨、雷电、下雪等等动画场景,但是由于我使用了和风天气,返回的api里面并不能很好的和这个库搭配起来,所以这里不得不做映射处理。

WeatherType getWeatherTypeBy(String weatherText, String icon) {
if (weatherText == '晴') {
if (icon == '100') {
return WeatherType.sunny;
} else {
return WeatherType.sunnyNight;
}
} else if (weatherText.contains('云')) {
if (icon == '101' || icon == '102' || icon == '103') {
return WeatherType.cloudy;
} else {
return WeatherType.cloudyNight;
}
} else if (weatherText == '阴') {
// ...
}
// ...
}

按照道理讲,关于天气这一块所有的api请求,最好还是要走一层后端,如果再做厚一点,应该有个BFF层来专门处理数据的组装、转发等场景。比如类似这样的mapping,以及获取天气数据的信息等请求就可以由BFF给我返回了,这样做的好处是,将更多的细节封装到了内部,前端只需要更加纯粹地显示数据就好了,如果后续有改动,比如我的天气从和风API转成了XXX API,前端部分可以完全不用再改动了。

但是由于我是后来才想起我要做个常识模块,那个时候才引入了一个后台,所以前面的就懒得整了。【TODO】

滑动后退失效了

当我快要完成我的demo时,我突然想起来,试试滑动后退,发现怎么也不起作用。后来想想问题应该是出在了路由上,于是去网上扒了扒

找到个issue

将默认的TransitionType设为TransitionType.cupertino就解决了。

主题部分

准备了两套颜色,明亮色以及暗黑色【颜色部分可能还是得有设计师来,这块真是搞得我头痛】,然后使用ThemeData进行封装,然后在MaterialApp上进行设置。

MaterialApp(
debugShowCheckedModeBanner: false,
theme: globalState.isDarkMode ? darkTheme : lightTheme,
onGenerateRoute: Application.router.generator,
);

将用户的偏好存储在sharedPreferences中,这样当用户下次再次进入app时,就能记住上次是选择了哪个主题。

// user_preference.dart
class UserPreference { static Future<bool> getThemeMode() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
var isDarkMode = prefs.getBool(isDarkModeText);
return isDarkMode ?? false;
} static Future<void> updateThemeMode(bool isDarkMode) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setBool(isDarkModeText, isDarkMode);
}
} // global_state.dart
class GlobalState extends ChangeNotifier {
bool isDarkMode = true; GlobalState(this.isDarkMode); void toggleTheme() async {
isDarkMode = !isDarkMode;
UserPreference.updateThemeMode(isDarkMode);
notifyListeners();
}
}

还有一些可以讲讲

使用dotenv获取环境变量

final apiKey = dotenv.env['WEATHER_API_KEY'];

好处是配置与使用隔离,这样也安全一点。

使用Json To Dart插件生成model

网上也有使用json_serializable来实现序列化与反与反序列化的,但我个人觉得小项目还是这个插件好用,因为这个库会将文件分割成两个部分。

flutter_native_splash生成splash页面

使用这个库flutter_native_splash,详细用法参看官方文档。

# 更新splash页面,更新玩颜色以及背景图片后,运行以下命令
flutter clean && flutter pub get && flutter pub run flutter_native_splash:create

后端部分

分为august-server以及august-admin,server主要提供api服务,admin提供后台数据管理,admin的模版是从网上嫖的。感兴趣可以自己去看看 vue-manage-system

数据库采用了postgres,使用docker-compose做了服务编排,这里贴一下,感兴趣自己看看

version: '3.8'  

services:
postgresdb:
image: postgres:14.8
restart: unless-stopped
env_file: ./.env
environment:
- POSTGRES_DB=$POSTGRES_DATABASE
- POSTGRES_USER=$POSTGRES_USER
- POSTGRES_PASSWORD=$POSTGRES_PASSWORD
healthcheck:
test: pg_isready -U postgres
ports:
- $POSTGRES_LOCAL_PORT:$POSTGRES_DOCKER_PORT
volumes:
- ./data:/var/lib/postgresql/data
app:
depends_on:
postgresdb:
condition: service_healthy
build: ./august-server
restart: unless-stopped
env_file: ./.env
ports:
- $NODE_LOCAL_PORT:$NODE_DOCKER_PORT
environment:
- DB_HOST=postgresdb
- DB_USER=$POSTGRES_USER
- DB_PASSWORD=$POSTGRES_PASSWORD
- DB_NAME=$POSTGRES_DATABASE
- DB_PORT=$POSTGRES_DOCKER_PORT
stdin_open: true
tty: true admin:
depends_on:
- app
build: ./august-admin
restart: unless-stopped
env_file: ./.env
ports:
- $ADMIN_LOCAL_PORT:$ADMIN_LOCAL_PORT
environment:
- PROXY_PROT=$NODE_DOCKER_PORT

需要提一点的是,app服务需要完全等数据库服务启动之后,才能请求数据,否则直接报错。所以这块,我加了healthcheck(最开始我一直以为是mysql的问题,后来发现切换成postgres后依然有问题)。

总结

好了至此为止,想说的就已经说完了,整个功能来说相对简单,当然也躺了不少的坑,仅此供学习交流。

另外,针对一门新的技术,chatGPT能给你很好的入门指导,虽然胡说的不一定准,但是不说肯定是啥都不知道

最后贴贴代码仓库:

仅供学习交流,勿商用!!!

flutter小白是如何在一周内用chatGPT开发一款App的的更多相关文章

  1. Servlet 利用Cookie实现一周内不重复登录

    import java.io.IOException;import java.io.PrintWriter; import javax.servlet.ServletException;import ...

  2. Servlet课程0426(十一)Servlet Cookie实现两周内不用重复登录

    Welcome.java //登录界面 package com.tsinghua; import javax.servlet.http.*; import java.io.*; import java ...

  3. JS-两周内自动登录功能

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...

  4. 几周内搞定Java的10个方法

    不要将Java与JavaScript弄混了,Java的目标是“一次编译,到处调试”(呃,不对,是“到处运行”).简单来说,就是Java程序可以直接在任何设备上运行. Java语言是什么? 不管我们是否 ...

  5. Android JAVA如何判断两天在同一周内

    /** * <pre> * 判断date和当前日期是否在同一周内 * 注: * Calendar类提供了一个获取日期在所属年份中是第几周的方法,对于上一年末的某一天 * 和新年初的某一天在 ...

  6. 我是如何在一周内拿到4份offer的?

    前言 大概一个月没写博客了吧,这段时间事情比较多(家里有事,请了一段时间假,正好利用剩余几天时间面了几次试),也没抽出来时间写博客,还好所有的事情已经处理完了,今天闲来无事就整理一下这几次面试过程中遇 ...

  7. Spring Security框架下实现两周内自动登录"记住我"功能

    本文是Spring Security系列中的一篇.在上一篇文章中,我们通过实现UserDetailsService和UserDetails接口,实现了动态的从数据库加载用户.角色.权限相关信息,从而实 ...

  8. Mysql 查询当天、昨天、近7天、一周内、本月、上一月等的数据(函数执行日期的算术运算)

    注:where语句后中的字段last_login_time 替换成 时间字段名 即可 #查询昨天登录用户的账号 ; #查询当天登录用户的账号 ; #查询所有last_login_time值在最后1天内 ...

  9. Python计算给定日期的周内的某一天

    先理一下思路:1.weekday会根据某个日期返回0到6的一个数字来表示星期几对吧,0==星期一我们来列一个表: [0,1,2,3,4,5,6] 2.知道了星期几之后,你可以计算出那一周相对于这个0到 ...

  10. Flutter学习笔记(36)--常用内置动画

    如需转载,请注明出处:Flutter学习笔记(36)--常用内置动画 Flutter给我们提供了很多而且很好用的内置动画,这些动画仅仅需要简单的几行代码就可以实现一些不错的效果,Flutter的动画分 ...

随机推荐

  1. 连接MongoDB+Docker安装MongoDB

    一.连接MongoDB 工具:studio 3T 下载:https://studio3t.com/download-thank-you/?OS=win64 1.无设置密码 最终成功页面 2.设置了密码 ...

  2. 第6章. 部署到GithubPages

    依托GitHub Pages 服务,可以把 vuepress 编译后的 博客静态文件 放置到该平台,那么就可以把静态页面发布出来,就会实现了不用购买云服务器就可以发布静态页面的功能. 1. 创建仓库 ...

  3. Spring Boot 整合邮件服务

    参考教程 首先参考了 Spring Boot整合邮件配置,这篇文章写的很好,按照上面的操作一步步走下去就行了. 遇到的问题 版本配置 然后因为反复配置版本很麻烦,所以参考了 如何统一引入 Spring ...

  4. MySQL事务和锁实战篇

    文章目录 MySQL事务和锁 事务 事务的控制语句 事务隔离级别设置 脏读 不可重复读 幻读 锁机制 InnoDB的行级锁 锁实战 死锁 总结 MySQL事务和锁 事务 说到关系型的数据库的事务,相信 ...

  5. Swift WisdomProtocol 面向协议编程(下)

    WisdomProtocol 面向协议编程(下) @[TOC] WisdomProtocol SDK 面向协议编程 # Welcome to use WisdomProtocol WisdomProt ...

  6. #Powerquery 数据结构基础 一维数据与二维数据

    本文参考了采悟老师的文章,推荐大家看原文,本文为笔记随笔 https://mp.weixin.qq.com/s?__biz=MzA4MzQwMjY4MA==&mid=2484068871&am ...

  7. 2022-10-17:特殊的二进制序列是具有以下两个性质的二进制序列: 0 的数量与 1 的数量相等。 二进制序列的每一个前缀码中 1 的数量要大于等于 0 的数量。 给定一个特殊的二进制序列 S,以

    2022-10-17:特殊的二进制序列是具有以下两个性质的二进制序列: 0 的数量与 1 的数量相等. 二进制序列的每一个前缀码中 1 的数量要大于等于 0 的数量. 给定一个特殊的二进制序列 S,以 ...

  8. 2021-12-08:扑克牌中的红桃J和梅花Q找不到了,为了利用剩下的牌做游戏,小明设计了新的游戏规则: 1) A,2,3,4....10,J,Q,K分别对应1到13这些数字,大小王对应0; 2) 游

    2021-12-08:扑克牌中的红桃J和梅花Q找不到了,为了利用剩下的牌做游戏,小明设计了新的游戏规则: A,2,3,4-10,J,Q,K分别对应1到13这些数字,大小王对应0; 游戏人数为2人,轮流 ...

  9. Cypress 踩坑记 - DOM 遮挡

    Cypress 是一个非常流行的测试工具,然而实际使用过程中发现一些问题,这里做些记录. 问题发现 在 Cypress 下 click 是非常常用的指令,然而在一些特殊场景下 click 并不能如想象 ...

  10. Java发展史、JDK环境配置、运行原理及开发工具(学习的前期准备)

    一.Java的历史和三大版本 1.Java的发展史 Java由sun公司开发,Java之父James Gosling,Java是一门面向对象的编程语言,也称为"高级编程语言" Ja ...