创作初衷

这篇文章创作的初衷,只是为了写一个有关日历类的软件供自己使用,考虑到自己从来还没有使用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. Java设计模式 —— 装饰模式

    12 装饰模式 12.1 装饰模式概述 Decorator Pattern: 动态地给一个对象增加一些额外的职责.提供一种比使用子类更加灵活的方案来扩展功能. 装饰模式是一种用于替代继承的技术,通过一 ...

  2. Git提交代码仓库的两种方式

    目录 一: 两种本地与远程仓库同步 1 git 远程仓库 提交本地版本库操作 提交到远程版本库操作 1.Git 全局设置: 2.增加一个远程仓库地址 3.查询当前存在的远程仓库 5.本地版本库内容提交 ...

  3. 如何确定 this 指向?改变 this 指向的方式有哪些?

    this 指向: 1. 全局上下文(函数外) 无论是否为严格模式,均指向全局对象.注意:严格模式下全局对象为undifined 2. 函数上下文(函数内) 默认的,指向函数的调用对象,且是最直接的调用 ...

  4. php 中文地址伪静态,.htaccess实现含中文的url伪静态跳转

    Tags伪静态 RewriteRule ^tags.html/tags.php RewriteRule ^tags/(.)(??.))*.html$ tags.php?/$1 RewriteRule ...

  5. 学习C语言的第一天

    今天学习C语言学习了三个部分: 第一个部分是软件环境的搭建,如何搭建一个项目 使用工具:visual studio 2010 搭建过程:新建项目.配置设置(主要是解决运行后一闪而过的问题) 第二部分是 ...

  6. LangChain vs Semantic Kernel

    每当向他人介绍 Semantic Kernel, 会得到的第一个问题就是 Semantic Kernel 类似于LangChain吗,或者是c# 版本的LangChain吗? 为了全面而不想重复的回答 ...

  7. 【介绍】C++五种迭代器

    目录 1. 输入迭代器(Input Iterator): 2. 输出迭代器(Output Iterator): 3. 前向迭代器(Forward Iterator): 4. 双向迭代器(Bidirec ...

  8. KMP算法学习笔记

    总算把这个东西搞懂了...... KMP是一个求解字符串匹配问题的算法. 这个东西的核心是一个\(next\)数组,\(next_i\)表示字符串第\(0\sim i\)项的相同的前缀和后缀的最大长度 ...

  9. Django笔记三十六之单元测试汇总介绍

    本文首发于公众号:Hunter后端 原文链接:Django笔记三十六之单元测试汇总介绍 Django 的单元测试使用了 Python 的标准库:unittest. 在我们创建的每一个 applicat ...

  10. PM系统成本科目挂接教程-如何查手册和看帮助文档

    如果这么简单的问题都无法入门只能说回炉重造吧孩子. ---by SheZQ 正文 成本科目挂接作为PM系统最基本的取数依据,数据汇总的根本,是必须要会的技能.如果没有挂接,就会出现空值或者0值. 摘自 ...