多页面表单架构
我正在尝试构建一个系统来创建具有多个页面的表单。我的方法是将它分成三个不同的部分。
- FormPages:不同的表单页面(每个页面都有自己的逻辑来验证字段)。
- ProjectFormContainer:包含导航器内页面的容器页面。
- MultiPageFormController:管理表单页面之间导航的控制器。
我已经成功地取得一些进展,增加了ProjectFormContainer一个ChangeNotifierProvider的MultiPageFormController,但我不知道如何将单个连接formPages逻辑与其余元素,什么是做一个体面的架构此模型的最佳途径。
希望大家能给我一些建议。提前致谢!
回答
这是基于一个解决方案流构建从菲利克斯安格洛夫,笔者阵营国家管理。
我使用了以下软件包:
- Flow Builder来管理我们表单的流程
- Flutter Hooks摆脱 Stateful Widgets 并拥有更精简的代码库
- 冻结以管理我们用户信息的不变性
- [可选] Flutter 颜色选择器
如果您查看Flow Builder文档,您就会看到,它管理着一组表单小部件,每个小部件对应 Flow 的每个步骤。完成流程后,它会弹出小部件并返回带有用户信息的主页。
1. 创建流程
FlowBuilder<UserInfo>(
state: const UserInfo(),
onGeneratePages: (profile, pages) {
return [
MaterialPage(child: NameForm()),
if (profile.name != null) MaterialPage(child: AgeForm()),
if (profile.age != null) MaterialPage(child: ColorForm()),
];
},
),
2. 流量管理
在每一步结束时,我们要么继续流程:
context
.flow<UserInfo>()
.update((info) => info.copyWith(name: _name.value));
或者,我们完成它:
context
.flow<UserInfo>()
.complete((info) => info.copyWith(favoriteColor: _color.value));
3. 主页
并且,在我们的主页中,我们导航到OnboardingFlow
并等待它完成:
_userInfo.value = await Navigator.of(context).push(OnboardingFlow.route());
完整的源代码,便于复制粘贴
import 'package:flow_builder/flow_builder.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
part '66228603.flow_builder.freezed.dart';
void main() {
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flow Demo',
home: HomePage(),
),
);
}
// MAIN PAGE
class HomePage extends HookWidget {
@override
Widget build(BuildContext context) {
final _userInfo = useState<UserInfo>();
return Scaffold(
backgroundColor: _userInfo.value == null
? Colors.white
: _userInfo.value.favoriteColor,
appBar: AppBar(title: Text('Flow')),
body: Container(
padding: EdgeInsets.all(8.0),
alignment: Alignment.center,
child: _userInfo.value == null
? ElevatedButton(
onPressed: () async {
_userInfo.value =
await Navigator.of(context).push(OnboardingFlow.route());
},
child: Text('GET STARTED'),
)
: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Welcome, ${_userInfo.value.name}!',
style: TextStyle(fontSize: 48.0),
),
const SizedBox(height: 48.0),
Text(
'So, you are ${_userInfo.value.age} years old and this is your favorite color? Great!',
style: TextStyle(fontSize: 32.0),
),
],
),
),
);
}
}
// FLOW
class OnboardingFlow extends StatelessWidget {
static Route<UserInfo> route() {
return MaterialPageRoute(builder: (_) => OnboardingFlow());
}
@override
Widget build(BuildContext context) {
print('INFO: ${const UserInfo()}');
return Scaffold(
body: FlowBuilder<UserInfo>(
state: const UserInfo(),
onGeneratePages: (profile, pages) {
return [
MaterialPage(child: NameForm()),
if (profile.name != null) MaterialPage(child: AgeForm()),
if (profile.age != null) MaterialPage(child: ColorForm()),
];
},
),
);
}
}
// FORMS
class NameForm extends HookWidget {
@override
Widget build(BuildContext context) {
final _name = useState<String>();
return Scaffold(
appBar: AppBar(title: const Text('Name')),
body: Container(
padding: EdgeInsets.all(8.0),
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextField(
autofocus: true,
onChanged: (value) => _name.value = value,
decoration: InputDecoration(
labelText: 'Name',
hintText: 'Enter your name',
),
),
const SizedBox(height: 24.0),
RaisedButton(
child: const Text('Continue'),
onPressed: () {
if (_name.value.isNotEmpty) {
context
.flow<UserInfo>()
.update((info) => info.copyWith(name: _name.value));
}
},
)
],
),
),
);
}
}
class AgeForm extends HookWidget {
@override
Widget build(BuildContext context) {
final _age = useState<int>();
return Scaffold(
appBar: AppBar(title: const Text('Age')),
body: Container(
padding: EdgeInsets.all(8.0),
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
DropdownButtonFormField<int>(
items: List.generate(
200,
(index) => DropdownMenuItem(
value: index,
child: Text(index.toString()),
),
),
onChanged: (value) => _age.value = value,
decoration: InputDecoration(
labelText: 'Age',
hintText: 'How old are you?',
),
),
const SizedBox(height: 24.0),
RaisedButton(
child: const Text('Continue'),
onPressed: () {
if (_age.value != null) {
context
.flow<UserInfo>()
.update((info) => info.copyWith(age: _age.value));
}
},
)
],
),
),
);
}
}
class ColorForm extends HookWidget {
@override
Widget build(BuildContext context) {
final _color = useState<Color>(Colors.amber);
return Scaffold(
appBar: AppBar(title: const Text('Favorite Color')),
body: Container(
padding: EdgeInsets.all(8.0),
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ColorPicker(
pickerColor: _color.value,
onColorChanged: (value) => _color.value = value,
showLabel: true,
pickerAreaHeightPercent: 0.8,
),
const SizedBox(height: 24.0),
RaisedButton(
child: const Text('Continue'),
onPressed: () {
if (_color.value != null) {
context.flow<UserInfo>().complete(
(info) => info.copyWith(favoriteColor: _color.value));
}
},
)
],
),
),
);
}
}
// DOMAIN
@freezed
abstract class UserInfo with _$UserInfo {
const factory UserInfo({String name, int age, Color favoriteColor}) =
_UserInfo;
}