1. 项目背景与需求分析
剧本杀作为当下年轻人最热衷的社交娱乐方式之一,线上组队需求日益增长。这次我们要在OpenHarmony系统上使用Flutter框架开发一个剧本杀组队App,重点实现"发起组队"这一核心功能的表单交互。选择Flutter+OpenHarmony的组合,主要考虑到:
- Flutter的跨平台特性可以让我们用一套代码适配多种设备
- OpenHarmony作为新兴操作系统,其分布式能力非常适合社交类应用
- 表单作为用户输入的核心界面,直接关系到用户体验和转化率
这个表单需要收集的关键信息包括:剧本名称、游戏时间、参与人数、难度等级、特殊要求等。其中ChoiceChip组件将用于实现标签式的多选功能,比如剧本类型(推理/情感/机制等)的选择。
2. 技术选型与环境搭建
2.1 Flutter for OpenHarmony环境配置
在Windows+Ubuntu双系统下搭建开发环境:
- 安装Flutter SDK时特别注意:
bash复制git clone https://github.com/flutter/flutter.git -b stable
export PATH="$PATH:`pwd`/flutter/bin"
flutter doctor
- OpenHarmony开发环境需要配置DevEco Studio和SDK:
- 下载OpenHarmony标准系统镜像
- 配置x86虚拟机用于调试
- 安装Flutter for OpenHarmony插件
注意:环境变量配置要准确,特别是JAVA_HOME和OHOS_SDK的路径,这是很多新手容易出错的地方。
2.2 表单技术选型
经过对比几种方案后,我们决定:
- 使用Flutter原生的Form组件作为基础框架
- 选择ChoiceChip实现标签选择功能
- 采用flutter_bloc进行状态管理
- 表单验证使用validator属性配合自定义规则
这样选型的考虑是:
- 原生组件性能最优,兼容性最好
- ChoiceChip的视觉效果符合现代UI设计趋势
- Bloc模式适合处理表单的复杂状态变化
- 内置验证机制能满足基本需求,特殊规则可以扩展
3. 表单UI设计与实现
3.1 表单结构设计
整个发起组队表单分为四个部分:
- 剧本信息区(名称、类型、难度)
- 时间地点区(日期、时段、场地类型)
- 玩家要求区(人数、性别比例、经验要求)
- 补充说明区(特殊需求、联系方式)
dart复制Form(
key: _formKey,
child: Column(
children: [
// 剧本信息区
_buildScriptSection(),
// 时间地点区
_buildTimeLocationSection(),
// 玩家要求区
_buildPlayerRequirementSection(),
// 补充说明区
_buildAdditionalInfoSection(),
],
),
)
3.2 ChoiceChip的多选实现
剧本类型选择使用Wrap+ChoiceChip组合:
dart复制Wrap(
spacing: 8.0,
children: ScriptType.values.map((type) {
return ChoiceChip(
label: Text(type.displayName),
selected: _selectedTypes.contains(type),
onSelected: (selected) {
setState(() {
if (selected) {
_selectedTypes.add(type);
} else {
_selectedTypes.remove(type);
}
});
},
);
}).toList(),
)
这里有几个关键点:
- 使用Wrap而不是Row,保证标签自动换行
- ScriptType是预定义的枚举,包含所有剧本类型
- _selectedTypes用Set存储已选项,避免重复
- 通过selected属性控制选中状态
3.3 表单验证逻辑
为每个字段添加验证规则:
dart复制TextFormField(
decoration: InputDecoration(labelText: '剧本名称'),
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入剧本名称';
}
if (value.length > 20) {
return '名称过长,不超过20字';
}
return null;
},
onSaved: (value) => _scriptName = value,
)
特殊的时间验证逻辑:
dart复制validator: (value) {
final time = DateTime.tryParse(value);
if (time == null) return '无效时间格式';
if (time.isBefore(DateTime.now())) {
return '不能选择过去时间';
}
return null;
}
4. 状态管理与数据提交
4.1 使用BLoC管理表单状态
创建FormBloc处理所有表单逻辑:
dart复制class FormBloc extends Bloc<FormEvent, FormState> {
FormBloc() : super(FormInitial());
@override
Stream<FormState> mapEventToState(FormEvent event) async* {
if (event is FormSubmitted) {
yield FormLoading();
try {
final result = await _submitForm(event.formData);
yield FormSuccess(result);
} catch (e) {
yield FormFailure(e.toString());
}
}
}
}
4.2 表单数据模型设计
定义FormData类封装所有字段:
dart复制class FormData {
final String scriptName;
final Set<ScriptType> types;
final DateTime playTime;
final int playerCount;
// 其他字段...
Map<String, dynamic> toJson() {
return {
'scriptName': scriptName,
'types': types.map((e) => e.index).toList(),
'playTime': playTime.toIso8601String(),
// 其他字段转换...
};
}
}
4.3 提交到后端API
使用dio库处理网络请求:
dart复制final response = await Dio().post(
'https://api.example.com/groups',
data: formData.toJson(),
options: Options(
headers: {'Content-Type': 'application/json'},
),
);
处理可能出现的错误:
dart复制try {
// 提交逻辑...
} on DioError catch (e) {
if (e.response?.statusCode == 400) {
throw Exception('表单数据不完整');
} else if (e.response?.statusCode == 409) {
throw Exception('相同剧本的组队已存在');
} else {
throw Exception('网络错误,请重试');
}
}
5. 性能优化与体验提升
5.1 表单渲染优化
- 对复杂表单使用AutomaticKeepAliveClientMixin
dart复制class _ScriptSectionState extends State<ScriptSection>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
// ...
}
- 分步加载表单内容
dart复制ListView.builder(
itemCount: _sections.length,
itemBuilder: (context, index) {
return FutureBuilder(
future: _loadSection(index),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return _sections[index];
}
return CircularProgressIndicator();
},
);
},
)
5.2 输入体验优化
- 为时间选择添加智能默认值:
dart复制DateTime _defaultTime() {
final now = DateTime.now();
// 默认设置为下一个整点时间
return DateTime(
now.year,
now.month,
now.day,
now.hour + 1,
);
}
- 实现表单记忆功能:
dart复制// 保存草稿
void _saveDraft() {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('formDraft', jsonEncode(_formData.toJson()));
}
// 加载草稿
void _loadDraft() async {
final prefs = await SharedPreferences.getInstance();
final draft = prefs.getString('formDraft');
if (draft != null) {
setState(() {
_formData = FormData.fromJson(jsonDecode(draft));
});
}
}
6. 常见问题与解决方案
6.1 ChoiceChip常见问题
问题1:选择状态不更新
- 检查是否漏掉了setState调用
- 确认selected属性绑定正确
- 确保父组件没有不必要的const修饰
问题2:样式不符合预期
dart复制ChoiceChip(
labelStyle: TextStyle(
color: _selected ? Colors.white : Colors.black,
),
backgroundColor: Colors.grey[200],
selectedColor: Colors.blue,
// ...
)
6.2 表单验证问题
问题:验证消息不显示
- 检查Form的autovalidateMode是否设置
- 确认TextFormField的validator属性已正确实现
- 确保FormState通过GlobalKey正确关联
dart复制final _formKey = GlobalKey<FormState>();
// 提交时手动触发验证
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
// 提交逻辑...
}
6.3 OpenHarmony适配问题
问题:某些组件在OpenHarmony上表现异常
- 使用flutter_ohos_compat插件
- 针对特定组件编写平台适配代码
dart复制if (Platform.isOHOS) {
return OHOSCustomWidget();
} else {
return DefaultWidget();
}
7. 扩展功能与未来优化
- 添加图片上传功能:
- 使用image_picker插件选择图片
- 通过multipart/form-data上传
- 显示上传进度和预览
- 实现表单模板功能:
- 将常用配置保存为模板
- 提供模板管理和应用功能
- 支持云端同步模板
- 接入OpenHarmony分布式能力:
- 允许跨设备继续填写表单
- 同步表单数据到其他设备
- 利用分布式软总线提高传输效率
在实现过程中,我发现Flutter for OpenHarmony的表单开发与传统Android/iOS开发相比有几个显著优势:热重载大大提高了UI调试效率,声明式UI让代码更清晰,丰富的插件生态节省了大量开发时间。特别是ChoiceChip组件,通过简单的配置就能实现漂亮的标签选择效果,远比自定义View要高效得多。
