说起习惯养成,我自己就是个典型的"三分钟热度"患者。年初立下的flag,到年中就忘得一干二净。后来发现,其实不是我懒,而是缺少一个好用的工具来记录和提醒。所以在做这个生活助手App时,我特别重视习惯打卡这个功能。
在这里插入图片描述

为什么习惯打卡这么重要

在开始写代码之前,我先想清楚了这个功能到底要解决什么问题。

第一个问题是记录。很多人想养成好习惯,但总是记不住今天有没有做。有了打卡功能,每天点一下,就能清楚地知道自己的完成情况。

第二个问题是激励。看着连续打卡的天数不断增加,会有一种成就感。这种正向反馈能够激励我们坚持下去。心理学上有个说法,一个习惯需要21天才能养成,如果能坚持打卡21天,基本就成功了一半。

第三个问题是可视化。单纯的数字不够直观,我用了圆形进度条来展示今日完成度。看着进度条一点点填满,就像玩游戏通关一样,很有动力。

功能设计的几个关键点

在设计这个功能时,我考虑了以下几个方面:

简单直观的交互
打卡这个动作要足够简单,最好一步到位。我选择了Checkbox,点一下就完成打卡,不需要额外的确认步骤。

清晰的视觉反馈
用户打卡后,要立即看到变化。我用了不同的颜色来区分完成和未完成的状态,已完成的卡片边框变成绿色,给用户明确的反馈。

激励性的数据展示
除了显示今天的完成情况,还要展示连续坚持的天数。这个数字对用户来说很重要,没人想让自己的连续记录断掉。

灵活的习惯管理
每个人想养成的习惯都不一样,所以要支持自定义添加。同时,每个习惯可以设置不同的图标和颜色,让界面更加个性化。

从数据结构开始思考

在写UI之前,我先想好了数据结构。每个习惯需要哪些信息?

final List<Map<String, dynamic>> habits = [
  {'name': '早起', 'icon': Icons.wb_sunny, 'completed': true, 'streak': 15, 'color': Colors.orange},
  {'name': '运动', 'icon': Icons.fitness_center, 'completed': true, 'streak': 8, 'color': Colors.red},
  {'name': '阅读', 'icon': Icons.book, 'completed': false, 'streak': 12, 'color': Colors.blue},
];

这个数据结构包含了几个关键字段:

  • name:习惯的名称,比如"早起"、“运动”
  • icon:对应的图标,Flutter内置了很多Material Icons可以直接用
  • completed:今天是否已完成,这是打卡的核心状态
  • streak:连续坚持的天数,这个数字很重要,能激励用户
  • color:每个习惯的主题色,让界面更有辨识度

你可能会问,为什么用Map而不是定义一个Habit类?其实在实际项目中,确实应该定义一个类,这样类型更安全。但为了演示方便,我这里用了Map。

页面整体布局

接下来看页面的整体结构:

class HabitTrackerPage extends StatefulWidget {
  const HabitTrackerPage({super.key});

  
  State<HabitTrackerPage> createState() => _HabitTrackerPageState();
}

这里我用了StatefulWidget,因为打卡状态会变化,需要重新渲染UI。如果用StatelessWidget,就没法更新界面了。

build方法里,我先计算了完成度:

final completedCount = habits.where((h) => h['completed']).length;
final totalCount = habits.length;
final percentage = completedCount / totalCount;

这三行代码很关键

  • where方法筛选出所有completedtrue的习惯
  • length获取筛选后的数量,这就是已完成的习惯数
  • 用已完成数除以总数,得到完成百分比

这个百分比会传给圆形进度条,实时显示今日完成度。

顶部进度卡片的设计

页面最上面是一个醒目的进度卡片,我花了不少心思在这上面。先看核心代码:

CircularPercentIndicator(
  radius: 50.r,
  lineWidth: 8.w,
  percent: percentage,
  center: Text(
    '$completed/$total',
    style: TextStyle(
      color: Colors.white,
      fontSize: 18.sp,
      fontWeight: FontWeight.bold,
    ),
  ),
  progressColor: Colors.white,
  backgroundColor: Colors.white.withOpacity(0.3),
  circularStrokeCap: CircularStrokeCap.round,
)

渐变背景的选择

外层Container用了蓝色渐变作为背景,从Colors.blueColors.lightBlue。为什么选蓝色?因为蓝色给人一种平静、专注的感觉,很适合习惯养成这个主题。而且蓝色在各种屏幕上显示效果都不错。

圆形进度条的参数

CircularPercentIndicatorpercent_indicator包提供的组件,它有几个重要参数:

  • radius: 50.r:圆的半径,用了屏幕适配单位
  • lineWidth: 8.w:进度条的宽度,不能太粗也不能太细
  • percent: percentage:进度百分比,0到1之间
  • circularStrokeCap: CircularStrokeCap.round:让进度条两端是圆角的,看起来更柔和

中心文字的巧思

进度条中心显示"3/5"这样的文字,比单纯的百分比更直观。用户一眼就能看出"今天有5个习惯,完成了3个"。

右侧还加了一段鼓励的文字:“继续保持,你做得很棒!”。别小看这句话,正向反馈对用户的激励作用很大。

习惯卡片的精心设计

每个习惯用一个卡片来展示,这个卡片的设计我反复调整了好几次。先看边框部分:

decoration: BoxDecoration(
  color: Colors.white,
  borderRadius: BorderRadius.circular(12.r),
  border: Border.all(
    color: habit['completed'] ? Colors.green : Colors.grey[300]!,
    width: 2,
  ),
)

边框颜色的动态变化

注意看border这部分代码,我用了一个三元表达式。如果习惯已完成,边框就是绿色;如果未完成,边框就是浅灰色。这种视觉反馈很重要,用户一眼就能看出哪些习惯完成了。

接下来是图标部分:

Container(
  padding: EdgeInsets.all(12.w),
  decoration: BoxDecoration(
    color: (habit['color'] as Color).withOpacity(0.1),
    borderRadius: BorderRadius.circular(12.r),
  ),
  child: Icon(
    habit['icon'] as IconData,
    color: habit['color'] as Color,
    size: 28.sp,
  ),
)

图标背景的半透明效果

每个习惯都有自己的主题色,但如果直接用这个颜色做背景,会太抢眼。所以我用了withOpacity(0.1),这样背景色就变成了半透明的,既有颜色区分,又不会太刺眼。比如运动的主题色是红色,背景就是淡淡的粉红色。

连续天数的激励作用

"已坚持15天"这个信息放在习惯名称下方,用灰色小字显示:

Text(
  '已坚持 ${habit['streak']} 天',
  style: TextStyle(fontSize: 12.sp, color: Colors.grey),
)

别小看这个数字,它对用户的激励作用很大。我自己用这个功能时,有次早起已经坚持了20天,那天早上特别不想起,但想到连续记录要断了,还是咬牙爬起来了。这就是数字的力量。

圆形Checkbox的选择

默认的Checkbox是方形的,我觉得和整体的圆润风格不太搭,所以加了这行代码:

Checkbox(
  value: habit['completed'],
  onChanged: (value) {
    setState(() {
      habits[index]['completed'] = value;
    });
  },
  shape: const CircleBorder(),
)

shape: const CircleBorder()让Checkbox变成圆形的,和卡片的圆角、图标的圆角保持一致。

打卡交互的实现

打卡的核心逻辑其实很简单。当用户点击Checkbox时,onChanged回调会被触发,传入新的值(true或false)。我在回调里调用了setState,更新habits列表中对应习惯的completed字段。

setState的工作原理

setState会通知Flutter框架:“嘿,我的状态变了,需要重新构建UI”。然后Flutter会重新调用build方法,生成新的Widget树。

这时候,顶部的进度圆环也会跟着更新,因为completedCount重新计算了。这就是Flutter响应式编程的魅力,数据变了,UI自动跟着变。

实际项目中的改进

在真实项目里,这里还应该做几件事:

  1. 保存数据到本地:用SharedPreferences或SQLite把打卡记录存起来
  2. 更新连续天数:如果今天是第一次打卡,连续天数加1
  3. 记录打卡时间:保存具体的打卡时间,方便后续统计分析
  4. 添加动画效果:打卡时可以加个小动画,增加趣味性

日期显示的小细节

在进度卡片和习惯列表之间,我加了一个日期显示:

Text(
  DateFormat('yyyy年MM月dd日').format(DateTime.now()),
  style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold),
)

为什么要显示日期?

因为习惯打卡是按天来的,明确显示今天的日期,能让用户知道"这是今天的打卡记录"。而且,当用户连续多天打卡时,看到日期在变化,也是一种时间流逝的感觉。

DateFormat的使用

intl包提供的DateFormat很强大,可以自定义各种日期格式。yyyy是四位年份,MM是两位月份,dd是两位日期。这样格式化出来就是"2026年01月19日",符合中文的阅读习惯。

添加新习惯的功能

右上角的加号按钮,点击可以添加新习惯。我用了AlertDialog来实现:

void _showAddHabitDialog() {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('添加新习惯'),
      content: TextField(
        decoration: const InputDecoration(
          hintText: '输入习惯名称',
          border: OutlineInputBorder(),
        ),
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('取消'),
        ),
        TextButton(
          onPressed: () {
            Navigator.pop(context);
            ScaffoldMessenger.of(context).showSnackBar(
              const SnackBar(content: Text('习惯添加成功')),
            );
          },
          child: const Text('添加'),
        ),
      ],
    ),
  );
}

AlertDialog的三个部分

AlertDialog是Flutter提供的标准对话框组件,包含三个部分:

  • title:对话框标题
  • content:对话框内容,这里放了一个输入框
  • actions:底部的按钮,通常是"取消"和"确定"

SnackBar的提示

当用户点击"添加"按钮后,我用SnackBar显示一个提示:“习惯添加成功”。这种即时反馈很重要,让用户知道操作成功了。

实际项目中的完善

这里为了演示简化了逻辑,实际使用时应该:

  1. 获取输入框的内容
  2. 验证输入是否为空
  3. 创建新的习惯对象
  4. 添加到habits列表
  5. 调用setState刷新UI
  6. 保存到本地存储

屏幕适配的处理

整个页面用了flutter_screenutil来做屏幕适配。所有的尺寸都用了特殊的后缀:

  • .w:宽度适配,比如16.w
  • .h:高度适配,比如24.h
  • .sp:字体大小适配,比如18.sp
  • .r:圆角适配,比如16.r

这样在不同屏幕尺寸的设备上,UI比例都能保持一致。比如在小屏手机上,所有元素会按比例缩小;在大屏平板上,所有元素会按比例放大。

性能优化的考虑

习惯列表用了ListView.builder,这是个懒加载的列表,只会构建可见的item。虽然这个页面习惯数量不会太多,但养成好习惯总是对的。

ListView.builder(
  shrinkWrap: true,
  physics: const NeverScrollableScrollPhysics(),
  itemCount: habits.length,
  itemBuilder: (context, index) {
    return _buildHabitCard(habits[index], index);
  },
)

外层用了SingleChildScrollView包裹,这样内容超出屏幕时可以滚动。ListView.builder设置了shrinkWrap: truephysics: const NeverScrollableScrollPhysics(),让它不要自己处理滚动,交给外层的SingleChildScrollView统一管理。

实际使用体验

这个功能我自己用了一段时间,感觉还不错。每天早上打开应用,看到昨天的习惯都完成了,今天的还是空的,就有动力去完成。

圆形进度条的视觉反馈很直观,看着进度一点点填满,有种游戏通关的感觉。连续天数的显示也很有激励作用,不想让连续记录断掉。

有一次我连续早起了30天,看到那个数字真的很有成就感。后来因为周末睡懒觉,连续记录断了,还挺可惜的。这说明这个功能确实能起到激励作用。

可以改进的地方

如果要做得更完善,可以考虑以下几点:

数据持久化

现在数据只在内存里,应用关闭就没了。应该用SharedPreferences或SQLite把数据存起来。可以按日期存储每天的打卡记录,这样就能查看历史数据了。

历史记录查看

可以加个日历视图,查看过去每天的打卡情况。用不同的颜色标记完成度,比如全部完成是绿色,部分完成是黄色,一个都没完成是灰色。

提醒功能

设置每天固定时间提醒用户打卡,比如晚上9点提醒今天还有哪些习惯没完成。可以用flutter_local_notifications包来实现本地通知。

统计分析

可以做个统计页面,展示每个习惯的完成率、最长连续天数、本周完成情况等数据。用图表展示更直观,比如用折线图展示每周的完成趋势。

社交功能

可以加入好友系统,让用户可以和朋友一起打卡,互相监督。看到朋友都在坚持,自己也会更有动力。

习惯分组

当习惯数量多了以后,可以按分类分组,比如"健康类"、“学习类”、“工作类”。这样管理起来更方便。

小结

今天实现了习惯打卡功能,用到了圆形进度指示器、列表构建、对话框等组件。核心是用setState管理打卡状态,通过UI反馈激励用户坚持习惯。

这个功能虽然简单,但很实用。养成好习惯需要坚持,有个工具来记录和提醒,确实能提高成功率。从我自己的使用体验来看,视觉化的进度展示和连续天数的记录,对坚持习惯有很大帮助。

在实现过程中,我特别注重用户体验。简单的交互、清晰的反馈、激励性的数据展示,这些细节都是为了让用户更愿意使用这个功能。毕竟,一个好用的工具,才能真正帮助用户养成好习惯。

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

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐