起因
公司需要开发app项目,但是没有多余的flutter工程师,只能让我们的前端组去做flutter,基于java的开发经验快速的上手dart,但是一些开发思路还是偏前端,所以写了一套偏向于前端的flutter表单方案。
设计思路
给予之前写的element-plus的项目的表单经验,我将整个表单分成了两部分一个是用于承载组件的FormItem,另外一部分则是组件本身,给予设计我们需要支持的组件目前有输入框、多行文本、选择框、tag选择框、头像上传、开关等组件,这里我们只介绍一下输入框和选择框的设计思路,其他的组件设计是类似的。
承载组件的FormItem的设计
首先这里我们是需要支持用户传递他需要的字段的即label,然后对于布局方式我们也需要支持,在移动端有两种常见的布局方式对于表单项。
第二步item要支持对不同类型组件的渲染,这里我们可以用枚举来解决,比如我们支持inupt,select这两种类型。
第三步就是根据不同组件类型来渲染不同的组件。
datr
// ignore_for_file: curly_braces_in_flow_control_structures
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:ventora/components/form/input_text.dart';
import 'package:ventora/components/form/select.dart';
enum FormType { input, select }
class FormItem<T> extends StatelessWidget {
const FormItem(
{super.key,
required this.type,
this.lable,
this.value,
this.setValue,
this.backgroundColor,
this.lableColor,
this.buildItem,
this.lableWidth,
this.rowCrossAxisAlignment,
this.margin,
this.hintText,
this.selectList,
this.keyboardType,
this.valueList,
this.onClick,
this.padding,
this.isSameLine = true,
this.controller,
this.maxLength,
this.textHeight,});
final FormType type;
final String? lable;
final T? value;
final Function? setValue;
final Color? backgroundColor;
final Color? lableColor;
final Widget? Function()? buildItem;
final double? lableWidth;
final CrossAxisAlignment? rowCrossAxisAlignment;
final EdgeInsetsGeometry? margin;
final List<String>? selectList;
final String? hintText;
final TextInputType? keyboardType;
final Function? onClick;
final List<String>? itemList;
final bool isSameLine;
final EdgeInsets? padding;
final TextEditingController? controller;
final int? maxLength;
final double? textHeight;
final double? size;
Widget initExpanded(Widget child) => isSameLine ? Expanded(child: child) : child;
_formBuild() {
if (type == FormType.input)
return Expanded(
child: InputText(
setValue: setValue as Function(String)?,
controller: controller ?? TextEditingController(text: (value as Rx<String>).value),
hintText: hintText,
textHeight: textHeight,
enabled:enabled,
contentPadding:contentPadding,
keyboardType: keyboardType));
if (type == FormType.select) return Expanded(child: Select(value: value as String, setValue: setValue as Function, selectList: selectList!));
}
List<Widget> _listBuild() {
return [
if (lable != null)
Container(
width: lableWidth ?? 88,
alignment: Alignment.topLeft,
margin: margin ?? EdgeInsets.zero,
child: Text(lable!,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: lableColor ?? Colors.white,
)),
),
_formBuild()
];
}
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: padding ?? const EdgeInsets.only(top: 12, bottom: 12, left: 16, right: 16),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
color: backgroundColor ?? ColorsLandon.backgroundHome,
),
child: isSameLine
? Row(
crossAxisAlignment: rowCrossAxisAlignment ?? CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _listBuild(),
)
: Column(
crossAxisAlignment: rowCrossAxisAlignment ?? CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _listBuild(),
),
);
}
}
设计Input组件的编写
在vue里面我们是可以给input组件传递一个双向绑定的值,但是这件事在flutter中似乎不好实现,所以这里我们可以通过TextEditingController的方式绕一下实现双向绑定方案。
dart
import 'package:flutter/material.dart';
import 'package:ventora/styles/colors_lando.dart';
class InputText<T> extends StatelessWidget {
final Function(String)? setValue;
final String? hintText;
final TextInputType? keyboardType;
final TextEditingController controller;
final double? textHeight;
final EdgeInsetsGeometry? contentPadding;
final bool? enabled;
const InputText(
{super.key, required this.setValue, this.hintText, this.keyboardType, required this.controller, this.textHeight, this.contentPadding, this.enabled});
@override
Widget build(BuildContext context) {
return SizedBox(
height: textHeight ?? 15.0 * 1.5,
width: double.infinity,
child: TextField(
enabled: enabled ?? true,
style: TextStyle(color: enabled != false ? ColorsLandon.initColors : ColorsLandon.formHintColor, fontSize: 15, fontWeight: FontWeight.w600),
cursorColor: ColorsLandon.initColors,
cursorHeight: textHeight ?? 15.0 * 1.5,
decoration: InputDecoration(
border: InputBorder.none,
contentPadding: contentPadding ?? const EdgeInsets.only(bottom: 10),
hintText: hintText,
hintStyle: const TextStyle(color: ColorsLandon.hintInitColors, fontSize: 15),
),
controller: controller,
onChanged: setValue,
keyboardType: keyboardType ?? TextInputType.text),
);
}
}
设计select组件
首先给予移动端的交互逻辑我们可以参考一下小程序或者vant的ui逻辑可以设计成点击出现一个抽屉让用户选择。
既然设计定下来了我们可以写一下实现
dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:ventora/styles/colors_lando.dart';
import 'package:ventora/styles/text_style_lando.dart';
class Select extends StatelessWidget {
final String value;
final Function setValue;
final String? hintText;
final List<String> selectList;
final TextEditingController _controller = TextEditingController();
Select({super.key, required this.value, required this.setValue, this.hintText, required this.selectList});
@override
Widget build(BuildContext context) {
_controller.text = value;
return GestureDetector(
onTap: () => {
showModalBottomSheet(
context: context,
builder: (context) => Container(
color: ColorsLandon.backgroundColor,
width: double.infinity,
padding: const EdgeInsets.only(bottom: 38, top: 20, left: 20, right: 20),
child: Column(mainAxisSize: MainAxisSize.min, children: [
...selectList
.map((item) => GestureDetector(
onTap: () {
setValue(item);
Navigator.pop(context);
},
child: Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 16),
decoration: const BoxDecoration(color: ColorsLandon.hintBackground, borderRadius: BorderRadius.all(Radius.circular(12))),
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
item,
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600),
),
if (item == value) const Icon(Icons.done, color: ColorsLandon.initColors)
],
))))
.toList(),
const SizedBox(
height: 20,
)
]),
))
},
child: Container(
height: 18.0,
alignment: Alignment.centerRight,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
value,
style: TextStyleLando.formItemText,
),
const Icon(
Icons.expand_more,
color: Colors.white,
size: 16,
)
],
)));
}
}
结果
其实整个form难得地方在于设计思路不同的组件可以用flutter自带的组件来实现即可,主要是我们需要先设计formItem如何如何设计会对使用者更加友好,使用效果
dart
FormItem(
type: FormType.text,
label: 'Name',
controller: logic.iForm.nickname,
hintText: '4~30 characters',
FormItem(
type: FormType.select,
label: 'Gender',
setValue: (value) => {
logic.setValue(logic.iForm.gender, value),
},
value: logic.iForm.gender.value,
selectList: const ['👨 Male', '👩 Female', '⚧ Non-binary'],
)),
完工下机!