Flutter三方库适配OpenHarmony【habit_tracker】习惯追踪器项目完整实战

前言

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

habit_tracker 是一个基于 Flutter 的习惯追踪器项目,核心代码位于 lib/main.dart。项目默认展示 Exercise、Read、Meditate、Drink Water 四个习惯,每个习惯包含图标、名称、连续天数、一周 7 天打卡状态、周完成进度和删除按钮。用户可以点击每天的圆形按钮切换完成状态,也可以通过右下角 FloatingActionButton 打开新增习惯弹窗,输入习惯名称并选择图标后添加到列表。

这个项目适合讲解 Flutter 列表型工具应用在 OpenHarmony 上的适配过程。它覆盖了 数据模型 getter 派生状态ListView.builder 动态渲染GestureDetector 点击切换AlertDialog 新增弹窗StatefulBuilder 弹窗局部状态LinearProgressIndicator 进度展示FloatingActionButton 操作入口Material 3 卡片布局

在这里插入图片描述

图片说明:本文围绕 Flutter 动态列表、弹窗状态和 OpenHarmony 承载工程展开,所有关键代码均来自 habit_tracker 的真实源码。

习惯追踪器的关键不是“点亮一个圆点”,而是让模型、列表、进度、弹窗和状态刷新形成稳定闭环。

一、项目背景与目标

1.1 项目定位

habit_tracker 是一个轻量习惯打卡应用。页面以卡片列表展示多个习惯,每张卡片展示该习惯的一周完成情况。用户可以新增、删除习惯,也可以在每个习惯下方点击周一到周日的圆点切换完成状态。

当前项目真实支持的功能包括:

  • 默认展示 4 个习惯。
  • 每个习惯有名称、图标、周进度和连续天数。
  • 每个习惯有 7 个布尔打卡状态。
  • 点击任意一天圆点切换完成和未完成。
  • 完成状态显示绿色圆点和白色 check 图标。
  • 未完成状态显示浅灰圆点。
  • 使用 completedDays 统计本周完成天数。
  • 使用 progress 计算本周完成比例。
  • 使用 LinearProgressIndicator 展示每周进度。
  • 使用删除按钮移除习惯。
  • 使用 FloatingActionButton 打开新增弹窗。
  • 新增弹窗支持输入习惯名称。
  • 新增弹窗支持从 10 个图标中选择。
  • 弹窗内使用 StatefulBuilder 刷新选中图标边框。

1.2 技术目标

本文围绕真实源码拆解以下内容:

  1. Flutter 应用入口和绿色 Material 3 主题。
  2. Habit 模型如何保存习惯名称、图标、周进度和连续天数。
  3. completedDays 如何从 weekProgress 派生完成天数。
  4. progress 如何计算 0 到 1 的进度值。
  5. _habits 默认数据如何组织。
  6. _dayNames 如何渲染一周标签。
  7. _toggleHabitDay 如何切换某天打卡状态。
  8. _addHabit 如何使用弹窗新增习惯。
  9. StatefulBuilder 如何管理弹窗内部选中图标。
  10. ListView.builder 如何渲染习惯卡片。
  11. OpenHarmony 侧如何验证列表、弹窗、点击、进度和删除。

1.3 核心实现速览

能力 当前实现 适配关注点
应用入口 runApp(const HabitTrackerApp()) 确认首屏加载
主题 ColorScheme.fromSeed(seedColor: Colors.green) 确认绿色 Material 3 样式
数据模型 Habit 确认字段和 getter
默认列表 _habits 确认 4 个默认习惯
一周状态 List<bool> weekProgress 确认 7 天打卡
打卡切换 _toggleHabitDay 确认点击刷新
新增习惯 _addHabit + AlertDialog 确认输入和图标选择
删除习惯 _deleteHabit 确认列表移除
周进度 LinearProgressIndicator 确认比例展示
弹窗局部状态 StatefulBuilder 确认选中图标边框

二、环境准备与工程结构

2.1 工程结构

项目保持 Flutter 标准结构,同时包含 OpenHarmony 平台工程。

文件或目录 作用
lib/main.dart 应用入口、习惯模型、列表状态、弹窗和 UI
pubspec.yaml SDK 约束、Flutter 依赖和 Material 图标配置
analysis_options.yaml Flutter lint 规则
test/ Flutter 测试目录
ohos/ OpenHarmony 平台承载工程
README.md 项目说明文件

当前业务逻辑集中在 lib/main.dart,没有引入数据库、状态管理框架或持久化插件。

2.2 依赖配置

项目使用 Dart SDK ^3.9.2,依赖 Flutter SDK。

environment:
  sdk: ^3.9.2

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.8

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^5.0.0

flutter:
  uses-material-design: true

习惯列表、新增弹窗和进度展示全部由 Flutter Material 基础组件完成。

2.3 常用命令

flutter pub get
flutter analyze
flutter test
flutter run
命令 用途
flutter pub get 获取依赖
flutter analyze 执行静态分析
flutter test 执行测试
flutter run 在目标设备运行

OpenHarmony 调试时,还需要结合本地 Flutter OpenHarmony 工具链完成构建、安装和运行。

三、应用入口与主题配置

3.1 import 依赖

项目只引入 Flutter Material。

import 'package:flutter/material.dart';

material.dart 提供 MaterialAppScaffoldAppBarCardAlertDialogFloatingActionButtonLinearProgressIndicator 等组件。

3.2 main 函数

入口函数启动根组件。

void main() {
  runApp(const HabitTrackerApp());
}

3.3 HabitTrackerApp

根组件创建 MaterialApp

class HabitTrackerApp extends StatelessWidget {
  const HabitTrackerApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Habit Tracker',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
        useMaterial3: true,
      ),
      home: const HabitTrackerHomePage(title: 'Habit Tracker'),
    );
  }
}

这段代码包含三个关键点:

  • 应用标题为 Habit Tracker
  • 使用绿色作为主题种子色。
  • 首页为 HabitTrackerHomePage

四、Habit 数据模型

4.1 模型源码

项目定义了一个 Habit 类。

class Habit {
  String name;
  String icon;
  List<bool> weekProgress;
  int streak;

  Habit({required this.name, required this.icon, this.streak = 0})
      : weekProgress = List.generate(7, (_) => false);

  int get completedDays => weekProgress.where((b) => b).length;
  double get progress => completedDays / 7;
}

这个模型同时保存基础字段和派生进度。

4.2 字段说明

字段 类型 作用
name String 习惯名称
icon String 习惯图标文本
weekProgress List<bool> 一周 7 天完成状态
streak int 连续坚持天数
completedDays getter 本周完成天数
progress getter 本周完成比例

4.3 weekProgress 初始化

构造函数用 List.generate 创建 7 个 false

Habit({required this.name, required this.icon, this.streak = 0})
    : weekProgress = List.generate(7, (_) => false);

这表示新建习惯默认一周 7 天都未完成。

4.4 completedDays

完成天数从 weekProgress 中统计。

int get completedDays => weekProgress.where((b) => b).length;

如果一周中有 3 天为 truecompletedDays 就是 3。

4.5 progress

周进度是完成天数除以 7。

double get progress => completedDays / 7;

LinearProgressIndicator 需要 0 到 1 的值,因此这个 getter 可以直接作为进度条输入。

五、默认习惯数据

5.1 _habits 列表

页面初始有 4 个习惯。

final List<Habit> _habits = [
  Habit(name: 'Exercise', icon: '🏃', streak: 5),
  Habit(name: 'Read', icon: '📚', streak: 3),
  Habit(name: 'Meditate', icon: '🧘', streak: 7),
  Habit(name: 'Drink Water', icon: '💧', streak: 2),
];

5.2 默认数据表

名称 图标 streak
Exercise 🏃 5
Read 📚 3
Meditate 🧘 7
Drink Water 💧 2

这 4 个习惯覆盖运动、阅读、冥想、喝水等常见习惯场景。

5.3 streak 的当前边界

当前 streak 是初始展示值。

int streak;

源码中的打卡切换只修改 weekProgress,不会自动重新计算 streak。因此 streak 更像是卡片展示字段,而不是根据当前周打卡动态推导的连续天数。

5.4 一周标签

项目定义了一周 7 天的简写。

final List<String> _dayNames = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
索引 文案 含义
0 M Monday
1 T Tuesday
2 W Wednesday
3 T Thursday
4 F Friday
5 S Saturday
6 S Sunday

TS 各出现两次,这是英文星期缩写的常见简化写法。

六、打卡切换逻辑

6.1 _toggleHabitDay

点击某一天时,项目切换对应布尔值。

void _toggleHabitDay(int habitIndex, int dayIndex) {
  setState(() {
    _habits[habitIndex].weekProgress[dayIndex] =
        !_habits[habitIndex].weekProgress[dayIndex];
  });
}

habitIndex 表示第几个习惯,dayIndex 表示一周中的第几天。

6.2 状态变化示例

weekProgress = [false, false, false, false, false, false, false]
点击 dayIndex = 0
weekProgress = [true, false, false, false, false, false, false]

再次点击同一天会切回 false

6.3 UI 联动

一次打卡切换会影响三处 UI:

  1. 当天圆点颜色。
  2. 当天是否显示 check 图标。
  3. 底部周进度条和 x/7 days this week 文案。

6.4 点击区域

每个日期单元使用 GestureDetector

GestureDetector(
  onTap: () => _toggleHabitDay(index, dayIndex),
  child: Column(
    children: [
      Text(_dayNames[dayIndex]),
      Container(width: 36, height: 36),
    ],
  ),
)

这让文字和圆点区域都可以作为点击入口。

七、删除习惯逻辑

7.1 _deleteHabit

删除方法直接从列表中移除指定索引。

void _deleteHabit(int index) {
  setState(() {
    _habits.removeAt(index);
  });
}

ListView.builder 会根据新的 _habits.length 重新渲染列表。

7.2 删除按钮

卡片右上角使用红色删除图标。

IconButton(
  onPressed: () => _deleteHabit(index),
  icon: const Icon(Icons.delete, color: Colors.red),
)

7.3 删除后的状态

操作 列表变化 页面变化
删除第一个习惯 _habits.removeAt(0) 第一张卡片消失
删除最后一个习惯 列表长度减 1 底部列表收缩
删除全部习惯 列表为空 页面只剩 AppBar 和 FAB

当前源码没有删除确认弹窗,点击删除会立即移除。

八、新增习惯弹窗

8.1 _addHabit 方法

新增习惯通过弹窗完成。

void _addHabit() async {
  final nameController = TextEditingController();
  String selectedIcon = '⭐';

  await showDialog(
    context: context,
    builder: (context) => StatefulBuilder(
      builder: (context, setDialogState) => AlertDialog(
        title: const Text('Add New Habit'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            // 输入框和图标选择
          ],
        ),
        actions: [
          // Cancel 和 Add
        ],
      ),
    ),
  );
}

nameController 管理弹窗输入,selectedIcon 管理当前选中的图标。

8.2 AlertDialog

弹窗标题是 Add New Habit

AlertDialog(
  title: const Text('Add New Habit'),
  content: Column(
    mainAxisSize: MainAxisSize.min,
    children: [],
  ),
)

mainAxisSize: MainAxisSize.min 可以让弹窗内容按实际高度收缩。

8.3 名称输入框

弹窗内使用 TextField 输入习惯名称。

TextField(
  controller: nameController,
  decoration: const InputDecoration(
    labelText: 'Habit Name',
    border: OutlineInputBorder(),
  ),
)

如果输入为空,点击 Add 不会添加习惯。

九、弹窗图标选择

9.1 图标列表

弹窗内提供 10 个图标。

['⭐', '🏃', '📚', '💧', '🧘', '🍎', '😴', '✍️', '🎯', '💪']

这些图标覆盖星标、运动、阅读、喝水、冥想、饮食、睡眠、书写、目标和力量训练。

9.2 Wrap 布局

图标选择使用 Wrap

Wrap(
  spacing: 8,
  children: ['⭐', '🏃', '📚', '💧', '🧘', '🍎', '😴', '✍️', '🎯', '💪'].map((icon) {
    return GestureDetector(
      onTap: () => setDialogState(() => selectedIcon = icon),
      child: Container(
        padding: const EdgeInsets.all(8),
      ),
    );
  }).toList(),
)

Wrap 可以在弹窗空间不足时自动换行。

9.3 StatefulBuilder

弹窗内部使用 StatefulBuilder

StatefulBuilder(
  builder: (context, setDialogState) => AlertDialog(
    // 弹窗内容
  ),
)

点击图标时只需要刷新弹窗内部的选中状态,不必刷新整个页面。

9.4 选中边框

选中图标会显示蓝色边框。

border: Border.all(
  color: selectedIcon == icon ? Colors.blue : Colors.transparent,
)

这给用户提供明确的选择反馈。

十、添加和取消动作

10.1 Cancel 按钮

取消按钮只关闭弹窗。

TextButton(
  onPressed: () => Navigator.pop(context),
  child: const Text('Cancel'),
)

10.2 Add 按钮

添加按钮会校验名称非空。

ElevatedButton(
  onPressed: () {
    if (nameController.text.isNotEmpty) {
      setState(() {
        _habits.add(Habit(name: nameController.text, icon: selectedIcon));
      });
      Navigator.pop(context);
    }
  },
  child: const Text('Add'),
)

10.3 新建 Habit

新增习惯只设置名称和图标。

Habit(name: nameController.text, icon: selectedIcon)

streak 使用默认值 0,weekProgress 自动初始化为 7 个 false

10.4 新增流程表

步骤 操作 状态变化
1 点击 FAB 打开弹窗
2 输入名称 nameController.text 更新
3 选择图标 selectedIcon 更新
4 点击 Add _habits.add(...)
5 关闭弹窗 列表显示新习惯

十一、列表渲染

11.1 ListView.builder

主体内容使用 ListView.builder

body: ListView.builder(
  padding: const EdgeInsets.all(16),
  itemCount: _habits.length,
  itemBuilder: (context, index) {
    final habit = _habits[index];
    return Card(
      margin: const EdgeInsets.only(bottom: 12),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [],
        ),
      ),
    );
  },
)

动态列表适合习惯数量变化的场景。

11.2 卡片头部

每张卡片头部展示图标、名称、连续天数和删除按钮。

Row(
  children: [
    Text(habit.icon, style: const TextStyle(fontSize: 32)),
    const SizedBox(width: 12),
    Expanded(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(habit.name),
          Text('🔥 ${habit.streak} day streak'),
        ],
      ),
    ),
    IconButton(
      onPressed: () => _deleteHabit(index),
      icon: const Icon(Icons.delete, color: Colors.red),
    ),
  ],
)

Expanded 保证习惯名称区域占据剩余空间,删除按钮固定在右侧。

11.3 连续天数展示

Text(
  '🔥 ${habit.streak} day streak',
  style: TextStyle(
    color: Colors.orange.shade700,
    fontSize: 12,
  ),
)

连续天数使用橙色强调,符合坚持、热度和成就感的视觉语义。

十二、七天打卡 UI

12.1 一周 Row

每张卡片中间使用 Row 展示 7 天。

Row(
  mainAxisAlignment: MainAxisAlignment.spaceAround,
  children: List.generate(7, (dayIndex) {
    return GestureDetector(
      onTap: () => _toggleHabitDay(index, dayIndex),
      child: Column(
        children: [
          Text(_dayNames[dayIndex]),
          const SizedBox(height: 4),
          Container(width: 36, height: 36),
        ],
      ),
    );
  }),
)

spaceAround 让 7 个日期均匀分布。

12.2 日期标签

Text(
  _dayNames[dayIndex],
  style: TextStyle(
    fontWeight: FontWeight.bold,
    color: Colors.grey.shade600,
  ),
)

日期标签使用灰色,避免抢占打卡圆点的视觉重点。

12.3 圆点状态

Container(
  width: 36,
  height: 36,
  decoration: BoxDecoration(
    shape: BoxShape.circle,
    color: habit.weekProgress[dayIndex]
        ? Colors.green
        : Colors.grey.shade200,
  ),
  child: habit.weekProgress[dayIndex]
      ? const Icon(Icons.check, color: Colors.white, size: 20)
      : null,
)

已完成状态为绿色圆点加白色 check,未完成状态为浅灰圆点。

十三、周进度展示

13.1 LinearProgressIndicator

卡片底部使用进度条。

LinearProgressIndicator(
  value: habit.progress,
  backgroundColor: Colors.grey.shade200,
  valueColor: const AlwaysStoppedAnimation(Colors.green),
)

habit.progresscompletedDays / 7 得到。

13.2 完成天数文案

Text(
  '${habit.completedDays}/7 days this week',
  style: TextStyle(
    fontSize: 12,
    color: Colors.grey.shade600,
  ),
)

用户可以同时看到进度条和具体完成天数。

13.3 进度示例

完成天数 progress 文案
0 0 0/7 days this week
1 0.142857 1/7 days this week
3 0.428571 3/7 days this week
7 1 7/7 days this week

进度条对小数值支持良好,不需要手动换算百分比。

十四、OpenHarmony 适配要点

14.1 基础组件验证

当前项目使用的 Flutter 组件包括:

组件 作用 OpenHarmony 关注点
MaterialApp 应用根组件 首屏加载
Scaffold 页面骨架 AppBar、Body、FAB
ListView.builder 习惯列表 滚动和动态刷新
Card 习惯卡片 阴影、边距、圆角
GestureDetector 打卡点击 点击响应
AlertDialog 新增弹窗 弹窗布局
StatefulBuilder 弹窗局部状态 图标选中刷新
FloatingActionButton 新增入口 悬浮按钮点击
LinearProgressIndicator 周进度 进度值展示

14.2 列表验证

OpenHarmony 上应重点验证:

  1. 默认 4 张卡片能完整显示。
  2. 列表可以纵向滚动。
  3. 删除任意习惯后列表立即刷新。
  4. 删除后剩余卡片索引不会错乱。
  5. 新增习惯后列表新增一张卡片。

14.3 打卡验证

打卡交互要覆盖:

  • 点击周一圆点后变为绿色。
  • 再次点击同一圆点后恢复灰色。
  • 完成天数文案同步变化。
  • 进度条同步变化。
  • 多个习惯之间打卡状态互不影响。

14.4 弹窗验证

新增弹窗要验证:

  1. 点击 FAB 能打开弹窗。
  2. 输入框能正常输入习惯名称。
  3. 点击图标后蓝色边框切换。
  4. 点击 Cancel 关闭弹窗且不新增。
  5. 输入名称后点击 Add,列表新增习惯。
  6. 新增习惯 streak 为 0,周进度为空。

OpenHarmony 适配不能只看首屏。这个项目的重点在列表状态、弹窗局部状态和打卡状态三条链路。

十五、测试与验证

15.1 初始页面测试

Widget 测试可以验证默认习惯。

import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('habit tracker shows default habits', (tester) async {
    await tester.pumpWidget(const HabitTrackerApp());

    expect(find.text('Habit Tracker'), findsWidgets);
    expect(find.text('Exercise'), findsOneWidget);
    expect(find.text('Read'), findsOneWidget);
    expect(find.text('Meditate'), findsOneWidget);
    expect(find.text('Drink Water'), findsOneWidget);
  });
}

这类测试不依赖复杂交互,适合验证首屏数据。

15.2 打卡测试

可以点击第一张卡片的某个圆点,并观察完成文案。

testWidgets('tap habit day updates weekly progress', (tester) async {
  await tester.pumpWidget(const HabitTrackerApp());

  expect(find.text('0/7 days this week'), findsWidgets);

  await tester.tap(find.byType(GestureDetector).first);
  await tester.pump();

  expect(find.text('1/7 days this week'), findsOneWidget);
});

实际测试中可以进一步收窄查找范围,避免误点其他 GestureDetector

15.3 新增习惯测试

新增弹窗可以测试输入和添加。

testWidgets('add habit dialog creates new habit', (tester) async {
  await tester.pumpWidget(const HabitTrackerApp());

  await tester.tap(find.byType(FloatingActionButton));
  await tester.pumpAndSettle();

  await tester.enterText(find.byType(TextField), 'Sleep Early');
  await tester.tap(find.text('Add'));
  await tester.pumpAndSettle();

  expect(find.text('Sleep Early'), findsOneWidget);
});

15.4 手工验证矩阵

场景 操作 预期
首次打开 启动应用 显示 4 个默认习惯
打卡 点击任意日期圆点 圆点变绿,进度更新
取消打卡 再点同一圆点 圆点变灰,进度回退
新增习惯 点击 FAB 并输入名称 列表新增习惯
图标选择 弹窗内点击图标 蓝色边框切换
删除习惯 点击删除按钮 对应卡片移除
空名称添加 弹窗不输入名称点击 Add 不新增习惯

十六、常见问题与优化建议

16.1 为什么用 getter 计算完成天数

completedDaysweekProgress 的派生值。

int get completedDays => weekProgress.where((b) => b).length;

使用 getter 可以保证每次读取都是基于最新打卡状态计算,不需要额外维护一个计数字段。

16.2 为什么进度用 completedDays / 7

LinearProgressIndicator 接收 0 到 1 的进度值。

double get progress => completedDays / 7;

完成 7 天时值为 1,完成 0 天时值为 0。

16.3 为什么弹窗使用 StatefulBuilder

新增弹窗里的图标选择是局部状态。使用 StatefulBuilder 可以只刷新弹窗内容。

StatefulBuilder(
  builder: (context, setDialogState) => AlertDialog(),
)

这比把弹窗拆成完整的独立 StatefulWidget 更轻量。

16.4 streak 为什么不会自动变化

当前 _toggleHabitDay 只修改 weekProgress

_habits[habitIndex].weekProgress[dayIndex] =
    !_habits[habitIndex].weekProgress[dayIndex];

它不会修改 streak。如果要让连续天数随打卡变化,需要定义连续天数计算规则,例如按日期、按自然周或按最近连续完成记录计算。

16.5 如何增加删除确认

当前删除是立即执行。可以增加确认弹窗。

Future<void> confirmDelete(int index) async {
  final confirmed = await showDialog<bool>(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('Delete Habit'),
      content: const Text('Remove this habit from the list?'),
      actions: [
        TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')),
        TextButton(onPressed: () => Navigator.pop(context, true), child: const Text('Delete')),
      ],
    ),
  );

  if (confirmed == true) {
    _deleteHabit(index);
  }
}

这能降低误删风险。

16.6 如何增加持久化

当前习惯数据只存在内存中,应用重启后会恢复默认列表。可以把习惯数据序列化成本地存储。

class HabitDto {
  final String name;
  final String icon;
  final List<bool> weekProgress;
  final int streak;

  const HabitDto({
    required this.name,
    required this.icon,
    required this.weekProgress,
    required this.streak,
  });
}

OpenHarmony 上实现持久化时,需要结合可用插件和平台存储能力。

十七、工程扩展方向

17.1 抽取 HabitCard

卡片代码可以拆成独立组件。

class HabitCard extends StatelessWidget {
  final Habit habit;
  final List<String> dayNames;
  final void Function(int dayIndex) onToggleDay;
  final VoidCallback onDelete;

  const HabitCard({
    super.key,
    required this.habit,
    required this.dayNames,
    required this.onToggleDay,
    required this.onDelete,
  });
}

拆分后页面主体更清晰,也便于单独测试卡片。

17.2 抽取新增弹窗

新增弹窗也可以拆成组件或函数。

class AddHabitResult {
  final String name;
  final String icon;

  const AddHabitResult({
    required this.name,
    required this.icon,
  });
}

弹窗返回 AddHabitResult,页面负责添加到列表。

17.3 增加日期维度

当前 weekProgress 只表示一周 7 个布尔值,不包含真实日期。正式习惯应用通常需要保存日期。

class HabitCompletion {
  final DateTime date;
  final bool completed;

  const HabitCompletion({
    required this.date,
    required this.completed,
  });
}

加入日期后,可以跨周统计、计算连续天数和生成月历视图。

17.4 增加统计看板

可以基于当前数据增加统计区域。

int totalCompletedDays(List<Habit> habits) {
  return habits.fold(0, (sum, habit) => sum + habit.completedDays);
}

统计看板可以显示总完成天数、平均完成率、最高 streak 等指标。

总结

habit_tracker 是一个完整的 Flutter 习惯追踪器案例。它用 Habit 模型保存名称、图标、周进度和连续天数,用 completedDaysprogressweekProgress 派生完成天数和进度值,用 ListView.builder 渲染动态习惯列表,用 GestureDetector 切换每天打卡状态,用 LinearProgressIndicator 展示本周完成比例,并通过 AlertDialogStatefulBuilder 实现新增习惯弹窗。

从 OpenHarmony 适配角度看,这个项目适合验证 Flutter 动态列表、卡片布局、弹窗输入、弹窗局部刷新、图标文本、圆形打卡按钮、进度条和删除操作。排查路径也很清晰:列表不刷新看 _habitssetState,进度不对看 weekProgress,弹窗选择不刷新看 StatefulBuilder,删除异常看列表索引。

掌握这个项目后,可以继续扩展删除确认、数据持久化、日期维度、连续天数自动计算、统计看板和月历视图,让习惯追踪器从一周打卡 Demo 演进为更完整的跨平台习惯管理工具。

如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!


相关资源:

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐