Flutter for OpenHarmony衣橱管家App实战:编辑衣物实现

衣橱管家App里,用户添加衣物之后,难免会发现有些信息填错了,或者想要修改一下分类、颜色之类的属性。这时候就需要一个编辑衣物的功能。
今天这篇文章,我来详细讲讲编辑衣物页面的实现。这个页面和添加衣物页面有很多相似之处,但也有一些关键的不同点需要注意。
编辑页面的特殊之处
编辑页面和添加页面最大的区别在于:编辑页面打开时,表单里要预填充已有的数据。用户看到的应该是当前衣物的信息,而不是空白的表单。
另外,编辑页面需要接收一个衣物对象作为参数,这样才知道要编辑哪件衣物。
页面基础结构
先来看EditClothingScreen的定义:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../models/clothing_item.dart';
import '../../providers/wardrobe_provider.dart';
class EditClothingScreen extends StatefulWidget {
final ClothingItem item;
const EditClothingScreen({super.key, required this.item});
State<EditClothingScreen> createState() => _EditClothingScreenState();
}
这里用StatefulWidget是因为表单需要维护用户输入的状态,比如文本框的内容、下拉框的选中值等。
构造函数里有个required参数item,这就是要编辑的衣物对象,从上一个页面传过来的。
用final修饰item,因为这个参数在页面生命周期内不会改变,只是用来初始化表单数据。
状态变量的定义
State类里需要定义一堆变量来保存表单状态:
class _EditClothingScreenState extends State<EditClothingScreen> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _nameController;
late TextEditingController _brandController;
late TextEditingController _priceController;
late String _selectedCategory;
late String _selectedColor;
late String _selectedSeason;
late DateTime _purchaseDate;
late List<String> _selectedTags;
final List<String> _categories = ['上衣', '裤子', '裙子', '外套', '鞋子', '配饰'];
final List<String> _colors = ['白色', '黑色', '灰色', '红色', '蓝色', '绿色', '黄色', '粉色', '紫色', '棕色', '米色', '橙色'];
final List<String> _seasons = ['春季', '夏季', '秋季', '冬季', '四季'];
final List<String> _availableTags = ['休闲', '正装', '运动', '约会', '百搭', '经典', '时尚', '保暖', '清凉'];
}
_formKey用来标识Form组件,后面验证表单时会用到。
TextEditingController用来控制文本输入框,可以获取和设置输入框的内容。
late关键字表示这些变量会在initState里初始化,不是在声明时就赋值。
下面那几个List是选项数据,分类、颜色、季节、标签的可选值都在这里定义。
初始化和销毁
initState是初始化表单数据的地方:
void initState() {
super.initState();
_nameController = TextEditingController(text: widget.item.name);
_brandController = TextEditingController(text: widget.item.brand);
_priceController = TextEditingController(text: widget.item.price.toString());
_selectedCategory = widget.item.category;
_selectedColor = widget.item.color;
_selectedSeason = widget.item.season;
_purchaseDate = widget.item.purchaseDate;
_selectedTags = List.from(widget.item.tags);
}
这里是编辑页面的关键,所有表单控件的初始值都从widget.item里取。
TextEditingController的构造函数可以传入初始文本,这样输入框打开就有内容了。
_selectedTags用List.from创建一个新列表,而不是直接赋值,这样修改_selectedTags不会影响原来的item.tags。
dispose方法要记得释放Controller:
void dispose() {
_nameController.dispose();
_brandController.dispose();
_priceController.dispose();
super.dispose();
}
TextEditingController用完要dispose,否则会造成内存泄漏。
这是Flutter开发的基本规范,养成好习惯很重要。
页面布局
build方法构建整个页面:
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('编辑衣物'),
actions: [
TextButton(
onPressed: _saveChanges,
child: const Text('保存', style: TextStyle(color: Colors.white)),
),
],
),
body: Form(
key: _formKey,
child: SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 表单内容
],
),
),
),
);
}
AppBar右上角放保存按钮,用TextButton而不是IconButton,因为"保存"两个字比图标更直观。
Form组件包裹所有表单控件,这样可以统一做表单验证。
SingleChildScrollView确保内容多的时候可以滚动,不会溢出屏幕。
名称输入框
名称是必填项,需要做验证:
TextFormField(
controller: _nameController,
decoration: InputDecoration(
labelText: '名称',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.r)),
),
validator: (v) => v?.isEmpty ?? true ? '请输入名称' : null,
),
SizedBox(height: 16.h),
TextFormField是Form专用的输入框,支持validator验证。
validator返回null表示验证通过,返回字符串表示验证失败,字符串内容就是错误提示。
OutlineInputBorder给输入框加上边框,看起来更清晰。
分类下拉框
分类用DropdownButtonFormField实现:
DropdownButtonFormField<String>(
value: _selectedCategory,
decoration: InputDecoration(
labelText: '分类',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.r)),
),
items: _categories.map((c) => DropdownMenuItem(value: c, child: Text(c))).toList(),
onChanged: (v) => setState(() => _selectedCategory = v!),
),
SizedBox(height: 16.h),
DropdownButtonFormField和普通的DropdownButton区别在于,它可以放在Form里,样式也和TextFormField保持一致。
value是当前选中的值,items是所有选项,onChanged是选中值变化时的回调。
setState触发页面重建,这样下拉框才会显示新选中的值。
颜色选择器
颜色选择用圆形色块来展示,比下拉框更直观:
Text('颜色', style: TextStyle(fontSize: 14.sp, color: Colors.grey.shade700)),
SizedBox(height: 8.h),
Wrap(
spacing: 8.w,
runSpacing: 8.h,
children: _colors.map((color) {
final isSelected = color == _selectedColor;
return GestureDetector(
onTap: () => setState(() => _selectedColor = color),
child: Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
color: ClothingItem.getColorFromName(color),
shape: BoxShape.circle,
border: Border.all(
color: isSelected ? const Color(0xFFE91E63) : Colors.grey.shade300,
width: isSelected ? 3 : 1,
),
),
child: isSelected ? const Icon(Icons.check, color: Colors.white, size: 20) : null,
),
);
}).toList(),
),
SizedBox(height: 16.h),
Wrap组件可以自动换行,当一行放不下所有色块时,会自动换到下一行。
spacing是水平间距,runSpacing是行间距。
选中的颜色用粗边框和对勾图标来标识,用户一眼就能看出当前选的是哪个颜色。
ClothingItem.getColorFromName是一个工具方法,把颜色名称转换成Color对象。
季节下拉框
季节选择和分类选择类似:
DropdownButtonFormField<String>(
value: _selectedSeason,
decoration: InputDecoration(
labelText: '季节',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.r)),
),
items: _seasons.map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(),
onChanged: (v) => setState(() => _selectedSeason = v!),
),
SizedBox(height: 16.h),
季节有五个选项:春夏秋冬加上四季。四季表示这件衣服一年四季都能穿。
下拉框的样式和分类下拉框保持一致,整个表单看起来很统一。
品牌和价格输入
品牌和价格是可选项,不需要验证:
TextFormField(
controller: _brandController,
decoration: InputDecoration(
labelText: '品牌',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.r)),
),
),
SizedBox(height: 16.h),
TextFormField(
controller: _priceController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: '价格',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.r)),
),
),
SizedBox(height: 16.h),
价格输入框设置keyboardType为number,这样弹出的键盘是数字键盘,方便用户输入。
品牌和价格没有validator,因为这两个字段不是必填的。
标签选择
标签用FilterChip实现多选:
Text('标签', style: TextStyle(fontSize: 14.sp, color: Colors.grey.shade700)),
SizedBox(height: 8.h),
Wrap(
spacing: 8.w,
runSpacing: 8.h,
children: _availableTags.map((tag) {
final isSelected = _selectedTags.contains(tag);
return FilterChip(
label: Text(tag),
selected: isSelected,
selectedColor: const Color(0xFFE91E63).withOpacity(0.2),
onSelected: (selected) {
setState(() {
if (selected) {
_selectedTags.add(tag);
} else {
_selectedTags.remove(tag);
}
});
},
);
}).toList(),
),
SizedBox(height: 32.h),
FilterChip是Material Design里的筛选芯片,天生支持选中和未选中两种状态。
selected属性控制是否选中,selectedColor是选中时的背景色。
onSelected回调里根据selected参数决定是添加还是移除标签。
标签可以多选,所以用List来存储,而不是单个String。
保存修改
保存按钮点击后执行_saveChanges方法:
void _saveChanges() {
if (_formKey.currentState?.validate() ?? false) {
final updatedItem = widget.item.copyWith(
name: _nameController.text,
category: _selectedCategory,
color: _selectedColor,
season: _selectedSeason,
brand: _brandController.text,
price: double.tryParse(_priceController.text) ?? 0,
purchaseDate: _purchaseDate,
tags: _selectedTags,
);
Provider.of<WardrobeProvider>(context, listen: false).updateClothing(updatedItem);
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('修改已保存')),
);
}
}
先调用validate()验证表单,只有验证通过才继续执行。
copyWith是ClothingItem类的方法,用来创建一个新对象,只修改指定的属性,其他属性保持不变。
Provider.of获取WardrobeProvider实例,listen: false表示不监听变化,因为这里只是调用方法,不需要重建UI。
Navigator.pop返回上一页,SnackBar显示保存成功的提示。
copyWith模式
copyWith是Dart里很常用的一种模式,用来创建不可变对象的修改版本:
// ClothingItem类里的copyWith方法大概长这样
ClothingItem copyWith({
String? name,
String? category,
String? color,
// ... 其他属性
}) {
return ClothingItem(
id: id, // id不变
name: name ?? this.name,
category: category ?? this.category,
color: color ?? this.color,
// ... 其他属性
);
}
这种模式的好处是,原对象不会被修改,而是创建一个新对象。
传入的参数如果是null,就用原来的值;如果不是null,就用新值。
这样调用的时候只需要传想修改的属性,其他属性自动保持不变。
表单验证的细节
表单验证是编辑页面很重要的一环:
// 验证逻辑
validator: (v) => v?.isEmpty ?? true ? '请输入名称' : null
// 触发验证
if (_formKey.currentState?.validate() ?? false) {
// 验证通过
}
validator是每个表单控件的验证函数,返回null表示通过,返回字符串表示失败。
_formKey.currentState?.validate()会触发所有表单控件的validator,只有全部通过才返回true。
验证失败时,对应的输入框下方会显示红色的错误提示文字。
价格转换的处理
价格从字符串转换成数字需要注意异常处理:
price: double.tryParse(_priceController.text) ?? 0,
double.tryParse比double.parse更安全,解析失败时返回null而不是抛异常。
用??运算符提供默认值0,这样即使用户输入了非数字内容,也不会崩溃。
状态管理的配合
编辑页面和Provider的配合:
// 更新数据
Provider.of<WardrobeProvider>(context, listen: false).updateClothing(updatedItem);
WardrobeProvider里应该有个updateClothing方法,接收修改后的衣物对象,更新到数据列表里。
listen: false很重要,因为这里只是调用方法,不需要监听数据变化。如果设为true,会导致不必要的重建。
用户体验优化
编辑页面有几个用户体验的细节:
第一,打开页面时表单已经填好了数据,用户不需要重新输入。
第二,颜色选择用色块展示,比文字更直观。
第三,保存成功后自动返回上一页,并显示SnackBar提示。
第四,表单验证失败时,错误提示显示在对应的输入框下方,用户知道哪里出了问题。
// 保存成功的反馈
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('修改已保存')),
);
先pop再showSnackBar,SnackBar会显示在上一个页面上,用户能看到保存成功的提示。
这种交互方式很常见,用户体验也很好。
和添加页面的区别
编辑页面和添加页面的主要区别:
第一,编辑页面需要接收一个item参数,添加页面不需要。
第二,编辑页面在initState里用item的数据初始化表单,添加页面用默认值。
第三,编辑页面调用updateClothing更新数据,添加页面调用addClothing添加数据。
第四,编辑页面的AppBar标题是"编辑衣物",添加页面是"添加衣物"。
// 编辑页面
class EditClothingScreen extends StatefulWidget {
final ClothingItem item; // 需要参数
// ...
}
// 添加页面
class AddClothingScreen extends StatefulWidget {
const AddClothingScreen({super.key}); // 不需要参数
// ...
}
虽然两个页面很相似,但分开写更清晰,也更容易维护。
如果硬要合并成一个页面,需要加很多判断逻辑,代码会变得复杂。
总结
编辑衣物页面的实现涉及到表单处理、状态管理、数据验证等多个方面。关键点在于:
用initState初始化表单数据,用copyWith创建修改后的对象,用Provider更新全局状态。
表单验证确保用户输入的数据是有效的,用户体验的细节让操作更顺畅。
在OpenHarmony平台上,这套实现方式完全适用,Flutter的跨平台能力让我们可以用同一套代码在不同平台上运行。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐
所有评论(0)