【Android爬坑日记】Viewbinding使用和委托封装

2,187 阅读7分钟

这是Android爬坑日记第二篇,这也是【小鹅事务所】爬坑的第一篇。小鹅事务所是我开源的一个记账、事务的一个项目,没看过的可以看一下juejin.cn/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的一次生命周期中可能会回调多次onCreateViewonDestroyView,也就是说在Fragment的onDestroyView之后和onDestroy前的这一段时间,binding需要清除引用,否则会有内存泄漏的风险。并且此binding仅在onCreateViewonDestroyView之间可用。以下为官方的Fragment生命周期图。

fragment_lifecycle_U4B4KQer0S.png

封装

网络上有比较多封装ViewBinding的文章,我总结一下大家的封装方法。

  • 委托 + 反射

  • BaseFragment或者BaseActivity

  • 委托 + 利用Kotlin将方法作为参数传递

反射性能不太优秀,封装到Base基类中不太优雅,因此小鹅事务所决定选用第三种方法,利用Kotlin语法糖将方法作为参数传递 + 实现委托。

为什么封装起来这么复杂呢,还需要用到反射?

Android编译器根据XML生成的Binding类继承自一个叫ViewBinding的接口,让我们康康这个接口吧!

public interface ViewBinding {
    @NonNull
    View getRoot();
}

接口中只有一个getRoot方法,没有inflatebind方法。

于是我去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…

juejin.cn/post/684490…

juejin.cn/post/684490…

juejin.cn/post/690594…