Flutter for OpenHarmony移动数据使用监管助手App实战 - SIM卡管理实现
SIM卡管理页面帮助双卡用户更好地管理和分配流量使用。通过清晰的卡片展示、便捷的开关控制、直观的流量统计,用户可以轻松掌握每张卡的使用情况。支持智能切换数据卡添加流量共享功能支持按应用指定使用的SIM卡添加SIM卡流量对比图表欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net。
SIM卡管理页面让用户查看和管理双卡设备上的SIM卡信息,包括运营商、流量使用情况、套餐余量等。对于使用双卡的用户,这个功能可以帮助他们更好地分配流量使用。
功能设计
SIM卡管理页面需要实现:
- 显示每张SIM卡的基本信息
- 显示每张卡的流量使用情况
- 支持启用/禁用SIM卡数据
- 设置默认数据卡
页面整体结构
class SimManagerView extends GetView<SimManagerController> {
const SimManagerView({super.key});
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.backgroundColor,
appBar: AppBar(
上面这段代码定义了SIM卡管理页面的基础结构。SimManagerView继承自GetView,这样可以直接通过controller属性访问控制器。
Scaffold是Flutter的基础页面脚手架,提供了AppBar、body等标准布局区域。backgroundColor设置页面背景色为主题定义的背景色。
title: const Text('SIM卡管理'),
actions: [
IconButton(
icon: Icon(Icons.refresh),
onPressed: () => controller.refreshSimInfo(),
),
],
),
AppBar的title设置页面标题为"SIM卡管理"。actions数组中添加了一个刷新按钮,点击时调用controller的refreshSimInfo方法。
这个刷新按钮让用户可以手动更新SIM卡信息,比如插拔卡后需要重新获取数据。IconButton是Material Design风格的图标按钮。
body: SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Column(
children: [
_buildSimCard(0),
SizedBox(height: 16.h),
_buildSimCard(1),
body部分使用SingleChildScrollView包裹,确保内容超出屏幕时可以滚动。padding设置16.w的内边距,.w是flutter_screenutil的屏幕适配单位。
Column垂直排列子组件,_buildSimCard(0)和_buildSimCard(1)分别构建两张SIM卡的信息卡片,参数是卡槽索引。SizedBox添加16.h的垂直间距。
SizedBox(height: 16.h),
_buildDefaultDataCard(),
SizedBox(height: 16.h),
_buildTotalUsage(),
],
),
),
);
}
}
继续添加默认数据卡设置区域和双卡总流量统计区域。_buildDefaultDataCard()构建默认数据卡选择组件,_buildTotalUsage()构建流量汇总展示。
每个组件之间用SizedBox添加16.h的间距,保持视觉上的统一和整洁。整个页面从上到下依次展示:SIM1卡片、SIM2卡片、默认数据卡设置、总流量统计。
SIM卡信息卡片
SIM卡信息卡片是页面的核心组件,需要根据卡槽是否有卡、卡是否启用来显示不同的内容:
Widget _buildSimCard(int index) {
return Obx(() {
if (index >= controller.simCards.length) {
return _buildEmptySimSlot(index);
}
final sim = controller.simCards[index];
final isActive = sim['active'] as bool;
_buildSimCard方法接收卡槽索引参数,返回对应的SIM卡信息卡片。Obx是GetX的响应式包装器,当simCards数据变化时自动重建UI。
首先判断索引是否超出simCards列表长度,如果超出说明该卡槽没有SIM卡,调用_buildEmptySimSlot显示空卡槽状态。
从simCards列表中获取对应索引的SIM卡数据,使用as进行类型转换获取active状态。实际项目中建议定义Model类来保证类型安全。
final carrier = sim['carrier'] as String;
final usage = sim['usage'] as String;
final remaining = sim['remaining'] as String;
final phoneNumber = sim['phoneNumber'] as String;
return Container(
decoration: BoxDecoration(
继续从Map中解构出运营商名称、使用量、剩余量、手机号等字段。这些数据将在卡片中展示给用户。
Container是Flutter中最常用的布局容器,可以设置装饰、内边距、尺寸等属性。BoxDecoration用于定义容器的视觉样式。
color: Colors.white,
borderRadius: BorderRadius.circular(16.r),
border: isActive
? Border.all(color: AppTheme.primaryColor, width: 2)
: null,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
设置卡片背景为白色,圆角为16.r(屏幕适配单位)。border属性根据isActive状态决定是否显示边框,启用的卡有2像素宽的主色调边框。
boxShadow添加阴影效果,使用3%透明度的黑色,让卡片看起来有轻微的悬浮感。这种设计让启用和禁用的卡片在视觉上有明显区分。
blurRadius: 8.r,
offset: Offset(0, 2.h),
),
],
),
child: Column(
children: [
_buildSimHeader(index, sim),
blurRadius设置阴影模糊半径为8.r,offset设置阴影向下偏移2.h像素。这个参数组合形成自然的投影效果。
Column垂直排列卡片内容,首先调用_buildSimHeader构建卡片头部,传入卡槽索引和SIM卡数据。头部包含SIM卡图标、运营商信息和启用开关。
if (isActive) ...[
Divider(height: 1),
_buildSimUsage(sim),
Divider(height: 1),
_buildSimActions(index, sim),
],
],
),
);
});
}
使用if语句配合展开运算符…[]实现条件渲染,只有当SIM卡启用时才显示流量使用区域和操作按钮区域。
Divider是分隔线组件,height: 1表示分隔线占用1像素高度。_buildSimUsage显示流量使用情况,_buildSimActions显示操作按钮。
这种设计让禁用的卡片更简洁,只显示基本信息,启用的卡片则展示完整功能。
卡片头部实现
卡片头部展示SIM卡的基本信息和启用开关:
Widget _buildSimHeader(int index, Map<String, dynamic> sim) {
final isActive = sim['active'] as bool;
final carrier = sim['carrier'] as String;
final phoneNumber = sim['phoneNumber'] as String;
return Padding(
padding: EdgeInsets.all(16.w),
child: Row(
_buildSimHeader方法接收卡槽索引和SIM卡数据Map。首先解构出需要显示的字段:启用状态、运营商名称、手机号。
Padding为内容添加16.w的内边距,Row用于横向排列子组件。头部布局是:左侧图标区域、中间文字信息、右侧开关。
children: [
Container(
width: 56.w,
height: 56.w,
decoration: BoxDecoration(
color: isActive
? AppTheme.primaryColor.withOpacity(0.1)
: Colors.grey.shade100,
Container创建一个56x56的正方形图标容器。decoration设置背景色,启用状态用主色调的10%透明度,禁用状态用浅灰色。
这种颜色设计让用户一眼就能分辨SIM卡的启用状态,启用时有蓝色调,禁用时是灰色调。
borderRadius: BorderRadius.circular(14.r),
),
child: Stack(
alignment: Alignment.center,
children: [
Icon(
Icons.sim_card,
color: isActive ? AppTheme.primaryColor : Colors.grey,
圆角设置为14.r,和56的宽度形成约1:4的比例,看起来比较柔和。Stack允许子组件叠加显示,alignment设置子组件居中对齐。
Icon显示SIM卡图标,颜色根据启用状态变化。启用时用主色调,禁用时用灰色,和背景色形成呼应。
size: 32.sp,
),
Positioned(
bottom: 4.h,
right: 4.w,
child: Container(
width: 18.w,
height: 18.w,
图标大小设置为32.sp。Positioned用于在Stack中精确定位子组件,这里将卡槽编号小圆圈定位在右下角。
小圆圈容器大小为18x18,用于显示卡槽编号(1或2),让用户知道这是哪个卡槽的SIM卡。
decoration: BoxDecoration(
color: isActive ? AppTheme.wifiColor : Colors.grey,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
child: Center(
child: Text(
'${index + 1}',
小圆圈使用BoxShape.circle设置为圆形,启用时绿色背景,禁用时灰色背景。白色边框2像素宽,让小圆圈和底层图标有清晰的分隔。
Center将文字居中,显示卡槽编号。index从0开始,所以显示时加1,变成1和2。
style: TextStyle(
fontSize: 10.sp,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
),
],
),
),
编号文字使用10sp的小字号,粗体,白色,在彩色背景上清晰可见。这种设计在有限空间内传达了卡槽信息。
整个图标区域的设计:SIM卡图标居中,右下角叠加一个带编号的小圆圈,颜色根据状态变化。
SizedBox(width: 16.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
SizedBox添加16.w的水平间距。Expanded让中间的文字区域占据剩余空间。Column垂直排列文字信息,crossAxisAlignment设置左对齐。
内层Row用于横向排列SIM名称和状态标签。
'SIM ${index + 1}',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
if (isActive) ...[
显示"SIM 1"或"SIM 2",字号18sp,粗体,使用主要文字颜色。这是卡片的主标题,视觉上最突出。
if (isActive)配合展开运算符,只有启用状态才显示后面的状态标签。
SizedBox(width: 8.w),
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 2.h),
decoration: BoxDecoration(
color: AppTheme.wifiColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8.r),
),
状态标签和SIM名称之间有8.w的间距。Container创建标签容器,水平内边距8.w,垂直内边距2.h。
背景色使用绿色的10%透明度,圆角8.r,形成一个小药丸形状的标签。
child: Text(
'已启用',
style: TextStyle(
fontSize: 10.sp,
color: AppTheme.wifiColor,
fontWeight: FontWeight.w500,
),
),
),
],
],
),
标签文字"已启用",字号10sp,绿色,中等粗细。这个小标签让用户快速确认SIM卡的启用状态。
整个Row的效果是:SIM名称在左,启用状态标签紧跟其后(仅启用时显示)。
SizedBox(height: 4.h),
Text(
carrier,
style: TextStyle(fontSize: 14.sp, color: AppTheme.textSecondary),
),
SizedBox(height: 2.h),
Text(
phoneNumber,
style: TextStyle(fontSize: 12.sp, color: AppTheme.textSecondary),
),
SIM名称下方显示运营商名称,字号14sp,次要文字颜色。再下方显示手机号,字号12sp,同样是次要颜色。
三行文字形成层次:SIM名称18sp最大,运营商14sp中等,手机号12sp最小。这种递减的字号设计突出主要信息。
],
),
),
Switch(
value: isActive,
onChanged: (v) => controller.toggleSim(index, v),
activeColor: AppTheme.primaryColor,
),
],
),
);
}
Row的最后一个子组件是Switch开关。value绑定isActive状态,onChanged回调调用controller的toggleSim方法切换状态。
activeColor设置开关启用时的颜色为主色调。Switch是Flutter原生的开关组件,符合Material Design规范。
SIM卡流量使用
流量使用区域展示该SIM卡的本月使用量、套餐余量和使用进度:
Widget _buildSimUsage(Map<String, dynamic> sim) {
final usage = sim['usage'] as String;
final remaining = sim['remaining'] as String;
final usagePercent = sim['usagePercent'] as double;
return Padding(
padding: EdgeInsets.all(16.w),
_buildSimUsage方法接收SIM卡数据Map,解构出使用量、剩余量、使用百分比三个字段。
Padding为整个区域添加16.w的内边距,和卡片头部保持一致的边距。
child: Column(
children: [
Row(
children: [
Expanded(
child: _buildUsageItem(
'本月使用',
usage,
Column垂直排列内容,Row横向排列"本月使用"和"套餐余量"两个数据项。Expanded让两个数据项各占一半宽度。
_buildUsageItem是封装的数据项组件,接收标签、数值、颜色三个参数。
AppTheme.mobileColor,
),
),
Container(width: 1, height: 40.h, color: Colors.grey.shade200),
Expanded(
child: _buildUsageItem(
'套餐余量',
remaining,
AppTheme.wifiColor,
),
),
],
),
"本月使用"用橙色(mobileColor),"套餐余量"用绿色(wifiColor),颜色编码让用户快速区分。
中间用1像素宽、40高的灰色Container作为分隔线,视觉上分隔两个数据项。
SizedBox(height: 16.h),
Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'套餐使用进度',
SizedBox添加16.h的垂直间距。下方是进度条区域,先用Row显示标签和百分比,spaceBetween让它们分居两端。
"套餐使用进度"标签在左侧,百分比数值在右侧。
style: TextStyle(fontSize: 13.sp, color: AppTheme.textSecondary),
),
Text(
'${usagePercent.toStringAsFixed(1)}%',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.bold,
标签使用13sp字号和次要颜色。百分比使用14sp字号和粗体,toStringAsFixed(1)保留一位小数。
百分比数值比标签稍大且加粗,突出显示当前使用进度。
color: usagePercent >= 80 ? Colors.orange : AppTheme.primaryColor,
),
),
],
),
SizedBox(height: 8.h),
ClipRRect(
borderRadius: BorderRadius.circular(4.r),
百分比颜色根据数值变化:超过80%显示橙色警告,否则显示主色调蓝色。这种颜色变化提醒用户注意流量使用情况。
ClipRRect用于裁剪子组件,给进度条添加圆角效果。LinearProgressIndicator本身不支持圆角,需要用ClipRRect包裹。
child: LinearProgressIndicator(
value: usagePercent / 100,
backgroundColor: Colors.grey.shade200,
valueColor: AlwaysStoppedAnimation(
usagePercent >= 80 ? Colors.orange : AppTheme.primaryColor,
),
minHeight: 8.h,
),
),
],
),
],
),
);
}
LinearProgressIndicator是Flutter的线性进度条。value需要0-1之间的值,所以usagePercent除以100。
backgroundColor设置进度条背景色,valueColor设置进度色,同样根据80%阈值变化颜色。minHeight设置进度条高度为8.h。
流量数据项的封装组件:
Widget _buildUsageItem(String label, String value, Color color) {
return Column(
children: [
Text(label, style: TextStyle(fontSize: 12.sp, color: AppTheme.textSecondary)),
SizedBox(height: 4.h),
Text(
value,
_buildUsageItem封装了重复的UI结构,接收标签、数值、颜色三个参数。Column垂直排列标签和数值。
标签使用12sp小字号和次要颜色,SizedBox添加4.h的间距。
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
color: color,
),
),
],
);
}
数值使用18sp大字号、粗体、传入的颜色。这种封装避免了代码重复,修改样式时只需改一处。
标签在上、数值在下的布局,配合字号和颜色的差异,形成清晰的主次关系。
SIM卡操作按钮
操作按钮区域提供三个常用功能的快捷入口:
Widget _buildSimActions(int index, Map<String, dynamic> sim) {
return Padding(
padding: EdgeInsets.all(12.w),
child: Row(
children: [
Expanded(
child: _buildActionButton(
_buildSimActions方法接收卡槽索引和SIM卡数据。Padding设置12.w的内边距,比其他区域稍小因为按钮本身有内边距。
Row横向排列三个按钮,每个按钮用Expanded包裹实现等宽分布。
Icons.sim_card,
'套餐详情',
() => _showPlanDetail(sim),
),
),
SizedBox(width: 12.w),
Expanded(
child: _buildActionButton(
Icons.history,
第一个按钮是"套餐详情",使用SIM卡图标,点击回调传入sim数据显示套餐详情弹窗。
SizedBox添加12.w的水平间距分隔按钮。第二个按钮是"使用记录",使用历史图标。
'使用记录',
() => _showUsageHistory(index),
),
),
SizedBox(width: 12.w),
Expanded(
child: _buildActionButton(
Icons.settings,
'卡片设置',
() => _showSimSettings(index),
),
),
],
),
);
}
"使用记录"和"卡片设置"按钮的回调传入卡槽索引,用于获取对应卡槽的数据。
三个按钮等宽排列,间距12.w,形成整齐的操作区域。
单个操作按钮的封装:
Widget _buildActionButton(IconData icon, String label, VoidCallback onTap) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: EdgeInsets.symmetric(vertical: 12.h),
decoration: BoxDecoration(
color: Colors.grey.shade50,
_buildActionButton封装单个按钮,接收图标、标签、点击回调三个参数。GestureDetector处理点击事件。
Container设置垂直内边距12.h,背景色使用非常浅的灰色(grey.shade50),和白色卡片背景有微妙区分。
borderRadius: BorderRadius.circular(10.r),
),
child: Column(
children: [
Icon(icon, color: AppTheme.primaryColor, size: 22.sp),
SizedBox(height: 4.h),
Text(
label,
圆角10.r让按钮看起来更柔和。Column垂直排列图标和文字,图标在上文字在下是移动端常见的按钮样式。
图标使用主色调,大小22sp。SizedBox添加4.h的间距。
style: TextStyle(fontSize: 12.sp, color: AppTheme.textSecondary),
),
],
),
),
);
}
标签文字使用12sp小字号和次要颜色,作为图标的补充说明。
这种图标+文字的按钮设计直观易懂,用户一眼就能理解按钮功能。
空卡槽显示
当某个卡槽没有插入SIM卡时,显示简化的空状态卡片:
Widget _buildEmptySimSlot(int index) {
return Container(
padding: EdgeInsets.all(24.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.r),
border: Border.all(color: Colors.grey.shade300, style: BorderStyle.solid),
空卡槽使用24.w的内边距,比有卡的卡片(16.w)更大,因为内容更少需要更多留白。
使用灰色边框而不是阴影,和有卡的卡片形成视觉区分,暗示这是"空"的状态。
),
child: Row(
children: [
Container(
width: 56.w,
height: 56.w,
decoration: BoxDecoration(
color: Colors.grey.shade100,
Row横向排列图标和文字。图标容器大小和有卡的卡片保持一致(56x56),背景使用浅灰色。
保持尺寸一致让两种状态的卡片在视觉上对齐。
borderRadius: BorderRadius.circular(14.r),
),
child: Icon(Icons.sim_card_outlined, color: Colors.grey, size: 32.sp),
),
SizedBox(width: 16.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
使用空心版本的SIM卡图标(sim_card_outlined),进一步强调"空"的概念。图标颜色为灰色。
SizedBox添加间距,Expanded让文字区域占据剩余空间,Column垂直排列文字并左对齐。
children: [
Text(
'SIM ${index + 1}',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
color: Colors.grey,
),
),
显示卡槽名称"SIM 1"或"SIM 2",字号和有卡的卡片一致,但颜色改为灰色表示空状态。
粗体保持标题的视觉重量,灰色降低其存在感。
SizedBox(height: 4.h),
Text(
'未插入SIM卡',
style: TextStyle(fontSize: 14.sp, color: AppTheme.textSecondary),
),
],
),
),
],
),
);
}
下方显示"未插入SIM卡"提示文字,让用户明确知道这个卡槽的状态。
整个空卡槽设计简洁,只有必要的信息,没有开关和操作按钮。
默认数据卡设置
默认数据卡设置让用户选择哪张SIM卡作为移动数据的默认卡:
Widget _buildDefaultDataCard() {
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.r),
),
Container创建白色圆角卡片,内边距16.w,圆角16.r,和其他卡片保持一致的样式。
这个卡片用于选择默认使用移动数据的SIM卡。
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'默认数据卡',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
Column垂直排列内容,crossAxisAlignment设置左对齐。标题"默认数据卡"使用16sp字号和半粗体。
这是卡片的主标题,告诉用户这个区域的功能。
color: AppTheme.textPrimary,
),
),
SizedBox(height: 4.h),
Text(
'选择默认使用移动数据的SIM卡',
style: TextStyle(fontSize: 13.sp, color: AppTheme.textSecondary),
),
SizedBox(height: 16.h),
标题下方是说明文字,13sp字号,次要颜色,解释这个设置的作用。
SizedBox添加16.h的间距,分隔说明文字和选项按钮。
Obx(() => Row(
children: [
Expanded(
child: _buildDataCardOption(0, 'SIM 1', controller.defaultDataSim.value == 0),
),
SizedBox(width: 12.w),
Expanded(
child: _buildDataCardOption(1, 'SIM 2', controller.defaultDataSim.value == 1),
),
],
)),
],
),
);
}
Obx包裹选项Row,响应defaultDataSim的变化。两个选项用Expanded等宽排列,间距12.w。
_buildDataCardOption接收索引、标签、是否选中三个参数,根据defaultDataSim.value判断选中状态。
单个选项按钮的实现:
Widget _buildDataCardOption(int index, String label, bool isSelected) {
return GestureDetector(
onTap: () => controller.setDefaultDataSim(index),
child: Container(
padding: EdgeInsets.symmetric(vertical: 16.h),
decoration: BoxDecoration(
GestureDetector处理点击事件,调用controller的setDefaultDataSim方法设置默认卡。
Container设置垂直内边距16.h,让按钮有足够的点击区域。
color: isSelected ? AppTheme.primaryColor.withOpacity(0.1) : Colors.grey.shade50,
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: isSelected ? AppTheme.primaryColor : Colors.transparent,
width: 2,
),
),
选中状态:浅蓝色背景、蓝色边框。未选中状态:浅灰色背景、透明边框(保持2像素宽度避免布局跳动)。
这种设计让选中和未选中状态有明显的视觉区分。
child: Column(
children: [
Icon(
Icons.sim_card,
color: isSelected ? AppTheme.primaryColor : Colors.grey,
size: 28.sp,
),
SizedBox(height: 8.h),
Column垂直排列图标和文字。图标颜色根据选中状态变化,选中时蓝色,未选中时灰色。
图标大小28sp,比操作按钮的图标稍大,因为这是主要的选择区域。
Text(
label,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: isSelected ? AppTheme.primaryColor : AppTheme.textSecondary,
),
),
if (isSelected) ...[
标签文字14sp,半粗体,颜色同样根据选中状态变化。
if (isSelected)配合展开运算符,只有选中的选项才显示"默认"标签。
SizedBox(height: 4.h),
Text(
'默认',
style: TextStyle(fontSize: 11.sp, color: AppTheme.primaryColor),
),
],
],
),
),
);
}
"默认"标签使用11sp小字号和主色调,进一步强调当前选择。
整个选项设计:图标在上,SIM名称在中,选中时底部显示"默认"标签。
总流量统计
页面底部展示双卡的汇总数据:
Widget _buildTotalUsage() {
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [AppTheme.primaryColor, AppTheme.primaryColor.withOpacity(0.8)],
),
Container使用渐变背景,从主色调到80%透明度的主色调,形成从左到右逐渐变浅的效果。
渐变背景让这个汇总卡片在视觉上更突出,和上方的白色卡片形成对比。
borderRadius: BorderRadius.circular(16.r),
),
child: Obx(() => Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildTotalItem('双卡总使用', controller.totalUsage.value),
Container(width: 1, height: 40.h, color: Colors.white30),
圆角16.r保持一致。Obx响应totalUsage和totalRemaining的变化。Row横向排列两个数据项。
spaceAround让数据项均匀分布,两端也有间距。中间用30%透明度的白色分隔线。
_buildTotalItem('双卡总余量', controller.totalRemaining.value),
],
)),
);
}
两个数据项分别显示双卡总使用量和总余量,让用户了解整体的流量情况。
半透明白色分隔线在深色背景上既能看清又不会太突兀。
单个统计项的封装:
Widget _buildTotalItem(String label, String value) {
return Column(
children: [
Text(label, style: TextStyle(fontSize: 12.sp, color: Colors.white70)),
SizedBox(height: 4.h),
Text(
value,
Column垂直排列标签和数值。标签使用12sp字号和70%透明度的白色,在深色背景上形成次要层次。
SizedBox添加4.h的间距。
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
);
}
数值使用20sp大字号、粗体、纯白色,是视觉焦点。
标签70%白色、数值纯白色的对比,在深色背景上形成清晰的主次关系。
Controller实现
Controller负责管理SIM卡数据和处理用户操作:
class SimManagerController extends GetxController {
final simCards = <Map<String, dynamic>>[].obs;
final defaultDataSim = 0.obs;
final totalUsage = '0 GB'.obs;
final totalRemaining = '0 GB'.obs;
声明四个响应式变量:simCards是SIM卡数据列表,defaultDataSim是默认数据卡索引,totalUsage和totalRemaining是汇总数据。
.obs将普通变量转换为响应式变量,数据变化时UI自动更新。
void onInit() {
super.onInit();
loadSimCards();
}
void loadSimCards() {
simCards.value = [
{
onInit是GetX Controller的生命周期方法,在Controller创建后立即调用。这里调用loadSimCards加载数据。
loadSimCards方法设置模拟的SIM卡数据,实际项目中应该调用系统API获取真实数据。
'carrier': '中国移动',
'active': true,
'usage': '6.5 GB',
'remaining': '3.5 GB',
'usagePercent': 65.0,
'phoneNumber': '138****1234',
},
第一张SIM卡的模拟数据:中国移动运营商,已启用,使用了6.5GB,剩余3.5GB,使用率65%,手机号脱敏显示。
这些字段对应UI中显示的各项信息。
{
'carrier': '中国联通',
'active': false,
'usage': '0 B',
'remaining': '10 GB',
'usagePercent': 0.0,
'phoneNumber': '186****5678',
},
];
calculateTotal();
}
第二张SIM卡:中国联通,未启用,未使用流量,剩余10GB。加载完数据后调用calculateTotal计算汇总。
两张卡一启用一禁用,方便测试不同状态的UI显示。
void toggleSim(int index, bool value) {
simCards[index]['active'] = value;
simCards.refresh();
if (value) {
Get.snackbar('SIM卡', 'SIM ${index + 1} 已启用');
} else {
toggleSim方法切换SIM卡的启用状态。修改Map中的值后调用refresh()通知GetX数据已变化。
根据新状态显示不同的Snackbar提示,让用户确认操作结果。
Get.snackbar('SIM卡', 'SIM ${index + 1} 已禁用');
}
}
void setDefaultDataSim(int index) {
if (!simCards[index]['active']) {
Get.snackbar('提示', '请先启用该SIM卡');
return;
}
setDefaultDataSim方法设置默认数据卡。首先检查目标卡是否已启用,如果未启用则显示提示并返回。
这个验证逻辑防止用户将禁用的卡设为默认,避免逻辑错误。
defaultDataSim.value = index;
Get.snackbar('设置成功', 'SIM ${index + 1} 已设为默认数据卡');
}
void calculateTotal() {
totalUsage.value = '6.5 GB';
totalRemaining.value = '13.5 GB';
}
设置成功后更新defaultDataSim并显示成功提示。calculateTotal计算双卡总使用量和总余量。
实际项目中需要解析各卡的数据字符串,提取数值求和后格式化。这里用硬编码的模拟数据。
void refreshSimInfo() {
Get.snackbar('刷新中', '正在获取SIM卡信息...');
loadSimCards();
}
}
refreshSimInfo方法响应刷新按钮点击,显示加载提示后重新加载数据。
实际项目中这里应该是异步操作,需要加loading状态和错误处理。
写在最后
SIM卡管理页面帮助双卡用户更好地管理和分配流量使用。通过清晰的卡片展示、便捷的开关控制、直观的流量统计,用户可以轻松掌握每张卡的使用情况。
可以继续优化的方向:
- 支持智能切换数据卡
- 添加流量共享功能
- 支持按应用指定使用的SIM卡
- 添加SIM卡流量对比图表
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐




所有评论(0)