持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第17天,点击查看活动详情
为获取布局中的元素,传统方法是使用 findViewById 方法。如果页面元素很多,会使用一排该方法来获取对应的 View,显然会有大量重复代码。
传统的 findViewById 实现
Kotlin
findViewById<TextView>(R.id.sample_text).apply {
text = viewModel.userName
}
Java
TextView textView = findViewById(R.id.sample_text);
textView.setText(viewModel.getUserName());
简化 findViewById 方案
ButterKnife
Jake Wharton 开源了 ButterKnife 使用 @BindView 注解来完成视图的绑定
class ExampleActivity extends Activity {
@BindView(R.id.title) TextView title;
@BindView(R.id.subtitle) TextView subtitle;
@BindView(R.id.footer) TextView footer;
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.simple_activity);
ButterKnife.bind(this);
// TODO Use fields...
}
}
除了@BindView,该开源库也提供了其他的注解,如@OnClick, @BindBool, @BindColor 等,快捷的使用方式,备受 Android 开发者的青睐。
但作者也声明了,该工具不再维护了,而是推荐 View Binding,已发布的版本仍然可以使用。
Kotlin 插件
随着 kotlin 语言的发展,kotlin 提供了支持的视图绑定的插件 kotlin-android-extensions
应用插件
apply plugin: 'kotlin-android-extensions'
kotlin 中使用
// import kotlinx.android.synthetic.main.<布局>.*
import kotlinx.android.synthetic.main.activity_test.*
class TestActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test)
//引用布局后,直接调用 id 名称进行操作
tv_text.setText("aaaaaaaaa")
tv_text.setOnClickListener(View.OnClickListener {
Toast.makeText(this,"aaaaaaaa",Toast.LENGTH_SHORT).show()
})
}
}
使用起来也很方便,但是这个插件只针对于 kotlin 语言,java 还是没有办法使用的,而且在 kotlin 1.4.20-M2 中JetBrains 废弃了 Kotlin Android Extensions 编译插件,后面也不会再支持,官方推荐是使用 View Binding。 Migrate from Kotlin synthetics to Jetpack view binding
所以来看下备受推荐的 View Binding 是如何简化 findViewById 的
ViewBinding
Android Studio 3.6 开始支持 View binding,它可以将 layout 生成对应的的 java 文件
启动 ViewBinding
项目工程模块的 build.gradle 中加入以下配置:
// Available in Android Gradle Plugin 3.6.0
android {
viewBinding {
enabled = true
}
}
// Android Studio 4.0
android {
buildFeatures {
viewBinding = true
}
}
Binding 文件的生成
启动了 ViewBinding 功能之后,Android Studio 会自动为我们所编写的每一个布局文件都生成一个对应的 Binding 类。
Binding 类的命名规则是将布局文件按驼峰方式重命名后,再加上 Binding 作为结尾。
比如: activity_main.xml 布局,与之对应的 Binding 类就是 ActivityMainBinding
如果有些布局不希望生成对应的Binding 类,可以在布局文件的根元素位置添加声明
<LinearLayout
xmlns:tools="http://schemas.android.com/tools"
...
tools:viewBindingIgnore="true">
...
</LinearLayout>
Activity
在 Activiy 中使用方法
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.textView.text = "Hello"
}
}
首先我们要调用 activity_main.xml 布局文件对应的 Binding 类,也就是 ActivityMainBinding 的 inflate() 函数去加载该布局,inflate() 函数接收一个 LayoutInflater 参数,在 Activity 中是可以直接获取到的。
调用 Binding类的 getRoot() 函数可以得到 activity_main.xml 中根元素的实例,调用 getTextView() 函数可以获得 id 为 textView 的元素实例。
Fragment
假设我们有一个布局文件叫 fragment_main.xml,那么启用 ViewBinding 功能之后会生成一个与其对应的FragmentMainBinding 类。
如果我们想要在 MainFragment 中去显示这个布局,就可以这样写:
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
}
}
首先最核心的逻辑仍然是调用 FragmentMainBinding 的 inflate() 函数去加载 fragment_main.xml 布局文件,但由于这是在 Fragment 当中,所以使用了3个参数的 inflate() 函数重载,这和我们平时在Fragment中去加载布局文件的方式如出一辙。
不一样的地方在于,由于我们是在 onCreateView() 函数中加载的布局,那么理应在与其对应的 onDestroyView() 函数中对 binding 变量置空,从而保证 binding 变量的有效生命周期是在 onCreateView() 函数和 onDestroyView() 函数之间。
自定义View
自定义 View 使用方法大同小异
class MyView(context: Context) : ConstraintLayout(context) {
private var mBinding: ItemMineBinding
init {
val view = LayoutInflater.from(context).inflate(R.layout.item_mine, this)
mBinding = ItemMineBinding.bind(view)
}
}
include 标签
如果在 activity_main.xml 中使用了 <include> 标签
<?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=".viewbinding.ViewBindingActivity">
<include
android:id="@+id/include_test"
layout="@layout/layout_test"/>
</androidx.constraintlayout.widget.ConstraintLayout>
如果要引用 include 布局中的元素,可以使用 mBinding.includeTest.tvxxx
原理
View Binding 将模块中每个 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/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" />
</androidx.constraintlayout.widget.ConstraintLayout>
View Binding 将生成 ActivityMainBinding.java
public final class ActivityMainBinding implements ViewBinding {
@NonNull
private final ConstraintLayout rootView;
@NonNull
public final TextView textView;
View Binding 为每一个具有 id 的视图生成对应的属性,也会实现 ViewBinding 中的唯一方法 getRoot
在ActivityMainBinding.java中,视图绑定会生成一个公共的inflate方法。
它调用bind,在那里它将获取布局并绑定属性,并进行一些错误检查。
在bind方法中,生成的绑定对象将为每个要绑定的 View 调用findViewById。
比较
参考
【译】迁移被废弃的Kotlin Android Extensions插件