Flutter for OpenHarmony移动数据使用监管助手App实战 - 统计实现
统计页面是流量监控App的核心数据分析模块,主要展示流量使用趋势、WiFi/移动数据占比等关键指标。页面采用模块化设计,包含时间周期选择器(日/周/月切换)、流量汇总卡片(总流量及分类统计)、趋势图表、报告入口和每日详情列表五个功能区块。技术实现上使用Flutter框架,通过Obx响应式更新数据,采用Card布局和分段控件提升交互体验,并支持数据导出功能。页面整体采用滚动布局,以可视化图表和清晰的
统计页面是流量监控App的数据分析中心,用户在这里可以看到流量使用的趋势变化、WiFi和移动数据的占比、每日使用详情等。这个页面涉及到图表绑定、时间筛选、列表展示等多个技术点,是整个App中相对复杂的一个模块。
页面功能拆解
统计页面需要展示的信息比较多,我把它拆分成几个区块:
- 时间周期选择器:日、周、月三个维度切换
- 总量汇总卡片:选定周期内的总流量、WiFi流量、移动数据流量
- 趋势图表:柱状图展示每天的流量变化
- 报告入口:周报和月报的快捷入口
- 每日详情列表:可点击查看某一天的详细数据
页面基础结构
先搭建页面的整体框架:
class StatisticsView extends GetView<StatisticsController> {
const StatisticsView({super.key});
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.backgroundColor,
appBar: AppBar(
title: const Text('流量统计'),
actions: [
IconButton(
// StatisticsView继承GetView,自动获得controller属性访问控制器。
// Scaffold设置背景色为主题背景色,AppBar标题为"流量统计"。
// actions中放置导出按钮,方便用户导出统计数据。
onPressed: () => Get.toNamed(Routes.DATA_EXPORT),
icon: const Icon(Icons.download_outlined),
),
],
),
body: Obx(() => controller.isLoading.value
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Column(
children: [
_buildPeriodSelector(),
SizedBox(height: 16.h),
_buildSummaryCard(),
// 点击导出按钮跳转到数据导出页面。
// body部分用Obx包裹,根据isLoading状态显示加载指示器或内容。
// 内容区使用SingleChildScrollView包裹Column,确保可以滚动。
SizedBox(height: 16.h),
_buildChart(),
SizedBox(height: 16.h),
_buildReportButtons(),
SizedBox(height: 16.h),
_buildDailyList(),
],
),
)),
);
}
}
AppBar右侧放了个导出按钮,方便用户把统计数据导出成文件。整个内容区用SingleChildScrollView包裹,因为内容可能超出一屏。
时间周期选择器
用一个分段控件让用户切换日、周、月三个统计维度:
Widget _buildPeriodSelector() {
final periods = ['日', '周', '月'];
return Container(
padding: EdgeInsets.all(4.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.r),
),
child: Row(
children: List.generate(periods.length, (index) {
return Expanded(
child: GestureDetector(
// 定义periods数组包含三个选项:日、周、月。
// Container设置4像素内边距,白色背景和12像素圆角。
// 使用List.generate生成三个Tab按钮,每个用Expanded等宽分布。
onTap: () => controller.changePeriod(index),
child: Obx(() => Container(
padding: EdgeInsets.symmetric(vertical: 12.h),
decoration: BoxDecoration(
color: controller.selectedPeriod.value == index
? AppTheme.primaryColor
: Colors.transparent,
borderRadius: BorderRadius.circular(10.r),
),
child: Center(
child: Text(
periods[index],
style: TextStyle(
fontSize: 14.sp,
// GestureDetector包裹实现点击切换,调用controller.changePeriod方法。
// 选中的Tab背景色为主题色,未选中为透明。
// 内部Container设置10像素圆角,比外层稍小形成内嵌效果。
fontWeight: FontWeight.w600,
color: controller.selectedPeriod.value == index
? Colors.white
: AppTheme.textSecondary,
),
),
),
)),
),
);
}),
),
);
}
设计思路:选中的Tab用主题色填充背景,未选中的保持透明。用Expanded让三个Tab等宽分布。外层Container加4像素的padding,让选中状态的圆角和外框有一点间距,视觉上更精致。
流量汇总卡片
展示选定周期内的总流量,以及WiFi和移动数据的分项:
Widget _buildSummaryCard() {
return Container(
padding: EdgeInsets.all(20.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10.r,
offset: Offset(0, 4.h),
// 汇总卡片Container设置20像素内边距,白色背景。
// 16像素圆角配合轻微阴影,营造卡片悬浮效果。
// 阴影透明度5%,模糊半径10像素,向下偏移4像素。
),
],
),
child: Column(
children: [
Text('总流量', style: TextStyle(fontSize: 14.sp, color: AppTheme.textSecondary)),
SizedBox(height: 8.h),
Obx(() => Text(
controller.formatBytes(controller.totalUsage.value),
style: TextStyle(fontSize: 32.sp, fontWeight: FontWeight.bold, color: AppTheme.textPrimary),
)),
SizedBox(height: 20.h),
Row(
children: [
Expanded(child: _buildSummaryItem('WiFi', controller.formatBytes(controller.wifiUsage.value), AppTheme.wifiColor, Icons.wifi)),
// Column垂直布局,顶部是"总流量"标签,14sp次要颜色。
// 总流量数值使用32sp超大字号和粗体,突出显示核心数据。
// 下方Row横向排列WiFi和移动数据两个分项。
Container(width: 1, height: 50.h, color: Colors.grey.shade200),
Expanded(child: _buildSummaryItem('移动数据', controller.formatBytes(controller.mobileUsage.value), AppTheme.mobileColor, Icons.signal_cellular_alt)),
],
),
],
),
);
}
总流量用32sp的大字号突出显示,下面WiFi和移动数据用图标+颜色区分。中间的分隔线用Container画一条1像素宽的灰线。
分项展示的复用组件:
Widget _buildSummaryItem(String label, String value, Color color, IconData icon) {
return Column(
children: [
Icon(icon, color: color, size: 24.sp),
SizedBox(height: 8.h),
Text(label, style: TextStyle(fontSize: 12.sp, color: AppTheme.textSecondary)),
SizedBox(height: 4.h),
Text(value, style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600, color: AppTheme.textPrimary)),
// _buildSummaryItem是复用组件,接收标签、数值、颜色和图标参数。
// Column垂直布局,顶部是彩色图标,24sp大小。
// 依次是标签文字和数值,数值使用16sp和600字重突出显示。
],
);
}
柱状图实现
用fl_chart库绑制流量趋势图:
Widget _buildChart() {
return Container(
height: 200.h,
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.r),
),
child: Obx(() => BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
// 图表容器固定高度200像素,内边距16像素。
// 白色背景配合16像素圆角,与其他卡片风格一致。
// BarChart用Obx包裹,数据变化时自动重绑。
maxY: controller.chartData.isEmpty ? 1 : controller.chartData.reduce((a, b) => a > b ? a : b) * 1.2,
barTouchData: BarTouchData(enabled: true),
titlesData: FlTitlesData(
show: true,
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
final days = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
return Text(days[value.toInt() % 7], style: TextStyle(fontSize: 10.sp, color: AppTheme.textSecondary));
},
// alignment设为spaceAround让柱子均匀分布。
// maxY动态计算,取数据最大值乘以1.2,给柱子顶部留出空间。
// bottomTitles配置底部X轴标签,显示周一到周日。
),
),
leftTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
),
borderData: FlBorderData(show: false),
gridData: FlGridData(show: false),
barGroups: _buildBarGroups(),
),
)),
);
}
maxY的计算:取数据中的最大值乘以1.2,给柱子顶部留出一些空间,不然最高的柱子会顶到容器边缘。
隐藏多余元素:左、上、右的标题都隐藏掉,只保留底部的日期标签。边框和网格线也隐藏,让图表看起来更简洁。
柱子的生成:
List<BarChartGroupData> _buildBarGroups() {
return List.generate(controller.chartData.length, (index) {
return BarChartGroupData(
x: index,
barRods: [
BarChartRodData(
toY: controller.chartData[index],
color: AppTheme.primaryColor,
width: 20.w,
borderRadius: BorderRadius.vertical(top: Radius.circular(6.r)),
// _buildBarGroups方法生成柱状图的数据组。
// List.generate根据chartData长度生成对应数量的柱子。
// 每个BarChartGroupData包含x坐标和barRods柱子数据。
),
],
);
});
}
柱子只有顶部有圆角,底部是平的,这样看起来像是从底部"长"出来的。
报告快捷入口
周报和月报的入口按钮并排放置:
Widget _buildReportButtons() {
return Row(
children: [
Expanded(child: _buildReportButton('周报', Icons.calendar_view_week, Routes.WEEKLY_REPORT)),
SizedBox(width: 12.w),
Expanded(child: _buildReportButton('月报', Icons.calendar_month, Routes.MONTHLY_REPORT)),
],
);
}
Widget _buildReportButton(String label, IconData icon, String route) {
return GestureDetector(
onTap: () => Get.toNamed(route),
// Row横向排列周报和月报两个按钮,用Expanded等宽分布。
// 两个按钮之间间隔12像素。
// _buildReportButton是复用组件,接收标签、图标和路由参数。
child: Container(
padding: EdgeInsets.symmetric(vertical: 16.h),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.r),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: AppTheme.primaryColor, size: 20.sp),
SizedBox(width: 8.w),
Text(label, style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w600, color: AppTheme.textPrimary)),
// GestureDetector包裹实现点击跳转到对应路由。
// Container设置垂直内边距16像素,白色背景和12像素圆角。
// 内部Row居中排列图标和文字,图标使用主题色。
],
),
),
);
}
两个按钮用Expanded等宽,中间留12像素的间距。
每日详情列表
列表展示每天的流量使用情况,点击可以跳转到日详情页:
Widget _buildDailyList() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('每日详情', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600, color: AppTheme.textPrimary)),
SizedBox(height: 12.h),
Obx(() => Column(
children: controller.dailyUsageList.map((usage) {
return GestureDetector(
onTap: () => Get.toNamed(Routes.DAILY_DETAIL, arguments: usage),
child: Container(
margin: EdgeInsets.only(bottom: 8.h),
// _buildDailyList构建每日详情列表,Column垂直布局。
// 顶部是"每日详情"标题,16sp字号和600字重。
// Obx包裹列表内容,dailyUsageList变化时自动更新。
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10.r),
),
child: Row(
children: [
Text(DateFormat('MM/dd').format(usage.date), style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w500)),
SizedBox(width: 16.w),
Expanded(
child: LinearProgressIndicator(
value: usage.wifiUsage / usage.totalUsage,
backgroundColor: AppTheme.mobileColor.withOpacity(0.3),
// 每个列表项用GestureDetector包裹,点击跳转到日详情页并传递usage数据。
// Container设置底部8像素外边距,12像素内边距,白色背景和10像素圆角。
// Row横向排列日期、进度条和流量数值。
valueColor: AlwaysStoppedAnimation(AppTheme.wifiColor),
),
),
SizedBox(width: 16.w),
Text(usage.formattedTotal, style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w600, color: AppTheme.primaryColor)),
Icon(Icons.chevron_right, color: AppTheme.textSecondary),
],
),
),
);
}).toList(),
)),
],
);
}
进度条的巧妙用法:用LinearProgressIndicator展示WiFi和移动数据的占比,绿色部分是WiFi,橙色背景是移动数据。这样用户一眼就能看出每天的流量构成。
跳转传参:点击列表项时,把usage对象通过arguments传给日详情页,日详情页可以直接使用这个数据,不需要再次请求。
Controller层的数据处理
class StatisticsController extends GetxController {
final isLoading = false.obs;
final selectedPeriod = 0.obs;
final totalUsage = 0.obs;
final wifiUsage = 0.obs;
final mobileUsage = 0.obs;
final dailyUsageList = <DailyUsage>[].obs;
final chartData = <double>[].obs;
void onInit() {
super.onInit();
loadData();
// StatisticsController定义了页面所需的所有响应式状态变量。
// isLoading控制加载状态,selectedPeriod记录当前选中的时间周期。
// totalUsage、wifiUsage、mobileUsage分别存储总流量和分类流量。
}
void changePeriod(int index) {
selectedPeriod.value = index;
loadData();
}
}
切换时间周期时调用loadData重新加载数据,图表和列表会自动更新。
数据加载和汇总计算:
void loadData() {
isLoading.value = true;
final now = DateTime.now();
dailyUsageList.clear();
chartData.clear();
for (int i = 6; i >= 0; i--) {
final date = now.subtract(Duration(days: i));
final wifi = (500 + (i * 100)) * 1024 * 1024;
final mobile = (100 + (i * 50)) * 1024 * 1024;
dailyUsageList.add(DailyUsage(
id: '$i',
date: date,
// loadData方法加载统计数据,首先设置加载状态为true。
// 清空现有的dailyUsageList和chartData列表。
// 循环生成最近7天的模拟数据,从6天前到今天。
wifiUsage: wifi,
mobileUsage: mobile,
totalUsage: wifi + mobile,
));
chartData.add((wifi + mobile) / (1024 * 1024 * 1024));
}
totalUsage.value = dailyUsageList.fold(0, (sum, item) => sum + item.totalUsage);
wifiUsage.value = dailyUsageList.fold(0, (sum, item) => sum + item.wifiUsage);
mobileUsage.value = dailyUsageList.fold(0, (sum, item) => sum + item.mobileUsage);
isLoading.value = false;
// 每天的数据包含日期、WiFi流量、移动数据流量和总流量。
// chartData存储转换为GB单位的流量值,用于图表显示。
// 使用fold方法计算所有天数的流量总和。
}
fold方法的使用:fold是Dart集合的聚合方法,用来计算列表中所有元素的总和。第一个参数是初始值,第二个参数是累加函数。
统计页面的数据量比较大,实际项目中建议做好缓存,避免每次切换Tab都重新计算。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)