Android ViewBinding,记一次化繁为简的探索过程

Kotlin 升级到1.4.20之后,正式废弃了 kotlin-android-extensions 插件,将其Parcelable相关功能迁移到新插件kotlin-parcelize,并推荐使用**ViewBinding**进行视图绑定。

JakeWharton大神的ButterKnife也早已停止维护,也推荐迁移到ViewBinding

kotlin-android-extensions插件主要提供了两个很方便的功能:

  1. 直接通过布局的id就可以获得对应的控件对象(简直不要太爽)
  2. 提供了 @Parcelize 注解帮助开发者快速实现 Parcelize

这个插件优点就是用起来特别舒畅,直接用控件id就可以搞定了。主要缺点也是比较明显的,否则也不会被放弃了:

  1. 不能提供可空信息,不同layout文件,id不一致,不能在编译时发现(但会提供警告)
  2. 仅支持kotlin
  3. 污染全局命名空间

为了解决这些痛点,Android官方推出了ViewBinding解决方案,当不同layout文件,id不一致时,会在属性上声明为@Nullable,从而提供了可空信息

Android ViewBinding,记一次化繁为简的探索过程_第1张图片

官方的viewBinding虽然解决了上述的问题,缺也存在了明显的缺陷:使用过于繁琐!!,而且在fragment中使用时,还得额外在onDestroyView中释放binding,防止造成内存泄漏,会产生大量无意义的重复代码!!

二 ViewBinding的繁琐

这里只展示简单的fragment使用,介绍问题。更多使用细节可以查阅官方指南或Exploring Android View Binding in Depth(这里有篇很详细的译文)

ViewBinding从Android Studio 3.6开始,就是内置在Gradle插件中了,开发者不需要额外添加库来开启,在模块级的build.gradle文件

// Android Studio 3.6
android {
    viewBinding {
        enabled = true
    }
}

// Android Studio 4.0
android {
    buildFeatures {
        viewBinding = true
    }
}

启用视图绑定功能后,系统会为该模块中的每个 XML 布局文件生成一个绑定类。每个绑定类均包含对根视图以及具有 ID 的所有视图的引用。绑定类的名称生成方式为:将 XML 文件的名称转换为驼峰式大小写,并在末尾添加“Binding”一词。如fragment_binding则为FragmentBindingBinding

make project之后就会生成绑定类文件,可在模块级别的build/generated/data_binding_base_class_source_out/目录下找到

fragment中使用ViewBinding,常见问题就是内存的泄漏,因为fragment的生命周期长于其视图的生命周期。所以得在onDestroyView中释放视图,如使用指南中的代码所示:

...
    private var _binding: ResultProfileBinding? = null
    // This property is only valid between onCreateView and
    // onDestroyView.
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = ResultProfileBinding.inflate(inflater, container, false)
        val view = binding.root
        return view
    }

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

每次都得手动进行释放。如果只有简单几个页面,倒也不是什么问题,但当项目变大时,就得不断重复这些繁琐的代码,而且activity的使用方式也是不同的,这就造成项目中会生成大量重复代码

三 DRY — Don’t repeat yourself

这里讲两种可行性的思路,一种是改动基类进行封装,另一种则是借用kotlin的委托属性功能结合反射进行封装(但只适合kotlin,使用便捷)

3.1 基于基类的封装

这里就只提供了一个fragment的思路,其他的可以类似实现,相对也是比较简单的。

abstract class BaseFragment(layoutId: Int) : Fragment(layoutId) {
    private var _binding: T? = null

    val binding
        get() = _binding!!

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        _binding = initBinding(view)
    }

    abstract fun initBinding(view: View): T

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

class MainFragment :BaseFragment(R.layout.fragment_main) {

    override fun initBinding(view: View): FragmentMainBinding = FragmentMainBinding.bind(view)

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding.tvContent = "嗨起来"
    }
}

3.2 委托属性+反射

对kotlin的委托属性不了解的可以查看一文彻底搞懂Kotlin中的委托

这里也只提供了一个针对fragment的思路,其他的可以类似实现。

完成之后使用直接一个by bind()即可:

class BindingFragment:Fragment(R.layout.fragment_binding) {

    private val binding:FragmentBindingBinding by bind()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        Log.e("binding","${binding.tvContent.text}")
        binding.tvContent.text = "new value"
    }
}

首先实现扩展方法,返回直接委托对象FragmentBindingDelegate

inline fun  Fragment.bind(): FragmentBindingDelegate {
    return FragmentBindingDelegate(T::class.java, this)
}

委托对象代码如下:

class FragmentBindingDelegate(classes: Class, fragment: Fragment) :
    ReadOnlyProperty {

    private var binding: T? = null

    private val bindViewMethod by lazy { classes.getMethod("bind", View::class.java) }

    init {
        fragment.lifecycle.donOnDestroy { release() }
    }

    @Suppress("UNCHECKED_CAST")
    override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
        return binding ?: let {
            Log.e("binding","generate value")
            (bindViewMethod.invoke(null, thisRef.view) as T).also { binding = it }
        }
    }

    private fun release() {
        Log.e("binding","release binding")

        binding = null
    }
}
  1. binding属性,在不为null时,直接返回该对象:return binding
  2. binding属性还未赋值的时候,反射并调用ViewBindingbind(view)方法:classes.getMethod("bind", View::class.java),并赋值给binding属性
  3. 注册生命周期监听,在ON_DESTROY的时候释放持有的binding对象: fragment.lifecycle.donOnDestroy { release() }

Lifecycle的扩展函数如下,在ON_DESTROY事件时释放资源并移除自身监听

inline fun Lifecycle.donOnDestroy(crossinline destroyed: () -> Unit) {
    addObserver(object : LifecycleEventObserver {
        override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
            if (event == Lifecycle.Event.ON_DESTROY) {
                destroyed()
                source.lifecycle.removeObserver(this)
            }
        }
    })
}

这种实现方案思路也是比较清晰和简单的,但因为是使用了委托by,所以必须至少有一个地方使用了binding,才会触发一系列的赋值操作!!所以如果你使用了这种方案去实现activity,并将setContentView()也封装进委托方法里,但没有任何地方触发binding操作,就不会将view给设置进去

如果有其他更好的方案,强烈欢迎指出!!


 

你可能感兴趣的:(移动开发,Android,程序人生,Android,View,binding)