Flutter for OpenHarmony 家庭相册App实战 - 编辑家人实现
本文介绍了Flutter中编辑家人信息功能的实现方法。通过创建EditMemberScreen页面,预填充现有成员数据,用户可以修改姓名、关系、生日等字段。页面包含表单验证、关系下拉选择器和生日选择器,底部提供删除按钮。关键点包括:使用TextEditingController初始化表单数据,实现表单验证逻辑,以及通过FamilyProvider更新数据。这种设计模式确保了数据一致性和良好的用户体

编辑家人功能允许用户修改已有家庭成员的信息,包括更新姓名、关系、生日、联系方式等。一个好的编辑界面应该预填充现有数据,让用户能够方便地修改需要更新的字段。今天我们来实现这个功能。
设计思路
编辑家人页面与添加家人页面类似,但需要预填充现有数据。用户可以修改任何字段,也可以删除该家人。页面底部提供删除按钮,方便用户管理家庭成员。表单验证确保必填字段不为空。
创建页面结构
先搭建基本框架:
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
import '../providers/family_provider.dart';
import '../models/family_member.dart';
class EditMemberScreen extends StatefulWidget {
final FamilyMember member;
const EditMemberScreen({
super.key,
required this.member,
});
State<EditMemberScreen> createState() => _EditMemberScreenState();
}
class _EditMemberScreenState extends State<EditMemberScreen> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _nameController;
late TextEditingController _phoneController;
late TextEditingController _emailController;
late TextEditingController _addressController;
late TextEditingController _notesController;
late String _selectedRelationship;
DateTime? _birthday;
final List<String> _relationships = [
'父亲', '母亲', '祖父', '祖母', '外祖父', '外祖母',
'儿子', '女儿', '兄弟', '姐妹', '叔叔', '阿姨',
'表兄弟', '表姐妹', '配偶', '其他'
];
void initState() {
super.initState();
_nameController = TextEditingController(text: widget.member.name);
_phoneController = TextEditingController(text: widget.member.phone ?? '');
_emailController = TextEditingController(text: widget.member.email ?? '');
_addressController = TextEditingController(text: widget.member.address ?? '');
_notesController = TextEditingController(text: widget.member.notes ?? '');
_selectedRelationship = widget.member.relationship;
_birthday = widget.member.birthday;
}
void dispose() {
_nameController.dispose();
_phoneController.dispose();
_emailController.dispose();
_addressController.dispose();
_notesController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('编辑家人信息'),
elevation: 0,
actions: [
TextButton(
onPressed: _saveMember,
child: Text(
'保存',
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
fontWeight: FontWeight.w600,
),
),
),
],
),
body: Form(
key: _formKey,
child: ListView(
padding: EdgeInsets.all(16.w),
children: [
_buildNameField(),
SizedBox(height: 16.h),
_buildRelationshipField(),
SizedBox(height: 16.h),
_buildBirthdayField(),
SizedBox(height: 16.h),
_buildPhoneField(),
SizedBox(height: 16.h),
_buildEmailField(),
SizedBox(height: 16.h),
_buildAddressField(),
SizedBox(height: 16.h),
_buildNotesField(),
SizedBox(height: 32.h),
_buildDeleteButton(),
SizedBox(height: 24.h),
],
),
),
);
}
}
initState中使用现有数据初始化所有Controller和状态变量,这样用户打开页面时就能看到当前的信息。dispose方法里记得释放所有Controller资源。
姓名输入框
姓名是必填字段:
Widget _buildNameField() {
return TextFormField(
controller: _nameController,
decoration: InputDecoration(
labelText: '姓名',
hintText: '请输入姓名',
prefixIcon: const Icon(Icons.person, color: Color(0xFFE91E63)),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r),
borderSide: BorderSide(color: Colors.grey[300]!),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r),
borderSide: const BorderSide(
color: Color(0xFFE91E63),
width: 2,
),
),
filled: true,
fillColor: Colors.grey[50],
),
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入姓名';
}
if (value.length < 2) {
return '姓名至少2个字符';
}
return null;
},
textInputAction: TextInputAction.next,
);
}
输入框用圆角边框,聚焦时边框变成粉色。prefixIcon显示人物图标,验证器检查姓名不能为空且至少2个字符。
关系选择器
用下拉菜单选择关系:
Widget _buildRelationshipField() {
return DropdownButtonFormField<String>(
value: _selectedRelationship,
decoration: InputDecoration(
labelText: '关系',
prefixIcon: const Icon(Icons.family_restroom, color: Color(0xFFE91E63)),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r),
borderSide: BorderSide(color: Colors.grey[300]!),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r),
borderSide: const BorderSide(
color: Color(0xFFE91E63),
width: 2,
),
),
filled: true,
fillColor: Colors.grey[50],
),
items: _relationships.map((r) {
return DropdownMenuItem(
value: r,
child: Text(r),
);
}).toList(),
onChanged: (value) {
setState(() {
_selectedRelationship = value!;
});
},
validator: (value) {
if (value == null || value.isEmpty) {
return '请选择关系';
}
return null;
},
);
}
下拉菜单提供多种关系选项,用户可以方便地选择。选择后更新状态,验证器确保用户选择了关系。
生日选择器
用日期选择器选择生日:
Widget _buildBirthdayField() {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.r),
border: Border.all(color: Colors.grey[300]!),
),
child: ListTile(
leading: Container(
padding: EdgeInsets.all(8.w),
decoration: BoxDecoration(
color: const Color(0xFFE91E63).withOpacity(0.1),
borderRadius: BorderRadius.circular(8.r),
),
child: const Icon(
Icons.cake,
color: Color(0xFFE91E63),
),
),
title: const Text(
'生日',
style: TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text(
_birthday != null
? DateFormat('yyyy年MM月dd日').format(_birthday!)
: '未设置',
style: TextStyle(
color: _birthday != null
? const Color(0xFFE91E63)
: Colors.grey[600],
fontSize: 14.sp,
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (_birthday != null)
IconButton(
icon: const Icon(Icons.clear, size: 20),
onPressed: () {
setState(() {
_birthday = null;
});
},
),
const Icon(Icons.chevron_right, color: Colors.grey),
],
),
onTap: _selectBirthday,
),
);
}
Future<void> _selectBirthday() async {
final date = await showDatePicker(
context: context,
initialDate: _birthday ?? DateTime.now(),
firstDate: DateTime(1900),
lastDate: DateTime.now(),
locale: const Locale('zh', 'CN'),
builder: (context, child) {
return Theme(
data: Theme.of(context).copyWith(
colorScheme: const ColorScheme.light(
primary: Color(0xFFE91E63),
onPrimary: Colors.white,
surface: Colors.white,
onSurface: Colors.black,
),
),
child: child!,
);
},
);
if (date != null) {
setState(() {
_birthday = date;
});
}
}
生日选择器用ListTile包装在圆角容器里,左边有蛋糕图标。如果已设置生日,右边显示清除按钮。日期选择器用主题色包装,保持视觉一致。
电话输入框
电话是可选字段:
Widget _buildPhoneField() {
return TextFormField(
controller: _phoneController,
decoration: InputDecoration(
labelText: '电话(可选)',
hintText: '请输入电话号码',
prefixIcon: const Icon(Icons.phone, color: Color(0xFFE91E63)),
suffixIcon: _phoneController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear, size: 20),
onPressed: () {
setState(() {
_phoneController.clear();
});
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r),
borderSide: BorderSide(color: Colors.grey[300]!),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r),
borderSide: const BorderSide(
color: Color(0xFFE91E63),
width: 2,
),
),
filled: true,
fillColor: Colors.grey[50],
),
keyboardType: TextInputType.phone,
textInputAction: TextInputAction.next,
);
}
电话输入框用phone键盘类型,方便用户输入。如果有内容,右边显示清除按钮。
邮箱输入框
邮箱也是可选字段:
Widget _buildEmailField() {
return TextFormField(
controller: _emailController,
decoration: InputDecoration(
labelText: '邮箱(可选)',
hintText: '请输入邮箱地址',
prefixIcon: const Icon(Icons.email, color: Color(0xFFE91E63)),
suffixIcon: _emailController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear, size: 20),
onPressed: () {
setState(() {
_emailController.clear();
});
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r),
borderSide: BorderSide(color: Colors.grey[300]!),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r),
borderSide: const BorderSide(
color: Color(0xFFE91E63),
width: 2,
),
),
filled: true,
fillColor: Colors.grey[50],
),
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
validator: (value) {
if (value != null && value.isNotEmpty) {
if (!value.contains('@')) {
return '请输入有效的邮箱地址';
}
}
return null;
},
);
}
邮箱输入框用emailAddress键盘类型。验证器检查邮箱格式,但只在用户输入了内容时才验证。
地址输入框
地址输入框:
Widget _buildAddressField() {
return TextFormField(
controller: _addressController,
decoration: InputDecoration(
labelText: '地址(可选)',
hintText: '请输入地址',
prefixIcon: const Icon(Icons.location_on, color: Color(0xFFE91E63)),
suffixIcon: _addressController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear, size: 20),
onPressed: () {
setState(() {
_addressController.clear();
});
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r),
borderSide: BorderSide(color: Colors.grey[300]!),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r),
borderSide: const BorderSide(
color: Color(0xFFE91E63),
width: 2,
),
),
filled: true,
fillColor: Colors.grey[50],
),
textInputAction: TextInputAction.next,
);
}
地址输入框用定位图标,如果有内容显示清除按钮。
备注输入框
备注用多行输入框:
Widget _buildNotesField() {
return TextFormField(
controller: _notesController,
decoration: InputDecoration(
labelText: '备注(可选)',
hintText: '添加一些备注信息',
alignLabelWithHint: true,
prefixIcon: Padding(
padding: EdgeInsets.only(bottom: 60.h),
child: const Icon(Icons.note, color: Color(0xFFE91E63)),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r),
borderSide: BorderSide(color: Colors.grey[300]!),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r),
borderSide: const BorderSide(
color: Color(0xFFE91E63),
width: 2,
),
),
filled: true,
fillColor: Colors.grey[50],
),
maxLines: 3,
minLines: 3,
textInputAction: TextInputAction.done,
);
}
备注输入框用3行高度,图标用Padding调整位置让它在顶部对齐。
删除按钮
页面底部的删除按钮:
Widget _buildDeleteButton() {
return OutlinedButton.icon(
onPressed: () => _showDeleteDialog(),
icon: const Icon(Icons.delete_forever),
label: const Text('删除此家人'),
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFFF44336),
side: const BorderSide(
color: Color(0xFFF44336),
width: 2,
),
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
),
);
}
删除按钮用红色边框和文字,表示这是危险操作。按钮有图标和文字,点击后显示确认对话框。
保存更新
验证表单并保存更新:
void _saveMember() {
if (_formKey.currentState!.validate()) {
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => const Center(
child: CircularProgressIndicator(
color: Color(0xFFE91E63),
),
),
);
final updatedMember = widget.member.copyWith(
name: _nameController.text.trim(),
relationship: _selectedRelationship,
birthday: _birthday,
phone: _phoneController.text.trim().isNotEmpty
? _phoneController.text.trim()
: null,
email: _emailController.text.trim().isNotEmpty
? _emailController.text.trim()
: null,
address: _addressController.text.trim().isNotEmpty
? _addressController.text.trim()
: null,
notes: _notesController.text.trim().isNotEmpty
? _notesController.text.trim()
: null,
);
Future.delayed(const Duration(milliseconds: 500), () {
context.read<FamilyProvider>().updateMember(updatedMember);
Navigator.pop(context);
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.check_circle, color: Colors.white),
SizedBox(width: 8.w),
const Text('信息更新成功'),
],
),
backgroundColor: const Color(0xFF4CAF50),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
margin: EdgeInsets.all(16.w),
duration: const Duration(seconds: 2),
),
);
});
}
}
保存前先验证表单,验证通过后显示加载提示。用copyWith方法创建新对象,只更新修改的字段。空字符串转换为null。通过Provider更新数据,关闭页面并显示成功提示。
删除确认对话框
删除前需要用户确认:
void _showDeleteDialog() {
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
title: Row(
children: [
const Icon(
Icons.warning_amber_rounded,
color: Color(0xFFF44336),
),
SizedBox(width: 8.w),
const Text('删除家人'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'确定要删除"${widget.member.name}"吗?',
style: TextStyle(fontSize: 15.sp),
),
SizedBox(height: 8.h),
Text(
'删除后将无法恢复,相关的照片和回忆不会被删除。',
style: TextStyle(
fontSize: 13.sp,
color: Colors.grey[600],
),
),
],
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(
'取消',
style: TextStyle(
color: Colors.grey[600],
fontSize: 15.sp,
),
),
),
TextButton(
onPressed: () {
context.read<FamilyProvider>().deleteMember(widget.member.id);
Navigator.pop(dialogContext);
Navigator.pop(context);
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('已删除"${widget.member.name}"'),
behavior: SnackBarBehavior.floating,
backgroundColor: const Color(0xFFF44336),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
margin: EdgeInsets.all(16.w),
),
);
},
child: Text(
'删除',
style: TextStyle(
color: const Color(0xFFF44336),
fontSize: 15.sp,
fontWeight: FontWeight.w600,
),
),
),
],
),
);
}
删除对话框有警告图标,内容说明删除后无法恢复但相关数据不会被删除。删除按钮用红色表示危险操作。删除后连续pop三次,关闭对话框、编辑页面和详情页面,返回列表页。
总结
编辑家人页面通过预填充数据让用户能够方便地修改信息。所有输入框都有清除按钮,方便用户快速清空。表单验证确保必填字段不为空,邮箱格式正确。copyWith方法确保只更新修改的字段。删除功能提供了完整的家人管理能力,删除前有详细的确认提示。整个页面交互流畅,用户体验良好。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐
所有评论(0)