Flutter for OpenHarmony移动数据使用监管助手App实战 - 月报告实现

月报告是流量监控应用中帮助用户进行长期流量规划的重要功能。一个月的数据足够反映用户的使用习惯,也能帮助用户判断当前套餐是否合适,是否需要升级或降级。

页面定位与设计思路

月报告和周报告的定位不同。周报告侧重于短期趋势,月报告则更关注长期规划:

  • 本月总流量是否接近套餐上限,需不需要控制使用
  • 每周的使用量是否均匀,有没有某周特别高
  • WiFi和移动数据的比例是否合理,移动数据占比是否过高
  • 和套餐对比,利用率如何,是否需要调整套餐

基于这些需求,页面设计成两个主要模块:顶部汇总展示本月总量和分类数据,底部列表展示每周的使用明细。这种设计既有宏观概览,又有细节支撑。

页面结构实现

dart 复制代码
class MonthlyReportView extends GetView<MonthlyReportController> {
  const MonthlyReportView({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: AppTheme.backgroundColor,
      appBar: AppBar(title: const Text('月报')),
      body: SingleChildScrollView(
        padding: EdgeInsets.all(16.w),
        child: Column(
          children: [
            _buildSummary(),
            SizedBox(height: 16.h),
            _buildWeeklyBreakdown(),
          ],
        ),
      ),
    );
  }
}

结构简洁明了,两个卡片垂直排列。月报告不需要太复杂的图表,用列表展示每周数据更直观,用户可以快速扫描每周的使用情况。

SingleChildScrollView的选择 :页面内容固定,不需要懒加载,用SingleChildScrollViewListView更合适。如果后续要加更多内容,可以考虑换成CustomScrollView配合SliverList

月度汇总卡片

顶部卡片展示本月的流量概览:

dart 复制代码
Widget _buildSummary() {
  return Container(
    padding: EdgeInsets.all(20.w),
    decoration: BoxDecoration(
      gradient: const LinearGradient(colors: [AppTheme.primaryColor, AppTheme.accentColor]),
      borderRadius: BorderRadius.circular(16.r),
    ),
    child: Column(
      children: [
        Text('本月总流量', style: TextStyle(fontSize: 14.sp, color: Colors.white70)),
        SizedBox(height: 8.h),
        Obx(() => Text(
          controller.formatBytes(controller.monthlyTotal.value),
          style: TextStyle(fontSize: 32.sp, fontWeight: FontWeight.bold, color: Colors.white),
        )),
        SizedBox(height: 16.h),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            _buildItem('WiFi', controller.formatBytes(controller.monthlyWifi.value)),
            _buildItem('移动数据', controller.formatBytes(controller.monthlyMobile.value)),
          ],
        ),
      ],
    ),
  );
}

渐变色选择 :月报告用primaryColoraccentColor的渐变,和周报告的配色略有不同,让用户能区分不同的报告类型。颜色的微妙差异能帮助用户建立视觉记忆。

数据展示逻辑 :月流量通常比周流量大很多,formatBytes方法会自动选择合适的单位(MB或GB)。32sp的大字号让核心数据一目了然。

响应式更新Obx包裹数据展示部分,当数据变化时自动更新UI。这在数据异步加载完成后特别有用。

WiFi和移动数据的展示项:

dart 复制代码
Widget _buildItem(String label, String value) {
  return Column(
    children: [
      Text(label, style: TextStyle(fontSize: 12.sp, color: Colors.white70)),
      SizedBox(height: 4.h),
      Text(value, style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600, color: Colors.white)),
    ],
  );
}

这个方法在多个报告页面复用,保持了UI的一致性。用户在不同页面看到相同的布局,学习成本更低。

每周明细列表

用列表展示本月每周的流量使用情况,让用户看到更细粒度的数据:

dart 复制代码
Widget _buildWeeklyBreakdown() {
  return Container(
    padding: EdgeInsets.all(16.w),
    decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12.r)),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('每周详情', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600)),
        SizedBox(height: 12.h),
        ...List.generate(4, (i) => _buildWeekItem('第${i + 1}周', '${(i + 1) * 1.2} GB')),
      ],
    ),
  );
}

展开运算符的使用...List.generate()用展开运算符把生成的Widget列表展开到Column的children里。这比用ListView更简洁,因为数据量固定(一个月最多5周),不需要懒加载。

模拟数据生成(i + 1) * 1.2生成递增的数据,模拟流量逐周增加的场景。实际项目中应该从数据库查询真实数据,按周分组统计。

标题设计:卡片顶部有"每周详情"的标题,用16sp粗体,和下面的数据形成层次区分。

每周数据项的实现:

dart 复制代码
Widget _buildWeekItem(String week, String usage) {
  return Padding(
    padding: EdgeInsets.symmetric(vertical: 8.h),
    child: Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        Text(week, style: TextStyle(fontSize: 14.sp)),
        Text(usage, style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w600, color: AppTheme.primaryColor)),
      ],
    ),
  );
}

布局选择 :用Row配合MainAxisAlignment.spaceBetween让周名称和流量数值分别靠左和靠右,中间自动留白。这种布局在列表项中很常见,信息对齐清晰。

颜色强调:流量数值用主题色显示,和左边的普通文字形成对比,让数据更醒目。用户扫一眼就能看到每周的数值。

垂直间距:每个列表项有8.h的上下内边距,让列表不会太拥挤,也不会太松散。

Controller实现

dart 复制代码
class MonthlyReportController extends GetxController {
  final monthlyTotal = 0.obs;
  final monthlyWifi = 0.obs;
  final monthlyMobile = 0.obs;
  final weeklyBreakdown = <Map<String, dynamic>>[].obs;

  @override
  void onInit() {
    super.onInit();
    loadData();
  }

  void loadData() {
    monthlyTotal.value = 1024 * 1024 * 1024 * 20; // 20 GB
    monthlyWifi.value = 1024 * 1024 * 1024 * 15; // 15 GB
    monthlyMobile.value = 1024 * 1024 * 1024 * 5; // 5 GB
    
    weeklyBreakdown.value = [
      {'week': '第1周', 'usage': 1024 * 1024 * 1024 * 4},
      {'week': '第2周', 'usage': 1024 * 1024 * 1024 * 5},
      {'week': '第3周', 'usage': 1024 * 1024 * 1024 * 6},
      {'week': '第4周', 'usage': 1024 * 1024 * 1024 * 5},
    ];
  }

  String formatBytes(int bytes) {
    if (bytes < 1024) return '$bytes B';
    if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
    if (bytes < 1024 * 1024 * 1024) return '${(bytes / (1024 * 1024)).toStringAsFixed(2)} MB';
    return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB';
  }
}

Controller比较简单,主要负责数据加载和格式化。实际项目中loadData方法需要从数据库查询真实数据。

日期计算逻辑

获取本月数据需要正确计算日期范围,这是数据查询的基础:

dart 复制代码
DateTime getMonthStart() {
  final now = DateTime.now();
  return DateTime(now.year, now.month, 1);
}

DateTime getMonthEnd() {
  final now = DateTime.now();
  return DateTime(now.year, now.month + 1, 0); // 下月第0天就是本月最后一天
}

List<DateTimeRange> getWeeksInMonth() {
  final start = getMonthStart();
  final end = getMonthEnd();
  final weeks = <DateTimeRange>[];
  
  var weekStart = start;
  while (weekStart.isBefore(end)) {
    var weekEnd = weekStart.add(const Duration(days: 6));
    if (weekEnd.isAfter(end)) weekEnd = end;
    
    weeks.add(DateTimeRange(start: weekStart, end: weekEnd));
    weekStart = weekEnd.add(const Duration(days: 1));
  }
  
  return weeks;
}

月末计算技巧DateTime(year, month + 1, 0)这个写法很巧妙,下个月的第0天就是本月的最后一天,不用考虑每月天数不同的问题(28、29、30、31天)。

周分组逻辑:从月初开始,每7天为一周,最后一周可能不满7天。这种分组方式简单直观,用户容易理解。

数据查询实现

实际项目中,按周查询数据的逻辑:

dart 复制代码
Future<void> loadData() async {
  final monthStart = getMonthStart();
  final monthEnd = getMonthEnd();
  
  // 查询本月所有数据
  final data = await database.query(
    'daily_usage',
    where: 'date >= ? AND date <= ?',
    whereArgs: [monthStart.millisecondsSinceEpoch, monthEnd.millisecondsSinceEpoch],
  );
  
  // 计算总量
  int total = 0, wifi = 0, mobile = 0;
  for (final row in data) {
    total += row['total'] as int;
    wifi += row['wifi'] as int;
    mobile += row['mobile'] as int;
  }
  
  monthlyTotal.value = total;
  monthlyWifi.value = wifi;
  monthlyMobile.value = mobile;
  
  // 按周分组
  final weeks = getWeeksInMonth();
  final breakdown = <Map<String, dynamic>>[];
  
  for (var i = 0; i < weeks.length; i++) {
    final weekData = data.where((row) {
      final date = DateTime.fromMillisecondsSinceEpoch(row['date'] as int);
      return date.isAfter(weeks[i].start.subtract(const Duration(days: 1))) &&
             date.isBefore(weeks[i].end.add(const Duration(days: 1)));
    });
    
    int weekTotal = 0;
    for (final row in weekData) {
      weekTotal += row['total'] as int;
    }
    
    breakdown.add({
      'week': '第${i + 1}周',
      'usage': weekTotal,
    });
  }
  
  weeklyBreakdown.value = breakdown;
}

这段代码先查询本月所有数据,然后在内存中按周分组。对于月报告这种数据量(最多31条记录),这种方式性能足够。

功能扩展思路

月报告可以增加更多维度的分析,让数据更有价值:

套餐对比:如果用户设置了套餐,可以显示本月使用量占套餐的百分比,用进度条可视化。

dart 复制代码
Widget _buildPlanProgress() {
  return Container(
    padding: EdgeInsets.all(16.w),
    decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12.r)),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text('套餐使用', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600)),
            Obx(() => Text(
              '${controller.planUsagePercentage.toStringAsFixed(0)}%',
              style: TextStyle(fontSize: 14.sp, color: AppTheme.primaryColor),
            )),
          ],
        ),
        SizedBox(height: 12.h),
        Obx(() => LinearProgressIndicator(
          value: controller.planUsagePercentage / 100,
          backgroundColor: Colors.grey.shade200,
          valueColor: AlwaysStoppedAnimation(
            controller.planUsagePercentage > 80 ? Colors.red : AppTheme.primaryColor,
          ),
        )),
        SizedBox(height: 8.h),
        Obx(() => Text(
          '已用 ${controller.formatBytes(controller.monthlyTotal.value)} / ${controller.formatBytes(controller.planTotal.value)}',
          style: TextStyle(fontSize: 12.sp, color: AppTheme.textSecondary),
        )),
      ],
    ),
  );
}

进度条颜色会根据使用比例变化,超过80%变成红色警告。

同比环比:和上个月、去年同期对比,帮助用户发现长期趋势。

dart 复制代码
Widget _buildComparison() {
  return Container(
    padding: EdgeInsets.all(16.w),
    decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12.r)),
    child: Row(
      children: [
        Expanded(
          child: _buildComparisonItem(
            '环比上月',
            controller.monthOverMonthChange,
          ),
        ),
        Container(width: 1, height: 50.h, color: Colors.grey.shade200),
        Expanded(
          child: _buildComparisonItem(
            '同比去年',
            controller.yearOverYearChange,
          ),
        ),
      ],
    ),
  );
}

Widget _buildComparisonItem(String label, double change) {
  final isIncrease = change >= 0;
  return Column(
    children: [
      Text(label, style: TextStyle(fontSize: 12.sp, color: AppTheme.textSecondary)),
      SizedBox(height: 4.h),
      Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(
            isIncrease ? Icons.arrow_upward : Icons.arrow_downward,
            size: 16.sp,
            color: isIncrease ? Colors.red : AppTheme.wifiColor,
          ),
          Text(
            '${change.abs().toStringAsFixed(1)}%',
            style: TextStyle(
              fontSize: 16.sp,
              fontWeight: FontWeight.w600,
              color: isIncrease ? Colors.red : AppTheme.wifiColor,
            ),
          ),
        ],
      ),
    ],
  );
}

应用排行:展示本月流量消耗最多的几个应用,帮助用户了解流量都花在哪了。

dart 复制代码
Widget _buildTopApps() {
  return Container(
    padding: EdgeInsets.all(16.w),
    decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12.r)),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('流量消耗Top 5', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600)),
        SizedBox(height: 12.h),
        ...controller.topApps.map((app) => Padding(
          padding: EdgeInsets.symmetric(vertical: 6.h),
          child: Row(
            children: [
              Icon(Icons.apps, color: AppTheme.primaryColor, size: 20.sp),
              SizedBox(width: 8.w),
              Expanded(child: Text(app['name'], style: TextStyle(fontSize: 14.sp))),
              Text(controller.formatBytes(app['usage']), style: TextStyle(fontSize: 14.sp, color: AppTheme.textSecondary)),
            ],
          ),
        )).toList(),
      ],
    ),
  );
}

预测功能:根据当前使用速度,预测月底会用多少流量,提前预警。

dart 复制代码
double predictMonthEndUsage() {
  final now = DateTime.now();
  final monthStart = getMonthStart();
  final monthEnd = getMonthEnd();
  
  final daysPassed = now.difference(monthStart).inDays + 1;
  final totalDays = monthEnd.difference(monthStart).inDays + 1;
  
  final dailyAverage = monthlyTotal.value / daysPassed;
  return dailyAverage * totalDays;
}

性能考虑

月报告涉及的数据量比日报告和周报告都大,需要注意性能优化:

数据聚合:不要在客户端遍历每天的数据再求和,应该在数据库层面做聚合查询。SQLite支持SUM等聚合函数。

dart 复制代码
final result = await database.rawQuery('''
  SELECT SUM(total) as total, SUM(wifi) as wifi, SUM(mobile) as mobile
  FROM daily_usage
  WHERE date >= ? AND date <= ?
''', [monthStart.millisecondsSinceEpoch, monthEnd.millisecondsSinceEpoch]);

缓存策略:月报告数据变化不频繁,可以缓存起来,用户再次打开时直接显示缓存数据,后台静默更新。

分页加载:如果要支持查看历史月份,用分页加载而不是一次性加载所有数据。每次只加载一个月的数据。

写在最后

月报告页面通过汇总卡片和每周明细两个模块,帮助用户从长期角度了解自己的流量使用情况。相比日详情和周报告,月报告更适合做流量规划和套餐评估。

在实现过程中,我们重点关注了数据的组织方式和展示效果。简洁的列表布局、清晰的数据层次、合理的颜色运用,都是为了让用户能够快速获取有价值的信息。


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

相关推荐
灰灰勇闯IT2 小时前
【Flutter for OpenHarmonyDart 入门日记】第5篇:字典类型 Map 与动态类型 dynamic 全解析
开发语言·javascript·ecmascript
小李独爱秋2 小时前
计算机网络经典问题透视:EF PHB与AF PHB深度解析——它们有何不同,各适用于何种通信量?
网络·计算机网络·信息与通信·qos·phb
leaves falling2 小时前
c语言- 有序序列合并
c语言·开发语言·数据结构
雨季6662 小时前
Flutter for OpenHarmony 入门实践:从 Scaffold 到 Container 的三段式布局构建
开发语言·javascript·flutter
Dreamy smile2 小时前
JavaScript 继承与 this 指向操作详解
开发语言·javascript·原型模式
zilikew2 小时前
Flutter框架跨平台鸿蒙开发——脑筋急转弯小游戏的开发流程
flutter·华为·harmonyos·鸿蒙
副露のmagic2 小时前
更弱智的算法学习 day53
开发语言·python
HellowAmy2 小时前
我的C++规范 - 回调的设想
开发语言·c++·代码规范
Java程序员威哥2 小时前
SpringBoot多环境配置实战:从基础用法到源码解析与生产避坑
java·开发语言·网络·spring boot·后端·python·spring