1. 项目概述:构建专业级任务管理表单
在移动应用开发领域,任务管理类应用是最能检验框架能力的试金石之一。这次我们要在Flutter和OpenHarmony的跨平台环境下,构建一个具备完整CRUD功能的专业级任务表单系统。不同于简单的文本框+按钮组合,这个表单需要支持以下专业特性:
- 多字段结构化输入(标题、描述、优先级、截止时间)
- 实时表单验证与用户反馈
- 新建/编辑双模式支持
- 跨平台兼容的交互组件
- 页面间状态同步机制
这个需求看似简单,实则涉及MVVM架构扩展、状态管理、表单验证、跨平台组件适配等多个关键技术点。下面我将结合具体实现,分享如何用Flutter+OpenHarmony构建这样的专业表单系统。
2. 架构设计与模型扩展
2.1 为什么选择MVVM+Riverpod架构
在前期基础版本中,我们采用了Riverpod进行状态管理,这种架构在功能扩展时展现出明显优势:
- 关注点分离:UI、业务逻辑、数据持久化各司其职
- 测试友好:各层可以独立测试
- 扩展性强:新增功能只需扩展对应层级,不影响既有结构
对于表单这种复杂交互场景,MVVM模式能很好地处理用户输入与业务逻辑的映射关系。
2.2 Todo模型扩展实现
原始模型仅包含基础字段,为支持专业表单需要扩展属性:
dart复制class Todo {
final String id;
final String title;
final String? description; // 新增:任务描述
final bool completed;
final DateTime createdAt;
final DateTime? dueDate; // 新增:截止时间
final int priority; // 新增:优先级(0-2)
final String? category; // 新增:分类标签
// 构造方法及copyWith需要同步更新
Todo copyWith({
String? title,
String? description,
bool? completed,
DateTime? dueDate,
int? priority,
String? category,
}) {
return Todo(
id: id,
title: title ?? this.title,
description: description ?? this.description,
completed: completed ?? this.completed,
createdAt: createdAt,
dueDate: dueDate ?? this.dueDate,
priority: priority ?? this.priority,
category: category ?? this.category,
);
}
}
设计决策:使用copyWith模式而非直接属性修改,这保证了:
- 不可变性(immutability),利于状态管理
- 修改操作显式化,便于调试
- 与Riverpod的状态更新机制天然契合
2.3 ViewModel的功能增强
ViewModel需要新增对完整Todo对象的操作能力:
dart复制class TodoNotifier extends StateNotifier<AsyncValue<List<Todo>>> {
final Ref ref;
TodoNotifier(this.ref): super(const AsyncValue.loading()) {
_init();
}
// 新增:完整Todo对象操作
Future<void> addFullTodo(Todo todo) async {
state = const AsyncValue.loading();
try {
await ref.read(todoRepositoryProvider).addTodo(todo);
await _loadTodos();
} catch (e) {
state = AsyncValue.error(e, StackTrace.current);
}
}
// 更新现有Todo
Future<void> updateTodo(Todo todo) async {
state = const AsyncValue.loading();
try {
await ref.read(todoRepositoryProvider).updateTodo(todo);
await _loadTodos();
} catch (e) {
state = AsyncValue.error(e, StackTrace.current);
}
}
}
3. 表单页面核心实现
3.1 页面结构与状态管理
表单页面需要处理多种用户输入,我们采用ConsumerStatefulWidget配合多个控制器:
dart复制class TodoFormScreen extends ConsumerStatefulWidget {
final Todo? initialData; // null表示新建模式
const TodoFormScreen({super.key, this.initialData});
@override
ConsumerState<TodoFormScreen> createState() => _TodoFormScreenState();
}
class _TodoFormScreenState extends ConsumerState<TodoFormScreen> {
late final TextEditingController _titleController;
late final TextEditingController _descController;
late final FocusNode _titleFocusNode;
late Todo _currentTodo;
final _formKey = GlobalKey<FormState>();
@override
void initState() {
super.initState();
_currentTodo = widget.initialData ?? Todo.empty();
_titleController = TextEditingController(text: _currentTodo.title);
_descController = TextEditingController(text: _currentTodo.description);
_titleFocusNode = FocusNode();
// 延迟获取焦点避免动画冲突
Future.delayed(Duration.zero, () => _titleFocusNode.requestFocus());
}
}
3.2 表单验证系统设计
专业表单需要完善的验证机制,我们为每个字段设计验证规则:
dart复制// 标题验证
String? _validateTitle(String? value) {
if (value == null || value.isEmpty) return '标题不能为空';
if (value.length < 2) return '标题至少2个字符';
if (value.length > 100) return '标题不能超过100字符';
return null;
}
// 描述验证
String? _validateDescription(String? value) {
if (value != null && value.length > 500) return '描述不能超过500字符';
return null;
}
// 日期验证
String? _validateDate(DateTime? value) {
if (value != null && value.isBefore(DateTime.now())) {
return '截止时间不能早于当前时间';
}
return null;
}
在UI中使用验证:
dart复制TextFormField(
controller: _titleController,
focusNode: _titleFocusNode,
decoration: InputDecoration(
labelText: '任务标题',
hintText: '输入任务名称',
errorText: _validateTitle(_titleController.text),
),
validator: _validateTitle,
onChanged: (value) {
setState(() {
_currentTodo = _currentTodo.copyWith(title: value);
});
},
)
3.3 日期时间选择器集成
跨平台的日期时间选择需要特殊处理:
dart复制Future<void> _selectDateTime(BuildContext context) async {
final initialDate = _currentTodo.dueDate ?? DateTime.now().add(const Duration(days: 1));
// 选择日期
final pickedDate = await showDatePicker(
context: context,
initialDate: initialDate,
firstDate: DateTime.now(),
lastDate: DateTime(2100),
);
if (pickedDate == null) return;
// 选择时间
final pickedTime = await showTimePicker(
context: context,
initialTime: TimeOfDay.fromDateTime(initialDate),
);
if (pickedTime != null) {
final combinedDateTime = DateTime(
pickedDate.year,
pickedDate.month,
pickedDate.day,
pickedTime.hour,
pickedTime.minute,
);
setState(() {
_currentTodo = _currentTodo.copyWith(dueDate: combinedDateTime);
});
}
}
3.4 优先级选择组件实现
使用Chip组件实现美观的优先级选择器:
dart复制Wrap(
spacing: 8,
children: PriorityLevel.values.map((level) {
return ChoiceChip(
label: Text(level.displayName),
selected: _currentTodo.priority == level.index,
onSelected: (selected) {
if (selected) {
setState(() {
_currentTodo = _currentTodo.copyWith(priority: level.index);
});
}
},
selectedColor: Theme.of(context).colorScheme.primary.withOpacity(0.2),
labelStyle: TextStyle(
color: _currentTodo.priority == level.index
? Theme.of(context).colorScheme.primary
: null,
),
);
}).toList(),
)
4. 导航与状态同步
4.1 列表页到表单页的导航
在列表页实现两种导航方式:
dart复制// 快速添加按钮
FloatingActionButton(
onPressed: () async {
final result = await Navigator.push<bool>(
context,
MaterialPageRoute(
builder: (_) => const TodoFormScreen(),
fullscreenDialog: true,
),
);
if (result == true) {
ref.read(todoListProvider.notifier).refresh();
}
},
child: const Icon(Icons.add),
)
// 编辑现有项目
IconButton(
icon: const Icon(Icons.edit_outlined),
onPressed: () async {
final result = await Navigator.push<bool>(
context,
MaterialPageRoute(
builder: (_) => TodoFormScreen(initialData: todo),
),
);
if (result == true) {
ref.read(todoListProvider.notifier).refresh();
}
},
)
4.2 表单提交与结果返回
表单页的提交逻辑需要处理多种情况:
dart复制Future<void> _submitForm() async {
if (!_formKey.currentState!.validate()) return;
final todoToSave = _currentTodo.copyWith(
title: _titleController.text.trim(),
description: _descController.text.trim(),
);
try {
if (widget.initialData == null) {
await ref.read(todoListProvider.notifier).addFullTodo(todoToSave);
} else {
await ref.read(todoListProvider.notifier).updateTodo(todoToSave);
}
if (mounted) {
Navigator.pop(context, true); // 返回true表示需要刷新
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('保存失败: ${e.toString()}')),
);
}
}
}
5. OpenHarmony适配实践
5.1 跨平台兼容性设计
为确保在OpenHarmony上的良好运行,我们采取以下策略:
- 避免平台特定API:全部使用Flutter原生组件
- 响应式布局:适配不同屏幕尺寸
- 输入方式兼容:同时支持触摸和物理键盘
- 性能优化:减少不必要的重建
5.2 日期选择器的特殊处理
虽然使用了Flutter原生选择器,但我们封装了备用方案:
dart复制Future<DateTime?> _showAdaptiveDatePicker(BuildContext context) async {
try {
return await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime(2100),
);
} catch (e) {
// 备用方案:使用自定义选择器
return await showDialog<DateTime>(
context: context,
builder: (_) => CustomDatePickerDialog(),
);
}
}
6. 工程化实践与测试
6.1 测试策略设计
针对表单页面,我们设计三层测试:
-
单元测试:验证模型和业务逻辑
dart复制test('Todo copyWith should update correct fields', () { final original = Todo(id: '1', title: 'Test'); final updated = original.copyWith(title: 'Updated', priority: 2); expect(updated.title, 'Updated'); expect(updated.priority, 2); expect(updated.id, '1'); }); -
Widget测试:验证UI交互
dart复制testWidgets('Form shows validation errors', (tester) async { await tester.pumpWidget( ProviderScope(child: MaterialApp(home: TodoFormScreen())) ); await tester.tap(find.text('保存')); expect(find.text('标题不能为空'), findsOneWidget); }); -
集成测试:完整流程验证
6.2 代码质量保障
- 静态分析:配置严格的analysis_options.yaml
- 代码格式化:统一dart format规则
- 提交检查:通过git hooks运行检查
- CI/CD:自动化测试和构建流程
7. 性能优化技巧
在实际开发中,我们总结了以下优化经验:
-
控制器管理:确保所有Controller和FocusNode正确释放
dart复制@override void dispose() { _titleController.dispose(); _descController.dispose(); _titleFocusNode.dispose(); super.dispose(); } -
选择性重建:使用Consumer局部刷新
dart复制Consumer(builder: (context, ref, child) { final isLoading = ref.watch(todoListProvider).isLoading; return isLoading ? const CircularProgressIndicator() : child!; }) -
延迟加载:大数据集分页处理
-
动画优化:使用AnimatedBuilder减少重建范围
8. 扩展性与维护性设计
为应对未来需求变化,我们预留了以下扩展点:
- 多语言支持:封装所有字符串资源
- 主题系统:通过ThemeData统一样式
- 插件架构:通过接口抽象平台特定功能
- 配置系统:支持运行时配置调整
表单页面的开发过程让我深刻体会到良好架构的价值。前期在MVVM分层和状态管理上的投入,使得新增复杂功能时能够保持代码清晰。特别是Riverpod提供的灵活组合能力,让ViewModel可以按需组合和复用,大大提升了开发效率。