ViewBinding是Google在2019年I/O大会上公布的一款Android视图绑定工具,在Android Studio 3.6中添加的一个新功能,更准确的说,它是DataBinding的一个更轻量变体,为什么要使用View Binding呢?答案是性能。许多开发者使用Data Binding库来引用Layout XML中的视图,而忽略它的其他强大功能。相比来说,自动生成代码ViewBinding其实比DataBinding性能更好。但是传统的方式使用View Binding却不是很好,因为会有很多样板代码(垃圾代码)。
通过ViewBinding,你可以更轻松的编写可与视图交互的代码。在模块中启用视图绑定之后,系统会为该模块中的每个XML布局文件生成一个绑定类。绑定类的实例包含对在相应布局中具有ID的所有视图的直接引用。在大多数情况下,视图绑定会替代findViewById。
在build.gradle文件中的android节点添加如下代码:
android {
...
buildFeatures {
viewBinding true
}
}
重新编译后系统会为每个布局文件生成对应的Binding类,该类中包含对应布局中具有id的所有视图的直接饮用。生成类的目录在app/build/generated/data_binding_base_class_source_out中。 如果项目中存在多个模块,则需要在每个模块的build.gradle文件中都加上该配置。 假设某个布局文件的名称为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 根视图。
如果你希望在生成绑定类时忽略某个布局文件,可以将tools:viewBindingIgnore="true"属性添加到相应布局文件的根视图中:
<LinearLayout
...
tools:viewBindingIgnore="true" >
...
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/mtv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textSize="50sp"
android:textColor="@android:color/holo_green_dark"
android:text="welcome" />
</RelativeLayout>
class SplashActivity : BaseActivity() {
private lateinit var binding: ActivitySplashBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySplashBinding.inflate(layoutInflater)
setContentView(binding.root)
// mtv为xml中定义的id
binding.mtv.text = "Hello World"
}
}
在Fragment中,我们需要进行额外的工作来避免内存泄漏,方法是在onDestroyView方法中将ViewBinding引用设置为null。 具体如下:
class HomeFragment : Fragment() {
private var _binding: HomeFragmentBinding? = null
// 只在onCreateView和onDestroyView之间有效
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
_binding = ResultProfileBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.tvHelloWorld.text = "Hello Android!"
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
You also noticed that we are using two different variables and the _binding variable is set to null in onDestroyView().
That’s because the fragment’s lifecycle is different from the activity’s and the fragment can outlive their views so we can get memory leaks if we don’t set it to null.
The other variable is there to avoid a null check with !! by making one variable nullable and the other one non-null.
还有在Adapter中的使用,因为布局不是只创建一次,而是每个item都会创建,不能像上面那样在Adapter里写一个binding全局变量,不然binding只会得到最后一次创建的视图。所以binding对象应该是给ViewHolder持有。具体如下:
class TextAdapter(
private val list: List<String>
) : RecyclerView.Adapter<TextAdapter.TextViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TextViewHolder {
val binding = ItemTextBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return TextViewHolder(binding)
}
override fun onBindViewHolder(holder: TextViewHolder, position: Int) {
val content = list[position]
holder.binding.tvContent.text = content
}
override fun getItemCount() = list.size
class TextViewHolder(val binding : ItemTextBinding) : RecyclerView.ViewHolder(binding.root)
}
ViewBinding同样可以被用于include中。 include又分为两种形式:
- 一种是有标签的样式
- 一种是没有的
没有标签的时候需要对include指定id,通过id来获取,例如:
<?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"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="0dp"
android:layout_height="?actionBarSize"
android:background="?colorPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
android:id="@+id/appbar"
layout="@layout/app_bar"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: MainLayoutBinding = MainLayoutBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.appbar.toolbar)
}
在有merge标签的时候,placheholder.xml如下:
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:id="@+id/tvPlaceholder"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</merge>
fragment_order.xml内容如下:
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/placeholder" />
</androidx.constraintlayout.widget.ConstraintLayout>
这个时候即使我们对include指定了id,在ViewBinding中也不会像普通include那样生成获取对应id的变量。这种情况下,我们需要使用placeholder.xml自动生成的PlaceholderBinding类,然后调用它的bind()方法。
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentOrderBinding.inflate(layoutInflater, container, false)
placeholderBinding = PlaceholderBinding.bind(binding.root)
placeholderBinding.tvPlaceholder.text = getString(R.string.please_wait)
return binding.root
}
- 创建和销毁
viewBinding
的样板代码 - 如果有很多Fragment,每一个都要拷贝一份相同的代码
viewBinding
属性是可空的,并且可变的,这可不太妙
而且使用起来不方便,我们希望用更简单的方式,例如:
class MainActivity : AppCompatActivity() {
private val binding by viewBinding(ActivityMainBinding::inflate)
}
怎么办呢?用强大Kotlin委托来重构它。通过属性委托可以自动执行inflate()方法和setContentView()方法。
inline fun <T : ViewBinding> AppCompatActivity.viewBinding(crossinline bindingInflater: (LayoutInflater) -> T) =
lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
val invoke = bindingInflater.invoke(layoutInflater)
setContentView(invoke.root)
invoke
}
或
import android.os.Looper
import android.view.LayoutInflater
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import androidx.viewbinding.ViewBinding
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
inline fun <reified T : ViewBinding> AppCompatActivity.viewBinding(noinline initializer: (LayoutInflater) -> T) =
ViewBindingPropertyDelegate(this, initializer)
class ViewBindingPropertyDelegate<T : ViewBinding>(
private val activity: AppCompatActivity,
private val initializer: (LayoutInflater) -> T
) : ReadOnlyProperty<AppCompatActivity, T>, LifecycleObserver {
private var _value: T? = null
init {
activity.lifecycle.addObserver(this)
}
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
@Suppress("Unused")
fun onCreate() {
if (_value == null) {
_value = initializer(activity.layoutInflater)
}
activity.setContentView(_value?.root!!)
activity.lifecycle.removeObserver(this)
}
override fun getValue(thisRef: AppCompatActivity, property: KProperty<*>): T {
if (_value == null) {
// This must be on the main thread only
if (Looper.myLooper() != Looper.getMainLooper()) {
throw IllegalThreadStateException("This cannot be called from other threads. It should be on the main thread only.")
}
_value = initializer(thisRef.layoutInflater)
}
return _value!!
}
}
使用:
// 将ActivitySplashBinding的inflate方法当做参数传入
// 在函数中可以直接传一个方法。静态方法的lambda表达式也是把对象改成类名,所以我们要调用的inflate方法就可以写成 ActivityMainBinding::inflate。
private val binding: ActivitySplashBinding by viewBinding(ActivitySplashBinding::inflate)
binding.mtv.text = "Hello World"
不幸的是,该属性委托仅对Activity有效,而对Fragment无效。
需要在gradle中增加implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
的依赖:
import android.view.View
import androidx.fragment.app.Fragment
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import androidx.viewbinding.ViewBinding
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
class FragmentViewBindingDelegate<T : ViewBinding>(
val fragment: Fragment,
val viewBindingFactory: (View) -> T
) : ReadOnlyProperty<Fragment, T> {
private var binding: T? = null
init {
fragment.lifecycle.addObserver(object : DefaultLifecycleObserver {
val viewLifecycleOwnerLiveDataObserver =
Observer<LifecycleOwner?> {
val viewLifecycleOwner = it ?: return@Observer
viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
binding = null
}
})
}
override fun onCreate(owner: LifecycleOwner) {
fragment.viewLifecycleOwnerLiveData.observeForever(
viewLifecycleOwnerLiveDataObserver
)
}
override fun onDestroy(owner: LifecycleOwner) {
fragment.viewLifecycleOwnerLiveData.removeObserver(
viewLifecycleOwnerLiveDataObserver
)
}
})
}
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
val binding = binding
if (binding != null) {
return binding
}
val lifecycle = fragment.viewLifecycleOwner.lifecycle
if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
throw IllegalStateException("Should not attempt to get bindings when Fragment views are destroyed.")
}
return viewBindingFactory(thisRef.requireView()).also { this.binding = it }
}
}
fun <T : ViewBinding> Fragment.viewBinding(viewBindingFactory: (View) -> T) =
FragmentViewBindingDelegate(this, viewBindingFactory)
然后,使用我们定义的委托来重构ProfileFragment
,注意在Fragment中使用需要注意传入布局Id,因为在代理getValue的时候会获取requireView:
class ProfileFragment : Fragment(R.layout.profile) {
private val binding by viewBinding(ProfileBinding::bind)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Use viewBinding
}
}
很好,我们去掉了创建和销毁ViewBinding的样板代码,现在只需要声明一个委托属性就可以了,是不是简单了?
Android的新库ViewBinding是一个去掉项目中findViewByid()
很好的解决方案,同时它也替代了著名的Butter Knife
。ViewBinding 与Kotlin委托属性的巧妙结合,可以让你的代码更加简洁易读。
完整的代码可以查看github:https://github.com/kirich1409/ViewBindingPropertyDelegate
与使用 findViewById 相比,视图绑定具有一些很显著的优点:
- Null 安全 由于视图绑定会创建对视图的直接引用,因此不存在因视图ID无效而引发Null指针异常的风险。此外,如果视图仅出现在布局的某些配置中,则绑定类中包含其引用的字段会使用@Nullable标记。
- 类型安全 每个绑定类中的字段均具有与它们在XML文件中引用的视图相匹配的类型。这意味着不存在发生类转换异常的风险。
这些差异意味着布局和代码之间的不兼容将会导致构建在编译时(而非运行时)失败。
ViewBinding与DataBinding均会生成可用于直接引用视图的绑定类。但是,ViewBinding旨在处理更简单的用例,与DataBinding相比,具有以下优势:
- 更快的编译速度 视图绑定不需要处理注释,因此编译时间更短。
- 易于使用 视图绑定不需要特别标记的XML布局文件,因此在应用中采用速度更快。在模块中启用视图绑定后,它会自动应用于该模块的所有布局。
反过来,与数据绑定相比,视图绑定也具有以下限制:
- 视图绑定不支持布局变量或布局表达式,因此不能用于直接在XML布局文件中声明动态界面内容。
- 视图绑定不支持双向数据绑定。
考虑到这些因素,在某些情况下,最好在项目中同时使用视图绑定和数据绑定。您可以在需要高级功能的布局中使用数据绑定,而在不需要高级功能的布局中使用视图绑定。
- View Binding 与Kotlin委托属性的巧妙结合,告别垃圾代码!
- Make Android View Binding great with Kotlin
- How to Simplify your Android View Binding Delegation
- 邮箱 :charon.chui@gmail.com
- Good Luck! `