Google挖坑后人埋-ViewBinding(下)

1,369 阅读5分钟

ViewBinding的封装

ViewBinding的使用比较复杂,存在很大模板代码,所以,一定的封装是很有必要的。

初试

首先,我们尝试抽取一层Base来进行封装。

abstract class BaseBindingActivity<T : ViewBinding> : BaseActivity() {

    val binding by lazy { getViewBinding() }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        contentView = getViewBinding().root
    }

    protected abstract fun getViewBinding(): T
}

使用的时候:

class XXXActivity : BaseBindingActivity<XXXLayoutBinding>() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        contentView = binding.root

        binding.title.text = "XXX"
    }

    override fun getViewBinding(): XXXLayoutBinding = XXXLayoutBinding.inflate(layoutInflater)
}

好像并没有什么鬼用?

我们来看看问题出在哪,其实很难封装的原因就是我们针对不同的Activity(或者其它要使用的Fragment、dialog),都要根据不同的布局文件的Binding文件进行inflate操作,这就是问题的核心,看似无法通用,所以造成了前面的封装,必须把ViewBinding的具体实现传进去。

反射

这种情况,只有一种解法了,那就是通过反射来进行实例化,代码如下所示。

inline fun <reified T : ViewBinding> inflateViewBinding(layoutInflater: LayoutInflater) =
    T::class.java.getMethod("inflate", LayoutInflater::class.java).invoke(null, layoutInflater) as T

有了这个反射之后,就不用基类了,直接使用:

class PocketSquareActivity : BaseActivity() {

    val binding by lazy { inflateViewBinding<PocketSquareLayoutBinding>(layoutInflater) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        contentView = binding.root

        binding.title.text = "XXX"
    }
}

这...TMD跟我直接用有啥区别???

所以我们把lazy那一段也抽取出去。创建一个拓展函数。

inline fun <reified T : ViewBinding> Activity.inflate() = lazy {
    inflateViewBinding<T>(layoutInflater).apply { setContentView(root) }
}

再使用的时候,就只需要传递T就可以了,还顺带解决了setContentView的调用。

class PocketSquareActivity : BaseActivity() {

    val binding by inflate<PocketSquareLayoutBinding>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding.title.text = "XXX"
    }
}

好像这样还可以,不能再省了,毕竟T这个是死的,必须要单独设置。

如果是Dialog,同样也可以创建类似的拓展函数:

inline fun <reified T : ViewBinding> Dialog.inflate() = lazy {
    inflateViewBinding<T>(layoutInflater).apply { setContentView(root) }
}

基类

不过这样还是要创建一个变量binding,能不能也省了呢?回到最初的基类设计,既然都用反射了,那就把这块逻辑都放基类吧。

abstract class BaseBindingActivity<T : ViewBinding> : BaseActivity() {

    protected lateinit var binding: T

    @Suppress("UNCHECKED_CAST")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val type = javaClass.genericSuperclass
        if (type is ParameterizedType) {
            val clazz = type.actualTypeArguments[0] as Class<T>
            val method = clazz.getMethod("inflate", LayoutInflater::class.java)
            binding = method.invoke(null, layoutInflater) as T
            contentView = binding.root
        }
    }
}

这样我们的调用终于干净了。

class PocketSquareActivity : BaseBindingActivity<PocketSquareLayoutBinding>() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding.title.text = "XXX"
    }
}

既然Activity这样可以,那么Fragment也同样的。

abstract class BaseBindingFragment<T : ViewBinding> : Fragment() {

    private var _binding: T? = null
    protected val binding get() = _binding!!

    @Suppress("UNCHECKED_CAST")
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        val type = javaClass.genericSuperclass
        val clazz = (type as ParameterizedType).actualTypeArguments[0] as Class<T>
        val method = clazz.getMethod("inflate", LayoutInflater::class.java, ViewGroup::class.java, Boolean::class.java)
        _binding = method.invoke(null, layoutInflater, container, false) as T
        return binding.root
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

但是这个地方有个问题,那就是onDestroyView里面的释放,下面来继续简化。

abstract class BaseBindingFragment<T : ViewBinding> : Fragment() {

    private var _binding: T? = null
    protected val binding get() = _binding!!

    @Suppress("UNCHECKED_CAST")
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        val type = javaClass.genericSuperclass
        val clazz = (type as ParameterizedType).actualTypeArguments[0] as Class<T>
        val method = clazz.getMethod("inflate", LayoutInflater::class.java, ViewGroup::class.java, Boolean::class.java)
        _binding = method.invoke(null, layoutInflater, container, false) as T
        this.viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
            override fun onDestroy(owner: LifecycleOwner) {
                _binding = null
            }
        })
        return binding.root
    }
}

通过lifecycle,就可以把释放的代码也包在基类中了,这样Fragment和Activity的使用就基本类似了。

而对于RecyclerView来说,我觉得没太大必要封装,具体使用可以查看前一篇文章。

不使用基类

上面都是基于继承来实现的封装,这样的好处是可以尽可能的简化子类的代码调用,但是缺点呢就是造成了一定的代码侵入,所以,如果不使用继承基类的方式,可以借助Kotlin的委托来实现调用的简化。

对于Activity来说,前面已经实现了。

inline fun <reified T : ViewBinding> Activity.inflate() = lazy {
    inflateViewBinding<T>(layoutInflater).apply { setContentView(root) }
}

val binding by inflate<PocketSquareLayoutBinding>()

而对于Fragment来说,稍微复杂一点,因为在Fragment里面inflate需要三个参数,和Activity不一样,特别是parent参数,不太好获取,所以这里可以使用bind来创建Binding,官网中也给出了这样的建议。

Note: The inflate() method requires you to pass in a layout inflater. If the layout has already been inflated, you can instead call the binding class's static bind() method. To learn more, see an example from the view binding GitHub sample.

所以,我们可以这样来创建委托。

inline fun <reified T : ViewBinding> Fragment.inflate() = FragmentViewBindingDelegate(T::class.java)

class FragmentViewBindingDelegate<T : ViewBinding>(private val clazz: Class<T>) : ReadOnlyProperty<Fragment, T> {
    private var binding: T? = null

    @Suppress("UNCHECKED_CAST")
    override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
        if (binding == null) {
            binding = clazz.getMethod("bind", View::class.java).invoke(null, thisRef.requireView()) as T
            thisRef.viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
                override fun onDestroy(owner: LifecycleOwner) {
                    binding = null
                }
            })
        }
        return binding!!
    }
}

使用方式和Activity一致。

这里实际上体现了Fragment的两种初始化Binding的方法,一种是在onCreateView中,一种是在onViewCreated中。

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,savedInstanceState: Bundle?): View? {
		val binding = XXXXXBinding.inflate(inflater, container, false)
		return binding.root
}
    
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
		super.onViewCreated(view, savedInstanceState)
		val binding = XXXXBinding.bind(view)
}

奇技淫巧

最近在medium上看到一种更有意思的封装方法,如果你能看懂这种方式,说明你对Kotlin的理解是比较深入的了。

首先,我们要了解Kotlin的「函数引用」。

在Kotlin中,通过::method这种方式,来将一个函数转为对象,转为对象之后,就可以当做参数进行传递了,这是一个很重要的高阶函数特性。我们可以把一个函数当做参数传递给另一个函数,这样在这个函数里面,就可以调用传进来的函数,从而——避免使用反射。

我们再回头看下前面的拓展函数,归根到底,我们实际上就是为了下面这个东西:

XXXLayoutBinding.inflate(layoutInflater)

这不就是XXXLayoutBinding的一个函数吗?所以我们可以借助函数引用,做下面的操作:

fun <T : ViewBinding> Activity.inflate(inflater: (LayoutInflater) -> T) = lazy {
    inflater(layoutInflater).apply { setContentView(root) }
}

函数引用不支持inline操作

对象函数的引用方式是——对象::函数名

静态函数的引用方式是——类名::函数名

我们将inflate拓展增加了一个高阶函数参数,这个高阶函数,就是XXXLayoutBinding.inflate方法。

使用也很简单:

val binding by inflate(PocketSquareLayoutBinding::inflate)

比前面使用反射的方式多了一个参数,但是好处是避免了反射。

文章出处在这里 zhuinden.medium.com/simple-one-…

再简单一点?

好像有点麻烦,但是好在Android Studio有代码模板功能,我们还可以尽量再简化一点。

打开AS的Live Templates,在Kotlin下增加一个模板,触发代码设置为「byVB」(当然你可以自定义):

private val binding by inflate($CLASS$::inflate)

这样我们在Kotlin代码中,直接输入byVB,代码就自动帮我们补全了,只用输入Binding类名即可。

ViewBinding的封装其实见仁见智,它的使用确实不如kotlin-android-extensions方便,但是天下大势所趋,也没有办法,随着Kotlin的升级,早晚还是要切换到ViewBinding。

向大家推荐下我的网站 xuyisheng.top/ 专注 Android-Kotlin-Flutter 欢迎大家访问