在 Flutter 适配 OpenHarmony 这件事上,我踩过的坑比写过的代码还多。前面三篇实战我们解决了环境搭建、工程初始化和首页框架,这次终于轮到重头戏——发起组队的表单实现。这个功能不复杂,但它覆盖了表单校验、单选多选、时间选择器和键盘交互这些日常开发里逃不掉的硬骨头。尤其是在 OpenHarmony 这种非标准 Android 环境下,很多你以为“能跑就行”的组件,真跑起来全是惊喜。这篇就把第四期的完整实现思路、关键代码和踩坑记录一次说透。
1. 发起组队表单的需求拆解与方案设计
1.1 剧本杀组队场景里的表单定位
先想清楚一个问题:为什么剧本杀 App 的“发起组队”不做成直接发一条消息,非要搞一个表单?
因为组队信息的结构化程度要求很高。玩家找车的时候关心什么:玩什么本、几点开、在哪家店、还缺几个人、有没有性别/段位要求、有什么备注。这些信息如果不做结构化,全塞在描述文字里,后续的筛选、匹对、提醒全都没法做。所以表单不是走形式,它是整个组队流程的数据入口,后端接口、列表渲染、消息推送,全都依赖这一份表单产出的数据。
我们的字段设计最终定了六个:剧本名称(必填)、开本时间(必填)、剧本店/地点(必填)、需要人数(必填,数字)、性别偏好(单选,不限制/男/女)、备注(选填)。这套字段基本覆盖了主流剧本杀拼车群里的信息要素,多余的东西一个没加。对了,本来还想加一个“剧本类型”的多选,比如恐怖、情感、硬核,后来砍掉了——因为第一版上线最重要的目标是验证“发起组队 → 列表展示 → 加入组队”这条主链路,类型筛选放到后面迭代加更合理,第一版保持足够薄。
1.2 Flutter 表单技术选型:Form 还是裸 TextField?
这是新手最容易纠结的地方。Flutter 官方提供了 Form + TextFormField + Validator 这套组合,也有很多人喜欢完全自己用 TextEditingController + TextFormField 手搓校验逻辑。
我的建议是:**项目里凡是涉及多个输入框、互相之间有联动校验的页面,统一用 Form 组件,不要手工拼。**这跟我用全局状态管理还是局部 setState 是两码事——Form 解决的是表单内字段的注册、校验触发和重置,它天然地把一组输入框组织成了一个“可统一操作的整体”。你提交时调 formKey.currentState.validate(),它会遍历所有已注册的 FormField 依次执行各自的 validator,任何一个不通过,对应字段会自己渲染错误文案,光标和滚动也自动定位过去。这套机制比自己维护一个“错误状态 Map”要省太多事。
有人担心 Form 性能,担心每个字段的 validator 都跑一遍会不会卡。实测在组队表单这种六七个字段的页面上,完全感知不到性能问题。不要为了“追求先进”去选 Bloc 或者复杂的状态管理库,先把 Form 玩明白,80% 的表单页面都够用了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 表单界面实现与核心代码解读
2.1 表单页面的整体骨架
我们最终的页面结构大概是这个样子:
dart复制@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('发起组队'),
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
// 返回前做一次草稿保存
_saveDraft();
Navigator.of(context).maybePop();
},
),
),
body: SafeArea(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => FocusScope.of(context).unfocus(),
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 32),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildTitleField(),
_buildTimeField(),
_buildStoreField(),
_buildPlayerCountField(),
_buildGenderField(),
_buildRemarkField(),
_buildSubmitButton(),
],
),
),
),
),
),
);
}
几个细节单独说一下。
AppBar 的左上角我用了关闭图标而不是默认的返回箭头。原因很简单:这个页面是从首页和个人中心两个入口都能进的,用户点关闭的心理预期是“我不组了,退出这个流程”,而返回箭头更像“回到上一个页面”。配合关闭按钮,我在 onPressed 里做了一次草稿保存,这一点后面数据持久化部分会细讲。
GestureDetector + onTap: unfocus 是一个很容易被忽视但极其影响体验的
