[EP-005] 在 Flutter 中优雅的抽离小组件
总是会有新手开发者说:“Flutter 很难用,简直是嵌套地狱”。oh,well,那你来找优雅实践实验室啊。
虽然不是 Flutter 代码,但是很经典的表情包
问题提出
在 Flutter 开发中常见的需要抽离小组件来解决一些嵌套地狱的情况,但是在哪里抽离?应该怎么抽离?事实上这已经成为一种面试题而存在了,毕竟会抽离小组件的 Flutter 开发者,代码质量一般会高一些。如果你喜欢视频教学,我相信你会喜欢这个来自 Flutter 官方的视频:
实践探索
抽离小组件的方式其实有三种,虽然视频中只提及了两种——helper method 和小组件,还有一种是抽离为局部变量。这在快速修复中可以找到:
抽离为小组件的争议应该不大,视频里也说得很明白了,重点在于抽离为局部变量还是抽离为函数方法。
这里给出茴香豆的四种写法:
- 抽离为函数,放在和 build 函数外;
- 抽离为函数,放在 build 函数内部,return 语句之前;
- 抽离为常量,放在和 build 函数外;
- 抽离为常量,放在 build 函数内部,return 语句之前。
可能有小伙伴看到这里,就想骂人了,为什么我们要把那么简单的问题搞得那么复杂,其实存在即合理,四种方案我都看见过。1 应该是常见的写法;4 是大家最熟悉的 Container 的内部实现的写法,干净优雅。
3 是违规的写法,但群里确实有人提出来过,由于没有上下文和生命周期,定义出来的小组件很容易出现奇奇怪怪的错误;2 是接手过一个愚蠢的 React 开发者转 Flutter 开发写出来的,他期望所有的代码都长得和 React 一样,但是:
- 但 Flutter 没有函数组件的概念,函数化并不会优化刷新范围;
- 方案 2 相对于方案 4 来说,4 可以少编写额外的
(){}
,而且保证了函数是执行逻辑,变量存放的是小组件,即使是使用 flutter_hooks,也更可读更清晰。 - 方案 2 相对于方案 1 来说,虽然减少了对 context 的传递,但是增加对 context 的隐性依赖,具体表现为无法轻松的将函数直接提出到小组件之外了(quick fix 也失效)。
- 而且既然是使用了 flutter_hooks 的写法,方案 1 加上
useContext()
钩子将更方便传递上下文,虽然在 Flutter 中,这个钩子无法共享状态。
那唯一有争议的也就是 1 和 4,这里我们更推荐使用方案 4,原因如下:
- 这种写法是官方认可的,在 Container 中使用的;
- 你不需要传递 context 到函数中,因为你就在上下文内;
- 你可以执行任何条件/循环语句,而不担心可读性,因为在 build 函数内总是顺序阅读,而不需要在函数间跳转;
- build 函数外编写的函数,不应该带有组件,而应该只是一些纯粹的业务功能,例如:设置监听器、定义异步初始化、局部状态管理等;同理,现在你可以保证所有的 UI 都在 build 函数中可见,不需要四处翻找。
优雅实践
抽离位置
首先,关注分离点,哪些部分是重复的,或者哪个位置开始可以提取出来作为新的小组件,这个比较吃经验,我只能给出一些参考:
- 一般的 Row/Column 下的子组件可以抽离为子组件,但是其中的 Expaned/Flexible 组件不包括在内。Stack 布局的 Positioned 同理;
- 小组件可能有些修饰性的父组件,例如 Padding、Center 等,不应该和小组件一起抽离;
- 需要刷新状态的子组件,一定要避开其他无刷新需求的父组件单独提出来。(也可以使用消费者组件去约束刷新范围);
抽离方式
官方的视频中的抽离方式有两种,一种是作为小组件单独提出来,一种是作为组件内部的函数提出来(helper method),但这里不再比较两种抽离方式的作用和优劣,省流一下就是:
如果没有特殊需求,抽离为小组件是最好的。
抽离为小组件
抽离为小组件之后也有一些需要考虑的问题,这里直接给出最佳实践:
- 命名:抽离的小组件类名最好可以直接描述小组件的作用,避免阅读负担。
- 私有化:如果其他页面不需要复用,应放置于当前文件下,类名前加
_
定义为私有小组件; - 复用:如果其他页面需要复用,应考虑单独创建同名文件,注意使用蛇形命名法,然后一个公共组件一个文件;
- 抽离状态:被抽离的小组件,如果没有特殊需求,应尽可能不要通过参数传递状态,而是交由状态管理组件去共享。
抽离为局部常量
但是有时候还是会面对一些情况,我有一个很大的,无状态的页面,但是没有抽离出来刷新子组件的需求,只是单纯需要分解一下代码结构,不想创建太多无意义的小组件。
- 位置:抽离为局部常量的组件,应放置于 build 函数内,组件的原语依赖之下和 return 语句之间;
- 逻辑性:你可以使用 if 语句来组装需要的小组件,这可能会比传统写在 return 语句中更繁琐,但是在处理复杂的语句中会更为清晰(因为传统的三目运算可能会找不到孩子);
- 复用:虽然这种程度复用优化不大,比如共用的
SizedBox(width: 4)
。复杂的 item 都被抽离为小组件了; - 常量:尽可能使用
const
而不是final
修饰组件,可以获得更优的性能的同时,也避免和普通的状态常量混淆(虽然快速修复会教你做事)。
@override
Widget build(BuildContext context) {
final navigatorState = Navigator.of(context);
final scaffoldMessengerState = ScaffoldMessenger.of(context);
const dismissible = Dismissible(
key: Key('value'),
child: Card(
child: ListTile(
leading: Icon(Icons.add),
title: Text('data'),
),
),
);
return ...;
抽离技巧
这里我们使用 vscode 举例,as 类似的操作不再重复。
- 鼠标单击需要抽离的小组件;
- 使用快速修复
Ctrl + .
; - 选择
Extract Widget
抽离为小组件,或者选择Extract Local Variable
抽离为局部常量; - 重命名。
千万不要在评论区问我如何选中一个小组件的全部代码,然后复制粘贴,工具都给得有的,我们需要优雅的实践。
转载自:https://juejin.cn/post/7368521915151745033