創作初衷
這篇文章創作的初衷,只是為了寫一個有關日歷類的軟體供自己使用,考慮到自己從來還沒有使用flutter正式創作一個app,因此磨刀霍霍想試一試,
至于為什么要做一款日歷軟體,因為發現市面上的關于萬年歷的軟體都有很多廣告,想著自己也能做,就做個給自己用,同時里面包含了額外的模塊,包括萬年歷、天氣以及小常識等等,,,
創作程序
由于自己是flutter小白,對Dart語言也是一知半解,因此想在快速的時間內去完成一款app,就可能得翻破flutter官網相關的檔案,效率不見得很高,因此主要結合chatGPT給我做知識掃盲以及方案選型建議,
比如我讓chatGPT給我生成一段日歷的核心邏輯:
然后不斷加以修正,比如可以支持從星期日開始:
雖然不是很熟悉dart語法,但是并不是很影響我讀懂代碼,一般做過React或者宣告式語言(android compose/swift)語言的人,上手flutter會相當快,
chatGPT在問答的程序中,也會說一些胡話,比如我在做天氣模塊的時候,需要實作一個向上滾動,標題部分自動縮小,并保證滾動條在標題下方滾動的功能,但是chatGPT并不能給我正確的回答,準確的說,它能給我回答,但是大多都是它胡謅的,
所以在使用這類AI工具的時候,需要自己識別它給出的到底是不是一個正確的答案,可以不斷去試錯,切不可一條路走到黑,無腦去相信,
關于如何精準使用chatGPT做問答、搜索、創作,以及原始碼決議,我司郵件每天都有討論,歡迎加入探討,
說(遇)說(到)重(的)點(坑)
幾個重要的庫或選擇
- 路由:fluro
- 狀態管理:provider
- 網路:dio
- 本地sqlite:sqflite
- 農歷:lunar
- 天氣Api:和風天氣
至于為什么這么選擇?我都是在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能給你很好的入門指導,雖然胡說的不一定準,但是不說肯定是啥都不知道????
最后貼貼代碼倉庫:
- august:八月 flutter app
- august-backend:后臺server以及管理
我只是一只小菜鳥,但我并沒有停下學習的腳步^_^ 另外,覺得這篇文章不錯,可以隨手點個贊么?如需轉載,請標明出處喲,求星星????僅供學習交流,勿商用!!!
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/556818.html
標籤:其他
下一篇:返回列表