统计页面是流量监控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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐