在这里插入图片描述

添加猫咪是整个 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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐