kotlin-android-extensions和ViewBinding
背景在kotlin-android-extensions和ViewBinding出现之前,我们进行View控件的实例化通常都是使用findViewById或者ButterKnife之类的第三方库,总的来说就是需要写很多重复且无意义的代码。接下来我们就来介绍kotlin-android-extensions和ViewBinding的使用。使用kotlin-android-extensions插件1.
背景
在kotlin-android-extensions
和ViewBinding
出现之前,我们进行View控件的实例化通常都是使用
findViewById
或者ButterKnife
之类的第三方库,总的来说就是需要写很多重复且无意义的代码。
接下来我们就来介绍kotlin-android-extensions
和ViewBinding
的使用。
使用kotlin-android-extensions插件
1.首先在模块的 build.gradle文件中启用 Gradle 安卓扩展插件即可
apply plugin: 'kotlin-android-extensions'
2.导入合成属性
import kotlinx.android.synthetic.main.<布局>.*
3.MainActivity.kt文件
package com.example.kotlinandroidextensions
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
textView.setText("111")
}
}
activity_main.xml文件
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
但是kotlin-android-extensions插件已被废弃,Google现在推荐使用ViewBinding来进行替代,那为什么会被废弃呢,接下来我们来看下。
我们可以通过点击Android Studio顶部导航栏的Tools -> Kotlin -> Show Kotlin Bytecode来查看之前MainActivity这段代码对应的Kotlin字节码,然后在弹出窗口中点击Decompile按钮将字节码反编译成Java代码。
public final class MainActivity extends AppCompatActivity {
private HashMap _$_findViewCache;
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(1300023);
TextView var10000 = (TextView)this._$_findCachedViewById(id.textView);
var10000.setText((CharSequence)"111");
}
public View _$_findCachedViewById(int var1) {
if (this._$_findViewCache == null) {
this._$_findViewCache = new HashMap();
}
View var2 = (View)this._$_findViewCache.get(var1);
if (var2 == null) {
var2 = this.findViewById(var1);
this._$_findViewCache.put(var1, var2);
}
return var2;
}
}
可以看到,实际上kotlin-android-extensions插件会帮我们生成一个_$_findCachedViewById()函数(使用这种奇怪的命名方式是为了防止和开发者定义的函数名冲突)。在这个函数中首先会尝试从一个HashMap中获取传入的资源id参数所对应的控件实例缓存,如果还没有缓存的话,就调用findViewById()函数来查找控件实例,并写入HashMap缓存当中。这样当下次再获取相同控件实例的话,就可以直接从HashMap缓存中获取了。
这就是kotlin-android-extensions插件的实现原理,其实还是非常简单的。
然而这种实现原理同时也暴露出来了一些问题。
比如说每一个Activity都需要使用一个额外的HashMap数据结构来存储所有控件的实例,无形中增加了一些内存的开支。
还有,虽然HashMap是一种O(1)时间复杂度的数据结构,但这毕竟只是理论上的时间复杂度,实际调用肯定是没有直接访问控件实例快的,因此kotlin-android-extensions插件也在无形当中降低了程序的运行效率。
使用kotlin-android-extensions插件存在隐藏的“坑”,什么坑呢。如果我们在写RecyclerView的Adapter的时候,借助kotlin-android-extensions插件可能有人会这么写
class FruitAdapter(val fruitList: List<Fruit>) : RecyclerView.Adapter<FruitAdapter.ViewHolder>() {
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.fruit_item, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val fruit = fruitList[position]
holder.itemView.fruitImage.setImageResource(fruit.imageId)
holder.itemView.fruitName.text = fruit.name
}
override fun getItemCount() = fruitList.size
}
可以看到,这里ViewHolder中没有进行任何控件声明,相当于只是定义了一个空的ViewHolder。然后在onBindViewHolder()函数当中,直接调用holder.itemView再接上控件id的名称就可以使用它了。
这种写法确实简化了不少代码,但是这种写法对吗?
如果你的评判标准只是这段代码能不能正常工作,那么答案是肯定的,这样写确实可以正常工作。但是这种写法我可以说是完全不正确的,为什么呢?我们只需要使用刚才的手法把这段代码反编译一下,看看它对应的Java代码是什么样的就知道了。
同样为了方便阅读,我还是对代码进行了简化,只保留了关键部分,如下所示:
public final class FruitAdapter extends Adapter {
...
public final class ViewHolder extends androidx.recyclerview.widget.RecyclerView.ViewHolder {
public ViewHolder(@NotNull View view) {
super(view);
}
}
public void onBindViewHolder(@NotNull FruitAdapter.ViewHolder holder, int position) {
Fruit fruit = (Fruit)this.fruitList.get(position);
View var10000 = holder.itemView;
((ImageView)var10000.findViewById(id.fruitImage)).setImageResource(fruit.getImageId());
var10000 = holder.itemView;
TextView var4 = (TextView)var10000.findViewById(id.fruitName);
var4.setText((CharSequence)fruit.getName());
}
}
不知道你有没有发现问题,现在onBindViewHolder()函数当中,每次都是调用了findViewById()来获取控件实例,这样就导致ViewHolder的作用完全失效了。
所以,上面这种写法就是kotlin-android-extensions插件在Adapter当中一种比较典型的误用方式。同时也算是一个隐藏的“坑”,因为如果你不去将Kotlin代码进行反编译,可能都不知道自己的ViewHolder其实根本就没有起到任何作用。
Viewbinding使用
通过视图绑定功能,您可以更轻松地编写可与视图交互的代码。在模块中启用视图绑定之后,系统会为该模块中的每个 XML 布局文件生成一个绑定类。绑定类的实例包含对在相应布局中具有 ID 的所有视图的直接引用。
在大多数情况下,视图绑定会替代 findViewById
。
设置说明
注意:视图绑定在 Android Studio 3.6 Canary 11 及更高版本中可用。
视图绑定功能可按模块启用。要在某个模块中启用视图绑定,请将 viewBinding 元素添加到其 build.gradle 文件中,如下例所示:
android {
...
viewBinding {
enabled = true
}
}
如果您希望在生成绑定类时忽略某个布局文件,请将 tools:viewBindingIgnore=“true” 属性添加到相应布局文件的根视图中:
<LinearLayout
...
tools:viewBindingIgnore="true" >
...
</LinearLayout>
用法
为某个模块启用视图绑定功能后,系统会为该模块中包含的每个 XML 布局文件生成一个绑定类。每个绑定类均包含对根视图以及具有 ID 的所有视图的引用。系统会通过以下方式生成绑定类的名称:将 XML 文件的名称转换为驼峰式大小写,并在末尾添加“Binding”
一词。
例如,假设某个布局文件的名称为 result_profile.xml:
<LinearLayout ... >
<TextView android:id="@+id/name" />
<ImageView android:cropToPadding="true" />
<Button android:id="@+id/button"
android:background="@drawable/rounded_button" />
</LinearLayout>
所生成的绑定类的名称就为 ResultProfileBinding
。此类具有两个字段:一个是名为 name 的 TextView,另一个是名为 button 的 Button。该布局中的 ImageView 没有 ID,因此绑定类中不存在对它的引用。
每个绑定类还包含一个 getRoot() 方法,用于为相应布局文件的根视图提供直接引用。在此示例中,ResultProfileBinding 类中的 getRoot() 方法会返回 LinearLayout 根视图。
接下来我会从Activity、Fragment、Adapter、引入布局这4个方面,分别讨论ViewBinding的用法。
在Activity中使用ViewBinding
如需设置绑定类的实例以供 Activity 使用,请在 Activity 的 onCreate() 方法中执行以下步骤:
- 调用生成的绑定类中包含的静态 inflate() 方法。此操作会创建该绑定类的实例以供 Activity 使用。
- 通过调用 getRoot() 方法或使用 Kotlin 属性语法获取对根视图的引用。
- 将根视图传递到 setContentView(),使其成为屏幕上的活动视图。
kotlin代码
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.textView.text = "111"
}
}
Java代码
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
View view = binding.getRoot();
setContentView(view);
binding.getTextView.setText("111");
}
当然,如果你需要在onCreate()函数之外的地方对控件进行操作,那么就得将binding变量声明成全局变量,写法如下:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.textView.text = "111"
}
}
注意,Kotlin声明的变量都必须在声明的同时对其进行初始化。而这里我们显然无法在声明全局binding变量的同时对它进行初始化,所以这里又使用了lateinit关键字对binding变量进行了延迟初始化。
在 Fragment 中使用视图绑定
如需设置绑定类的实例以供 Fragment 使用,请在 Fragment 的 onCreateView() 方法中执行以下步骤:
- 调用生成的绑定类中包含的静态 inflate() 方法。此操作会创建该绑定类的实例以供 Fragment 使用。
- 通过调用 getRoot() 方法或使用 Kotlin 属性语法获取对根视图的引用。
- 从 onCreateView() 方法返回根视图,使其成为屏幕上的活动视图。
注意:inflate() 方法会要求您传入布局加载器。如果布局已加载,您可以调用绑定类的静态 bind() 方法。
kotlin代码
class MainFragment : Fragment() {
private var _binding: FragmentMainBinding? = null
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentMainBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Java代码
public class MainFragment extends Fragment {
private FragmentMainBinding binding;
@Override
public View onCreateView(@NotNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
binding = FragmentMainBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}
注意:Fragment 的存在时间比其视图长。请务必在 Fragment 的 onDestroyView()
方法中清除对绑定类实例的所有引用。
您现在即可使用该绑定类的实例来引用任何视图:
kotlin代码
binding.textView.text = "111"
Java代码
binding.getTextView().setText("111");
在Adapter中使用ViewBinding
接下来,让我们再来探讨一下在Adapter中使用ViewBinding的场景,这个场景会相对比较有趣,同时也是之前kotlin-android-extensions插件被误用比较多的地方,上文已经讲过,这里不再重复。
为什么我们要在ListView的Adapter当中去写ViewHolder。答案就是,为了不用在列表滚动的时候频繁调用findViewById(),从而减少了一些没必要的性能消耗。
而RecyclerView把ListView中的这个普遍应用的最佳实践直接作为默认实现集成了进去,所以只要我们使用RecyclerView,就一定要写ViewHolder。
假设我们定义了fruit_item.xml来作为RecyclerView子项的布局:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp">
<ImageView
android:id="@+id/fruitImage"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp" />
<TextView
android:id="@+id/fruitName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:layout_marginTop="10dp" />
</LinearLayout>
然后编写如下RecyclerView Adapter来加载和显示这个子项布局:
class FruitAdapter(val fruitList: List<Fruit>) : RecyclerView.Adapter<FruitAdapter.ViewHolder>() {
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
val fruitName: TextView = view.findViewById(R.id.fruitName)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.fruit_item, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val fruit = fruitList[position]
holder.fruitImage.setImageResource(fruit.imageId)
holder.fruitName.text = fruit.name
}
override fun getItemCount() = fruitList.size
}
这是比较标准和传统的一种写法,没有任何问题。
接下来我们还是看一下如何在Adapter中使用ViewBinding
class FruitAdapter(val fruitList: List<Fruit>) : RecyclerView.Adapter<FruitAdapter.ViewHolder>() {
inner class ViewHolder(binding: FruitItemBinding) : RecyclerView.ViewHolder(binding.root) {
val fruitImage: ImageView = binding.fruitImage
val fruitName: TextView = binding.fruitName
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = FruitItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val fruit = fruitList[position]
holder.fruitImage.setImageResource(fruit.imageId)
holder.fruitName.text = fruit.name
}
override fun getItemCount() = fruitList.size
}
这段代码的核心基本都在onCreateViewHolder()函数和ViewHolder当中。
首先,我们在onCreateViewHolder()函数中调用FruitItemBinding的inflate()函数去加载fruit_item.xml布局文件,这和ViewBinding在Fragment中的用法是一模一样的。
接下来需要改造ViewHolder,让其构造函数接收FruitItemBinding这个参数。但是注意,ViewHolder的父类RecyclerView.ViewHolder它只会接收View类型的参数,因此我们需要调用binding.root获得fruit_item.xml中根元素的实例传给RecyclerView.ViewHolder。
这样,我们就不需要再使用findViewById()函数来查找控件实例了,而是调用binding.fruitImage和binding.fruitName就可以直接引用到相应控件的实例。
这就是ViewBinding在Adapter中的用法。
对引入布局使用ViewBinding
关于ViewBinding的使用其实还有另外一种比较特殊的场景,那就是如何对引入布局使用ViewBinding。
接下来我们开始分别学习如何在include和merge的布局中使用ViewBinding。
先来看include,这个情况比较简单。假设我们有如下titlebar.xml布局,是希望作为一个通用布局引入到各布局当中的:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<Button
android:id="@+id/back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:text="Back" />
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Title"
android:textSize="20sp" />
<Button
android:id="@+id/done"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:text="Done" />
</RelativeLayout>
那么如果我想要在activity_main.xml中引入这个布局,只需要这样写:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include
layout="@layout/titlebar" />
...
</LinearLayout>
这种写法虽然的确可以将titlebar.xml引入到activity_main.xml布局当中,但问题是,你会发现ViewBinding是关联不到titlebar.xml中的控件的。
那么如何解决这个问题呢?很简单,我们只需要在include的时候给被引入的布局添加一个id,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include
android:id="@+id/titleBar"
layout="@layout/titlebar" />
...
</LinearLayout>
然后,在MainActivity中,我们即可通过如下的写法引用到titlebar.xml中定义的控件:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.titleBar.title.text = "Title"
binding.titleBar.back.setOnClickListener {
}
binding.titleBar.done.setOnClickListener {
}
}
}
接下来我们再来看一下merge。merge和include最大的区别在于,使用merge标签引入的布局在某些情况下可以减少一层布局的嵌套,而更少的布局嵌套通常就意味着更高的效率。
比如说我们对titlebar.xml进行如下修改:
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<Button
android:id="@+id/back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:text="Back" />
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Title"
android:textSize="20sp" />
<Button
android:id="@+id/done"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:text="Done" />
</merge>
可以看到,这里最外层的布局使用了merge标签,这就表示当有任何一个地方去include这个布局时,会将merge标签内包含的内容直接填充到include的位置,不会再添加任何额外的布局结构。
但是很遗憾,如果使用这种写法的话,运行程序将会直接崩溃。因为merge标签并不是一个布局,所以我们无法像刚才那样在include的时候给它指定一个id。
那么这种情况下应该怎么使用ViewBinding呢?首先为了避免崩溃,我们应该将activity_main.xml中引入布局时指定的id移除,如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include
layout="@layout/titlebar" />
</LinearLayout>
然后修改MainActivity中的代码,如下所示:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var titlebarBinding: TitlebarBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
titlebarBinding = TitlebarBinding.bind(binding.root)
setContentView(binding.root)
titlebarBinding.title.text = "Title"
titlebarBinding.back.setOnClickListener {
}
titlebarBinding.done.setOnClickListener {
}
}
}
可以看到,这里我们又定义了一个titlebarBinding变量。很明显,TitlebarBinding就是Android Studio根据我们的titlebar.xml布局文件自动生成的Binding类。
在onCreate()函数中,我们调用TitlebarBinding.bind()函数,让titlebar.xml布局和activity_main.xml布局能够关联起来。
接下来的事情就很简单了,直接使用titlebarBinding变量就可以引用到titlebar.xml中定义的各个控件了。
这大概就是关于ViewBinding的所有内容了。
与 findViewById 的区别
与使用 findViewById 相比,视图绑定具有一些很显著的优点:
- Null 安全:由于视图绑定会创建对视图的直接引用,因此不存在因视图 ID 无效而引发 Null
指针异常的风险。此外,如果视图仅出现在布局的某些配置中,则绑定类中包含其引用的字段会使用 @Nullable 标记。 - 类型安全:每个绑定类中的字段均具有与它们在 XML 文件中引用的视图相匹配的类型。这意味着不存在发生类转换异常的风险。
这些差异意味着布局和代码之间的不兼容将会导致构建在编译时(而非运行时)失败。
与数据绑定的对比
视图绑定和数据绑定均会生成可用于直接引用视图的绑定类。但是,视图绑定旨在处理更简单的用例,与数据绑定相比,具有以下优势:
- 更快的编译速度:视图绑定不需要处理注释,因此编译时间更短。
- 易于使用:视图绑定不需要特别标记的 XML 布局文件,因此在应用中采用速度更快。在模块中启用视图绑定后,它会自动应用于该模块的所有布局。
反过来,与数据绑定相比,视图绑定也具有以下限制:
- 视图绑定不支持布局变量或布局表达式,因此不能用于直接在 XML 布局文件中声明动态界面内容。
- 视图绑定不支持双向数据绑定。
考虑到这些因素,在某些情况下,最好在项目中同时使用视图绑定和数据绑定。您可以在需要高级功能的布局中使用数据绑定,而在不需要高级功能的布局中使用视图绑定。
参考文章
kotlin-android-extensions插件也被废弃了?扶我起来
Android 开发者文档ViewBinding
更多推荐
所有评论(0)