• flutter小白是如何在一周内用chatGPT开发一款App的


    创作初衷

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

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

    calendar sense settings

    创作过程

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

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

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

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

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

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

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

    discuss.png

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

    几个重要的库或选择

    至于为什么这么选择?我都是在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了:

    tab.gif

    原因是这个组件在路由切换的时候,都会重新渲染一份,所以动画肯定是没有的,无奈之下,就提取了一个公共页,采用分支逻辑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的动画。

    calendar.gif

    关于本地存储

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

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

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

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

    日历事件采用本地sqlite

    class DatabaseProvider {
    // ...
    Future _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> 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】

    天气滑动动画

    weather.gif

    为了实现上面的动画,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就解决了。

    主题部分

    theme.gif

    准备了两套颜色,明亮色以及暗黑色【颜色部分可能还是得有设计师来,这块真是搞得我头痛】,然后使用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

    jsonToDart.png

    网上也有使用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能给你很好的入门指导,虽然胡说的不一定准,但是不说肯定是啥都不知道😂😂

    最后贴贴代码仓库:

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

  • 相关阅读:
    【Hadoop】第三篇--Hadoop运行模式
    韦东山D1S板子——汇编启动代码第一行分析(.long 0x0300006f)
    宝安水环境管控平台(Ionic/Angular 移动端) 问题记录
    【Java面试】面试自閟了!工作5年的小伙伴今天面试被吊打问我,并行和并发有什么区别?
    关于地图GIS的一次实践整理(下) Redis的GIS实践
    Chromebook文件夹应用新功能
    【win10常用命令】
    GALIL运动控制卡维修控制器维修DMC-1840
    猿如意开发工具|JetBrains GoLand
    2024年保安员证考试题库
  • 原文地址:https://www.cnblogs.com/rynxiao/p/17532662.html