在这里插入图片描述

1. 这个功能解决什么问题

现场巡检时经常需要“手动录入新发现的井盖”,该功能核心解决以下实际巡检场景的痛点:

  • 编号输入:唯一标识,用于后续检索与工单关联,避免井盖信息混乱
  • 片区下拉:快速归属到行政区,便于按区域管理和统计井盖数据
  • 地址输入:详细位置描述,让巡检人员能精准定位井盖实际位置
  • 风险滑块:初始风险值录入,为后续运维优先级判定提供基础数据
  • 表单校验:关键字段不能为空,保证录入数据的完整性
  • 提交反馈:模拟保存成功提示,让用户明确操作结果

这个页面是典型的“表单录入”场景,适合做 Form + TextEditingController 的最佳实践示例。

2. 相关文件一览

  • lib/feature_pages.dartAddPointPage):新增点位页面的核心UI与交互逻辑文件
  • lib/models.dartManholeCover):定义井盖点位的数据模型,统一数据结构

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;
}

关于上述代码的核心设计要点:

  1. _formKey:作为表单的全局标识,用于触发全表单的校验方法validate()
  2. TextEditingController:专门管理输入框的文本状态,包括获取输入值、清空输入等
  3. _district:默认值设为“东城区”,贴合实际业务中某区域为高频录入场景的需求
  4. _risk:初始值0.2(对应20%风险),是基于巡检经验的常规初始风险值设定

4. 编号输入框

井盖编号是唯一标识,属于必填字段,因此需要通过TextFormField结合校验规则实现:

TextFormField(
  controller: _code,
  decoration: const InputDecoration(
    border: OutlineInputBorder(),
    labelText: '井盖编号',
  ),
  validator: (v) {
    if ((v ?? '').trim().isEmpty) return '请输入井盖编号';
    return null;
  },
)

编号输入框的设计细节说明:

  1. 校验逻辑:先通过??处理null值,再用trim()去除首尾空格,避免纯空格“伪输入”
  2. 装饰器InputDecoration:使用OutlineInputBorder实现带边框的输入框样式,符合Material Design规范
  3. labelText:明确提示用户输入内容,提升表单易用性
  4. 校验返回值:返回null表示校验通过,返回字符串则作为错误提示展示在输入框下方

5. 片区下拉

片区选择采用DropdownButtonFormField组件,实现下拉选择并关联表单状态:

DropdownButtonFormField<String>(
  value: _district,
  decoration: const InputDecoration(
    border: OutlineInputBorder(),
    labelText: '片区',
  ),
  items: const [
    DropdownMenuItem(value: '东城区', child: Text('东城区')),
    DropdownMenuItem(value: '西城区', child: Text('西城区')),
  ],
)

片区下拉组件的核心设计思路:

  1. 初始值绑定:value属性绑定_district,确保默认选中“东城区”
  2. 样式统一:与输入框使用相同的OutlineInputBorder边框,保持表单视觉一致性
  3. 选项硬编码:暂时硬编码5个片区(示例仅展示2个),与Mock数据保持一致,实际项目可替换为接口请求数据

补充下拉组件的onChanged事件处理,保证选中值同步到状态:

onChanged: (v) => setState(() => 
  _district = v ?? '东城区'
),

该事件处理的细节说明:

  1. 使用setState更新状态,触发UI重绘
  2. ??兜底处理:防止null值赋值给_district,保证状态变量始终有有效值
  3. 简化写法:箭头函数简化代码,保持逻辑清晰

6. 地址输入框

地址作为井盖定位的核心信息,同样设置为必填字段,输入框实现如下:

TextFormField(
  controller: _address,
  decoration: const InputDecoration(
    border: OutlineInputBorder(),
    labelText: '详细地址',
  ),
  validator: (v) {
    if ((v ?? '').trim().isEmpty) return '请输入地址';
    return null;
  },
)

地址输入框的设计考量:

  1. 校验逻辑:与编号输入框保持一致,保证表单校验规则统一
  2. 输入体验:未设置maxLines,保持单行输入,避免表单高度过度拉伸
  3. 业务适配:地址字段通常内容较长,但单行输入更符合移动端表单的操作习惯

7. 风险滑块

风险值采用滑块组件实现可视化调整,结合文本展示当前风险百分比:

Text('初始风险: ${(100 * _risk).round()}%'),
Slider(value: _risk, 
  onChanged: (v) => setState(() => _risk = v)
),

风险滑块的设计细节:

  1. 数值转换:_risk是0~1的小数,乘以100并round()取整,更符合用户对“百分比”的认知
  2. 交互逻辑:onChanged直接更新状态,无需额外校验(滑块本身限制取值范围0~1)
  3. 实时反馈:文本与滑块联动,用户操作时能直观看到风险值变化

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('提交'),
)

提交按钮的核心逻辑说明:

  1. 表单校验触发:_formKey.currentState?.validate()遍历所有表单字段的validator
  2. 空安全处理:?? false处理currentState为null的情况,避免空指针
  3. 反馈机制:使用SnackBar展示提交结果,符合Material Design的交互规范
  4. 按钮样式:FilledButton.icon结合图标与文字,提升按钮辨识度

9. 资源释放

页面销毁时必须释放TextEditingController,避免内存泄漏:


void dispose() {
  _code.dispose();
  _address.dispose();
  super.dispose();
}

资源释放的重要性说明:

  1. 内存管理:TextEditingController持有上下文引用,不释放会导致内存泄漏
  2. 生命周期规范:在dispose生命周期方法中释放资源,是Flutter的最佳实践
  3. 场景适配:频繁进出的表单页面,资源释放能显著提升应用性能

10. 完整页面代码(核心片段拆分讲解)

10.1 页面基础结构

首先定义页面的StatefulWidget基础结构:

class AddPointPage extends StatefulWidget {
  const AddPointPage({super.key});

  
  State<AddPointPage> createState() => _AddPointPageState();
}

基础结构说明:

  1. 采用StatefulWidget:因为页面包含可变状态(输入值、选中值等)
  2. 构造方法:使用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();
  }
}

这段代码的核心要点:

  1. 状态变量集中声明:便于维护和管理
  2. 资源释放时机:在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结构设计说明:

  1. Scaffold:提供基础页面骨架,包含AppBar和Body
  2. ListView:适配移动端滚动,避免表单内容超出屏幕
  3. 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: '片区',
    ),
  ),
]

表单布局细节:

  1. SizedBox:设置字段间距,提升表单可读性
  2. 字段顺序:按“编号-片区-地址-风险”的逻辑顺序排列,符合用户录入习惯

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;
}

数据模型核心字段说明:

  1. 基础标识:id(唯一ID)、code(井盖编号),用于数据检索
  2. 位置信息:district(片区)、address(地址)、经纬度,实现精准定位
  3. 业务属性:riskLevel(风险等级),支撑运维决策

补充模型的扩展字段,适配更多业务场景:

final PointStatus status;
final PointType type;
final DateTime createdAt;
final DateTime? installedAt;
final String? material;
final String? manufacturer;

扩展字段设计思路:

  1. 状态字段:status(使用枚举)标识井盖当前状态
  2. 时间字段:createdAt(创建时间)、installedAt(安装时间),记录数据生命周期
  3. 可选字段:material(材质)、manufacturer(生产厂家),适配不同井盖类型

定义枚举类型,规范状态和类型的取值:

enum PointStatus {
  normal,
  damaged,
  maintenance,
  replaced,
}

enum PointType {
  standard,
  heavy,
  composite,
  smart,
}

枚举设计的优势:

  1. 类型安全:避免字符串硬编码导致的取值错误
  2. 语义清晰:每个枚举值对应明确的业务含义
  3. 易扩展:后续可新增状态/类型,不影响现有逻辑

12. 新增点位状态管理

使用Provider实现新增点位的状态管理,定义核心状态类:

class AddPointProvider extends ChangeNotifier {
  ManholePoint? _currentPoint;
  List<PointPhoto> _photos = [];
  bool _loading = false;
  String? _error;
  bool _submitting = false;
}

状态管理类的核心设计:

  1. 私有状态变量:通过下划线私有化,避免外部直接修改
  2. 多状态管理:涵盖点位数据、照片、加载状态、错误信息、提交状态
  3. ChangeNotifier:继承该类,实现状态变化通知

添加状态获取的getter方法,封装状态访问:

ManholePoint? get currentPoint => _currentPoint;
List<PointPhoto> get photos => _photos;
bool get loading => _loading;
String? get error => _error;
bool get submitting => _submitting;

Getter方法的作用:

  1. 封装性:外部只能读取状态,不能直接修改,保证状态可控
  2. 简洁性:简化外部访问方式,如provider.currentPoint
  3. 可扩展:后续可在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方法的初始处理:

  1. 状态重置:设置提交中状态,清空错误信息
  2. 通知更新:调用notifyListeners(),通知UI刷新
  3. 入参规范:使用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();
}

核心逻辑说明:

  1. 模拟异步:Future.delayed模拟接口请求延迟
  2. 生成ID:基于时间戳生成唯一ID,避免重复
  3. 异常处理:捕获错误并记录,保证应用稳定性
  4. 状态更新:创建成功后更新点位状态,结束提交状态

实现照片管理的核心方法:

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();
}

照片添加方法的设计:

  1. 唯一ID:为每张照片生成唯一标识,便于删除操作
  2. 完整信息:包含URL、描述、创建时间、类型,满足业务需求
  3. 状态通知:添加后通知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();
}

辅助方法说明:

  1. removePhoto:通过ID删除指定照片,精准操作
  2. clearError:清空错误信息,用于用户重新操作
  3. reset:重置所有状态,适配表单重置场景

13. 高级新增点位组件

创建功能更丰富的高级新增点位组件,基础结构如下:

class AdvancedAddPointWidget extends StatefulWidget {
  const AdvancedAddPointWidget({super.key});

  
  State<AdvancedAddPointWidget> createState() => 
    _AdvancedAddPointWidgetState();
}

高级组件的基础设计:

  1. 独立组件:与基础新增页面解耦,适配更复杂的录入场景
  2. 状态管理:同样采用StatefulWidget,管理更多表单状态

状态类中声明更多表单控制器,适配丰富的字段:

class _AdvancedAddPointWidgetState extends State<AdvancedAddPointWidget> {
  final _formKey = GlobalKey<FormState>();
  final _codeController = TextEditingController();
  final _nameController = TextEditingController();
  final _addressController = TextEditingController();
}

多控制器设计:

  1. 一一对应:每个输入字段对应独立的控制器,便于单独管理
  2. 扩展灵活:后续新增字段可直接添加对应控制器

补充更多状态变量,适配高级字段需求:

final _materialController = TextEditingController();
final _manufacturerController = TextEditingController();
final _notesController = TextEditingController();
  
String _selectedDistrict = '东城区';
PointType _selectedType = PointType.standard;
double _riskLevel = 0.2;

高级状态变量说明:

  1. 扩展字段:材质、生产厂家、备注,满足精细化录入需求
  2. 类型选择:PointType枚举,支持不同井盖类型选择
  3. 风险值:与基础页面逻辑一致,保持体验统一

实现组件的资源释放,避免内存泄漏:


void dispose() {
  _codeController.dispose();
  _nameController.dispose();
  _addressController.dispose();
  _materialController.dispose();
  _manufacturerController.dispose();
  _notesController.dispose();
  super.dispose();
}

高级组件的资源释放:

  1. 全量释放:所有控制器都需在dispose中释放
  2. 顺序无关:释放顺序不影响,但建议按声明顺序编写,便于维护

构建高级组件的核心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设计:

  1. Consumer:监听AddPointProvider状态变化,自动刷新UI
  2. SingleChildScrollView:适配长表单滚动,避免内容溢出
  3. 分区块构建:将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),
              ),
            ],
          ),
        ],
      ),
    ),
  );
}

头部组件设计细节:

  1. Card组件:带阴影的卡片样式,提升视觉层次感
  2. CircleAvatar:圆形图标容器,结合主题色,增强品牌感
  3. 主题适配:使用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,
        ),
      ),
    ],
  ),
),

头部文本设计:

  1. 标题样式:加粗+大号字体,突出核心标题
  2. 描述文本:灰色小号字体,补充说明,提升可读性
  3. 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),
        ],
      ),
    ),
  );
}

基本信息区块设计:

  1. 标题区分:明确标注“基本信息”,便于用户理解表单分区
  2. 内边距设置:EdgeInsets.all(16)保证内容与卡片边框有足够间距
  3. 样式统一:与头部组件使用相同的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;
  },
)

高级编号字段设计:

  1. prefixIcon:添加标签图标,增强字段辨识度
  2. helperText:提示用户输入规则,减少错误输入
  3. 强化校验:增加长度校验,保证编号符合业务规范

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

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐