HabitGo:一个 Flutter 习惯追踪 App 的深度技术剖析
写在前面最近用 Flutter 从零到一撸了一个习惯追踪应用HabitGo,代码量不大但五脏俱全,涵盖了状态管理、本地存储、AI 集成、自定义动画、主题系统、国际化、订阅变现等多个技术点。本文不打算写「如何用 Flutter 做习惯 App」的入门教程,而是聚焦在架构设计思路和具体技术实现细节上,希望能给同样在做 Flutter 中大型项目的同学一些参考。先放一张架构总览:lib/ ├── app/ # 应用级配置(路由、主题入口) ├── core/ # 基础设施层 │ ├── ai/ # AI 服务(DeepSeek + 离线兜底 + 个性化) │ ├── coins/ # 金币系统 │ ├── database/ # Hive 初始化 │ ├── feedback/ # 自定义 Toast │ ├── insights/ # 智能洞察 │ ├── monetization/ # 免费版限制 │ ├── notifications/ # 本地通知 │ ├── profile/ # 用户档案 │ ├── purchases/ # RevenueCat 订阅 │ ├── security/ # 设备完整性检测 │ └── theme/ # 12 套主题预设 ├── features/ # 功能模块(Feature-First) │ ├── ai/ # AI 对话 │ ├── anniversary/ # 周年纪念 │ ├── checkin/ # 打卡 │ ├── discovery/ # 发现页 │ ├── favorites/ # 收藏 │ ├── feedback/ # 用户反馈 │ ├── habits/ # 习惯管理(核心) │ ├── lesson/ # 每日一课 │ ├── mailbox/ # 成就邮箱 │ ├── mood/ # 心情日记 │ ├── profile/ # 个人中心 │ ├── settings/ # 设置/隐私/条款 │ ├── splash/ # 启动页 │ ├── statistics/ # 数据统计 │ ├── templates/ # 模板市场 │ └── wishbank/ # 心愿银行 ├── l10n/ # 10 种语言翻译 └── shared/widgets/ # 共享 UI 组件一、技术栈一览分类选型原因框架Flutter 3.x + Dart 3.12+跨平台,Material 3 原生支持状态管理Riverpod 2.6编译安全,无 BuildContext 依赖,Provider 的进化版路由GoRouter 14.8声明式路由,支持 ShellRoute、深度链接本地数据库Hive 2.2纯 Dart 实现,无原生依赖,极快的键值存储配置存储SharedPreferences轻量 KV,适合非结构化配置图表fl_chart月度/年度热力图、周度折线图订阅支付RevenueCat 9.8跨平台订阅管理,开箱即用的 Paywall UI本地通知flutter_local_notifications 18精确定时 + 智能推送AI 大模型DeepSeek API (deepseek-chat)高性价比,中文能力强国际化Flutter 原生 Intl + ARB10 种语言,含阿拉伯语 RTL二、架构设计:Feature-First + 分层架构2.1 为什么选择 Feature-First?Flutter 项目常见的目录结构有三种:Layer-First(按层分):models/、services/、views/全挤在一起,模块一多就成灾难。Type-First:按文件类型分(providers/、widgets/、pages/),跨了模块理解成本高。Feature-First:按功能域分,每个 feature 自成一体,内部分层。HabitGo 选择了Feature-First + 内部分层的组合:features/habits/ ├── domain/ # 数据模型(Habit) ├── data/ # 仓库(HabitRepository) ├── providers/ # Riverpod Provider 定义 └── presentation/ # UI 层 ├── pages/ # 页面 └── widgets/ # 组件这样做的优势很明显:高内聚:一个 feature 的所有代码在一个目录下,改习惯相关功能不需要跨 5 个目录。低耦合:feature 之间通过core/层和 Provider 通信,不直接依赖彼此的内部实现。可迁移:如果将来想把某个 feature 拆出去独立 package,直接拎走即可。2.2 core/ 层的职责边界core/是项目的基础设施层,提供跨 feature 共享的服务和能力:ai/:AI 服务抽象接口 + DeepSeek 实现 + 离线兜底 + 个性化进化系统theme/:12 套预设主题 + ThemeMode 管理feedback/:自定义 Toast 服务notifications/:本地推送,带智能判断逻辑purchases/:RevenueCat 订阅封装,带 Web 平台 stubcoins/、insights/、monetization/:通用业务规则核心原则:core 不依赖任何 feature,feature 通过 Provider 消费 core 的能力。三、状态管理:Riverpod 的五种 Provider 实践项目中用到了 Riverpod 的 5 种 Provider,每种都有明确的适用场景:3.1 StateNotifierProvider — 复杂可变状态当状态需要多个方法去修改,且逻辑比较复杂时,用StateNotifier封装:// 习惯列表的状态管理classHabitListNotifierextendsStateNotifierListHabit{finalHabitRepository_repo;HabitListNotifier(this._repo):super([]);voidaddHabit(Habithabit){/* ... */}voidupdateHabit(Habithabit){/* ... */}voiddeleteHabit(Stringid){/* ... */}voidtoggleActive(Stringid){/* ... */}}finalhabitListProvider=StateNotifierProviderHabitListNotifier,ListHabit((ref){returnHabitListNotifier(ref.read(habitRepositoryProvider));});同样的模式用于ThemeModeNotifier、ThemePresetNotifier等。3.2 StateProvider — 简单值状态适合单一值、不需要复杂逻辑的场景:finalcurrentLocaleCodeProvider=StateProviderString((ref)='zh');finalcurrentTabProvider=StateProviderint((ref)=0);3.3 Provider — 依赖注入 / 只读服务适合注入 Repository、Service 等不变化的对象:finalhabitRepositoryProvider=ProviderHabitRepository((ref){returnHabitRepository();});finalrouterProvider=ProviderGoRouter((ref){returnGoRouter(/* ... */);});finalaiServiceProvider=ProviderAiService((ref){returnRemoteAiService();});3.4 StreamProvider — 流式数据RevenueCat 的授权状态需要持续监听变化:finalrevenueCatEntitlementProvider=StreamProviderEntitlementInfo?((ref){returnRevenueCatService.entitlementStream;});3.5 FutureProvider — 异步一次性数据finalaiChatRemainingProvider=FutureProviderint((ref)async{finalprefs=awaitSharedPreferences.getInstance();returnprefs.getInt('ai_chat_count')??0;});小结Provider 类型适用场景StateNotifierProvider复杂可变状态 + 多操作方法StateProvider简单值(布尔、数字、字符串)Provider依赖注入、只读服务StreamProvider持续变化的异步数据流FutureProvider一次性异步数据这种细粒度使用不同 Provider 类型的做法,避免了「一把梭用 ChangeNotifier」带来的性能和不必要的 rebuild。四、本地存储:Hive + SharedPreferences 双轨策略4.1 为什么是双轨?存储方式存放内容原因HiveHabit、CheckinLog、WishItem结构化数据、需要高性能读写、数据量大SharedPreferences主题设置、金币数、AI 用量、用户资料轻量 KV,存取简单如果全扔 Hive,配置类数据需要开 Box 很浪费;如果全扔 SharedPreferences,结构化数据序列化成本高。两者互补是最佳实践。4.2 Hive 的 Adapter 手写Hive 官方推荐用hive_generator自动生成 Adapter,但我选择了手写,原因有二:减少依赖:少一个 build_runner 的构建步骤,CI/CD 更快。完全可控:字段变更时直接改,不需要每次flutter pub run build_runner build。classHabitAdapterextendsTypeAdapterHabit{@overridefinalint typeId=0;@overrideHabit