Flutter步数追踪器


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

项目概述

运行效果图在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、项目背景与目标

随着健康意识的提升,越来越多的人开始关注日常运动量。步行作为一种简单易行的运动方式,受到各年龄段人群的青睐。然而,缺乏有效的追踪工具,使得很多人难以坚持运动计划。本项目基于Flutter框架开发一款步数追踪器应用,旨在帮助用户实时记录步数,设定运动目标,分析运动数据,从而培养良好的运动习惯。

项目的核心目标涵盖多个维度:构建完整的步数记录系统,实现精确的运动数据计算,设计直观的可视化展示,打造便捷的操作体验,以及确保应用的稳定性和性能表现。通过本项目的开发,不仅能够深入理解Flutter在健康类应用中的应用,更能掌握定时器、数据可视化、图表绘制等核心技术要点。

应用场景分析
应用场景 功能需求 实现方式
日常运动 记录每日步数,监控运动量 实时步数统计和进度展示
目标管理 设定每日运动目标,激励坚持 目标进度环和达成提示
数据分析 查看历史数据,分析运动趋势 周报表和统计图表
健康管理 计算卡路里消耗,评估运动效果 多维度运动数据计算
核心价值主张
  1. 实时追踪:精确记录每一步,实时更新运动数据
  2. 目标激励:可视化进度展示,激励用户达成目标
  3. 数据分析:多维度数据统计,洞察运动趋势
  4. 健康评估:科学计算卡路里消耗,评估运动效果

二、技术选型与架构设计

技术栈分析

本项目选用Flutter作为开发框架,主要基于以下考量:

  • 跨平台能力:Flutter的跨平台特性能够同时支持Android和iOS平台,降低开发成本
  • 声明式UI:声明式UI编程范式能够高效构建复杂的运动数据展示界面
  • 丰富组件库:丰富的Widget组件库为应用UI开发提供了坚实基础
  • 优秀性能:优秀的性能表现确保了实时数据更新的流畅性

Dart语言作为Flutter的开发语言,具备强类型、异步编程支持、优秀的性能表现等特性。项目采用单文件架构,将所有应用逻辑集中在main_step_tracker.dart文件中,这种设计既便于代码管理,又利于理解应用整体架构。

架构层次划分

应用架构采用分层设计思想,主要分为以下几个层次:

渲染表现层

状态管理层

业务逻辑层

数据模型层

Material Design组件

CustomPaint图表

动画效果

当前步数状态

页面索引状态

目标设置状态

步数计算

距离换算

卡路里计算

数据统计

StepData实体

运动数据模型

数据模型层:定义应用中的核心数据结构,包括StepData(步数数据实体)等类。这些模型类封装了运动数据的属性和计算逻辑,构成了应用逻辑的基础。

业务逻辑层:实现应用的核心功能逻辑,包括步数计算、距离换算、卡路里计算、数据统计等。这一层是应用的心脏,决定了应用的功能性和可用性。

渲染表现层:负责应用界面的绘制和UI展示,使用Flutter的Material Design组件库实现现代化的界面设计,通过CustomPaint组件实现进度环、柱状图、饼图等图表的绘制。

状态管理层:管理应用的各种状态,包括当前步数、每日目标、页面索引、历史数据等,确保应用状态的一致性和可预测性。

核心功能模块详解

一、步数数据模型

数据属性定义

步数数据实体封装了完整的运动信息:

class StepData {
  final DateTime date;        // 日期
  final int steps;            // 步数
  final double distance;      // 距离(公里)
  final int calories;         // 消耗卡路里
  final int activeMinutes;    // 活动时长(分钟)
}

基础信息包括日期和步数;计算信息包括距离、卡路里和活动时长。这种设计既满足了展示需求,又支持灵活的数据分析。

运动数据计算

步数与其他运动数据的换算关系:

double get stepLength => 0.762;  // 平均步长(米)

double get distance => steps * 0.762 / 1000;  // 距离 = 步数 × 步长

int get calories => (steps * 0.04).toInt();  // 卡路里 = 步数 × 0.04

int get activeMinutes => (steps / 100).toInt();  // 活动时长 = 步数 / 100

这些计算公式基于运动科学研究,确保了数据的科学性和准确性。

二、实时步数追踪系统

步数模拟器实现

为了演示效果,应用使用定时器模拟步数增长:

void _startStepSimulation() {
  _stepTimer = Timer.periodic(const Duration(seconds: 3), (_) {
    setState(() {
      _currentSteps += math.Random().nextInt(10) + 1;
    });
  });
}


void dispose() {
  _stepTimer?.cancel();
  super.dispose();
}

定时器每3秒随机增加1-10步,模拟真实场景下的步数增长。页面销毁时取消定时器,避免内存泄漏。

进度计算

实时计算目标完成进度:

double get _progress => (_currentSteps / _dailyGoal).clamp(0.0, 1.0);

进度值限制在0-1之间,避免超出范围的显示问题。

三、目标管理系统

目标设置功能

用户可以自定义每日运动目标:

void _showGoalDialog() {
  final controller = TextEditingController(text: _dailyGoal.toString());
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('设置每日目标'),
      content: TextField(
        controller: controller,
        keyboardType: TextInputType.number,
        decoration: const InputDecoration(
          labelText: '目标步数',
          suffix: Text('步'),
        ),
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('取消'),
        ),
        ElevatedButton(
          onPressed: () {
            final goal = int.tryParse(controller.text);
            if (goal != null && goal > 0) {
              setState(() {
                _dailyGoal = goal;
              });
              Navigator.pop(context);
            }
          },
          child: const Text('确定'),
        ),
      ],
    ),
  );
}

目标设置通过对话框实现,支持数字输入和验证。

目标达成提示

当步数达到目标时,显示达成标识:

final isGoalAchieved = _currentSteps >= _dailyGoal;

if (isGoalAchieved)
  Container(
    padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
    decoration: BoxDecoration(
      color: Colors.white.withOpacity(0.2),
      borderRadius: BorderRadius.circular(12),
    ),
    child: const Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        Icon(Icons.check_circle, color: Colors.white, size: 16),
        SizedBox(width: 4),
        Text('目标达成', style: TextStyle(color: Colors.white, fontSize: 12)),
      ],
    ),
  ),

目标达成后,界面顶部显示"目标达成"徽章,提供成就感激励。

四、数据可视化系统

圆形进度环

主界面使用圆形进度环展示当日步数:

class CircularProgressPainter extends CustomPainter {
  final double progress;
  final Color color;
  final Color backgroundColor;

  
  void paint(Canvas canvas, Size size) {
    final center = Offset(size.width / 2, size.height / 2);
    final radius = math.min(size.width, size.height) / 2 - 10;
    final strokeWidth = 12.0;

    // 绘制背景环
    final backgroundPaint = Paint()
      ..color = backgroundColor
      ..strokeWidth = strokeWidth
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round;
    canvas.drawCircle(center, radius, backgroundPaint);

    // 绘制进度环
    final progressPaint = Paint()
      ..color = color
      ..strokeWidth = strokeWidth
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round;
    final sweepAngle = 2 * math.pi * progress;
    canvas.drawArc(
      Rect.fromCircle(center: center, radius: radius),
      -math.pi / 2,
      sweepAngle,
      false,
      progressPaint,
    );
  }
}

进度环使用CustomPaint绘制,背景环和进度环分别绘制,进度环从顶部(-π/2)开始绘制。

周趋势柱状图

历史页面使用柱状图展示一周步数趋势:

class WeeklyChartPainter extends CustomPainter {
  final List<StepData> data;
  final int goal;

  
  void paint(Canvas canvas, Size size) {
    final maxSteps = data.map((d) => d.steps).reduce(math.max);
    final chartHeight = size.height - 40;
    final barWidth = (size.width - 40) / data.length - 8;

    for (var i = 0; i < data.length; i++) {
      final item = data[i];
      final barHeight = (item.steps / maxSteps) * chartHeight;
      final x = 20 + i * (barWidth + 8);
      final y = chartHeight - barHeight;

      final paint = Paint()
        ..color = item.steps >= goal ? Colors.green : Colors.orange
        ..style = PaintingStyle.fill;

      final rect = RRect.fromRectAndRadius(
        Rect.fromLTWH(x, y, barWidth, barHeight),
        const Radius.circular(4),
      );
      canvas.drawRRect(rect, paint);
    }

    // 绘制目标线
    final goalY = chartHeight - (goal / maxSteps) * chartHeight;
    final goalPaint = Paint()
      ..color = Colors.red.withOpacity(0.5)
      ..strokeWidth = 1;
    canvas.drawLine(Offset(20, goalY), Offset(size.width - 20, goalY), goalPaint);
  }
}

柱状图根据步数是否达标显示不同颜色,并绘制目标线作为参考。

步数分布饼图

统计页面使用饼图展示一周步数分布:

class DistributionChartPainter extends CustomPainter {
  final List<StepData> data;

  
  void paint(Canvas canvas, Size size) {
    final total = data.fold(0, (sum, d) => sum + d.steps);
    final center = Offset(size.width / 2, size.height / 2);
    final radius = math.min(size.width, size.height) / 2 - 40;

    double startAngle = -math.pi / 2;
    for (var i = 0; i < data.length; i++) {
      final item = data[i];
      final sweepAngle = 2 * math.pi * item.steps / total;
      paint.color = colors[i % colors.length];
      canvas.drawArc(
        Rect.fromCircle(center: center, radius: radius),
        startAngle,
        sweepAngle,
        true,
        paint,
      );
      startAngle += sweepAngle;
    }
  }
}

饼图展示每天步数占比,图例显示具体数值和百分比。

五、历史记录系统

历史数据加载

模拟加载一周历史数据:

void _loadWeeklyData() {
  final now = DateTime.now();
  final random = math.Random();
  
  _weeklyData = List.generate(7, (index) {
    final date = now.subtract(Duration(days: 6 - index));
    final steps = random.nextInt(8000) + 4000;
    return StepData(
      date: date,
      steps: steps,
      distance: steps * 0.762 / 1000,
      calories: (steps * 0.04).toInt(),
      activeMinutes: (steps / 100).toInt(),
    );
  });
}

历史数据随机生成,步数范围在4000-12000之间,模拟真实场景。

历史记录展示

历史记录使用卡片列表展示:

Card(
  child: InkWell(
    onTap: () => _showDayDetail(data),
    child: Row(
      children: [
        // 日期显示
        Container(
          width: 56,
          height: 56,
          child: Column(
            children: [
              Text('${data.date.day}'),
              Text(_getWeekday(data.date)),
            ],
          ),
        ),
        // 步数和详情
        Expanded(
          child: Column(
            children: [
              Text('${data.steps} 步'),
              Row(
                children: [
                  Text('${data.distance.toStringAsFixed(2)} km'),
                  Text('${data.calories} kcal'),
                ],
              ),
            ],
          ),
        ),
        // 迷你进度环
        CustomPaint(
          painter: MiniProgressPainter(
            progress: (data.steps / _dailyGoal).clamp(0.0, 1.0),
          ),
        ),
      ],
    ),
  ),
)

每条记录显示日期、步数、距离、卡路里和迷你进度环,点击可查看详情。

六、统计分析功能

周统计计算

计算一周运动数据汇总:

int get _weeklySteps => _weeklyData.fold(0, (sum, data) => sum + data.steps);

double get _weeklyDistance => _weeklyData.fold(0, (sum, data) => sum + data.distance);

int get _weeklyCalories => _weeklyData.fold(0, (sum, data) => sum + data.calories);

final avgSteps = _weeklySteps / 7;

final maxSteps = _weeklyData.map((d) => d.steps).reduce(math.max);

final minSteps = _weeklyData.map((d) => d.steps).reduce(math.min);

使用foldmap方法进行数据聚合,计算总步数、总距离、总消耗、日均步数、最高步数、最低步数等指标。

目标达成统计

统计一周内目标达成天数:

int achievedDays = _weeklyData.where((d) => d.steps >= _dailyGoal).length;

使用where方法过滤达标天数,计算达成率。

UI界面开发

一、主界面布局

主界面采用底部导航栏设计,包含四个主要页面:

BottomNavigationBar(
  currentIndex: _currentIndex,
  onTap: (index) => setState(() => _currentIndex = index),
  selectedItemColor: Colors.green,
  unselectedItemColor: Colors.grey,
  type: BottomNavigationBarType.fixed,
  items: const [
    BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
    BottomNavigationBarItem(icon: Icon(Icons.history), label: '历史'),
    BottomNavigationBarItem(icon: Icon(Icons.bar_chart), label: '统计'),
    BottomNavigationBarItem(icon: Icon(Icons.settings), label: '设置'),
  ],
)

四个页面分别是首页、历史记录、统计分析和设置,覆盖了应用的主要功能入口。

页面结构图

首页

圆形进度环

运动指标

周概览

周趋势图

历史页

日期卡片列表

迷你进度环

详情弹窗

统计页

周统计

运动数据

步数分布图

设置页

目标设置

提醒设置

数据同步

二、圆形进度环设计

圆形进度环是应用的核心视觉元素:

Stack(
  alignment: Alignment.center,
  children: [
    SizedBox(
      width: 200,
      height: 200,
      child: CustomPaint(
        painter: CircularProgressPainter(
          progress: _progress,
          color: Colors.white,
          backgroundColor: Colors.white.withOpacity(0.2),
        ),
      ),
    ),
    Column(
      children: [
        Text(_currentSteps.toString(), style: TextStyle(fontSize: 48)),
        Text('步', style: TextStyle(fontSize: 16)),
        Text('目标: $_dailyGoal', style: TextStyle(fontSize: 14)),
      ],
    ),
  ],
)

进度环居中显示,内部显示当前步数、单位和目标值。

视觉设计要点
  1. 渐变背景:绿色渐变背景,营造健康活力氛围
  2. 圆角设计:24px圆角,符合现代设计趋势
  3. 阴影效果:20px模糊半径,10px垂直偏移,营造悬浮感
  4. 进度环:12px线宽,圆角端点,视觉柔和
  5. 目标达成:达成后显示徽章,提供成就感

三、运动指标展示

运动指标使用图标+数值的组合展示:

Row(
  mainAxisAlignment: MainAxisAlignment.spaceAround,
  children: [
    _buildMetricItem(Icons.directions_walk, '距离', '${_distance.toStringAsFixed(2)} km'),
    Container(width: 1, height: 40, color: Colors.white.withOpacity(0.2)),
    _buildMetricItem(Icons.local_fire_department, '卡路里', '$_calories kcal'),
    Container(width: 1, height: 40, color: Colors.white.withOpacity(0.2)),
    _buildMetricItem(Icons.timer, '活动', '$_activeMinutes 分钟'),
  ],
)

三个指标(距离、卡路里、活动时长)均匀分布,使用分隔线区分。

性能优化方案

一、定时器优化

定时器在页面销毁时取消:


void dispose() {
  _stepTimer?.cancel();
  super.dispose();
}

避免后台持续运行消耗资源。

二、列表渲染优化

历史记录使用ListView.builder实现按需渲染:

ListView.builder(
  padding: const EdgeInsets.all(16),
  itemCount: _weeklyData.length,
  itemBuilder: (context, index) {
    return _buildDayCard(_weeklyData[index]);
  },
)

只有可见区域的卡片才会被创建和渲染,大幅降低了内存占用。

三、图表绘制优化

图表使用shouldRepaint优化重绘:


bool shouldRepaint(covariant CircularProgressPainter oldDelegate) {
  return progress != oldDelegate.progress;
}

只有数据变化时才重新绘制,避免不必要的性能消耗。

测试方案与步骤

一、功能测试

步数追踪测试:验证步数是否实时更新;测试手动添加步数功能;检查步数计算准确性。

目标管理测试:验证目标设置功能;测试目标达成提示;检查进度计算正确性。

数据可视化测试:验证进度环绘制正确性;测试柱状图和饼图展示;检查图表交互功能。

历史记录测试:验证历史数据加载;测试详情查看功能;检查日期显示正确性。

二、边界测试

零步数测试:测试步数为零时的界面展示。

超大步数测试:测试步数超过目标时的处理。

目标设置测试:测试设置极大或极小目标值。

数据为空测试:测试没有历史数据时的界面展示。

三、用户体验测试

界面响应测试:测试页面切换的流畅性。

视觉体验测试:评估界面设计和颜色搭配。

操作便捷性测试:评估手动添加步数和设置目标的流程。

常见问题与解决方案

一、步数计算问题

问题:步数与实际不符

解决方案:集成系统计步器API或第三方计步SDK

// 使用sensors_plus包
import 'package:sensors_plus/sensors_plus.dart';

void _listenToSteps() {
  pedometerEvents.listen((event) {
    setState(() {
      _currentSteps = event.steps;
    });
  });
}

二、数据持久化问题

问题:应用重启后数据丢失

解决方案:使用shared_preferencessqflite实现数据持久化

// 使用shared_preferences
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('currentSteps', _currentSteps);
await prefs.setInt('dailyGoal', _dailyGoal);

三、性能问题

问题:图表绘制卡顿

解决方案:使用RepaintBoundary隔离重绘区域

RepaintBoundary(
  child: CustomPaint(
    painter: CircularProgressPainter(progress: _progress),
  ),
)

项目总结与展望

一、项目成果总结

本项目成功实现了一款功能完整、界面现代的步数追踪器应用,涵盖了健康类应用开发的核心要素。通过Flutter框架的应用,实现了跨平台的应用体验,证明了Flutter在健康类应用开发领域的可行性。

项目采用模块化设计思想,将应用功能划分为步数追踪、目标管理、数据可视化、历史记录等独立模块,各模块职责明确,耦合度低,便于维护和扩展。

二、技术亮点总结

实时追踪:使用Timer.periodic实现步数的实时更新,提供即时的运动反馈。

圆形进度环:使用CustomPaint绘制精美的圆形进度环,直观展示目标完成度。

多维度数据:科学计算距离、卡路里、活动时长等多维度运动数据。

数据可视化:使用柱状图和饼图展示运动趋势和分布,帮助用户洞察运动规律。

目标激励:目标达成提示和徽章系统,激励用户坚持运动。

三、未来优化方向

真实计步:集成系统计步器API或第三方计步SDK,实现真实的步数追踪。

数据持久化:使用shared_preferencessqflite实现数据持久化,确保运动数据不丢失。

提醒通知:集成本地通知功能,定时提醒用户运动。

社交分享:支持运动数据分享到社交平台,增加社交属性。

成就系统:设计成就徽章系统,激励用户达成运动目标。

健康建议:根据运动数据提供个性化的健康建议。

云端同步:实现数据云端同步,支持多设备共享。

运动计划:支持制定运动计划,帮助用户科学运动。

四、开发经验总结

通过本项目的开发,积累了宝贵的Flutter应用开发经验:

数据可视化的重要性:健康类应用的核心是数据展示,掌握CustomPaint的使用,理解图表绘制原理,是开发此类应用的基础。

实时更新的技巧:定时器需要谨慎使用,确保在合适的时机启动和取消,避免资源泄漏和性能问题。

用户体验的核心地位:健康类应用最终服务于用户的健康目标,从进度展示到目标达成,每个细节都需要精心打磨。

科学计算的必要性:运动数据的计算需要基于科学原理,确保数据的准确性和可信度。

本项目为Flutter健康类应用开发提供了一个完整的实践案例,展示了如何实现步数追踪、目标管理、数据可视化等核心功能,希望能够为相关开发者提供参考和启发,推动Flutter在健康类应用开发领域的应用和发展。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐