【Flutter】基于 StatefulWidget 实现的自定义的多选组与单选组
自定义CheckBox与RadioButton以及其要点
前言
难道 Flutter 自带的 Widget 没有 CheckBox 吗?难道就非得要自己造一个?
还真得这样,得满足设计和产品的需求啊。
- UI 设计要求:
如果您的应用需要使用特定的图标、样式或动画效果,Flutter 自带的 Checkbox 可能无法满足这些需求。自定义组件可以灵活地进行 UI 样式调整,符合设计规范。
- 复选框组的管理:
CustomCheckBox 组件可以封装多个选项,并且对选中项进行管理。这使得在复杂的场景中(比如动态生成的复选框组)更易于处理。
- 功能扩展:
您可能需要额外的功能,比如选中项的回调、全选/全不选功能、状态恢复等。自定义组件可以更好地处理这种特定的逻辑。
并且不管是 CheckBox 多选组,和 Radio 的单选组,还是其他的基础控件,例如文本,图片等,我都会封装一层,一是为了适配自己的项目,二是为了统一管理。方便后期全局的实现多主题或者其他需求。而不需要每一个地方去修改 Text Image 等需求,毕竟 Fultter 并不好像 Android 一样可以插桩去自动修改一些配置。
那么话说回来,我们的项目中有各种的多选和单选的场景,并且风格统一,我们就可以基于产品实现自定义的布局。
一、实现自定义布局
真正来说布局可能是很简单,就是一个图片加上一个文本组成
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
MyAssetImage(path, width: 20.5, height: 20.5),
SizedBox(width: 10),
MyTextView(
text.tr,
textColor: ColorConstants.white,
fontSize: 14,
isFontRegular: true,
),
],
)
如果想要做出组的形式,我们需要使用 Warp 来包裹这些子组件,达到自动换行的效果,并且根据提供的选项动态生成数据:
Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: widget.options.map((option) {
return _buildRadioWithIconAndText(
path: option == _selectedOption ? Assets.cptAuthLoginRadioChecked : Assets.cptAuthLoginRadioUncheck,
text: option,
value: option == _selectedOption,
onChanged: (value) {
setState(() {
_selectedOption = option;
int selectedIndex = widget.options.indexOf(option);
widget.onOptionSelected(selectedIndex, option);
});
},
);
}).toList(),
)
可以看到我内部是做了复显的逻辑,那么问题来了,我们是否需要考虑使用 StatefulWidget 还是 StatelessWidget 呢?
二、动态的数据与复选中
-
StatelessWidget:用于构建不需要维护状态的组件。它的构建是基于其构造函数中的参数,且在生命周期内不支持任何状态的更新或持久化。每次需要更新 UI 时,都会重建整个 Widget。
-
StatefulWidget:用于构建需要维护状态的组件。它有一个与之关联的 State 对象,该对象可以在生命周期内保存状态,并通过 setState 方法更新 UI。当需要在 Widget 重新构建时,didUpdateWidget 方法会被调用
其实在现代开发中,一个页面是 StatefulWidget 还是 StatelessWidget 都能完成对应功能,都能做对应的刷新,不同的状态管理插件使用不同的封装方式,可以直接刷新页面。
但是一些内部的子布局实现我们还是需要判断使用 StatefulWidget 还是 StatelessWidget 的,主要还是在你是否需要对应的生命周期回调。
比如我们的此场景,当页面或父容器刷新的时候,我们的子布局如果想要根据状态刷新本身的状态或布局,那么我们需要用到 didUpdateWidget 重新整理对应的状态和布局,那么我们是需要使用 StatefulWidget 来完成的。
比如在上面我们的 build 方法中我们用到的状态是 options 数据:
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: widget.options.map((option) {
return _buildRadioWithIconAndText(
path: option == _selectedOption ? Assets.cptAuthLoginRadioChecked : Assets.cptAuthLoginRadioUncheck,
text: option,
value: option == _selectedOption,
onChanged: (value) {
setState(() {
_selectedOption = option;
int selectedIndex = widget.options.indexOf(option);
widget.onOptionSelected(selectedIndex, option);
});
},
);
}).toList(),
);
}
那么当我们页面中的网络数据回来之后我们刷新了选项,此时 build 重新加载了,我们的页面是可以正常的显示选项了,但是复显效果就没了,为什么?因为我们在刷新的时候没有处理 selectedOption 的数据。
所以我们需要在 didUpdateWidget 中处理对应的逻辑:
class _CustomRadioCheckState extends State<CustomRadioCheck> {
String? _selectedOption;
@override
void initState() {
super.initState();
_initializeSelectedOption();
}
void _initializeSelectedOption() {
if (widget.selectedPosition != null && widget.selectedPosition! >= 0 && widget.selectedPosition! < widget.options.length) {
_selectedOption = widget.options[widget.selectedPosition!];
} else {
_selectedOption = widget.options.isNotEmpty ? widget.options[0] : null;
}
}
@override
void didUpdateWidget(CustomRadioCheck oldWidget) {
super.didUpdateWidget(oldWidget);
// 如果 selectedPosition 发生变化,重新初始化选中项
if (oldWidget.selectedPosition != widget.selectedPosition) {
_initializeSelectedOption();
}
}
...
}
此时我们就能完成对应的逻辑,进入页面,展示空的选项,加载请求,拿到数据展示选项并选中已选中的选项。
三、完整代码与示例
其实单选与多选的区别,在布局和数据都是相同的逻辑,只是在选择的时候我们做了不同的处理,他们的逻辑大致是相同的,这里给出完整的代码:
CustomRadioCheck:
class CustomRadioCheck extends StatefulWidget {
final List<String> options;
int? selectedPosition;
final Function(int index, String text) onOptionSelected;
CustomRadioCheck({
required this.options,
required this.onOptionSelected,
this.selectedPosition = 0,
});
@override
_CustomRadioCheckState createState() => _CustomRadioCheckState();
}
class _CustomRadioCheckState extends State<CustomRadioCheck> {
String? _selectedOption;
@override
void initState() {
super.initState();
_initializeSelectedOption();
}
void _initializeSelectedOption() {
if (widget.selectedPosition != null && widget.selectedPosition! >= 0 && widget.selectedPosition! < widget.options.length) {
_selectedOption = widget.options[widget.selectedPosition!];
} else {
_selectedOption = widget.options.isNotEmpty ? widget.options[0] : null;
}
}
@override
void didUpdateWidget(CustomRadioCheck oldWidget) {
super.didUpdateWidget(oldWidget);
// 如果 selectedPosition 发生变化,重新初始化选中项
if (oldWidget.selectedPosition != widget.selectedPosition) {
_initializeSelectedOption();
}
}
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: widget.options.map((option) {
return _buildRadioWithIconAndText(
path: option == _selectedOption ? Assets.cptAuthLoginRadioChecked : Assets.cptAuthLoginRadioUncheck,
text: option,
value: option == _selectedOption,
onChanged: (value) {
setState(() {
_selectedOption = option;
int selectedIndex = widget.options.indexOf(option);
widget.onOptionSelected(selectedIndex, option);
});
},
);
}).toList(),
);
}
Widget _buildRadioWithIconAndText({
required String path,
required String text,
required bool value,
required Function(bool) onChanged,
}) {
return Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
MyAssetImage(path, width: 22, height: 22),
SizedBox(width: 10),
MyTextView(
text.tr,
textColor: ColorConstants.white,
fontSize: 14,
isFontRegular: true,
),
],
).marginOnly(right: 20, bottom: 5).onTap(() {
onChanged(true);
});
}
}
CustomCheckBox:
class CustomCheckBox extends StatefulWidget {
final List<String> options;
final Function(List<int> selectedIndexes) onOptionsSelected; // 选中项的索引回调
final List<String> selectedOptions; // 已选中的选项列表
CustomCheckBox({
required this.options,
required this.onOptionsSelected,
required this.selectedOptions,
});
@override
_CustomCheckBoxState createState() => _CustomCheckBoxState();
}
class _CustomCheckBoxState extends State<CustomCheckBox> {
late List<String> _selectedOptions;
late List<int> _selectedIndexes;
@override
void initState() {
super.initState();
_initializeSelectedOptions();
}
void _initializeSelectedOptions() {
_selectedOptions = List.from(widget.selectedOptions);
_selectedIndexes =
widget.options.asMap().entries.where((entry) => _selectedOptions.contains(entry.value)).map((entry) => entry.key).toList(); // 根据选项匹配初始化索引
}
@override
void didUpdateWidget(CustomCheckBox oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.selectedOptions != widget.selectedOptions) {
_initializeSelectedOptions();
}
}
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 15.0,
runSpacing: 15.0,
children: widget.options.asMap().entries.map((entry) {
int index = entry.key;
String option = entry.value;
return _buildCheckBoxWithIconAndText(
path: _selectedIndexes.contains(index) ? Assets.baseServiceCheckBoxChecked : Assets.baseServiceCheckBoxUncheck,
text: option,
value: _selectedIndexes.contains(index),
onChanged: (isChecked) {
setState(() {
if (isChecked) {
if (!_selectedOptions.contains(option)) {
_selectedOptions.add(option);
_selectedIndexes.add(index);
}
} else {
_selectedOptions.remove(option);
_selectedIndexes.remove(index);
}
widget.onOptionsSelected(_selectedIndexes);
});
},
);
}).toList(),
);
}
Widget _buildCheckBoxWithIconAndText({
required String path,
required String text,
required bool value,
required Function(bool) onChanged,
}) {
return Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
MyAssetImage(path, width: 20.5, height: 20.5),
SizedBox(width: 10),
MyTextView(
text.tr,
textColor: ColorConstants.white,
fontSize: 14,
isFontRegular: true,
),
],
).onTap(() {
onChanged(!value); // 点击时切换选中状态
});
}
}
内部有一些封装的控件和扩展,你完全可以自己实现,或者查看我的项目【传送门】来参考实现。因为这不是本文的重点,我就不介绍了。
如何使用呢,也是很简单:
在我们进入页面的时候 state.indexEntity 是 null, 所以是空的选项,当我们请求了接口之后,给 state.indexEntity 赋值,并刷新页面之后就会展示对应的选项和复显。
不管你是用什么状态管理工具,Getx?Bloc 都是一样的操作逻辑。
选项和复显逻辑如下:
在我们这种表单很多的页面,还是很有必要这么做的,那么你怎么看呢?
总结
本文我们简单的回顾了 StatefulWidget 和 StatelessElement 的区别,以及 didUpdateWidget 的触发时机和需要的状态处理,继而实现了选项与复显的结合。
在开发中对应重复出现的一些控件,常用的组合最好是自行进行封装,一是为了UI和产品的需求,二也是为了后期的扩展。
本文的代码比较简单,相对比较基础,并且全部的代码也已经在文中展出,有需要可以参考我的 Demo 实现【传送门】
那么今天的分享就到这里啦,当然如果你有其他的更多的更好的实现方式,也希望大家能评论区交流一起学习进步。如果我的文章有错别字,不通顺的,或者代码、注释、有错漏的地方,同学们都可以指出修正。
如果感觉本文对你有一点的启发和帮助,还望你能点赞
支持一下,你的支持对我真的很重要。
Ok,这一期就此完结。
转载自:https://juejin.cn/post/7397285224349351974