Flutter 重构:大文件拆分
一、来源
工作中涉及到 IM 页面开发,随着功能开发页面逻辑越来越多,就想把耦合度不高的需求模块分拆出去,经过研究发现一种 Mixin 文件拆分办法,同时支持接口拆分。 下面以 IM 常用语为例,效果图如下:
点击常用语弹窗如下:
二、使用示例
1、复用代码:
class _IMChatPageState extends State<IMChatPage> with
BottomSheetPhrasesMixin {
//...
2、使用:
//展示提示语弹窗
choosePhrases(
cb: (val) {
debugPrint(val.phrases ?? "-");
},
onCancel: (){
Navigator.of(context).pop();
},
onAdd: () {
debugPrint("onAdd");
Navigator.of(context).pop();
}
);
三、源码
/// 常用语封装 mixin
mixin BottomSheetPhrasesMixin<T extends StatefulWidget> on State<T> {
final _scrollController = ScrollController();
List<IMPhrasesDetailModel> _phrases = [];
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
void initState() {
requestIMPhrasesList();
super.initState();
}
/// 常用语弹窗源方法
showPhrasesSheet<E>({
Text? title,
required List<E> items,
required String Function(E) titleCb,
TextStyle? textStyle,
required ValueChanged<E> cb,
required VoidCallback onCancel,
required VoidCallback onAdd,
ScrollController? controller,
}) {
controller ??= _scrollController;
final bottom = MediaQuery.of(context).padding.bottom;
final child = Container(
height: 400,
padding: EdgeInsets.only(bottom: bottom.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(12.w),
topRight: Radius.circular(12.w),
)),
child: Column(
children: [
Container(
// color: Colors.green,
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
buildTextButton(
text: Text("取消",),
onPressed: onCancel,
),
if(title != null) Expanded(child: title,),
buildTextButton(
text: Text("自定义",),
onPressed: onAdd,
),
],
),
),
Divider(height: 1,),
Expanded(
child: Scrollbar(
controller: controller,
child: ListView.separated(
controller: controller,
itemCount: items.length,
itemBuilder: (context, int i) {
final e = items[i];
return InkWell(
onTap: () {
cb.call(e);
Navigator.of(context).pop();
},
child: Container(
// margin: EdgeInsets.symmetric(horizontal: 16.w),
padding: EdgeInsets.symmetric(
horizontal: 16.w,
// vertical: 8.h,
),
// decoration: BoxDecoration(
// color: Colors.white,
// borderRadius: BorderRadius.all(Radius.circular(18.w)),
// border: Border.all(color: Color(0xFFe3e3e3), width: 1.w),
// // border: Border.all(color: Colors.red, width: 1),
// ),
constraints: BoxConstraints(
minHeight: 38.w,
),
alignment: Alignment.centerLeft,
child: NExpandText(
text: titleCb.call(e),
textStyle: textStyle,
),
),
);
},
separatorBuilder: (context, int index) {
return Divider(
height: 16.w,
indent: 16.w,
endIndent: 16.w,
);
},
),
),
),
],
),
);
child.toShowModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
);
}
Widget buildTextButton({
required Text text,
required VoidCallback? onPressed
}) {
return TextButton(
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
minimumSize: Size(50, 18),
textStyle: TextStyle(
color: context.primaryColor,
fontSize: 16.sp,
fontWeight: FontWeight.w500,
)
),
onPressed: () {
Navigator.of(context).pop();
onPressed?.call();
},
child: text,
);
}
/// 选择常用语
choosePhrases({
required VoidCallback onCancel,
required VoidCallback onAdd,
required ValueChanged<IMPhrasesDetailModel>? cb,
}) {
showPhrasesSheet<IMPhrasesDetailModel>(
items: _phrases,
titleCb: (e) => e.phrases ?? "",
textStyle: const TextStyle(overflow: TextOverflow.ellipsis),
cb: cb ?? (val) {
debugPrint(val.phrases ?? "-");
},
onCancel: onCancel,
onAdd: onAdd,
);
}
/// 获取问候语列表
requestIMPhrasesList() {
_phrases = List.generate(20, (i) {
return IMPhrasesDetailModel(
id: "$i",
phrases: "常用语_$i"*(i + 1),
);
});
}
}
class IMPhrasesDetailModel {
IMPhrasesDetailModel({
this.id,
this.phrases,
this.orderNum,
this.createBy,
});
String? id;
String? phrases;
int? orderNum;
String? createBy;
IMPhrasesDetailModel.fromJson(Map<String, dynamic> json) {
id = json['id'];
phrases = json['phrases'];
orderNum = json['orderNum'];
createBy = json['createBy'];
}
Map<String, dynamic> toJson() {
final data = Map<String, dynamic>();
data['id'] = id;
data['phrases'] = phrases;
data['orderNum'] = orderNum;
data['createBy'] = createBy;
return data;
}
}
总结
1、核心是通过 Mixin 约束实现:
mixin BottomSheetPhrasesMixin<T extends StatefulWidget> on State<T>
2、此种方案好处可以在mixin文件中关联 dispose 和 initState 方法,可以实现接口请求从主模块分离。如果不走接口分离,主页面动辄一堆请求接口,想想就可怕。只会把复杂度无限堆高,后期维护就成了老大难问题。
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
void initState() {
requestIMPhrasesList();
super.initState();
}
3、Mixin 文件封装弹窗也是极好的方式,flutter 中弹窗方法多且杂,我们可以通过mixin 文件将通用弹窗分类。大体分为 Alert、Dialog、Sheet等,将方法命名也可以进行约束,在此基础上封装形成标签多选、标签单选、DatePicker,Picker等,使用时只需要调用一个方法,大部分属性设置为默认参数即可。这样就可建立一个弹窗封装体系,一次性搞定现在的、未来的所有弹窗基础,不再需要新的弹窗每次都从零到一,而是从1-10000。
转载自:https://juejin.cn/post/7245583330242117689