Flutter for OpenHarmony 猫咪管家App实战:添加猫咪功能深度解析
本文详细介绍了Flutter猫咪管理App中"添加猫咪"表单页面的设计与实现。重点包括:1) 使用StatefulWidget管理表单状态,包括文本控制器和各类状态变量;2) 表单验证机制和资源释放处理;3) 常见猫咪品种下拉选择实现;4) 表单UI组件设计,如头像上传区、必填字段验证、性别单选按钮和日期选择器等。通过合理使用Form组件、验证逻辑和一致性UI设计,构建了用户友

添加猫咪是整个 App 的起点,没有猫咪数据,其他功能都没法用。这篇专门讲讲表单设计和数据验证的细节。
一、StatefulWidget 的选择
添加页面需要维护多个输入状态,必须用 StatefulWidget:
class AddCatScreen extends StatefulWidget {
const AddCatScreen({super.key});
State<AddCatScreen> createState() => _AddCatScreenState();
}
表单页面的特点是用户输入会改变界面状态。
比如选了日期,界面上要显示选中的日期。
状态类里定义表单 key 和控制器:
class _AddCatScreenState extends State<AddCatScreen> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _breedController = TextEditingController();
final _weightController = TextEditingController();
GlobalKey<FormState>是表单验证的关键。
每个输入框都需要一个TextEditingController。
非文本类型的状态用普通变量:
DateTime _birthDate = DateTime.now().subtract(const Duration(days: 365));
String _gender = '公';
bool _isNeutered = false;
日期、单选、开关这些不需要控制器。
默认值要合理,减少用户操作。
二、资源释放
控制器用完必须释放:
void dispose() {
_nameController.dispose();
_breedController.dispose();
_weightController.dispose();
_microchipController.dispose();
_notesController.dispose();
super.dispose();
}
不释放会导致内存泄漏,App 越用越卡。
super.dispose()要放最后调用。
三、品种数据
常见猫咪品种列表:
final List<String> _commonBreeds = [
'中华田园猫',
'英国短毛猫',
'美国短毛猫',
'布偶猫',
'波斯猫',
'暹罗猫',
'缅因猫',
'苏格兰折耳猫',
'俄罗斯蓝猫',
'孟加拉猫',
'其他',
];
中华田园猫放第一个,毕竟是最常见的。
"其他"兜底,覆盖不在列表里的品种。
四、页面结构
用 Form 包裹所有输入组件:
body: Form(
key: _formKey,
child: SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Form提供统一的验证机制。SingleChildScrollView处理键盘弹出时的滚动。
五、头像上传区
头像用 Stack 叠加实现:
Center(
child: Stack(
children: [
CircleAvatar(
radius: 50.r,
backgroundColor: Colors.orange[100],
child: Icon(Icons.pets, size: 50.sp, color: Colors.orange),
),
暂时用图标占位,后续可以接入图片选择。
圆形头像是宠物类 App 的标配设计。
相机图标定位到右下角:
Positioned(
bottom: 0,
right: 0,
child: CircleAvatar(
radius: 18.r,
backgroundColor: Colors.orange,
child: Icon(Icons.camera_alt, size: 18.sp, color: Colors.white),
),
),
Positioned必须在Stack里面用。
小圆圈里放相机图标,暗示可以点击上传。
六、名字输入
名字是必填字段:
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: '猫咪名字 *',
hintText: '请输入猫咪名字',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.pets),
),
星号表示必填,这是通用的表单设计规范。
OutlineInputBorder是带边框的样式,比下划线更正式。
验证逻辑:
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入猫咪名字';
}
return null;
},
返回字符串表示验证失败,显示错误信息。
返回 null 表示验证通过。
七、品种选择
下拉框比手动输入更友好:
DropdownButtonFormField<String>(
value: _breedController.text.isEmpty ? null : _breedController.text,
decoration: const InputDecoration(
labelText: '品种 *',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.category),
),
value为 null 时显示 placeholder。
样式和文本框保持一致,视觉统一。
生成下拉选项:
items: _commonBreeds.map((breed) {
return DropdownMenuItem(value: breed, child: Text(breed));
}).toList(),
onChanged: (value) {
setState(() {
_breedController.text = value ?? '';
});
},
map把字符串列表转成DropdownMenuItem列表。
选中后更新控制器,方便后续读取。
验证也不能少:
validator: (value) {
if (value == null || value.isEmpty) {
return '请选择品种';
}
return null;
},
下拉框也可以加 validator。
没选择时提示用户。
八、性别单选
先放个标签:
Text('性别 *', style: TextStyle(fontSize: 14.sp, color: Colors.grey[700])),
SizedBox(height: 8.h),
单选按钮没有自带标签,需要手动加。
颜色和其他标签保持一致。
两个选项并排:
Row(
children: [
Expanded(
child: RadioListTile<String>(
title: Row(
children: [
const Icon(Icons.male, color: Colors.blue),
SizedBox(width: 8.w),
const Text('公'),
],
),
value: '公',
groupValue: _gender,
onChanged: (value) => setState(() => _gender = value!),
contentPadding: EdgeInsets.zero,
),
),
Expanded让两个选项平分宽度。
图标加文字,比纯文字更直观。
母猫选项:
Expanded(
child: RadioListTile<String>(
title: Row(
children: [
const Icon(Icons.female, color: Colors.pink),
SizedBox(width: 8.w),
const Text('母'),
],
),
value: '母',
groupValue: _gender,
onChanged: (value) => setState(() => _gender = value!),
contentPadding: EdgeInsets.zero,
),
),
粉色女性图标代表母猫,颜色区分明显。
groupValue相同才能实现互斥。
九、出生日期
日期选择器的触发:
InkWell(
onTap: () => _selectBirthDate(context),
child: InputDecorator(
decoration: const InputDecoration(
labelText: '出生日期 *',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.cake),
),
child: Text(DateFormat('yyyy-MM-dd').format(_birthDate)),
),
),
InputDecorator让任意组件看起来像输入框。
蛋糕图标暗示这是生日。
日期选择方法:
Future<void> _selectBirthDate(BuildContext context) async {
final picked = await showDatePicker(
context: context,
initialDate: _birthDate,
firstDate: DateTime(2000),
lastDate: DateTime.now(),
);
if (picked != null) {
setState(() => _birthDate = picked);
}
}
showDatePicker是 Flutter 内置的日期选择器。
用户取消选择时picked为 null,不更新状态。
十、体重输入
数字键盘更方便:
TextFormField(
controller: _weightController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: '体重 (kg) *',
hintText: '请输入体重',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.monitor_weight),
),
TextInputType.number弹出数字键盘。
单位写在标签里,用户不会搞混。
验证要检查数字格式:
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入体重';
}
if (double.tryParse(value) == null) {
return '请输入有效的数字';
}
return null;
},
double.tryParse转换失败返回 null。
这样能拦截"abc"这种非法输入。
十一、绝育开关
SwitchListTile 一行搞定:
SwitchListTile(
title: const Text('是否绝育'),
value: _isNeutered,
onChanged: (value) => setState(() => _isNeutered = value),
contentPadding: EdgeInsets.zero,
),
开关比单选更适合是/否的场景。
默认关闭,用户按需开启。
十二、选填字段
芯片号不是必填:
TextFormField(
controller: _microchipController,
decoration: const InputDecoration(
labelText: '芯片号 (选填)',
hintText: '请输入芯片号',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.memory),
),
),
标签里写明"选填",降低用户心理负担。
没有 validator,空着也能提交。
备注支持多行:
TextFormField(
controller: _notesController,
maxLines: 3,
decoration: const InputDecoration(
labelText: '备注 (选填)',
hintText: '请输入备注信息',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.note),
),
),
maxLines: 3让输入框变高,适合长文本。
可以记录猫咪的特殊情况,比如过敏信息。
十三、提交按钮
按钮撑满宽度:
SizedBox(
width: double.infinity,
height: 48.h,
child: ElevatedButton(
onPressed: _saveCat,
child: const Text('保存'),
),
),
大按钮更容易点击,用户体验好。
高度 48 是 Material Design 推荐的最小触摸区域。
十四、保存逻辑
先验证表单:
void _saveCat() {
if (_formKey.currentState!.validate()) {
validate()会触发所有字段的 validator。
有任何一个失败就返回 false。
构造数据模型:
final cat = CatModel(
name: _nameController.text,
breed: _breedController.text,
birthDate: _birthDate,
weight: double.parse(_weightController.text),
gender: _gender,
isNeutered: _isNeutered,
microchipId: _microchipController.text.isEmpty ? null : _microchipController.text,
notes: _notesController.text.isEmpty ? null : _notesController.text,
);
空字符串转成 null,数据更干净。
double.parse这里不会出错,因为前面验证过了。
保存并返回:
context.read<CatProvider>().addCat(cat);
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${cat.name} 添加成功!')),
);
context.read只读取一次,不监听变化。
SnackBar 给用户即时反馈,体验更好。
小结
表单页面看着简单,细节其实很多。验证逻辑、键盘类型、资源释放这些都要考虑到。好的表单设计能大大提升用户体验,减少输入错误。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐

所有评论(0)