体重管理是现代健康生活方式的重要组成部分。作为一名长期关注健康科技领域的开发者,我发现很多用户虽然定期记录体重数据,却难以从零散的数字中洞察变化趋势。这正是我们开发这个体重趋势功能的核心动机 - 通过数据可视化帮助用户建立清晰的健康认知。
这个基于Flutter的体重趋势模块需要解决三个关键问题:
在OpenHarmony生态中,我们选择Flutter作为开发框架,主要考虑到:
页面采用经典的"卡片+图表"布局结构,这种设计模式在健康类应用中已被验证有效:
dart复制Scaffold(
backgroundColor: const Color(0xFFFAFAFC), // 浅灰背景提升可读性
appBar: AppBar(
backgroundColor: Colors.transparent, // 透明AppBar保持视觉轻盈
title: Text('体重趋势', style: TextStyle(fontSize: 17.sp)),
),
body: SingleChildScrollView(
child: Column(
children: [
_buildSummaryCard(), // 数据概览卡片
SizedBox(height: 20.h), // 使用.h适配不同屏幕
_buildChartSection(), // 图表区域
],
),
),
)
关键设计决策:使用.h/.w单位而非固定像素,确保在不同屏幕尺寸下保持比例协调。这是OpenHarmony多设备适配的最佳实践。
我们建立了明确的色彩语义系统:
这种色彩编码借鉴了交通信号灯系统,能帮助用户快速理解状态:
dart复制Color _getStatusColor(double change, GoalType goal) {
switch(goal) {
case GoalType.lose:
return change < 0 ? successColor : warningColor;
case GoalType.gain:
return change > 0 ? successColor : warningColor;
default:
return primaryColor;
}
}
汇总卡片采用三等分布局,每个数据项包含:
dart复制Widget _buildSummaryItem(String label, String value, Color color) {
return Expanded(
child: Column(
children: [
Text(label, style: TextStyle(fontSize: 12.sp, color: Colors.grey[500])),
SizedBox(height: 6.h), // 精细控制间距
Text(value, style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w700,
color: color
)),
],
),
);
}
实战经验:使用Expanded而非固定宽度,确保在不同语言环境下都能保持对齐。德语等语言标签文字较长,需要弹性布局。
原始数据需要经过适当格式化才能展示:
dart复制String _formatWeight(double kg) => '${kg.toStringAsFixed(1)} kg';
String _formatChange(double change) {
if (change > 0) return '+${change.toStringAsFixed(1)} kg';
if (change < 0) return '${change.toStringAsFixed(1)} kg';
return '±0 kg'; // 明确表示无变化
}
这种格式化确保:
使用fl_chart绘制折线图时,关键配置包括:
dart复制LineChart(
LineChartData(
gridData: FlGridData(show: false), // 隐藏网格减少干扰
titlesData: FlTitlesData(
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) => _buildDateLabel(value),
)
),
// 其他三边不显示标签
),
borderData: FlBorderData(show: false), // 无边框
minX: 0,
maxX: _days.toDouble(),
minY: _calcMinY(),
maxY: _calcMaxY(),
),
)
智能计算Y轴范围确保数据展示效果最佳:
dart复制double _calcMinY(List<double> weights) {
final min = weights.reduce((a, b) => a < b ? a : b);
return (min - 2).floorToDouble(); // 下扩2kg作为缓冲
}
double _calcMaxY(List<double> weights) {
final max = weights.reduce((a, b) => a > b ? a : b);
return (max + 2).ceilToDouble(); // 上扩2kg
}
性能提示:对于大量数据点(如1年数据),应考虑采样策略避免性能问题。
通过以下方式提升图表表现力:
isCurved: truebelowBarData设置半透明填充dart复制LineChartBarData(
spots: _convertToSpots(weights),
isCurved: true,
color: primaryColor,
barWidth: 3,
belowBarData: BarAreaData(
show: true,
color: primaryColor.withOpacity(0.1) // 淡色填充
),
)
实现可切换的时间范围控件:
dart复制Widget _buildRangeSelector() {
return ToggleButtons(
constraints: BoxConstraints.expand(width: 80.w),
selectedColor: Colors.white,
fillColor: primaryColor,
renderBorder: false,
children: [
Text('7天'), Text('30天'), Text('90天'), Text('1年')
],
onPressed: (index) {
_loadDataForRange(Range.values[index]);
},
);
}
数据加载策略:
实现点击交互显示详细信息:
dart复制GestureDetector(
onTapDown: (details) {
final spot = _chart.getSpotAtPosition(details.localPosition);
if (spot != null) {
_showTooltip(context, spot);
}
},
child: LineChart(_chartData),
)
void _showTooltip(BuildContext context, LineBarSpot spot) {
final date = _dates[spot.x.toInt()];
showDialog(
context: context,
builder: (_) => AlertDialog(
content: Text('${date}: ${spot.y.toStringAsFixed(1)}kg'),
),
);
}
对于大数据集(如1年数据):
LineChartData的clipData属性限制绘制范围showingTooltipIndicators仅当交互时显示标记Interval减少标签密度dart复制titlesData: FlTitlesData(
bottomTitles: AxisTitles(
sideTitles: SideTitles(
interval: _calculateLabelInterval(_data.length),
),
),
),
实现三级缓存体系:
dart复制Future<List<WeightRecord>> _loadData(Range range) async {
// 1. 检查内存缓存
if (_memoryCache.containsKey(range)) {
return _memoryCache[range]!;
}
// 2. 查询本地数据库
final localData = await _db.query(range);
if (localData.isNotEmpty) {
_memoryCache[range] = localData;
return localData;
}
// 3. 从网络获取
final cloudData = await _api.fetch(range);
await _db.save(cloudData);
_memoryCache[range] = cloudData;
return cloudData;
}
确保覆盖以下场景:
dart复制test('Y轴范围计算', () {
expect(_calcMinY([60,61,62]), equals(58));
expect(_calcMaxY([60,61,62]), equals(64));
expect(_calcMinY([]), equals(0)); // 空数据保护
});
需要在以下设备验证:
重点关注:
未来可扩展支持:
dart复制LineChart(
LineChartData(
lineBarsData: [
_buildWeightLine(),
_buildFatPercentageLine(),
],
),
)
基于机器学习提供:
python复制# 示例Python分析代码
from sklearn.linear_model import LinearRegression
def predict_trend(weights):
X = [[i] for i in range(len(weights))]
model = LinearRegression().fit(X, weights)
return model.predict([[len(weights)+7]])[0] # 预测一周后
这个体重趋势模块从设计到实现,每个环节都考虑了实际用户体验。在OpenHarmony生态中,Flutter的跨平台能力让我们可以用一套代码服务多种设备,而精心设计的数据可视化帮助用户将抽象的数字转化为直观的健康认知