
地址编辑是地址管理系统中的核心功能。用户需要能够添加新地址、编辑现有地址、验证地址信息的完整性和正确性。一个完整的地址编辑系统需要支持表单验证、数据持久化、默认地址管理、地址标签分类等功能。本文将详细讲解如何在 Flutter for OpenHarmony 项目中实现一个功能完整的地址编辑页面,包括表单设计、数据验证、状态管理、用户反馈和错误处理等功能。
表单控制器管理
地址编辑页面需要管理多个表单字段的状态和数据。
dart
// 地址编辑页面状态管理
class _AddressEditPageState extends State<AddressEditPage> {
// 表单验证key
final _formKey = GlobalKey<FormState>();
// 各字段的文本编辑控制器
final _nameController = TextEditingController(); // 收货人
final _phoneController = TextEditingController(); // 手机号
final _provinceController = TextEditingController(); // 省份
final _cityController = TextEditingController(); // 城市
final _districtController = TextEditingController(); // 区/县
final _detailController = TextEditingController(); // 详细地址
// 地址标签和默认状态
String? _tag;
bool _isDefault = false;
// 正在编辑的地址对象
Address? _editingAddress;
@override
void dispose() {
// 释放所有控制器资源
_nameController.dispose();
_phoneController.dispose();
_provinceController.dispose();
_cityController.dispose();
_districtController.dispose();
_detailController.dispose();
super.dispose();
}
}
这个表单控制器管理展示了如何组织表单状态:
控制器管理:
- 为每个表单字段创建独立的
TextEditingController - 在
dispose()中释放所有控制器资源 - 避免内存泄漏和资源浪费
状态变量:
_formKey用于表单验证_editingAddress记录正在编辑的地址_tag和_isDefault管理地址属性
资源管理:
- 及时释放控制器
- 防止内存泄漏
- 确保应用性能
地址数据初始化
编辑现有地址时,需要从路由参数中获取地址数据并填充表单。
dart
// 地址数据初始化
@override
void didChangeDependencies() {
super.didChangeDependencies();
// 从路由参数获取要编辑的地址
final address = ModalRoute.of(context)?.settings.arguments as Address?;
// 只在第一次时初始化,避免重复加载
if (address != null && _editingAddress == null) {
_editingAddress = address;
// 将地址数据填充到各个表单字段
_nameController.text = address.name;
_phoneController.text = address.phone;
_provinceController.text = address.province;
_cityController.text = address.city;
_districtController.text = address.district;
_detailController.text = address.detail;
// 恢复地址标签和默认状态
_tag = address.tag;
_isDefault = address.isDefault;
}
}
这个初始化逻辑展示了如何加载地址数据:
数据获取:
- 从
ModalRoute的settings.arguments获取地址参数 - 类型转换为
Address对象 - 检查是否为编辑模式
表单填充:
- 将地址各字段值填充到对应的控制器
- 恢复地址标签和默认状态
- 使用
_editingAddress == null检查避免重复初始化
生命周期管理:
- 在
didChangeDependencies()中初始化 - 确保依赖关系正确处理
- 只初始化一次
表单验证
表单验证确保用户输入的地址数据完整和正确。
dart
// 表单字段验证
class AddressFormValidator {
// 验证收货人名字
static String? validateName(String? value) {
if (value == null || value.trim().isEmpty) {
return '请输入收货人名字';
}
if (value.length > 20) {
return '收货人名字不能超过20个字符';
}
return null;
}
// 验证手机号码
static String? validatePhone(String? value) {
if (value == null || value.trim().isEmpty) {
return '请输入手机号';
}
// 移除空格后验证
final cleanPhone = value.replaceAll(' ', '');
// 检查是否为11位数字
if (!RegExp(r'^1[3-9]\d{9}$').hasMatch(cleanPhone)) {
return '请输入有效的11位手机号';
}
return null;
}
// 验证省份
static String? validateProvince(String? value) {
if (value == null || value.trim().isEmpty) {
return '请输入省份';
}
return null;
}
// 验证城市
static String? validateCity(String? value) {
if (value == null || value.trim().isEmpty) {
return '请输入城市';
}
return null;
}
// 验证详细地址
static String? validateDetail(String? value) {
if (value == null || value.trim().isEmpty) {
return '请输入详细地址';
}
if (value.length > 100) {
return '详细地址不能超过100个字符';
}
return null;
}
}
// 在表单字段中使用验证器
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: '收货人',
prefixIcon: Icon(Icons.person),
),
validator: AddressFormValidator.validateName,
)
TextFormField(
controller: _phoneController,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: '手机号',
prefixIcon: Icon(Icons.phone),
),
validator: AddressFormValidator.validatePhone,
)
TextFormField(
controller: _detailController,
maxLines: 2,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: '详细地址',
),
validator: AddressFormValidator.validateDetail,
)
这个验证逻辑展示了如何验证表单数据:
验证规则:
- 收货人:不能为空,不超过20个字符
- 手机号:必须是11位数字,以1开头,第二位为3-9
- 省份/城市:不能为空
- 详细地址:不能为空,不超过100个字符
验证方法:
- 创建专门的验证器类
- 每个字段对应一个验证方法
- 在
TextFormField的validator参数中使用
用户体验:
- 实时显示验证错误
- 清晰的错误提示信息
- 帮助用户快速修正
地址保存逻辑
保存地址时需要验证表单、创建地址对象、更新应用状态。
dart
// 保存地址的核心逻辑
void _saveAddress() {
// 第一步:验证表单
if (!_formKey.currentState!.validate()) {
return;
}
// 获取应用状态
final appState = AppStateScope.of(context);
// 第二步:创建地址对象
final address = Address(
// 如果是编辑模式,使用原地址ID;否则生成新ID
id: _editingAddress?.id ??
'addr_${DateTime.now().millisecondsSinceEpoch}',
// 从表单控制器获取数据并去除空格
name: _nameController.text.trim(),
phone: _phoneController.text.trim(),
province: _provinceController.text.trim(),
city: _cityController.text.trim(),
district: _districtController.text.trim(),
detail: _detailController.text.trim(),
// 地址标签和默认状态
tag: _tag,
isDefault: _isDefault,
);
// 第三步:更新应用状态
if (_editingAddress != null) {
// 编辑模式:更新现有地址
appState.updateAddress(address);
} else {
// 添加模式:添加新地址
appState.addAddress(address);
}
// 第四步:显示成功提示
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
_editingAddress != null ? '地址已更新' : '地址已添加',
),
backgroundColor: Colors.green,
),
);
// 第五步:返回上一页
Navigator.of(context).pop();
}
这个保存逻辑展示了如何处理地址保存:
保存流程:
- 验证表单:确保所有必填字段都有效
- 创建对象 :构建
Address对象 - 更新状态 :调用
AppState的方法 - 用户反馈:显示成功提示
- 页面返回:返回到上一页
数据处理:
- 使用
trim()移除空格 - 生成唯一的地址ID
- 区分添加和编辑模式
状态管理:
- 调用
updateAddress()或addAddress() - 自动处理默认地址逻辑
- 通知所有监听者更新UI
收货人信息表单
收货人信息是地址的基本部分,包括名字和电话。
dart
// 构建收货人信息卡片
Widget _buildReceiverCard() {
return ShopCard(
child: Column(
children: [
// 收货人名字输入框
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: '收货人',
prefixIcon: Icon(Icons.person),
hintText: '请输入收货人名字',
),
validator: (v) => v?.trim().isEmpty == true
? '请输入收货人'
: null,
),
const SizedBox(height: 16),
// 手机号输入框
TextFormField(
controller: _phoneController,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: '手机号',
prefixIcon: Icon(Icons.phone),
hintText: '请输入11位手机号',
),
validator: (v) => v?.trim().isEmpty == true
? '请输入手机号'
: null,
),
],
),
);
}
这个收货人信息表单展示了如何设计基本信息输入:
表单字段:
- 收货人名字 :使用
Icons.person图标 - 手机号 :使用
TextInputType.phone键盘类型
输入优化:
- 提供清晰的标签和提示文本
- 使用合适的键盘类型
- 添加前缀图标提示
验证反馈:
- 实时验证用户输入
- 显示清晰的错误提示
- 帮助用户快速修正
地址信息表单
地址信息包括省份、城市、区/县和详细地址。
dart
// 构建地址信息卡片
Widget _buildAddressCard() {
return ShopCard(
child: Column(
children: [
// 省份输入框
TextFormField(
controller: _provinceController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: '省份',
hintText: '如:上海市',
),
validator: (v) => v?.trim().isEmpty == true
? '请输入省份'
: null,
),
const SizedBox(height: 16),
// 城市输入框
TextFormField(
controller: _cityController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: '城市',
hintText: '如:浦东新区',
),
validator: (v) => v?.trim().isEmpty == true
? '请输入城市'
: null,
),
const SizedBox(height: 16),
// 区/县输入框(可选)
TextFormField(
controller: _districtController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: '区/县',
hintText: '如:陆家嘴',
),
),
const SizedBox(height: 16),
// 详细地址输入框
TextFormField(
controller: _detailController,
maxLines: 2,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: '详细地址',
hintText: '请输入街道、楼号、房间号等',
),
validator: (v) => v?.trim().isEmpty == true
? '请输入详细地址'
: null,
),
],
),
);
}
这个地址信息表单展示了如何组织地址输入:
表单结构:
- 省份:必填,如"上海市"
- 城市:必填,如"浦东新区"
- 区/县:可选,提供更详细的位置
- 详细地址:必填,支持多行输入
输入特性:
- 详细地址使用
maxLines: 2支持多行 - 提供示例提示帮助用户理解
- 清晰的字段标签
验证规则:
- 省份、城市、详细地址为必填
- 区/县为可选字段
- 实时验证和错误提示
地址标签和默认设置
地址标签用于分类,默认地址在结算时自动使用。
dart
// 构建标签和默认设置卡片
Widget _buildTagAndDefaultCard() {
return ShopCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 地址标签选择
Text(
'标签',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
// 标签选择芯片
Wrap(
spacing: 8,
children: ['家', '公司', '学校'].map((tag) {
return ChoiceChip(
label: Text(tag),
selected: _tag == tag,
onSelected: (selected) {
setState(() {
_tag = selected ? tag : null;
});
},
);
}).toList(),
),
const SizedBox(height: 12),
// 默认地址开关
SwitchListTile(
title: const Text('设为默认地址'),
subtitle: const Text('在结算时自动使用此地址'),
value: _isDefault,
onChanged: (v) {
setState(() => _isDefault = v);
},
contentPadding: EdgeInsets.zero,
),
],
),
);
}
这个标签和默认设置展示了如何管理地址属性:
标签选择:
- 使用
ChoiceChip组件 - 支持"家"、"公司"、"学校"等标签
- 单选模式,只能选择一个标签
默认地址:
- 使用
SwitchListTile开关 - 提供清晰的说明文本
- 在结算时自动使用
用户交互:
- 点击芯片切换标签
- 切换开关设置默认地址
- 实时更新状态
默认地址管理
应用状态需要正确处理默认地址的逻辑。
dart
// 添加地址时的默认地址处理
void addAddress(Address address) {
// 如果新地址设置为默认,取消其他地址的默认标记
if (address.isDefault) {
for (int i = 0; i < _addresses.length; i++) {
if (_addresses[i].isDefault) {
// 创建新的地址对象,将isDefault设为false
_addresses[i] = _addresses[i].copyWith(isDefault: false);
}
}
}
// 添加新地址
_addresses.add(address);
notifyListeners();
}
// 更新地址时的默认地址处理
void updateAddress(Address address) {
final index = _addresses.indexWhere((a) => a.id == address.id);
if (index >= 0) {
// 如果更新的地址设置为默认
if (address.isDefault) {
for (int i = 0; i < _addresses.length; i++) {
// 取消其他地址的默认标记(除了当前地址)
if (_addresses[i].isDefault && i != index) {
_addresses[i] = _addresses[i].copyWith(isDefault: false);
}
}
}
// 更新地址
_addresses[index] = address;
notifyListeners();
}
}
// 获取默认地址
Address? get defaultAddress {
try {
return _addresses.firstWhere((a) => a.isDefault);
} catch (e) {
return null;
}
}
这个默认地址管理展示了如何处理默认地址逻辑:
添加地址:
- 检查新地址是否设置为默认
- 如果是,取消其他地址的默认标记
- 确保只有一个默认地址
更新地址:
- 检查更新的地址是否设置为默认
- 排除当前地址,取消其他地址的默认标记
- 避免重复处理
查询默认地址:
- 使用
firstWhere()查找默认地址 - 使用
try-catch处理异常 - 返回
null如果没有默认地址
业务规则:
- 最多只有一个默认地址
- 自动管理默认地址状态
- 保证数据一致性
页面完整实现
地址编辑页面的完整实现包括表单、验证和保存。
dart
// 完整的地址编辑页面
class AddressEditPage extends StatefulWidget {
const AddressEditPage({super.key});
@override
State<AddressEditPage> createState() => _AddressEditPageState();
}
class _AddressEditPageState extends State<AddressEditPage> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _phoneController = TextEditingController();
final _provinceController = TextEditingController();
final _cityController = TextEditingController();
final _districtController = TextEditingController();
final _detailController = TextEditingController();
String? _tag;
bool _isDefault = false;
Address? _editingAddress;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final address = ModalRoute.of(context)?.settings.arguments as Address?;
if (address != null && _editingAddress == null) {
_editingAddress = address;
_nameController.text = address.name;
_phoneController.text = address.phone;
_provinceController.text = address.province;
_cityController.text = address.city;
_districtController.text = address.district;
_detailController.text = address.detail;
_tag = address.tag;
_isDefault = address.isDefault;
}
}
@override
void dispose() {
_nameController.dispose();
_phoneController.dispose();
_provinceController.dispose();
_cityController.dispose();
_districtController.dispose();
_detailController.dispose();
super.dispose();
}
void _save() {
if (!_formKey.currentState!.validate()) return;
final appState = AppStateScope.of(context);
final address = Address(
id: _editingAddress?.id ??
'addr_${DateTime.now().millisecondsSinceEpoch}',
name: _nameController.text.trim(),
phone: _phoneController.text.trim(),
province: _provinceController.text.trim(),
city: _cityController.text.trim(),
district: _districtController.text.trim(),
detail: _detailController.text.trim(),
tag: _tag,
isDefault: _isDefault,
);
if (_editingAddress != null) {
appState.updateAddress(address);
} else {
appState.addAddress(address);
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
_editingAddress != null ? '地址已更新' : '地址已添加',
),
),
);
Navigator.of(context).pop();
}
@override
Widget build(BuildContext context) {
return SimpleScaffoldPage(
title: _editingAddress != null ? '编辑地址' : '添加地址',
child: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
// 收货人信息
ShopCard(
child: Column(
children: [
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: '收货人',
prefixIcon: Icon(Icons.person),
),
validator: (v) => v?.trim().isEmpty == true
? '请输入收货人'
: null,
),
const SizedBox(height: 16),
TextFormField(
controller: _phoneController,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: '手机号',
prefixIcon: Icon(Icons.phone),
),
validator: (v) => v?.trim().isEmpty == true
? '请输入手机号'
: null,
),
],
),
),
const SizedBox(height: 16),
// 地址信息
ShopCard(
child: Column(
children: [
TextFormField(
controller: _provinceController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: '省份',
),
validator: (v) => v?.trim().isEmpty == true
? '请输入省份'
: null,
),
const SizedBox(height: 16),
TextFormField(
controller: _cityController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: '城市',
),
validator: (v) => v?.trim().isEmpty == true
? '请输入城市'
: null,
),
const SizedBox(height: 16),
TextFormField(
controller: _districtController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: '区/县',
),
),
const SizedBox(height: 16),
TextFormField(
controller: _detailController,
maxLines: 2,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: '详细地址',
),
validator: (v) => v?.trim().isEmpty == true
? '请输入详细地址'
: null,
),
],
),
),
const SizedBox(height: 16),
// 标签和默认设置
ShopCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('标签', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 12),
Wrap(
spacing: 8,
children: ['家', '公司', '学校'].map((tag) {
return ChoiceChip(
label: Text(tag),
selected: _tag == tag,
onSelected: (selected) {
setState(() => _tag = selected ? tag : null);
},
);
}).toList(),
),
const SizedBox(height: 12),
SwitchListTile(
title: const Text('设为默认地址'),
value: _isDefault,
onChanged: (v) => setState(() => _isDefault = v),
contentPadding: EdgeInsets.zero,
),
],
),
),
const SizedBox(height: 24),
// 保存按钮
ShopButton(
label: '保存',
icon: Icons.save,
onPressed: _save,
),
],
),
),
);
}
}
这个完整实现展示了地址编辑页面的全部功能:
页面结构:
- 收货人信息卡片
- 地址信息卡片
- 标签和默认设置卡片
- 保存按钮
功能特性:
- 支持添加和编辑模式
- 完整的表单验证
- 自动加载现有地址数据
- 默认地址管理
用户体验:
- 清晰的表单分组
- 实时验证反馈
- 成功提示信息
- 便捷的保存操作
地址数据模型扩展
地址模型提供了便捷的数据操作方法。
dart
// 地址模型的扩展功能
class Address {
const Address({
required this.id,
required this.name,
required this.phone,
required this.province,
required this.city,
required this.district,
required this.detail,
this.isDefault = false,
this.tag,
});
final String id;
final String name;
final String phone;
final String province;
final String city;
final String district;
final String detail;
final bool isDefault;
final String? tag;
// 获取完整地址字符串
String get fullAddress => '$province $city $district $detail';
// 复制方法,用于创建修改后的副本
Address copyWith({
String? id,
String? name,
String? phone,
String? province,
String? city,
String? district,
String? detail,
bool? isDefault,
String? tag,
}) {
return Address(
id: id ?? this.id,
name: name ?? this.name,
phone: phone ?? this.phone,
province: province ?? this.province,
city: city ?? this.city,
district: district ?? this.district,
detail: detail ?? this.detail,
isDefault: isDefault ?? this.isDefault,
tag: tag ?? this.tag,
);
}
}
这个地址模型展示了如何设计数据结构:
模型属性:
- 包含地址的所有必要信息
- 支持地址标签和默认标记
- 使用
const构造函数确保不可变性
便捷方法:
fullAddress获取完整地址字符串copyWith()创建修改后的副本- 支持灵活的数据操作
不可变性:
- 所有属性都是
final - 使用
copyWith()创建新对象 - 便于状态管理和调试
错误处理和用户反馈
地址编辑过程中需要处理各种错误情况。
dart
// 错误处理和用户反馈
class AddressEditErrorHandler {
// 显示验证错误
static void showValidationError(
BuildContext context,
String message,
) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
duration: const Duration(seconds: 2),
),
);
}
// 显示成功提示
static void showSuccessMessage(
BuildContext context,
String message,
) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.green,
duration: const Duration(seconds: 2),
),
);
}
// 显示加载对话框
static void showLoadingDialog(BuildContext context) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('保存中...'),
],
),
),
);
}
// 隐藏加载对话框
static void hideLoadingDialog(BuildContext context) {
Navigator.of(context).pop();
}
// 处理保存异常
static Future<void> handleSaveError(
BuildContext context,
dynamic error,
) async {
showValidationError(
context,
'保存失败:${error.toString()}',
);
}
}
// 在地址编辑页面中使用
Future<void> _saveAddressWithErrorHandling() async {
try {
if (!_formKey.currentState!.validate()) {
AddressEditErrorHandler.showValidationError(
context,
'请填写所有必填项',
);
return;
}
// 显示加载对话框
AddressEditErrorHandler.showLoadingDialog(context);
final appState = AppStateScope.of(context);
final address = Address(
id: _editingAddress?.id ??
'addr_${DateTime.now().millisecondsSinceEpoch}',
name: _nameController.text.trim(),
phone: _phoneController.text.trim(),
province: _provinceController.text.trim(),
city: _cityController.text.trim(),
district: _districtController.text.trim(),
detail: _detailController.text.trim(),
tag: _tag,
isDefault: _isDefault,
);
// 模拟网络延迟
await Future.delayed(const Duration(milliseconds: 500));
if (!mounted) return;
// 隐藏加载对话框
AddressEditErrorHandler.hideLoadingDialog(context);
if (_editingAddress != null) {
appState.updateAddress(address);
} else {
appState.addAddress(address);
}
// 显示成功提示
AddressEditErrorHandler.showSuccessMessage(
context,
_editingAddress != null ? '地址已更新' : '地址已添加',
);
// 返回上一页
Navigator.of(context).pop();
} catch (e) {
if (!mounted) return;
AddressEditErrorHandler.hideLoadingDialog(context);
await AddressEditErrorHandler.handleSaveError(context, e);
}
}
这个错误处理展示了如何提供良好的用户反馈:
错误提示:
- 验证错误显示为红色 SnackBar
- 清晰的错误信息
- 自动消失的提示
成功反馈:
- 成功提示显示为绿色 SnackBar
- 确认操作完成
- 自动返回上一页
加载状态:
- 显示加载对话框
- 防止用户重复操作
- 提示用户等待
异常处理:
- 使用
try-catch捕获异常 - 隐藏加载对话框
- 显示错误信息
总结
地址编辑的实现涉及多个重要的技术点。首先是表单控制器的管理,确保资源正确释放。其次是表单验证,提供清晰的错误提示。再次是地址数据的初始化和保存,支持添加和编辑模式。最后是默认地址的管理和用户反馈,提供完整的用户体验。
这种设计确保了地址编辑的功能完整性和用户体验的流畅性。用户可以轻松添加和编辑地址、设置默认地址、获得清晰的操作反馈,整个地址编辑流程自然而直观。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net