这是Android爬坑日记第二篇,这也是【小鹅事务所】爬坑的第一篇。小鹅事务所是我开源的一个记账、事务的一个项目,没看过的可以看一下/post/713285…
大家都在学Compose了,不知道有没有人看ViewBinding呢。Viewbinding是Android的继Kotlin-androd-extensions和DataBinding之后推出的又一个视图绑定框架。
ViewBinding的功能相当于DataBinding的阉割版,仅仅拥有视图绑定功能,没有数据绑定的功能。但是但是,ViewBinding配合StateFlow
或者LiveData
使用也能够达到数据绑定的效果!并且使用上也更灵活,小鹅事务所大量使用StateFlow
管理数据流,因此选用ViewBinding视图绑定。
原理
在启用视图绑定之后,系统会为该模块中的每个XML布局生成一个绑定类,绑定类包含在相应布局中具有ID的所有视图的直接引用。
设置
在某个模块(例如app模块)的build.gradle中的android代码块中加入以下代码
android {
...
viewBinding {
enabled = true
}
}
先看看一般的用法吧
Activity
只需要三行代码即可使用,分别是声明,Inflate,setContentView。
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.textView.text = "鹅!"
}
}
Fragment
Fragment使用ViewBinding会复杂一点,多一步清除引用。
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 onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.textView.text = "鹅!"
}
override fun onDestroyView() {
super.onDestroyView()
// 清除引用
_binding = null
}
}
自定义View
而自定义View则只需要调用bind
绑定视图就好啦。
class FloatView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : MaterialCardView(context, attrs, defStyleAttr) {
private var binding: ItemFlowButtonBinding
init {
val view = inflate(context, R.layout.item_flow_button, this)
binding = ItemFlowButtonBinding.bind(view)
binding.textView.text = "鹅!"
}
}
RecyclerView
RecyclerView的话就复杂一点点,先自定义ViewHolder,再将binding的root传给父类
class MemorialRcvViewHolder(
private val binding: ItemMemorialBinding
...
) : RecyclerView.ViewHolder(binding.root) {
fun bindView(memorial: Memorial) {
tvMemorialTitle.text = memorial.content.appendTimeSuffix(memorial.time)
...
}
}
再自定义Adapter,然后大功告成!
class MemorialRcvAdapter(...) : RecyclerView.Adapter<MemorialRcvViewHolder>() {
override fun onCreateViewHolder(
parent: ViewGroup, viewType: Int
): MemorialRcvViewHolder {
// 实例binding
val binding = ItemMemorialBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return MemorialRcvViewHolder(binding, multipleChoseHandler)
}
override fun onBindViewHolder(holder: MemorialRcvViewHolder, position: Int) {
val memorial = list[position]
holder.bindView(memorial)
}
}
爬坑
使用并不难,但是大家有没有发现,有比较多样板代码其实是没有必要写的。例如Fragment的样板代码。
Fragment的View的生命周期是不跟着Fragment走的,Fragment的一次生命周期中可能会回调多次onCreateView
和onDestroyView
,也就是说在Fragment的onDestroyView
之后和onDestroy
前的这一段时间,binding需要清除引用,否则会有内存泄漏的风险。并且此binding仅在onCreateView
和onDestroyView
之间可用。以下为官方的Fragment生命周期图。
封装
网络上有比较多封装ViewBinding的文章,我总结一下大家的封装方法。
-
委托 + 反射
-
用
BaseFragment
或者BaseActivity
类 -
委托 + 利用Kotlin将方法作为参数传递
反射性能不太优秀,封装到Base基类中不太优雅,因此小鹅事务所决定选用第三种方法,利用Kotlin语法糖将方法作为参数传递 + 实现委托。
为什么封装起来这么复杂呢,还需要用到反射?
Android编译器根据XML生成的Binding类继承自一个叫ViewBinding的接口,让我们康康这个接口吧!
public interface ViewBinding {
@NonNull
View getRoot();
}
接口中只有一个getRoot
方法,没有inflate
和bind
方法。
于是我去build文件夹中找到生成的类。
public final class ActivityMainBinding implements ViewBinding {
@NonNull
private final ConstraintLayout rootView;
...
@Override
@NonNull
public ConstraintLayout getRoot() {
return rootView;
}
@NonNull
public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater) {
return inflate(inflater, null, false);
}
@NonNull
public static ActivityMainBinding bind(@NonNull View rootView) {
...
}
}
我才想起来它们都是静态方法,没有办法在接口定义并继承重写。
想获取到这些方法第一个想到的当然是反射,将ViewBinding接口通过泛型传入,再通过反射获取里面的inflate
或者bind
方法,再返回一个binding
方便使用。
其次也就是后面会讨论的方法,将函数作为参数传入到委托中。
封装Activity使用方式
@Suppress("unused")
fun <V : ViewBinding> Activity.viewBinding(
viewInflater: (LayoutInflater) -> V
): ReadOnlyProperty<Activity, V> = ActivityViewBindingProperty(viewInflater)
class ActivityViewBindingProperty<V : ViewBinding>(
private val viewInflater: (LayoutInflater) -> V
) : ReadOnlyProperty<Activity, V> {
private var binding: V? = null
override fun getValue(thisRef: Activity, property: KProperty<*>): V {
return binding ?: viewInflater(thisRef.layoutInflater).also {
thisRef.setContentView(it.root)
binding = it
}
}
}
这是一个泛型方法,传入一个<V : ViewBinding>
类型,因为所有生成类都是继承ViewBinding的,并传入一个(LayoutInflater) → V
参数,这个也就是上面的inflate静态方法。然后再实现仅可读委托接口ReadOnlyProperty
。为什么用ReadOnlyProperty
接口就能实现委托呢?我这里开个坑,有空再填。
class MainActivity : AppCompatActivity() {
private val binding by viewBinding(ActivityMainBinding::inflate)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding.textView.text = "鹅!"
}
}
看到这里,可能有人没见过::
,::
就是可以将函数作为参数传递到委托中,反编译成Java可以看到这个方法被编译成了一个Function
,这个Function
方便委托内部使用。可以避免通过反射获取这个函数。在Activity中仅仅只需要一行代码就可以实现视图绑定。
但是要注意,这个委托是懒初始化的,也就是说在代码中没有调用binding的话,就会导致没有setContentView
,视图就会一片空白。
封装Fragment使用方式
@Suppress("unused")
fun <V : ViewBinding> Fragment.viewBinding(viewBinder: (View) -> V)
: ReadOnlyProperty<Fragment, V> = FragmentViewBindingProperty(viewBinder)
class FragmentViewBindingProperty<V : ViewBinding>(private val viewBinder: (View) -> V) :
ReadOnlyProperty<Fragment, V> {
private var binding: V? = null
override fun getValue(thisRef: Fragment, property: KProperty<*>): V {
return binding ?: run {
thisRef.viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
Handler(Looper.getMainLooper()).post { binding = null }
thisRef.viewLifecycleOwner.lifecycle.removeObserver(this)
}
})
val view = thisRef.requireView()
viewBinder(view).also { binding = it }
}
}
}
封装方式和Activity差不多,但是多了一个监听view的生命周期,在view destroy的时候将binding置空清除引用,由于Fragment.viewLifecycleOwner
回调LifecycleObserver.onDestroy()
会在Fragment.onDestroyView
之前,所以可以用主线程Handler post置空操作。
这里传入的是ViewBinding的bind方法,也就是说在调用bind的时候,这个view已经被inflate并传给fragment了,我们来看看Fragment的其中一个构造函数。
public Fragment ... {
@ContentView
public Fragment(@LayoutRes int contentLayoutId) {
this();
mContentLayoutId = contentLayoutId;
}
@MainThread
@Nullable
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
if (mContentLayoutId != 0) {
return inflater.inflate(mContentLayoutId, container, false);
}
return null;
}
}
这个构造函数传入一个Layout资源,而这个资源将会在onCreateView
的时候被inflate,我们就可以偷懒少些一个onCreateView方法了!
下面来看看使用方式吧。
class HomeFragment : Fragment(R.layout.fragment_home) {
private val binding by viewBinding(FragmentHomeBinding::bind)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.textView.text = "鹅!"
}
}
跟Activity使用一样简单、优雅。
封装自定义View使用方式
而View的封装需要多传一个layoutRes
@Suppress("unused")
fun <V : ViewBinding> ViewGroup.viewBinding(
@LayoutRes layoutRes: Int,
viewBinder: (View) -> V
): ReadOnlyProperty<ViewGroup, V> = ViewViewBindingProperty(layoutRes, viewBinder)
class ViewViewBindingProperty<V : ViewBinding>(
@LayoutRes private val layoutRes: Int,
private val viewBinder: (View) -> V
) : ReadOnlyProperty<ViewGroup, V> {
private var binding: V? = null
override fun getValue(thisRef: ViewGroup, property: KProperty<*>): V {
return binding ?: viewBinder(View.inflate(thisRef.context, layoutRes, thisRef)).also {
binding = it
}
}
}
使用方式
class FloatView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : MaterialCardView(context, attrs, defStyleAttr) {
private val binding by viewBinding(R.layout.item_flow_button, ItemFlowButtonBinding::bind)
init {
binding.textView.text = "鹅!"
}
}
当然要是你Lazy,也可以用Lazy来实现委托。
@Suppress("unused")
fun <V : ViewBinding> View.viewBinding(
viewBinder: () -> V
): Lazy<V> = lazy(LazyThreadSafetyMode.NONE, viewBinder)
使用了LazyThreadSafetyMode.NONE
模式,因为View的操作一般在主线程来执行,因此是线程安全的。
使用方式:
class FloatView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : MaterialCardView(context, attrs, defStyleAttr) {
private val binding by viewBinding {
ItemFlowButtonBinding.bind(inflate(context, R.layout.item_flow_button, this))
}
init {
binding.textView.text = "鹅!"
}
}
需要注意的是,这种方式也要在init代码块对binding进行调用,不然也可能会出现没有界面的情况。
ViewBinding委托封装类
感谢看到这里,以下为整个实现,如果有更好的封装欢迎友好交流。
@Suppress("unused")
fun <V : ViewBinding> Fragment.viewBinding(viewBinder: (View) -> V)
: ReadOnlyProperty<Fragment, V> = FragmentViewBindingProperty(viewBinder)
class FragmentViewBindingProperty<V : ViewBinding>(private val viewBinder: (View) -> V) :
ReadOnlyProperty<Fragment, V> {
private var binding: V? = null
override fun getValue(thisRef: Fragment, property: KProperty<*>): V {
return binding ?: run {
thisRef.viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
// Fragment.viewLifecycleOwner call LifecycleObserver.onDestroy() before Fragment.onDestroyView().
// That's why we need to postpone reset of the viewBinding
Handler(Looper.getMainLooper()).post { binding = null }
thisRef.viewLifecycleOwner.lifecycle.removeObserver(this)
}
})
val view = thisRef.requireView()
viewBinder(view).also { binding = it }
}
}
}
@Suppress("unused")
fun <V : ViewBinding> Activity.viewBinding(
viewInflater: (LayoutInflater) -> V
): ReadOnlyProperty<Activity, V> = ActivityViewBindingProperty(viewInflater)
class ActivityViewBindingProperty<V : ViewBinding>(
private val viewInflater: (LayoutInflater) -> V
) : ReadOnlyProperty<Activity, V> {
private var binding: V? = null
override fun getValue(thisRef: Activity, property: KProperty<*>): V {
return binding ?: viewInflater(thisRef.layoutInflater).also {
thisRef.setContentView(it.root)
binding = it
}
}
}
@Suppress("unused")
fun <V : ViewBinding> ViewGroup.viewBinding(
@LayoutRes layoutRes: Int,
viewBinder: (View) -> V
): ReadOnlyProperty<ViewGroup, V> = ViewViewBindingProperty(layoutRes, viewBinder)
class ViewViewBindingProperty<V : ViewBinding>(
@LayoutRes private val layoutRes: Int,
private val viewBinder: (View) -> V
) : ReadOnlyProperty<ViewGroup, V> {
private var binding: V? = null
override fun getValue(thisRef: ViewGroup, property: KProperty<*>): V {
return binding ?: viewBinder(View.inflate(thisRef.context, layoutRes, thisRef)).also {
binding = it
}
}
}
总结
// todo 这篇文章是站在巨人的肩膀上的呢,发出去之前记得总结一下!
参考
developer.android.google.cn/topic/libra…
/post/684490…
/post/684490…
/post/690594…