0%

自定义 View 推荐实践

尽可能避免使用自定义 View

Android View 系统是个技术债务极重的系统:API 30 的 View.java 有 30409 行!这一债务如此沉重,以至于 Google 最终决定另起一套视图系统(Jetpack Compose)而非在现有系统基础上继续修补。

当你继承 View 类时,你也继承了这一沉重的技术债务。

  • 对 View 进行单元测试非常困难,因为其严重依赖 Android 运行时环境。
  • 如果你的自定义 View 中有业务相关的初始化逻辑,在 Layout Inspector 中预览视图可能会失败,因为其不能脱离真实 Android 设备正确模拟视图创建过程。这很影响开发效率。
  • 对于重建 Activity/Fragment 的场景,系统会重新创建视图层级,这可能会绕过你自定义的初始化逻辑。

如果你只是想要一个编写业务逻辑的地方,请使用 Presenter 设计模式。如果你需要重写 View 方法以实现业务逻辑,请考虑使用监听器能否实现相同功能。

使用 Presenter 代替自定义 View 封装数据绑定逻辑

经常有需求需要封装一组视图,并对外提供数据绑定的方法。相对于使用自定义视图,更推荐使用 Presenter。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class MyTextView extends TextView {
// 要写四个对你业务毫无作用的构造方法
public MyTextView(@NonNull Context context) {
super(context);
}

public MyTextView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

public MyTextView(@NonNull Context context, @Nullable AttributeSet attrs,
@AttrRes int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}

public MyTextView(@NonNull Context context, @Nullable AttributeSet attrs,
@AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initViews();
}

private void initViews() {
// do whatever
}

public void setMyText(String text) {
super.setText("my " + text);
}
}

MyTextView mtv = findViewById(R.id.my_text);
mtv.setMyText("computer");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public final class MyTextPresenter extends TextView {
private final TextView myView;
// 推荐用工厂模式提供实例,以避免对一个视图创建多个 Presenter,造成不一致
public static MyTextPresenter bind(TextView tv) {
// 可以在这里添加去重逻辑
return new MyTextPresenter(tv);
}
// 只有一个构造器,好耶
private MyTextPresenter(TextView tv) {
this.myView = tv;
initViews();
}

private void initViews() {
// do whatever
}

public void setMyText(String text) {
myView.setText("my " + text);
}
}

MyTextPresenter presenter = MyTextPresenter.bind(findViewById(R.id.my_text));
presenter.setMyText("computer");

这样做有以下好处:

  • 封装了 View 的接口方法,避免外界随意调用篡改其内容;
  • 将视图布局本身与数据绑定接口解耦,便于以后修改布局;

使用注册回调代替重写 View 方法

有时自定义 View 的目的是为了重写特定的 View 以实现业务逻辑,但大部分这样的场景都可以通过注册 Listener 实现,以下是一些例子。

可重写的方法 相应的监听器或接口
onClick OnClickListener
onLongClick OnLongClickListener
onFocusChange OnFocusChangeListener
onKey OnKeyListener
onTouchEvent OnTouchListener
onGenericMotion OnGenericMotionListener
onSystemUiVisibilityChange OnSystemUiVisibilityChangeListener

View 的职责仅有展示数据与响应交互,将其他所有逻辑移出自定义 View

这样符合单一职责原则,也便于业务逻辑复用。

另外,业务逻辑的生命周期不一定与 View 一致,将两者分离可以减少潜在的 bug 风险。