你好, View Binding! 再次再见, findViewById!

// Kotter Knife

val fab: FloatingActionButton by bindView(R.id.fab)

val toolbar: Toolbar by bindView(R.id.toolbar)

setSupportActionBar(toolbar)

fab.setOnClickListener { view ->

}

// Kotlin Android Extensions

import kotlinx.android.synthetic.main.activity_main.*

setSupportActionBar(toolbar)

fab.setOnClickListener { view ->

}

// Data Binding & View Binding

val binding = ActivityMainBinding.inflate(layoutInflater)

setSupportActionBar(binding.toolbar)

binding.fab.setOnClickListener { view ->

}

优雅程度


可以确定的是 findViewById 和 Kotter Knife 是最不优雅的. 每初始化一个 view 都需要调用一次 findViewById 或 bindView 方法, 导致 activity 或 fragment 中充斥着许多模版代码. Kotlin Android Extensions(view cache map), Data Binding 与 View Binding (binding class)则通过生成一个中间变量的方式减少了模版代码的产生, 想象一下如果你有 20 个 view 需要初始化.

类型安全


// API 26 之前

public final View findViewById(int id) {

if (id < 0) {

return null;

}

return findViewTraversal(id);

}

protected View findViewTraversal(int id) {

if (id == mID) {

return this;

}

return null;

}

// API 26 及以后

@Nullable

public final T findViewById(@IdRes int id) {

if (id == NO_ID) {

return null;

}

return findViewTraversal(id);

}

protected T findViewTraversal(@IdRes int id) {

if (id == mID) {

return (T) this;

}

return null;

}

在 API 26 之前, 初始化 view需要进行强转, 比如 val fab = findViewById(R.id.fab) as FloatingActionButton. 众所周知(并且我相信你也遇到过), 强转是有可能产生 ClassCastException 的. 尽管 Google 在 API 26 更新 findViewById 方法为泛型实现, 但是仍然存在强转的问题, 想象一下这段代码: val fab = findViewById(R.id.fab), 在编译期不会有任何的问题, 但是很明显运行时会出错. Butter Knife 和 Kotter Knife 同样存在这样的问题。

空安全


首先说说Kotlin Android Extensions存在的问题.

import kotlinx.android.synthetic.main.fragment_main.*

class MainFragment : Fragment() {

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {

val rootView = inflater.inflate(R.layout.fragment_main, container, false)

text.setOnClickListener {

}

return rootView

}

}

<androidx.constraintlayout.widget.ConstraintLayout …>

<androidx.appcompat.widget.AppCompatTextView

android:id=“@+id/text”

… />

</androidx.constraintlayout.widget.ConstraintLayout>

看起来没有问题对吧, 实际上运行之后:

Caused by: android.view.InflateException: Binary XML file line #24 in io.tonnyl.demo:layout/activity_main: Binary XML file line #8 in io.tonnyl.demo:layout/content_main: Error inflating class fragment

Caused by: android.view.InflateException: Binary XML file line #8 in io.tonnyl.demo:layout/content_main: Error inflating class fragment

Caused by: java.lang.NullPointerException: Attempt to invoke virtual method ‘void androidx.appcompat.widget.AppCompatTextView.setOnClickListener(android.view.View$OnClickListener)’ on a null object reference

让我们看看生成的代码:

public final class MainFragment extends Fragment {

private HashMap _$_findViewCache;

@Nullable

public View onCreateView(@NotNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {

Intrinsics.checkParameterIsNotNull(inflater, “inflater”);

View rootView = inflater.inflate(1300003, container, false);

((AppCompatTextView)this._$_findCachedViewById(id.text)).setOnClickListener((OnClickListener)null.INSTANCE);

return rootView;

}

public View _$_findCachedViewById(int var1) {

if (this._$_findViewCache == null) {

this._$_findViewCache = new HashMap();

}

View var2 = (View)this._$_findViewCache.get(var1);

if (var2 == null) {

View var10000 = this.getView();

if (var10000 == null) {

return null;

}

var2 = var10000.findViewById(var1);

this._$_findViewCache.put(var1, var2);

}

return var2;

}

}

问题就出在 _$_findCachedViewById 方法 this.getView() 这一行, 调用时 onCreateView() 方法还没有返回值, 所以 this.getView() 返回 null, 在 onCreateView 中调用 text.setOnClickListener {} 不会有任何的错误提示, 因为 text 在这里会被认为是非空的.

当然, 上面的问题还是可以被解决的.

import kotlinx.android.synthetic.main.fragment_main.view.*

class MainFragment : Fragment() {

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {

val rootView = inflater.inflate(R.layout.fragment_main, container, false)

rootView.text.setOnClickListener {}

return rootView

}

}

注意导入的类的变化. 再看看生成的代码的变化:

public final class MainFragment extends Fragment {

@Nullable

public View onCreateView(@NotNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {

Intrinsics.checkParameterIsNotNull(inflater, “inflater”);

View rootView = inflater.inflate(1300003, container, false);

Intrinsics.checkExpressionValueIsNotNull(rootView, “rootView”);

((AppCompatTextView)rootView.findViewById(id.text)).setOnClickListener((OnClickListener)null.INSTANCE);

return rootView;

}

}

这里就不再通过 _$_findViewCache 而是直接通过 findViewById 实现了.

Kotlin Android Extensions 还存在一些其他的问题, Google 内部也不再使用, 具体原因可以参考 Why kotlinx synthetic is no longer a recommended practice.

再说说 findViewById 和 Butter Knife(Kotter Knife) 的问题. 考虑下面的代码:

val fab = findViewById(View.NO_ID)

val fab2: FloatingActionButton by bindView(0)

我们都希望在编译期就可以发现问题, 而不是运行时. 但不幸的是上面的代码正好在编译期不会有任何问题而运行时会出错.

view binding 会直接创建对 view 的引用, 所以不用担心因为无效的 view ID 而产生的空指针错误. 并且, 如果一个 view 只出现在部分配置的布局中, 那么, binding class 中包含改引用的字段会被标记为 @Nullable.

为什么不是 Data Binding ?

只有布局文件的根标签是时, Data Binding 才会生成对应的 binding class, View Binding 没有这样的要求;

Data Binding 会影响构建的速度. Data Binding 底层其实是通过 annotation processor 实现的, 对构建速度是有负面影响的. 而 View Binding 并不是通过 annotation processor 实现的, 因此解决了 Data Binding 的性能问题.

什么是 View Binding ?

View Binding 是一项使你能更轻松地编写与视图交互的代码的功能. 在模块中启用 View Binding 后, 它会为该模块中存在的每一个 XML 文件生成一个对应的绑定类(binding class). 绑定类的实例包含了对应布局中所有具有 ID 的 view 的直接引用.

大多数情况下, View Binding 可以替换 findViewById.

来自 Android Developers.

View Binding 第一次出现是在 2019 年的 Google I/O 大会 What’s New in Android (Google I/O’19) 演讲.

如何使用 View Binding ?

使用要求

你至少需要使用 Android Studio 3.6 Canary 11 及以上版本 才可以开启 View Binding.

设置指南

View Binding 可以逐模块(module)开启. 比如我们的项目由 2 个模块(A 和 B)组成, 我们可以选择只在模块 A 启用 View Binding 而不会对模块 B 产生影响. 在模块中启用 View Binding, 首先需要在该模块的 build.gradle 文件中添加以下代码:

写在最后

很多人在刚接触这个行业的时候或者是在遇到瓶颈期的时候,总会遇到一些问题,比如学了一段时间感觉没有方向感,不知道该从哪里入手去学习,对此我整理了一些资料

如果你熟练掌握以下列出的知识点,相信将会大大增加你通过前两轮技术面试的几率!这些内容都供大家参考,互相学习。

①「Android面试真题解析大全」PDF完整高清版+②「Android面试知识体系」学习思维导图压缩包,最后觉得有帮助、有需要的朋友可以点个赞


《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

很多人在刚接触这个行业的时候或者是在遇到瓶颈期的时候,总会遇到一些问题,比如学了一段时间感觉没有方向感,不知道该从哪里入手去学习,对此我整理了一些资料

如果你熟练掌握以下列出的知识点,相信将会大大增加你通过前两轮技术面试的几率!这些内容都供大家参考,互相学习。

①「Android面试真题解析大全」PDF完整高清版+②「Android面试知识体系」学习思维导图压缩包,最后觉得有帮助、有需要的朋友可以点个赞

[外链图片转存中…(img-pszm5mYg-1715184386070)]

[外链图片转存中…(img-qZNqGpW6-1715184386071)]

[外链图片转存中…(img-pi9pjq5P-1715184386071)]
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值