Flutter for OpenHarmony移动数据使用监管助手App实战 - 首页实现
做流量监控类应用,首页的设计至关重要。用户打开App第一眼看到的就是首页,所以这个页面需要把最核心的信息直观地呈现出来。今天我们就来聊聊如何用Flutter实现一个功能完善、体验流畅的流量监控首页。
整体思路
首页要解决的核心问题是:让用户快速了解今天用了多少流量、当前网络状态如何、套餐还剩多少。围绕这几个需求,我把首页拆分成了几个模块:
- 顶部标题栏:展示App名称和实时网速
- 流量使用卡片:今日总流量、WiFi和移动数据分开统计
- 网络状态卡片:当前连接的网络类型和信号强度
- 套餐进度卡片:套餐使用百分比和剩余天数
- 快捷功能入口:日历、排行、提醒、测速四个常用功能
这种模块化的设计思路,不仅让代码结构清晰,后期维护也方便。每个模块独立成一个方法,改动某个模块不会影响其他部分。
页面基础结构
先搭建页面的骨架,用Scaffold作为页面容器:
class HomeView extends GetView<HomeController> {
const HomeView({super.key});
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.backgroundColor,
body: SafeArea(
child: Obx(() => controller.isLoading.value
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
SizedBox(height: 20.h),
_buildUsageCard(),
SizedBox(height: 16.h),
_buildNetworkCard(),
SizedBox(height: 16.h),
_buildPlanCard(),
SizedBox(height: 16.h),
_buildQuickActions(),
],
),
)),
),
);
}
}
这段代码有几个关键点需要注意:
GetView的使用:GetView<HomeController>是GetX提供的便捷基类,它会自动帮我们找到对应的controller实例,不需要手动Get.find()。这样写起来更简洁,也不容易出错。
Obx响应式监听:整个内容区域用Obx包裹,监听isLoading状态。数据加载中显示转圈,加载完成后渲染实际内容。这种写法比用FutureBuilder更灵活,因为后续数据更新时UI也会自动刷新。
SafeArea处理:不同设备的刘海屏、挖孔屏形态各异,SafeArea能自动处理这些安全区域,避免内容被遮挡。
ScreenUtil适配:16.w、20.h这种写法是flutter_screenutil的语法,会根据屏幕尺寸自动计算实际像素值,实现不同设备上的UI一致性。
顶部标题栏实现
标题栏要展示两个信息:App名称和当前网速。右边放一个按钮,点击跳转到实时监控页面。
Widget _buildHeader() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'流量监控',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
SizedBox(height: 4.h),
Obx(() => Text(
'当前速度: ${controller.formatSpeed(controller.currentSpeed.value)}',
style: TextStyle(
fontSize: 14.sp,
color: AppTheme.textSecondary,
),
)),
],
),
IconButton(
onPressed: () => Get.toNamed(Routes.REALTIME),
icon: Icon(Icons.speed, size: 28.sp, color: AppTheme.primaryColor),
),
],
);
}
这里有个细节:网速文字单独用Obx包裹,而不是整个Column。这样做的好处是,网速变化时只会重建这一个Text组件,而不是整个标题栏。虽然性能差异不大,但养成精细化控制重建范围的习惯是好事。
formatSpeed方法在controller里定义,负责把数字转换成人类可读的格式。比如1256.5会显示成"1.23 MB/s",而不是一长串数字。
流量使用卡片
这是首页最醒目的部分,用渐变背景让它从视觉上突出:
Widget _buildUsageCard() {
return Container(
padding: EdgeInsets.all(20.w),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [AppTheme.primaryColor, AppTheme.secondaryColor],
),
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: AppTheme.primaryColor.withOpacity(0.3),
blurRadius: 15.r,
offset: Offset(0, 8.h),
),
],
),
child: Column(
children: [
Text('今日流量使用', style: TextStyle(fontSize: 14.sp, color: Colors.white70)),
SizedBox(height: 10.h),
Obx(() => Text(
controller.formatBytes(controller.todayUsage.value),
style: TextStyle(fontSize: 36.sp, fontWeight: FontWeight.bold, color: Colors.white),
)),
SizedBox(height: 20.h),
// WiFi和移动数据分开显示
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildUsageItem('WiFi', controller.formatBytes(controller.todayWifi.value), Icons.wifi),
Container(width: 1, height: 40.h, color: Colors.white30),
_buildUsageItem('移动数据', controller.formatBytes(controller.todayMobile.value), Icons.signal_cellular_alt),
],
),
],
),
);
}
渐变背景的选择:从primaryColor到secondaryColor的渐变,让卡片看起来更有质感。纯色背景容易显得单调,渐变能增加视觉层次。
阴影的处理:阴影颜色用主色调的30%透明度,而不是黑色。这样阴影会带有一点蓝色调,和卡片更协调。offset设置为向下偏移8像素,模拟光源从上方照射的效果。
数字字号的选择:今日总流量用36sp的大字号,这是整个页面最重要的数据,要让用户一眼就能看到。下面WiFi和移动数据的字号小一些,形成主次分明的视觉层次。
WiFi和移动数据的展示项封装成独立方法,避免代码重复:
Widget _buildUsageItem(String label, String value, IconData icon) {
return Column(
children: [
Icon(icon, color: Colors.white, size: 24.sp),
SizedBox(height: 8.h),
Text(label, style: TextStyle(fontSize: 12.sp, color: Colors.white70)),
SizedBox(height: 4.h),
Text(value, style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold, color: Colors.white)),
],
);
}
这个方法接收三个参数:标签文字、数值、图标。图标放在最上面,因为图标的辨识度比文字高,用户扫一眼就知道这是WiFi还是移动数据。
网络状态卡片
这个卡片展示当前连接的网络信息,点击可以跳转到详情页:
Widget _buildNetworkCard() {
return GestureDetector(
onTap: () => Get.toNamed(Routes.NETWORK_STATUS),
child: Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10.r, offset: Offset(0, 4.h)),
],
),
child: Obx(() {
final network = controller.networkInfo.value;
return Row(
children: [
// 左侧图标容器
Container(
width: 50.w,
height: 50.w,
decoration: BoxDecoration(
color: network.type == NetworkType.wifi
? AppTheme.wifiColor.withOpacity(0.1)
: AppTheme.mobileColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12.r),
),
child: Icon(
network.type == NetworkType.wifi ? Icons.wifi : Icons.signal_cellular_alt,
color: network.type == NetworkType.wifi ? AppTheme.wifiColor : AppTheme.mobileColor,
size: 28.sp,
),
),
SizedBox(width: 16.w),
// 中间文字信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(network.name.isNotEmpty ? network.name : '未连接',
style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600, color: AppTheme.textPrimary)),
SizedBox(height: 4.h),
Text('信号${network.signalLevel} · ${network.typeString}',
style: TextStyle(fontSize: 13.sp, color: AppTheme.textSecondary)),
],
),
),
// 右侧箭头
Icon(Icons.chevron_right, color: AppTheme.textSecondary),
],
);
}),
),
);
}
颜色区分网络类型:WiFi用绿色(wifiColor),移动数据用橙色(mobileColor)。这种颜色编码让用户不用看文字就能快速分辨当前网络类型。图标背景用10%透明度的对应颜色,既有区分度又不会太抢眼。
右侧箭头的作用:chevron_right图标是一个视觉暗示,告诉用户这个卡片可以点击。这是移动端常见的交互模式,用户看到箭头就知道点击会有更多内容。
Expanded的使用:中间的文字区域用Expanded包裹,让它占据剩余空间。这样不管网络名称多长,布局都不会乱。如果名称太长,Text会自动省略。
Controller层的数据管理
页面只负责展示,数据的获取和处理都放在Controller里:
class HomeController extends GetxController {
final isLoading = false.obs;
final todayUsage = 0.obs;
final todayWifi = 0.obs;
final todayMobile = 0.obs;
final currentSpeed = 0.0.obs;
final networkInfo = Rx<NetworkInfo>(NetworkInfo());
final dataPlan = Rx<DataPlan?>(null);
void onInit() {
super.onInit();
loadData();
}
}
响应式变量声明:所有需要在UI上展示的数据都用.obs声明。GetX会追踪这些变量的变化,当值改变时自动通知UI更新。
onInit生命周期:onInit在controller被创建时调用,适合做初始化工作。这里调用loadData()加载数据。如果需要在页面每次显示时刷新数据,可以用onReady或者在View的initState里处理。
数据加载方法的实现:
void loadData() {
isLoading.value = true;
// 实际项目中这里会调用API或读取本地数据
todayUsage.value = 1024 * 1024 * 856; // 856 MB
todayWifi.value = 1024 * 1024 * 650; // 650 MB
todayMobile.value = 1024 * 1024 * 206; // 206 MB
currentSpeed.value = 1256.5; // KB/s
networkInfo.value = NetworkInfo(
type: NetworkType.wifi,
name: 'Home_WiFi_5G',
signalStrength: 85,
isConnected: true,
);
isLoading.value = false;
}
这里用的是模拟数据,实际项目中需要对接系统API获取真实的流量统计。数据单位统一用字节,显示时再转换成KB、MB、GB,这样计算更方便,也不容易出错。
流量格式化方法:
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';
}
这个方法根据数值大小自动选择合适的单位。小于1KB显示B,小于1MB显示KB,小于1GB显示MB,否则显示GB。toStringAsFixed控制小数位数,KB保留1位,MB和GB保留2位,避免显示过长的数字。
主题配置
统一的主题配置让整个App风格一致,也方便后期换肤:
class AppTheme {
static const Color primaryColor = Color(0xFF2196F3);
static const Color secondaryColor = Color(0xFF03A9F4);
static const Color wifiColor = Color(0xFF4CAF50);
static const Color mobileColor = Color(0xFFFF9800);
static const Color warningColor = Color(0xFFF44336);
static const Color backgroundColor = Color(0xFFF5F5F5);
static const Color textPrimary = Color(0xFF212121);
static const Color textSecondary = Color(0xFF757575);
}
颜色语义化命名:不要用blue、green这种命名,而是用primaryColor、wifiColor这种语义化的名字。这样改主题色时只需要改这一个文件,不用全局搜索替换。
文字颜色分级:textPrimary用于标题和重要文字,textSecondary用于次要信息和说明文字。这种分级让页面有层次感,用户能快速找到重点。
快捷功能入口
首页底部放四个常用功能的快捷入口,方便用户一键直达:
Widget _buildQuickActions() {
final actions = [
{'icon': Icons.calendar_today, 'label': '日历', 'route': Routes.DATA_CALENDAR},
{'icon': Icons.leaderboard, 'label': '排行', 'route': Routes.DATA_RANKING},
{'icon': Icons.notifications_outlined, 'label': '提醒', 'route': Routes.DATA_ALERT},
{'icon': Icons.speed, 'label': '测速', 'route': Routes.SPEED_TEST},
];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('快捷功能', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600, color: AppTheme.textPrimary)),
SizedBox(height: 12.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: actions.map((action) {
return GestureDetector(
onTap: () => Get.toNamed(action['route'] as String),
child: Container(
width: 75.w,
padding: EdgeInsets.symmetric(vertical: 16.h),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12.r)),
child: Column(
children: [
Icon(action['icon'] as IconData, size: 28.sp, color: AppTheme.primaryColor),
SizedBox(height: 8.h),
Text(action['label'] as String, style: TextStyle(fontSize: 12.sp, color: AppTheme.textSecondary)),
],
),
),
);
}).toList(),
),
],
);
}
数据驱动UI:用一个Map数组定义所有入口的配置,然后用map方法遍历生成Widget。这种写法比写四个重复的Container简洁多了,后续要加新入口只需要往数组里加一项。
固定宽度的考量:每个入口Container设置了固定宽度75.w,配合MainAxisAlignment.spaceBetween,四个入口会均匀分布在一行。如果用Expanded,入口会随屏幕宽度拉伸,在大屏设备上可能不太好看。
套餐进度卡片
这个卡片用环形进度条展示套餐使用情况,需要引入percent_indicator库:
Widget _buildPlanCard() {
return GestureDetector(
onTap: () => Get.toNamed(Routes.DATA_PLAN),
child: Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.r),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10.r, offset: Offset(0, 4.h))],
),
child: Obx(() {
final plan = controller.dataPlan.value;
if (plan == null) return Center(child: Text('暂无套餐', style: TextStyle(fontSize: 14.sp)));
return Row(
children: [
CircularPercentIndicator(
radius: 40.r,
lineWidth: 8.w,
percent: plan.usagePercentage / 100,
center: Text('${plan.usagePercentage.toStringAsFixed(0)}%',
style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.bold)),
progressColor: plan.isOverThreshold ? AppTheme.warningColor : AppTheme.primaryColor,
backgroundColor: Colors.grey.shade200,
),
SizedBox(width: 16.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(plan.name, style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600)),
SizedBox(height: 4.h),
Text('已用 ${plan.formattedUsed} / ${plan.formattedTotal}',
style: TextStyle(fontSize: 13.sp, color: AppTheme.textSecondary)),
SizedBox(height: 2.h),
Text('剩余 ${plan.daysRemaining} 天',
style: TextStyle(fontSize: 12.sp, color: AppTheme.textSecondary)),
],
),
),
Icon(Icons.chevron_right, color: AppTheme.textSecondary),
],
);
}),
),
);
}
进度条颜色变化:当使用量超过阈值(比如80%)时,进度条颜色从蓝色变成红色(warningColor),给用户一个视觉警告。这个阈值可以在套餐设置里让用户自定义。
空状态处理:如果用户还没设置套餐,dataPlan会是null,这时显示"暂无套餐"的提示。实际项目中可以做得更友好,比如加个"点击设置套餐"的引导。
写在最后
首页作为App的门面,既要信息丰富又不能显得杂乱。通过合理的卡片布局、清晰的视觉层次、流畅的交互反馈,可以让用户快速获取想要的信息。
这个首页还有很多可以优化的地方:
- 加载状态可以用骨架屏代替转圈,体验更好
- 网速可以做成实时刷新,比如每秒更新一次
- 套餐快到期时可以在卡片上加个醒目的标签
- 下拉刷新功能,让用户手动更新数据
这些优化点就留给大家自己探索了,动手实践才是最好的学习方式。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)