几种绑定视图方式对比
一、第一种,传统方式绑定视图(findViewById)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:gravity="center">
<Button
android:id="@+id/btn_login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="登录"/>
</LinearLayout>
private lateinit var mLoginBtn:Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mLoginBtn = findViewById(R.id.btn_login) as Button
mLoginBtn.setOnClickListener {
Toast.makeText(this@MainActivity,"登录",Toast.LENGTH_SHORT).show()
}
}
在这种方式里面,一般情况下我们会定义一个成员变量来接收视图,同时使用findViewById并做一次类型转换。
二、第二种,框架注解绑定视图(ButterKnife)
/* 引入编译插件和依赖包。略过... */
@BindView(R.id.btn_login)
lateinit var mLoginBtn:Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
ButterKnife.bind(this)
mLoginBtn.setOnClickListener {
Toast.makeText(this@MainActivity,"登录",Toast.LENGTH_SHORT).show()
}
}
可以看到,使用ButterKnife,不但要引入它的编译插件和库文件,在绑定视图的过程中,依然需要定义成员变量,代码量并没有减少。
1、ButterKnife原理
ButterKnife通过最前沿的Java技术(最初的版本可能是反射,未加考究)--Java编译时注解处理器,在编译时自动生成findViewById的代码。例如,上边的例子通过ButterKnife会生成一个MainActivity_ViewBinding 类,在该类中通过findViewById为mLoginBtn赋值。这一操作省去了开发者手动编写findViewById的时间,大大简化了代码,同时提高了开发效率。在当时的开发者看来ButterKnife不得不说是一个神器,以至于到后来成了Android项目开发的标配。
2、绝境
随着Android Studio的诞生,Eclipse开发Android项目逐渐淡出历史舞台。Android Studio的出现,带来了全新的技术,模块化风靡一时。大概在这个时候,Google官方似乎就已经有了改造R类的想法。在Android项目的library模块中,生成R类中的成员变量就已经改为了非final修饰。同时,Google官方也不再建议在app模块的代码中使用像:switch(view.getId())这样的代码。
这一改变直接致使ButterKnife无法在Android项目的library模块中使用。而此时,ButterKnife正是如日中天,追随的开发者不计其数。为了能够让ButterKnife运行在library模块,ButterKnife的作者Jake Wharton大佬曲线救国,通过生成R2类让ButterKnife在library模块中复活,并且得以发展壮大。但不得不说,此时的ButterKnife就已经埋下了深深的隐患,并导致了其最终的溃败。
Google在Android Studio 3.6 Canary 11版本中正式推出视图绑定(View Binding),相对有findViewById或者ButterKnife等现有的视图访问方式更有优势,JakeWharton也因此宣布了Butter Knife的终结。
三、第三种,插件绑定视图(Kotlin-Android-Extensions)
Kotlin Android Extensions是Kotlin团队开发的一个插件,目的是让我们在开发过程中更少的编写代码。目前包括了视图绑定的功能。
1、KAE使用步骤
1.1、在module中的build.gradle文件添加插件配置
apply plugin: 'kotlin-android-extensions'
1.2、在需要绑定视图的Activity、Fragment、Adapter及自定义View中引入资源文件
import kotlinx.android.synthetic.main.布局文件.*
例如
import kotlinx.android.synthetic.main.activity_main.*
1.3、在使用的位置,直接使用xml中对应的id访问视图,完整代码如下:
import kotlinx.android.synthetic.main.activity_main.*
...........
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
btn_login.setOnClickListener {
Toast.makeText(this@MainActivity,"登录",Toast.LENGTH_SHORT).show()
}
}
}
1.4、引入文件详细说明
import kotlinx.android.synthetic.main.activity_main.*
固定前缀:import kotlinx.android.synthetic.main
布局文件名称:activity_main
.*表示引入布局下所有视图。当然,也可以只引入需要的视图,把换成对应的id就行啦,如下:
import kotlinx.android.synthetic.main.activity_main.btn_login
2、KAE绑定视图范围
2.1、在Activity中使用
引入资源文件,直接使用id访问视图,代码如下:
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
btn_login.setOnClickListener {
Toast.makeText(this@MainActivity,"登录",Toast.LENGTH_SHORT).show()
}
}
}
2.2、在Fragment中使用
引入资源文件,直接使用id访问视图,有一点特别注意:在onCreateView中不直接访问视图,因为视图没有加载完成,容易引起空指针,需要在onViewCreated中访问视图,代码如下:
import kotlinx.android.synthetic.main.view_login.*
class LoginFragment:Fragment() {
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
return inflater?.inflate(R.layout.view_login, container, false)
}
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btn_login.setOnClickListener {
Toast.makeText(context,"登录", Toast.LENGTH_SHORT).show()
}
}
}
2.3、在Adapter中使用
在Adapter和自定义View中引入,需要在布局文件名view_login后添加view节点,如下:
import kotlinx.android.synthetic.main.view_login.view.*
引入布局文件需要添加view节点,可使用ViewHolder中的itemView直接访问视图(当然,也可以在ViewHolder中做一次视图绑定,与传统ViewHolder类似),代码如下:
import kotlinx.android.synthetic.main.view_login.view.*
class LoginAdapter(var context: Context):RecyclerView.Adapter<LoginAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder {
val view = LayoutInflater.from(context)
.inflate(R.layout.view_login,parent,false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.itemView.btn_login.setOnClickListener {
Toast.makeText(context,"登录", Toast.LENGTH_SHORT).show()
}
}
override fun getItemCount(): Int {
return 3
}
class ViewHolder(view: View) : RecyclerView.ViewHolder(view)
}
2.4、在自定义View中使用
引入布局文件需要添加view节点,在自定义视图中,可直接使用id访问视图,代码如下:
import kotlinx.android.synthetic.main.view_login.view.*
class LoginView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
init {
View.inflate(context,R.layout.view_login,this)
btn_login.setOnClickListener {
Toast.makeText(context,"登录", Toast.LENGTH_SHORT).show()
}
}
}
3、KAE存在的问题
通过Kotlin的扩展插件来find view,无疑是一种优秀的方案。但这一方案并不是无懈可击。它存在以下几个缺点:
- 类型安全:res下的任何id都可以被访问,有可能因访问了非当前Layout下的id而出错;
- 空安全:这主要体现在Configuration中的对应布局不全时,运行时可能出现NPE;
- 兼容性:只能在kotlin中使用,java不友好;
- 局限性:不能跨module使用;
也正是这几个缺点导致了KAE的大溃败。随着Google对亲儿子ViewBinding的大力推广,KAE最终也招架不住,只能缴械投降---Jetbrains在官网宣布废弃KAE,并推荐开发者使用ViewBinding。
在Kotlin 1.4.20-M2中,JetBrains废弃了Kotlin Android Extensions编译插件,kotlin的github链接。
类型安全
我们知道KAE可以将layout文件中的id映射为View对象直接在Activity或Fragment中使用,但是无法保证layout是Activity/Fragment的当前视图。
举例说明:
activity_main.xml和activity_second.xml代码如下:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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"
tools:context=".MainActivity">
<TextView
android:id="@+id/textAccount1"
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" />
</androidx.constraintlayout.widget.ConstraintLayout>
activity_second.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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"
tools:context=".MainActivity">
<TextView
android:id="@+id/textAccount2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello Second!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity代码如下:
package com.example.firstkotlin
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activty_second.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
textAccount2.text = "123456789"
}
}
程序会出现闪退:
Application will crash because "textAccount2" doesn't exist in "activity_main"
原因:
KAE会扫描res下所有layout中的id,作为代码自动补全的候选项,textAccount2的使用虽然可以通过编译,但不是当前layout中的id,所以运行时会出错。
四、第四种,Google的ViewBinding的使用详解
到这里,以上提到的多种findView方案都已经有被废弃的趋势,Google官方正在大力推广的ViewBinding组件。ViewBinding是Google在2019年I/O大会上公布的一款Android视图绑定工具。它的使用方式有点类似DataBinding,但相比DataBinding,ViewBinding是一个更轻量级、更纯粹的findViewById的替代方案。它具有以下几个优点:
- 类型安全: ViewBinding会基于布局中的View生成类型正确的属性。比如,在布局中放入了一个 TextView ,视图绑定就会暴露出一个 TextView 类型的属性供开发中使用;
- 空安全:ViewBinding会检测某个视图是不是只在一些配置下存在,并依据结果生成带有 @Nullable 注解的属性。所以即使在多种配置下定义的布局文件,视图绑定依然能够保证空安全;
- ViewBinding生成的绑定类是一个Java类,并且添加了Kotlin的注解,可以很好的支持 Java 和 Kotlin 两种编程语言;
同时,Google官方还给出了一个ViewBinding、ButterKnife以及KAE的对比,如下图:
1、在 build.gradle 中开启视图绑定
开启视图绑定无须引入额外依赖,从 Android Studio 3.6 开始,视图绑定将会内建于 Android Gradle 插件中。需要打开视图绑定的话,只需要在 build.gradle 文件中配置 viewBinding 选项:
// 需要 Android Gradle Plugin 3.6.0
android {
viewBinding {
enabled = true
}
}
在 Android Studio 4.0 中,viewBinding 变成属性被整合到了 buildFeatures 选项中,所以配置要改成:
// Android Studio 4.0
android {
buildFeatures {
viewBinding = true
}
}
如果,你的项目存在多个模块,则需要在每个模块的gradle中添加上述配置。完成以上配置后ViewBinding会为该 Module 内所有布局文件自动生成对应的绑定类。且无须修改原有布局的 xml 文件,ViewBinding会根据现有的布局自动完成所有工作。
您可以在任何需要填充布局的地方使用绑定对象,比如 Fragment、Activity、甚至是 RecyclerView Adapter(或者说是 ViewHolder 中)。
2、在Activty中使用ViewBinding
activity_main.xml代码如下:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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"
tools:context=".MainActivity">
<TextView
android:id="@+id/textAccount"
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" />
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity的代码如下:
package com.example.firstkotlin
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.example.firstkotlin.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//gradle插件会自动生成一个名为ActivityMainBinding的Java类,
//通过ActivityMainBinding获取Binding实例,
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.textAccount.text = "123456789"
}
}
在 Activity 中使用视图绑定时,无须再调用 findViewById 方法,只要直接调用绑定对象中的对应属性即可。布局的根视图(无论有没有 id)都会自动生成一个名为 root 的属性。在 Activity 的 onCreate 方法中,要将 root 传入 setContentView 方法,从而让 Activity 可以使用绑定对象中的布局。
一个常见的错误用法是:
在开启了视图绑定的同时,依然在 setContentView(...) 中传入布局的 id 而不是绑定对象。这将造成同一布局被填充两次,同时监听器也会被添加到错误的布局对象中。
解决方案:
在 Activity 中使用视图绑定时,一定要将绑定对象的 root 属性传入 setContentView() 方法中。
3、在include标签的情况下使用ViewBinding
前面已经讲过,视图绑定会为 module 下的每一个布局文件生成一个绑定对象,这个说法在布局文件被另一个布局文件使用 include 引入时依然适用。 activity_main.xml代码如下:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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"
tools:context=".MainActivity">
<include
android:id="@+id/include"
layout="@layout/activity_title" />
<TextView
android:id="@+id/textAccount"
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" />
</androidx.constraintlayout.widget.ConstraintLayout>
activity_title.xml代码如下:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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"
tools:context=".MainActivity">
<TextView
android:id="@+id/textTitle"
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="title"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
上述两个布局文件会分别生成ActivityMainBinding与ActivityTitleBinding两个Java类,并且ActivityMainBinding类中通过组合依赖了ActivityTitleBinding类。因此,使用方式如下:
package com.example.firstkotlin
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.example.firstkotlin.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.textAccount.text = "123456789"
// 从ActivityMainBinding中获取ActivityTitleBinding
val include = binding.include
// 通过ActivityTitleBinding为TextView赋值
include.textTitle.text = "标题"
}
}
如果layout_include.xml文件位于子模块,经实践与以上代码的使用方式并无任何差异,但一定要在子模块中开启ViewBinding才行。
注意:
include 标签必须有一个 id,才能生成对应的属性。
在使用引入布局的时候,视图绑定会创建一个被引入布局绑定对象的引用。注意 include>标签有一个 id: android:id="@+id/include"。这里的逻辑跟使用普通视图一样, include 标签也需要有一个 id 才能在绑定对象中生成对应的属性。
4、在Fragment中的使用ViewBinding
在Fragment中使用ViewBinding与Activity中有些差异,则Fragment的代码如下:
package com.example.firstkotlin
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.example.firstkotlin.databinding.ActivityMainBinding
class FragmentA:Fragment() {
lateinit var binding: ActivityMainBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = ActivityMainBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.textAccount.text = "987654321"
}
}
5、在RecyclerView#Adapter中的使用ViewBinding
item_test.xml代码如下:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textItem"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
tools:ignore="MissingConstraints" />
</androidx.constraintlayout.widget.ConstraintLayout>
TestAdapter代码如下:
package com.example.firstkotlin
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.example.firstkotlin.databinding.ItemTestBinding
class TestAdapter : RecyclerView.Adapter<TestAdapter.TestViewHolder>() {
lateinit var binding: ItemTestBinding
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TestViewHolder {
binding = ItemTestBinding.inflate(LayoutInflater.from(parent.context))
return TestViewHolder(binding)
}
override fun onBindViewHolder(holder: TestViewHolder, position: Int) {
holder.binding.textItem.text = "item"
}
override fun getItemCount(): Int {
return 10
}
class TestViewHolder(var binding: ItemTestBinding) :
RecyclerView.ViewHolder(binding.root)
}
五、总结
通过以上几个实例可以看到ViewBinding的使用是非常简单的。而ViewBinding的实现原理也并不难,Gradle插件会根据布局文件在项目的build目录下生成相应的ViewBinding类。