flutter_for_openharmony城市井盖地图app实战+新增点位实现
Flutter井盖巡检表单功能实现摘要:该功能解决现场巡检时手动录入新井盖的需求,包含编号输入、片区选择、地址定位和风险评估等功能。通过Form组件结合GlobalKey实现表单统一校验,使用TextEditingController管理输入状态。关键组件包括:带校验的编号输入框、片区下拉选择、详细地址输入和风险滑块。提交按钮触发表单校验并显示操作反馈,最后在页面销毁时释放控制器资源。该实现遵循F

1. 这个功能解决什么问题
现场巡检时经常需要“手动录入新发现的井盖”,该功能核心解决以下实际巡检场景的痛点:
- 编号输入:唯一标识,用于后续检索与工单关联,避免井盖信息混乱
- 片区下拉:快速归属到行政区,便于按区域管理和统计井盖数据
- 地址输入:详细位置描述,让巡检人员能精准定位井盖实际位置
- 风险滑块:初始风险值录入,为后续运维优先级判定提供基础数据
- 表单校验:关键字段不能为空,保证录入数据的完整性
- 提交反馈:模拟保存成功提示,让用户明确操作结果
这个页面是典型的“表单录入”场景,适合做 Form + TextEditingController 的最佳实践示例。
2. 相关文件一览
lib/feature_pages.dart(AddPointPage):新增点位页面的核心UI与交互逻辑文件lib/models.dart(ManholeCover):定义井盖点位的数据模型,统一数据结构
3. 表单字段定义
在AddPointPage中,我们通过Form组件结合GlobalKey实现表单的统一校验,核心状态变量定义如下:
class _AddPointPageState extends State<AddPointPage> {
final _formKey = GlobalKey<FormState>();
final _code = TextEditingController();
final _address = TextEditingController();
String _district = '东城区';
double _risk = 0.2;
}
关于上述代码的核心设计要点:
_formKey:作为表单的全局标识,用于触发全表单的校验方法validate()TextEditingController:专门管理输入框的文本状态,包括获取输入值、清空输入等_district:默认值设为“东城区”,贴合实际业务中某区域为高频录入场景的需求_risk:初始值0.2(对应20%风险),是基于巡检经验的常规初始风险值设定
4. 编号输入框
井盖编号是唯一标识,属于必填字段,因此需要通过TextFormField结合校验规则实现:
TextFormField(
controller: _code,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: '井盖编号',
),
validator: (v) {
if ((v ?? '').trim().isEmpty) return '请输入井盖编号';
return null;
},
)
编号输入框的设计细节说明:
- 校验逻辑:先通过
??处理null值,再用trim()去除首尾空格,避免纯空格“伪输入” - 装饰器
InputDecoration:使用OutlineInputBorder实现带边框的输入框样式,符合Material Design规范 labelText:明确提示用户输入内容,提升表单易用性- 校验返回值:返回null表示校验通过,返回字符串则作为错误提示展示在输入框下方
5. 片区下拉
片区选择采用DropdownButtonFormField组件,实现下拉选择并关联表单状态:
DropdownButtonFormField<String>(
value: _district,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: '片区',
),
items: const [
DropdownMenuItem(value: '东城区', child: Text('东城区')),
DropdownMenuItem(value: '西城区', child: Text('西城区')),
],
)
片区下拉组件的核心设计思路:
- 初始值绑定:
value属性绑定_district,确保默认选中“东城区” - 样式统一:与输入框使用相同的
OutlineInputBorder边框,保持表单视觉一致性 - 选项硬编码:暂时硬编码5个片区(示例仅展示2个),与Mock数据保持一致,实际项目可替换为接口请求数据
补充下拉组件的onChanged事件处理,保证选中值同步到状态:
onChanged: (v) => setState(() =>
_district = v ?? '东城区'
),
该事件处理的细节说明:
- 使用
setState更新状态,触发UI重绘 ??兜底处理:防止null值赋值给_district,保证状态变量始终有有效值- 简化写法:箭头函数简化代码,保持逻辑清晰
6. 地址输入框
地址作为井盖定位的核心信息,同样设置为必填字段,输入框实现如下:
TextFormField(
controller: _address,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: '详细地址',
),
validator: (v) {
if ((v ?? '').trim().isEmpty) return '请输入地址';
return null;
},
)
地址输入框的设计考量:
- 校验逻辑:与编号输入框保持一致,保证表单校验规则统一
- 输入体验:未设置
maxLines,保持单行输入,避免表单高度过度拉伸 - 业务适配:地址字段通常内容较长,但单行输入更符合移动端表单的操作习惯
7. 风险滑块
风险值采用滑块组件实现可视化调整,结合文本展示当前风险百分比:
Text('初始风险: ${(100 * _risk).round()}%'),
Slider(value: _risk,
onChanged: (v) => setState(() => _risk = v)
),
风险滑块的设计细节:
- 数值转换:
_risk是0~1的小数,乘以100并round()取整,更符合用户对“百分比”的认知 - 交互逻辑:
onChanged直接更新状态,无需额外校验(滑块本身限制取值范围0~1) - 实时反馈:文本与滑块联动,用户操作时能直观看到风险值变化
8. 提交按钮
提交按钮置于表单外部,手动触发表单校验并处理提交逻辑:
FilledButton.icon(
onPressed: () {
if (!(_formKey.currentState?.validate() ?? false)) return;
// 模拟提交成功反馈
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('已创建点位 ${_code.text.trim()} (模拟)')),
);
},
icon: const Icon(Icons.add_location_alt_rounded),
label: const Text('提交'),
)
提交按钮的核心逻辑说明:
- 表单校验触发:
_formKey.currentState?.validate()遍历所有表单字段的validator - 空安全处理:
?? false处理currentState为null的情况,避免空指针 - 反馈机制:使用
SnackBar展示提交结果,符合Material Design的交互规范 - 按钮样式:
FilledButton.icon结合图标与文字,提升按钮辨识度
9. 资源释放
页面销毁时必须释放TextEditingController,避免内存泄漏:
void dispose() {
_code.dispose();
_address.dispose();
super.dispose();
}
资源释放的重要性说明:
- 内存管理:
TextEditingController持有上下文引用,不释放会导致内存泄漏 - 生命周期规范:在
dispose生命周期方法中释放资源,是Flutter的最佳实践 - 场景适配:频繁进出的表单页面,资源释放能显著提升应用性能
10. 完整页面代码(核心片段拆分讲解)
10.1 页面基础结构
首先定义页面的StatefulWidget基础结构:
class AddPointPage extends StatefulWidget {
const AddPointPage({super.key});
State<AddPointPage> createState() => _AddPointPageState();
}
基础结构说明:
- 采用
StatefulWidget:因为页面包含可变状态(输入值、选中值等) - 构造方法:使用
super.key传递key,符合Flutter组件规范
10.2 状态类与资源释放
状态类定义核心变量并实现资源释放:
class _AddPointPageState extends State<AddPointPage> {
final _formKey = GlobalKey<FormState>();
final _code = TextEditingController();
final _address = TextEditingController();
String _district = '东城区';
double _risk = 0.2;
void dispose() {
_code.dispose();
_address.dispose();
super.dispose();
}
}
这段代码的核心要点:
- 状态变量集中声明:便于维护和管理
- 资源释放时机:在
dispose中释放控制器,与组件生命周期绑定
10.3 页面构建方法
构建页面的UI结构,包含AppBar和表单内容:
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('新增点位')),
body: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(12),
children: [
// 表单字段将逐一添加
],
),
),
);
}
UI结构设计说明:
Scaffold:提供基础页面骨架,包含AppBar和BodyListView:适配移动端滚动,避免表单内容超出屏幕Form组件:绑定_formKey,统一管理所有表单字段
10.4 表单字段组装
将编号、片区、地址等字段添加到ListView中:
children: [
TextFormField(
controller: _code,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: '井盖编号',
),
validator: (v) {
if ((v ?? '').trim().isEmpty) return '请输入井盖编号';
return null;
},
),
const SizedBox(height: 12),
DropdownButtonFormField<String>(
value: _district,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: '片区',
),
),
]
表单布局细节:
SizedBox:设置字段间距,提升表单可读性- 字段顺序:按“编号-片区-地址-风险”的逻辑顺序排列,符合用户录入习惯
11. 新增点位数据模型
为统一管理新增点位数据,设计完整的ManholePoint数据模型:
class ManholePoint {
final String id;
final String code;
final String name;
final String district;
final String address;
final double latitude;
final double longitude;
final double riskLevel;
}
数据模型核心字段说明:
- 基础标识:
id(唯一ID)、code(井盖编号),用于数据检索 - 位置信息:
district(片区)、address(地址)、经纬度,实现精准定位 - 业务属性:
riskLevel(风险等级),支撑运维决策
补充模型的扩展字段,适配更多业务场景:
final PointStatus status;
final PointType type;
final DateTime createdAt;
final DateTime? installedAt;
final String? material;
final String? manufacturer;
扩展字段设计思路:
- 状态字段:
status(使用枚举)标识井盖当前状态 - 时间字段:
createdAt(创建时间)、installedAt(安装时间),记录数据生命周期 - 可选字段:
material(材质)、manufacturer(生产厂家),适配不同井盖类型
定义枚举类型,规范状态和类型的取值:
enum PointStatus {
normal,
damaged,
maintenance,
replaced,
}
enum PointType {
standard,
heavy,
composite,
smart,
}
枚举设计的优势:
- 类型安全:避免字符串硬编码导致的取值错误
- 语义清晰:每个枚举值对应明确的业务含义
- 易扩展:后续可新增状态/类型,不影响现有逻辑
12. 新增点位状态管理
使用Provider实现新增点位的状态管理,定义核心状态类:
class AddPointProvider extends ChangeNotifier {
ManholePoint? _currentPoint;
List<PointPhoto> _photos = [];
bool _loading = false;
String? _error;
bool _submitting = false;
}
状态管理类的核心设计:
- 私有状态变量:通过下划线私有化,避免外部直接修改
- 多状态管理:涵盖点位数据、照片、加载状态、错误信息、提交状态
ChangeNotifier:继承该类,实现状态变化通知
添加状态获取的getter方法,封装状态访问:
ManholePoint? get currentPoint => _currentPoint;
List<PointPhoto> get photos => _photos;
bool get loading => _loading;
String? get error => _error;
bool get submitting => _submitting;
Getter方法的作用:
- 封装性:外部只能读取状态,不能直接修改,保证状态可控
- 简洁性:简化外部访问方式,如
provider.currentPoint - 可扩展:后续可在getter中添加逻辑(如数据过滤)
实现创建点位的核心方法createPoint:
Future<void> createPoint({
required String code,
required String name,
required String district,
required String address,
required double latitude,
required double longitude,
required double riskLevel,
}) async {
_submitting = true;
_error = null;
notifyListeners();
}
createPoint方法的初始处理:
- 状态重置:设置提交中状态,清空错误信息
- 通知更新:调用
notifyListeners(),通知UI刷新 - 入参规范:使用
required关键字,保证必填参数不缺失
补充createPoint的核心逻辑:
try {
await Future.delayed(const Duration(seconds: 1));
final point = ManholePoint(
id: 'POINT_${DateTime.now().millisecondsSinceEpoch}',
code: code,
name: name,
district: district,
address: address,
latitude: latitude,
longitude: longitude,
riskLevel: riskLevel,
);
_currentPoint = point;
_submitting = false;
notifyListeners();
} catch (e) {
_error = e.toString();
_submitting = false;
notifyListeners();
}
核心逻辑说明:
- 模拟异步:
Future.delayed模拟接口请求延迟 - 生成ID:基于时间戳生成唯一ID,避免重复
- 异常处理:捕获错误并记录,保证应用稳定性
- 状态更新:创建成功后更新点位状态,结束提交状态
实现照片管理的核心方法:
Future<void> addPhoto(String url, String description,
PhotoType type) async {
final photo = PointPhoto(
id: 'PHOTO_${DateTime.now().millisecondsSinceEpoch}',
url: url,
description: description,
createdAt: DateTime.now(),
type: type,
);
_photos.add(photo);
notifyListeners();
}
照片添加方法的设计:
- 唯一ID:为每张照片生成唯一标识,便于删除操作
- 完整信息:包含URL、描述、创建时间、类型,满足业务需求
- 状态通知:添加后通知UI刷新,展示新增照片
实现照片删除和状态清理方法:
void removePhoto(String photoId) {
_photos.removeWhere((photo) => photo.id == photoId);
notifyListeners();
}
void clearError() {
_error = null;
notifyListeners();
}
void reset() {
_currentPoint = null;
_photos.clear();
_error = null;
_submitting = false;
notifyListeners();
}
辅助方法说明:
removePhoto:通过ID删除指定照片,精准操作clearError:清空错误信息,用于用户重新操作reset:重置所有状态,适配表单重置场景
13. 高级新增点位组件
创建功能更丰富的高级新增点位组件,基础结构如下:
class AdvancedAddPointWidget extends StatefulWidget {
const AdvancedAddPointWidget({super.key});
State<AdvancedAddPointWidget> createState() =>
_AdvancedAddPointWidgetState();
}
高级组件的基础设计:
- 独立组件:与基础新增页面解耦,适配更复杂的录入场景
- 状态管理:同样采用
StatefulWidget,管理更多表单状态
状态类中声明更多表单控制器,适配丰富的字段:
class _AdvancedAddPointWidgetState extends State<AdvancedAddPointWidget> {
final _formKey = GlobalKey<FormState>();
final _codeController = TextEditingController();
final _nameController = TextEditingController();
final _addressController = TextEditingController();
}
多控制器设计:
- 一一对应:每个输入字段对应独立的控制器,便于单独管理
- 扩展灵活:后续新增字段可直接添加对应控制器
补充更多状态变量,适配高级字段需求:
final _materialController = TextEditingController();
final _manufacturerController = TextEditingController();
final _notesController = TextEditingController();
String _selectedDistrict = '东城区';
PointType _selectedType = PointType.standard;
double _riskLevel = 0.2;
高级状态变量说明:
- 扩展字段:材质、生产厂家、备注,满足精细化录入需求
- 类型选择:
PointType枚举,支持不同井盖类型选择 - 风险值:与基础页面逻辑一致,保持体验统一
实现组件的资源释放,避免内存泄漏:
void dispose() {
_codeController.dispose();
_nameController.dispose();
_addressController.dispose();
_materialController.dispose();
_manufacturerController.dispose();
_notesController.dispose();
super.dispose();
}
高级组件的资源释放:
- 全量释放:所有控制器都需在
dispose中释放 - 顺序无关:释放顺序不影响,但建议按声明顺序编写,便于维护
构建高级组件的核心UI结构:
Widget build(BuildContext context) {
return Consumer<AddPointProvider>(
builder: (context, provider, child) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(context),
const SizedBox(height: 24),
_buildBasicInfoSection(context),
],
),
),
);
},
);
}
高级组件UI设计:
Consumer:监听AddPointProvider状态变化,自动刷新UISingleChildScrollView:适配长表单滚动,避免内容溢出- 分区块构建:将UI拆分为头部、基本信息、位置信息等模块,便于维护
实现头部组件_buildHeader,提升页面视觉体验:
Widget _buildHeader(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(
backgroundColor: Theme.of(context).primaryColor.withOpacity(0.1),
child: Icon(Icons.add_location_alt,
color: Theme.of(context).primaryColor),
),
],
),
],
),
),
);
}
头部组件设计细节:
Card组件:带阴影的卡片样式,提升视觉层次感CircleAvatar:圆形图标容器,结合主题色,增强品牌感- 主题适配:使用
Theme.of(context)获取主题色,保证风格统一
补充头部组件的文本内容,完善信息展示:
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'新增点位',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'录入新的井盖点位信息',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 14,
),
),
],
),
),
头部文本设计:
- 标题样式:加粗+大号字体,突出核心标题
- 描述文本:灰色小号字体,补充说明,提升可读性
Expanded:占满剩余空间,保证布局美观
实现基本信息区块_buildBasicInfoSection,封装核心表单字段:
Widget _buildBasicInfoSection(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'基本信息',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
],
),
),
);
}
基本信息区块设计:
- 标题区分:明确标注“基本信息”,便于用户理解表单分区
- 内边距设置:
EdgeInsets.all(16)保证内容与卡片边框有足够间距 - 样式统一:与头部组件使用相同的
Card样式,保持视觉一致性
添加井盖编号字段到基本信息区块,强化校验规则:
TextFormField(
controller: _codeController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: '井盖编号',
prefixIcon: Icon(Icons.tag),
helperText: '请输入唯一的井盖编号',
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '请输入井盖编号';
}
if (value.trim().length < 3) {
return '井盖编号至少需要3个字符';
}
return null;
},
)
高级编号字段设计:
prefixIcon:添加标签图标,增强字段辨识度helperText:提示用户输入规则,减少错误输入- 强化校验:增加长度校验,保证编号符合业务规范
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐
所有评论(0)