HabitGo:一个 Flutter 习惯追踪 App 的深度技术剖析

写在前面

最近用 Flutter 从零到一撸了一个习惯追踪应用 HabitGo ,代码量不大但五脏俱全,涵盖了状态管理、本地存储、AI 集成、自定义动画、主题系统、国际化、订阅变现等多个技术点。本文不打算写「如何用 Flutter 做习惯 App」的入门教程,而是聚焦在架构设计思路具体技术实现细节上,希望能给同样在做 Flutter 中大型项目的同学一些参考。

先放一张架构总览:

bash 复制代码
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 + ARB 10 种语言,含阿拉伯语 RTL

二、架构设计:Feature-First + 分层架构

2.1 为什么选择 Feature-First?

Flutter 项目常见的目录结构有三种:

  • Layer-First (按层分):models/services/views/ 全挤在一起,模块一多就成灾难。
  • Type-First :按文件类型分(providers/widgets/pages/),跨了模块理解成本高。
  • Feature-First:按功能域分,每个 feature 自成一体,内部分层。

HabitGo 选择了 Feature-First + 内部分层 的组合:

bash 复制代码
features/habits/
├── domain/          # 数据模型(Habit)
├── data/            # 仓库(HabitRepository)
├── providers/       # Riverpod Provider 定义
└── presentation/    # UI 层
    ├── pages/       # 页面
    └── widgets/     # 组件

这样做的优势很明显:

  1. 高内聚:一个 feature 的所有代码在一个目录下,改习惯相关功能不需要跨 5 个目录。
  2. 低耦合 :feature 之间通过 core/ 层和 Provider 通信,不直接依赖彼此的内部实现。
  3. 可迁移:如果将来想把某个 feature 拆出去独立 package,直接拎走即可。

2.2 core/ 层的职责边界

core/ 是项目的基础设施层,提供跨 feature 共享的服务和能力:

  • ai/:AI 服务抽象接口 + DeepSeek 实现 + 离线兜底 + 个性化进化系统
  • theme/:12 套预设主题 + ThemeMode 管理
  • feedback/:自定义 Toast 服务
  • notifications/:本地推送,带智能判断逻辑
  • purchases/:RevenueCat 订阅封装,带 Web 平台 stub
  • coins/insights/monetization/:通用业务规则

核心原则:core 不依赖任何 feature,feature 通过 Provider 消费 core 的能力


三、状态管理:Riverpod 的五种 Provider 实践

项目中用到了 Riverpod 的 5 种 Provider,每种都有明确的适用场景:

3.1 StateNotifierProvider --- 复杂可变状态

当状态需要多个方法去修改,且逻辑比较复杂时,用 StateNotifier 封装:

dart 复制代码
// 习惯列表的状态管理
class HabitListNotifier extends StateNotifier<List<Habit>> {
  final HabitRepository _repo;

  HabitListNotifier(this._repo) : super([]);

  void addHabit(Habit habit) { /* ... */ }
  void updateHabit(Habit habit) { /* ... */ }
  void deleteHabit(String id) { /* ... */ }
  void toggleActive(String id) { /* ... */ }
}

final habitListProvider =
    StateNotifierProvider<HabitListNotifier, List<Habit>>((ref) {
  return HabitListNotifier(ref.read(habitRepositoryProvider));
});

同样的模式用于 ThemeModeNotifierThemePresetNotifier 等。

3.2 StateProvider --- 简单值状态

适合单一值、不需要复杂逻辑的场景:

dart 复制代码
final currentLocaleCodeProvider = StateProvider<String>((ref) => 'zh');
final currentTabProvider = StateProvider<int>((ref) => 0);

3.3 Provider --- 依赖注入 / 只读服务

适合注入 Repository、Service 等不变化的对象:

dart 复制代码
final habitRepositoryProvider = Provider<HabitRepository>((ref) {
  return HabitRepository();
});

final routerProvider = Provider<GoRouter>((ref) {
  return GoRouter(/* ... */);
});

final aiServiceProvider = Provider<AiService>((ref) {
  return RemoteAiService();
});

3.4 StreamProvider --- 流式数据

RevenueCat 的授权状态需要持续监听变化:

dart 复制代码
final revenueCatEntitlementProvider = StreamProvider<EntitlementInfo?>((ref) {
  return RevenueCatService.entitlementStream;
});

3.5 FutureProvider --- 异步一次性数据

dart 复制代码
final aiChatRemainingProvider = FutureProvider<int>((ref) async {
  final prefs = await SharedPreferences.getInstance();
  return prefs.getInt('ai_chat_count') ?? 0;
});

小结

Provider 类型 适用场景
StateNotifierProvider 复杂可变状态 + 多操作方法
StateProvider 简单值(布尔、数字、字符串)
Provider 依赖注入、只读服务
StreamProvider 持续变化的异步数据流
FutureProvider 一次性异步数据

这种细粒度使用不同 Provider 类型的做法,避免了「一把梭用 ChangeNotifier」带来的性能和不必要的 rebuild。


四、本地存储:Hive + SharedPreferences 双轨策略

4.1 为什么是双轨?

存储方式 存放内容 原因
Hive Habit、CheckinLog、WishItem 结构化数据、需要高性能读写、数据量大
SharedPreferences 主题设置、金币数、AI 用量、用户资料 轻量 KV,存取简单

如果全扔 Hive,配置类数据需要开 Box 很浪费;如果全扔 SharedPreferences,结构化数据序列化成本高。两者互补是最佳实践

4.2 Hive 的 Adapter 手写

Hive 官方推荐用 hive_generator 自动生成 Adapter,但我选择了手写,原因有二:

  1. 减少依赖:少一个 build_runner 的构建步骤,CI/CD 更快。
  2. 完全可控 :字段变更时直接改,不需要每次 flutter pub run build_runner build
dart 复制代码
class HabitAdapter extends TypeAdapter<Habit> {
  @override
  final int typeId = 0;

  @override
  Habit read(BinaryReader reader) {
    final numOfFields = reader.readByte();
    final fields = <int, dynamic>{
      for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
    };
    return Habit(
      id: fields[0] as String,
      name: fields[1] as String,
      icon: fields[2] as String?,
      color: fields[3] as int,
      weekDays: (fields[4] as List).cast<int>(),
      createdAt: fields[5] as DateTime,
      isActive: fields[6] as bool? ?? true,
      category: fields[7] as String?,
    );
  }

  @override
  void write(BinaryWriter writer, Habit obj) {
    writer.writeByte(8);  // 8 个字段
    writer.writeByte(0); writer.write(obj.id);
    writer.writeByte(1); writer.write(obj.name);
    writer.writeByte(2); writer.write(obj.icon);
    writer.writeByte(3); writer.write(obj.color);
    writer.writeByte(4); writer.write(obj.weekDays);
    writer.writeByte(5); writer.write(obj.createdAt);
    writer.writeByte(6); writer.write(obj.isActive);
    writer.writeByte(7); writer.write(obj.category);
  }
}

手写 Adapter 的关键在于字段编号(0-7)必须与 @HiveField(n) 注解保持一致,添加新字段时往后追加即可,不会破坏已有数据的兼容性。

4.3 初始化流程

dart 复制代码
class HiveInit {
  static late Box<Habit> habitsBox;
  static late Box<CheckinLog> checkinsBox;

  static Future<void> init() async {
    final appDocDir = await getApplicationDocumentsDirectory();
    Hive.init(appDocDir.path);

    Hive.registerAdapter(HabitAdapter());
    Hive.registerAdapter(CheckinLogAdapter());

    habitsBox = await Hive.openBox<Habit>('habits');
    checkinsBox = await Hive.openBox<CheckinLog>('checkins');
  }
}

全局只开两个 Box,通过 static late 暴露给各处使用,避免了到处 open/close 的资源泄漏。


五、AI 深度集成:不止是套个 API

这是整个项目技术密度最高的部分。AI 不是简单的「接个聊天接口」,而是深度融入产品各个角落

5.1 服务抽象层

先看看接口设计:

dart 复制代码
abstract class AiService {
  Future<String> generateEncouragement({
    required int streakDays,
    required int todayDone,
    required int todayTotal,
  });

  Future<String> analyzeInsights();
  Future<List<HabitTemplate>> recommendTemplates();
  Future<String> explainMissedDay({required DateTime date});
}

为什么要有抽象接口?三个原因:

  1. Mock 友好:测试时可以注入 Mock 实现。
  2. 离线兜底 :网络挂了有 OfflineAiService 顶上,用户感知不到异常。
  3. 模型切换 :将来换 OpenAI、Claude 或自有模型,RemoteAiService 内部切换即可,调用方零改动。

5.2 DeepSeek API 调用

核心调用链路很简单:

bash 复制代码
UI → Provider → AiService → http.post(deepseek/v1/chat/completions)

但细节远不止这么简单:

  • 多语言 System Prompt:根据当前 locale 动态选择中文/英文/日文等系统提示词,确保 AI 回答语言匹配。
  • 每日用量限制 :免费版每天 10 次对话,通过 SharedPreferences 计数 + FutureProvider 暴露剩余次数。
  • 个性化注入:用户对 AI 回答点「有用」后,Q&A 样本会被追加到 system prompt 的 addendum 中(上限 30 条),让 AI 越来越懂用户。

5.3 AI 个性化进化系统

这是我最喜欢的设计之一。实现很简单但效果很好:

dart 复制代码
class AiPersonalization {
  static const _maxDaily = 3;
  static const _maxTotal = 30;

  Future<void> addFeedback(String question, String answer) async {
    final prefs = await SharedPreferences.getInstance();

    // 每日上限检查
    final today = DateTime.now().toIso8601String().substring(0, 10);
    final todayCount = prefs.getInt('ai_feedback_$today') ?? 0;
    if (todayCount >= _maxDaily) return;

    // 总数上限检查
    final samples = prefs.getStringList('ai_feedback_samples') ?? [];
    if (samples.length >= _maxTotal) return;

    // 存入样本
    samples.add('Q: $question\nA: $answer');
    await prefs.setStringList('ai_feedback_samples', samples);
    await prefs.setInt('ai_feedback_$today', todayCount + 1);
  }

  String buildAddendum() {
    final prefs = SharedPreferences.getInstance(); // 实际代码用了 await
    final samples = prefs.getStringList('ai_feedback_samples') ?? [];
    if (samples.isEmpty) return '';
    return '\n---\n以下是用户之前标记为"有用"的对话,请参考其风格:\n${samples.join('\n\n')}';
  }
}

这样每次 AI 调用时,buildAddendum() 返回的内容会被拼接到 system prompt 末尾,AI 的回答风格会逐渐向用户偏好靠拢。用极低的成本实现了「千人千面」的效果。

5.4 自研 Markdown 轻量渲染器

AI 返回的内容通常带 Markdown 格式。现有方案如 flutter_markdown 太重(依赖 webview 级别的解析器),所以造了个轻量轮子:

dart 复制代码
class MarkdownLite extends StatelessWidget {
  final String source;
  // 支持的语法:
  // - `**bold**` 加粗
  // - `*italic*` 斜体
  // - `` `code` `` 行内代码
  // - `# ` `## ` `### ` 标题
  // - `- ` 无序列表
  // - `1. ` 有序列表

  List<Widget> _parse(String input, TextStyle style, BuildContext context) {
    final lines = input.split('\n');
    // 逐行匹配正则,构建对应的 Text.rich() 或 Row
    // ...
  }
}

核心思路:逐行扫描 + 正则匹配,每行生成对应的 Widget。对于 DeepSeek 的输出格式来说完全够用,而且性能远优于通用 Markdown 解析器。


六、几个有意思的技术实现

6.1 CustomPainter 实现打卡勾选动画

这是用户每天都会交互的核心组件,体验优化到了细节:

dart 复制代码
class CheckinCheckbox extends StatefulWidget {
  final bool checked;
  final Color color;
  final VoidCallback onTap;
  // ...
}

class _CheckinCheckboxState extends State<CheckinCheckbox>
    with SingleTickerProviderStateMixin {
  late final AnimationController _ctrl;
  late final Animation<double> _scale;
  late final Animation<double> _checkProgress;

  @override
  void initState() {
    super.initState();
    _ctrl = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 360),
    );
    _scale = Tween(begin: 0.85, end: 1.0)
        .animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeOutBack));
    _checkProgress = CurvedAnimation(
        parent: _ctrl, curve: Curves.easeOutCubic);
  }

  @override
  void didUpdateWidget(CheckinCheckbox old) {
    super.didUpdateWidget(old);
    if (widget.checked != old.checked) {
      if (!kIsWeb) HapticFeedback.lightImpact(); // 触觉反馈
      if (widget.checked) {
        _ctrl.forward(from: 0);
      } else {
        _ctrl.reverse();
      }
    }
  }
}

动画拆解为三段:

  1. scale 变化 (0.85 → 1.0),使用 easeOutBack 曲线产生弹性回弹效果。
  2. 颜色过渡 (透明 → 主题色),通过 Color.lerp() 插值。
  3. 勾号绘制 ,通过 _CheckPainter_checkProgress 参数控制路径绘制进度。

三段动画由同一个 AnimationController 驱动,didUpdateWidget 中根据状态变化自动触发,完全无手动状态管理。

6.2 纯 Overlay Toast 系统

Flutter 原生的 SnackBar 有个痛点:在 Material 3 下会继承 ThemeData 的配色,导致背景色不可控。于是直接基于 Overlay 实现了自定义 Toast:

dart 复制代码
class ToastService {
  static OverlayEntry? _current;
  static final _queue = <_ToastItem>[];

  static void _enqueue(String message, {IconData? icon, ...}) {
    _queue.add(_ToastItem(/* ... */));
    _showNext();
  }

  static void _showNext() {
    if (_current != null || _queue.isEmpty) return;
    final item = _queue.removeAt(0);

    late final OverlayEntry entry;
    entry = OverlayEntry(
      builder: (context) => _ToastWidget(
        message: item.message,
        onDismiss: () {
          entry.remove();
          _current = null;
          _showNext();  // 队列消费下一条
        },
      ),
    );
    _current = entry;
    overlay.insert(entry);
  }

  // 便捷方法
  static void success(String msg) => _enqueue(msg,
      icon: Icons.check_circle_rounded,
      background: Color(0xFF059669));
  static void error(String msg) => _enqueue(msg,
      icon: Icons.error_rounded,
      background: Color(0xFFDC2626));
}

设计亮点:

  • 队列机制:多个 Toast 不会重叠,逐条显示。
  • 完全绕过 SnackBar:不受任何 ThemeData 干扰,背景色、文字色完全自定义。
  • 内置三种样式success() / error() / info(),调用方只需一行代码。

6.3 可拖动的 FAB 按钮

首页的 FAB(+ 号按钮)支持水平拖动,拖动超过 50px 会自动切换收起/展开状态:

  • 展开态:显示 + 号,点击跳转新建习惯。
  • 收起态:显示小箭头,提示用户可拖动。
dart 复制代码
// 伪代码展示核心逻辑
GestureDetector(
  onHorizontalDragUpdate: (details) {
    _offset += details.delta.dx;
    setState(() {});
  },
  onHorizontalDragEnd: (details) {
    if (_offset.abs() > 50) {
      setState(() => _fabCollapsed = !_fabCollapsed);
    }
    _offset = 0;  // 复位
    setState(() {});
  },
  child: AnimatedContainer(
    duration: Duration(milliseconds: 300),
    curve: Curves.easeOutBack,
    width: _fabCollapsed ? 48 : 56,
    child: _fabCollapsed
        ? Icon(Icons.chevron_right)
        : Icon(Icons.add),
  ),
)

配合 AnimatedContainer + Curves.easeOutBack,状态切换有自然的弹性动效,交互手感很好。

6.4 年度热力图(GitHub 风格)

统计页的核心视觉组件,类似 GitHub 的 contribution graph:

dart 复制代码
class YearlyHeatmapWidget extends ConsumerWidget {
  final int year;
  final bool compact;  // 支持紧凑/大图两种模式

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final data = ref.watch(yearlyHeatmapProvider(year));
    return data.when(
      data: (checkins) => compact
          ? _CompactView(year: year, checkins: checkins)
          : _BigGridView(year: year, checkins: checkins),
      // loading / error ...
    );
  }
}

实现要点:

  • Map<DateTime, int> 作为数据源,key 是日期,value 是打卡数量。
  • 颜色深度由打卡数映射到 0-4 级色阶,视觉上区分「无 / 少量 / 中等 / 大量 / 全勤」。
  • 紧凑模式:12 列水平排列,适合列表页嵌入。
  • 大图模式:每行 7 格(周一至周日),每个月可点击展开查看详情。

6.5 平台条件导入

项目中多处使用了 Dart 的平台条件导入,解决 Web 和原生平台的差异化问题:

dart 复制代码
// 通知服务
import '_fln_stub.dart' if (dart.library.io) '_fln_real.dart';

// 时区支持
import '_tz_stub.dart'
    if (dart.library.io) '_tz_io.dart'
    if (dart.library.html) '_tz_web.dart';

// RevenueCat
import '_purchases_stub.dart' if (dart.library.io) '_purchases_real.dart';

这对 Flutter Web 支持非常关键:flutter_local_notificationstimezoneRevenueCat 在 Web 平台上不兼容,通过条件导入注入 stub 实现,确保代码在任何平台都能编译通过且不崩溃。


七、主题系统:Material 3 + 12 套预设

7.1 预设主题注册

dart 复制代码
class ThemePreset {
  final String name;
  final Color seed;
  final LinearGradient gradient;
  final bool free;  // 免费 or 会员专属
}

class ThemePresets {
  static const indigo = ThemePreset('靛蓝',
      Color(0xFF6366F1),
      LinearGradient(colors: [Color(0xFF6366F1), Color(0xFF8B5CF6)]),
      true);
  static const rose = ThemePreset('薄暮',
      Color(0xFFEC4899),
      LinearGradient(colors: [Color(0xFFEC4899), Color(0xFFF43F5E)]),
      true);
  static const amber = ThemePreset('暖阳', ...);   // 会员
  static const emerald = ThemePreset('翡翠', ...); // 会员
  // ... 共 12 套
}
  1. 前 2 套免费,后 10 套会员专属 --- 这是产品策略层面的技术落地。
  2. 每套有独立的 seed color (用于 ColorScheme.fromSeed())和 gradient(用于卡片、按钮等装饰)。
  3. 用户选择的主题通过 SharedPreferences 持久化,启动时恢复。

7.2 ThemeData 构建

dart 复制代码
ThemeData _buildTheme(Color seed, Brightness brightness) {
  final scheme = ColorScheme.fromSeed(
    seedColor: seed,
    brightness: brightness,
  );
  return ThemeData(
    useMaterial3: true,
    colorScheme: scheme,
    cardTheme: CardThemeData(
      elevation: 0,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(20),
        side: BorderSide(color: scheme.outlineVariant, width: 0.5),
      ),
    ),
    inputDecorationTheme: InputDecorationTheme(
      borderRadius: BorderRadius.circular(14),
      // ...
    ),
    // AppBarTheme、FABTheme、NavigationBarTheme 等全面自定义
  );
}

细节:

  • 圆角统一:卡片 20px、输入框 14px、按钮 14px,视觉一致。
  • 边框微妙outlineVariant + 0.5px 宽度,既区分层次又不喧宾夺主。
  • 深色模式:深色 scaffold 背景、不同的卡片色,暗色下也能保持好的可读性。

八、国际化的工程化实践

项目支持 10 种语言 ,含阿拉伯语(RTL 语言)。Flutter 官方国际化方案 .arb 文件 + AppLocalizations

bash 复制代码
lib/l10n/
├── app_ar.arb    # 阿拉伯语
├── app_de.arb    # 德语
├── app_en.arb    # 英语
├── app_es.arb    # 西班牙语
├── app_fr.arb    # 法语
├── app_ja.arb    # 日语
├── app_ko.arb    # 韩语
├── app_pt.arb    # 葡萄牙语
├── app_ru.arb    # 俄语
└── app_zh.arb    # 简体中文

AI 的系统提示词也随语言动态切换:

dart 复制代码
String _systemPrompt(BuildContext context) {
  final locale = Localizations.localeOf(context).languageCode;
  switch (locale) {
    case 'zh': return '你是一个习惯教练,请用中文回复...';
    case 'ja': return 'あなたは習慣コーチです。日本語で返信してください...';
    case 'en': return 'You are a habit coach. Reply in English...';
    // ... 其他语言
    default: return 'You are a habit coach...';
  }
}

九、商业化的技术实现

9.1 RevenueCat 订阅集成

dart 复制代码
class RevenueCatService {
  static final _entitlementController =
      StreamController<EntitlementInfo?>.broadcast();
  static Stream<EntitlementInfo?> get entitlementStream =>
      _entitlementController.stream;

  static Future<void> init() async {
    await Purchases.configure(
      PurchasesConfiguration('YOUR_API_KEY')
        ..entitlementVerificationMode =
            EntitlementVerificationMode.informational,
    );
    Purchases.addCustomerInfoUpdateListener((info) {
      _entitlementController.add(info.entitlements.active['premium']);
    });
  }
}

通过 StreamController 将 RevenueCat 的回调转为 Dart Stream,再包一层 StreamProvider,UI 层就能响应式地感知订阅状态变化。

9.2 免费版限制策略

dart 复制代码
// monetization 模块
bool canCreateMoreHabits(int currentCount) {
  if (isPremium) return true;
  return currentCount < 5;  // 免费版最多 5 个习惯
}

bool canUseAiChat(int todayCount) {
  if (isPremium) return true;
  return todayCount < 10;  // 免费版每天 10 次 AI 对话
}

限制规则集中在 core/monetization/ 中,UI 层调用时传入当前状态即可,策略清晰且容易调整。


十、总结与思考

技术收获

  1. Riverpod 的组合能力远超 Provider:不同类型的 Provider 各司其职,告别了「一切皆 ChangeNotifier」的混乱。
  2. Feature-First 架构值得投入:初期多花一点时间建好目录规范,后期维护成本大幅降低。
  3. 手写 Hive Adapter 并不难:Hive 的二进制协议很简单,手写一次就懂了,还能省掉代码生成的依赖。
  4. AI 集成不能止于「接个 API」:个性化、多语言 prompt、离线兜底、用量限制,这些「工程化」的细节才是产品质量的关键。
  5. Dart 的条件导入是跨平台利器 :比 kIsWeb 分支判断更优雅,编译期就能剔除不可用代码。

可改进的方向

  • 数据备份/云同步:目前纯本地存储,换设备数据就丢了。
  • Widget 测试和集成测试:目前测试覆盖不足。
  • SQLite 替代 Hive :当数据量增长到需要复杂查询时,可以考虑迁移到 drift
  • CI/CD 自动化:配一套 GitHub Actions 做 lint + test + build。

以上就是 HabitGo 项目在技术层面的完整剖析。如果你也在做 Flutter 项目,或者对某个技术点感兴趣,欢迎评论区交流讨论!

相关推荐
西西学代码1 小时前
Flutter---RefreshIndicator案例
flutter
西西学代码2 小时前
Flutter---RefreshIndicator
flutter
末代iOS程序员华仔2 小时前
从 Figma 到上架:使用 Codex 与 MCP 全流程实现 AI 聊天 App
flutter·ios·swift
西西学代码3 小时前
Flutter---StreamBuilder案例
javascript·windows·flutter
子非鱼94275 小时前
03-Flutter 鸿蒙实战 03:登录注册与本地会话持久化
网络·flutter·华为·harmonyos
AM艾玛5 小时前
Flutter 环境搭建实录
vscode·flutter
子非鱼94276 小时前
01-Flutter 鸿蒙实战 01:会说话的相册项目架构拆解
flutter·华为·harmonyos
子非鱼94276 小时前
02-Flutter 鸿蒙实战 02:Web 移植到 Flutter HarmonyOS 的页面结构
学习·flutter·华为·harmonyos
dragon7251 天前
一个 Flutter Loading 关不掉的诡异 Bug:从 showDialog 的竞态说起
flutter