• ViewBinding与Kotlin委托结合使用,去除setContentView,其实现原理解析


    1. 前言

    使用ViewBindingPropertyDelegateBindingViewBindingKTX等第三方库,可以简化Android ViewBinding的使用。

    比如常规的ViewBinding代码

    private lateinit var binding: ActivityMainBinding
    
    override fun onCreate(savedInstanceState: Bundle?) {
    	super.onCreate(savedInstanceState)
    	binding = ActivityMainBinding.inflate(layoutInflater)
    	setContentView(binding.root)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    可以替换为这样一句,甚至不需要setContentView()

    private val binding :ActivityMainBinding by viewbind()
    
    • 1

    这看上像是被施了魔法,很神奇,实际内部到底做了什么事呢 ?
    市面上的相关库,将ViewBinding与Kotlin委托结合使用,有几种不同的写法,下文来逐一介绍

    2. ViewBindingPropertyDelegate 不反射的方式

    这种是采用不反射的方式,性能上会比较好,但是在viewBinding()需要传参ActivityMainBinding::bind,在AppCompatActivity()中传入布局ID

    class MainActivity : AppCompatActivity(R.layout.activity_main) {
        private val binding : ActivityMainBinding by viewBinding(ActivityMainBinding::bind)
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    2.1 原理

    这里使用到了Kotlin委托ActivityViewBindings就是一个Kotlin委托类,当获取binding的时候,去触发fun getValue(thisRef: A, property: KProperty<*>): T
    关于Kotlin委托可以看我的另一篇博客 看似普通的Android开发黑科技 - Kotlin 委托

    class ActivityViewBindings<in A : ComponentActivity, out T : ViewBinding>(private val viewBinder: (A) -> T) :
        ReadOnlyProperty<A, T> {
        override fun getValue(thisRef: A, property: KProperty<*>): T {
            return viewBinder(thisRef)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    vbFactory: (View) -> T是我们从MainActivity传入的,实际就是在调用ActivityMainBinding::bind

    public inline fun <A : ComponentActivity, T : ViewBinding> ComponentActivity.viewBinding(
        crossinline vbFactory: (View) -> T,
        crossinline viewProvider: (A) -> View = ::findRootView
    ): ActivityViewBindings<A, T> {
        return ActivityViewBindings { activity -> vbFactory(viewProvider(activity)) }
    }
    
    fun findRootView(activity: Activity): View {
        val contentView = activity.findViewById<ViewGroup>(android.R.id.content)
        return when (contentView.childCount) {
            1 -> contentView.getChildAt(0)
            else -> error("error")
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    然后进行使用,就可以省略ActivityMainBinding.bind()这一步了

    3. ViewBindingPropertyDelegate 反射的方式一

    我们也可以采用反射的方式,下面介绍的反射的其中一种写法,这种方式需要在AppCompatActivity()中传入布局ID

    class MainActivity2 : AppCompatActivity(R.layout.activity_main) {
        private val binding :ActivityMainBinding by viewBinding()
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    3.1 原理

    class ActivityViewBindings<in A : ComponentActivity, out T : ViewBinding>(private val viewBinder: (A) -> T) :
        ReadOnlyProperty<A, T> {
        private var viewBinding: T? = null
    
        override fun getValue(thisRef: A, property: KProperty<*>): T {
            viewBinding?.let { return it }
            viewBinding = viewBinder(thisRef)
            return viewBinding!!
        }
    }
    
    public inline fun <A : ComponentActivity, T : ViewBinding> ComponentActivity.viewBinding(
        crossinline vbFactory: (View) -> T,
        crossinline viewProvider: (A) -> View = ::findRootView
    ): ActivityViewBindings<A, T> {
        return ActivityViewBindings { activity -> vbFactory(viewProvider(activity)) }
    }
    
    public fun <T : ViewBinding> ComponentActivity.viewBinding(
        viewBindingClass: Class<T>,
        rootViewProvider: (ComponentActivity) -> View,
    ): ActivityViewBindings<ComponentActivity, T> {
        return viewBinding<ComponentActivity, T>({ view ->
            BindViewBinding(viewBindingClass).bind(rootViewProvider(this))
        })
    }
    
    public inline fun <reified T : ViewBinding> ComponentActivity.viewBinding(): ActivityViewBindings<ComponentActivity, T> {
        return viewBinding(T::class.java, ::findRootView)
    }
    
    internal class BindViewBinding<out VB : ViewBinding>(viewBindingClass: Class<VB>) {
    
        private val bindViewBinding = viewBindingClass.getMethod("bind", View::class.java)
    
        fun bind(view: View): VB {
            //return bindViewBinding(null, view) as VB
            return bindViewBinding.invoke(null, view) as VB
        }
    }
    
    fun findRootView(activity: Activity): View {
        val contentView = activity.findViewById<ViewGroup>(android.R.id.content)
        return when (contentView.childCount) {
            1 -> contentView.getChildAt(0)
            else -> error("error")
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    可以看到,其内部,主要是先通过BindViewBinding中去调用viewBindingClass.getMethod("bind", View::class.java)通过反射获取到bind方法,然后,调用bind方法,从而获取到ActivityMainBinding

    ActivityMainBinding.java文件可以看这里,可以和这里的反射对照着看

    4. ViewBindingPropertyDelegate 反射的方式二

    这是另外一种反射实现方式的写法,这种的好处是可以使用setContentView()

    class MainActivity4 : AppCompatActivity() {
    	//ViewBindingPropertyDelegate中,实际是传入CreateMethod.INFLATE,这里示例简化了,改用viewBindingInflate
    	//private val viewBindingUsingReflection: ActivityProfileBinding by viewBinding(CreateMethod.INFLATE)
        private val binding :ActivityMainBinding by viewBindingInflate()
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(binding.root)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    4.1 原理

    public inline fun <reified T : ViewBinding> ComponentActivity.viewBindingInflate(): ActivityViewBindings<ComponentActivity, T> {
        return viewBindingInflate(T::class.java, ::findRootView)
    }
    
    private var mBinding: ActivityMainBinding? = null
    
    public fun <T : ViewBinding> ComponentActivity.viewBindingInflate(
        viewBindingClass: Class<T>,
        rootViewProvider: (ComponentActivity) -> View,
    ): ActivityViewBindings<ComponentActivity, T> {
    
        return ActivityViewBindings<ComponentActivity, T> { activity ->
            val inflateViewBinding = InflateViewBinding(viewBindingClass)
            inflateViewBinding.inflate(
                layoutInflater,
                null,
                false
            )
        }
    }
    
    internal abstract class InflateViewBinding<out VB : ViewBinding>(
        private val inflateViewBinding: Method
    ) {
        abstract fun inflate(
            layoutInflater: LayoutInflater,
            parent: ViewGroup?,
            attachToParent: Boolean
        ): VB
    }
    
    internal fun <VB : ViewBinding> InflateViewBinding(viewBindingClass: Class<VB>): InflateViewBinding<VB> {
        try {
            Log.i("TAG", "InflateViewBinding:$viewBindingClass")
            val method = viewBindingClass.getMethod(
                "inflate", LayoutInflater::class.java, ViewGroup::class.java, Boolean::class.java
            )
            return FullInflateViewBinding(method)
        } catch (e: NoSuchMethodException) {
            Log.e("TAG", "InflateViewBinding NoSuchMethodException:$e")
        }
    }
    
    internal class FullInflateViewBinding<out VB : ViewBinding>(
        private val inflateViewBinding: Method
    ) : InflateViewBinding<VB>(inflateViewBinding) {
    
        override fun inflate(
            layoutInflater: LayoutInflater,
            parent: ViewGroup?,
            attachToParent: Boolean
        ): VB {
            return inflateViewBinding(null, layoutInflater, parent, attachToParent) as VB
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    可以看到,其内部,主要是先通过FullInflateViewBinding中去调用viewBindingClass.getMethod("bind", View::class.java)获取到inflate方法,然后调用inflate方法,从而获取到ActivityMainBinding

    ActivityMainBinding.java文件可以看这里,可以和这里的反射对照着看

    5 Binding库 反射的方式

    看了这么多,总觉得不够完美,有没有一句代码就解决,不用在AppCompatActivity()中传入layout布局ID或不用写setContentView的方式呢 ?

    其实是有的,Binding这个库就支持,其写法如下,无需手动再写setContentView()

    class MainActivity : AppCompatActivity() {
        private val binding :ActivityMainBinding by viewbind()
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    5.1 原理

    其原理也很简单,就是把setContentView的过程也放入到了ActivityViewBinding这个kotlin委托类中了

    class ActivityViewBinding<T : ViewBinding>(
        classes: Class<T>,
        val activity: Activity
    ) : ReadOnlyProperty<Activity,T> {
    
        protected var viewBinding: T? = null
        private var layoutInflater = classes.inflateMethod()
    
        override fun getValue(thisRef: Activity, property: KProperty<*>): T {
            return viewBinding?.run {
                this
            } ?: let {
                // 获取 ViewBinding 实例
                val bind = layoutInflater.invoke(null, thisRef.layoutInflater) as T
                // setContentView
                thisRef.setContentView(bind.root)
                return bind.apply { viewBinding = this }
            }
        }
    }
    
    inline fun <reified T : ViewBinding> Activity.viewbind2() =
        ActivityViewBinding(T::class.java, this)
    
    fun <T> Class<T>.inflateMethod() = getMethod("inflate", LayoutInflater::class.java)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    6. ActivityMainBinding.java 文件

    ViewBInding生成的文件路径为app\build\generated\data_binding_base_class_source_out\debug\out\com\heiko\koltintest\databinding,此处提供该文件,用于和上述文中反射相对照

    public final class ActivityMainBinding implements ViewBinding {
      @NonNull
      private final ConstraintLayout rootView;
    
      @NonNull
      public final Button btn1;
    
      private ActivityMainBinding(@NonNull ConstraintLayout rootView, @NonNull Button btn1) {
        this.rootView = rootView;
        this.btn1 = btn1;
      }
    
      @Override
      @NonNull
      public ConstraintLayout getRoot() {
        return rootView;
      }
    
      @NonNull
      public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater) {
        return inflate(inflater, null, false);
      }
    
      @NonNull
      public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater,
          @Nullable ViewGroup parent, boolean attachToParent) {
        View root = inflater.inflate(R.layout.activity_main, parent, false);
        if (attachToParent) {
          parent.addView(root);
        }
        return bind(root);
      }
    
      @NonNull
      public static ActivityMainBinding bind(@NonNull View rootView) {
        // The body of this method is generated in a way you would not otherwise write.
        // This is done to optimize the compiled bytecode for size and performance.
        int id;
        missingId: {
          id = R.id.btn_1;
          Button btn1 = ViewBindings.findChildViewById(rootView, id);
          if (btn1 == null) {
            break missingId;
          }
    
          return new ActivityMainBinding((ConstraintLayout) rootView, btn1);
        }
        String missingId = rootView.getResources().getResourceName(id);
        throw new NullPointerException("Missing required view with ID: ".concat(missingId));
      }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    7. 总结

    看似炫酷的被施了魔法的private val binding :ActivityMainBinding by viewbind()实现ViewBinding,其实就是用到了Kotlin委托,并在委托类get()方法中,通过不反射 / 反射 的方式 调用ActivityMainBinding.javainflatebind方法,然后,可以将setContentView()也在此处进行调用,这样,就不用再去写ViewBinding常规的那些代码了。

    8. 本文代码下载

    具体代码可以下载这个 ViewBinding与Kotlin委托结合使用,原理伪代码,相当于是手写了一个简单的ViewBindingPropertyDelegateBinding

  • 相关阅读:
    「格创东智」获数亿元B轮融资,深度聚焦半导体和新能源数智升级
    大数据安全 | 【实验】RSA加密解密
    golang 切片(slice)简单使用
    STL容器 —— bitset
    从 jsonpath 和 xpath 到 SPL
    【Dison夏令营 Day 18】如何用 Python 中的 Pygame 制作国际象棋游戏
    JavaAgent寄生在目标进程中引起的ClassNotFoundException
    Jenkins - 邮件通知 Email Notification 插件
    Keras深度学习实战(24)——从零开始构建单词向量
    【HTML5期末大作业】制作一个简单HTML我的班级网页(HTML+CSS+JS)
  • 原文地址:https://blog.csdn.net/EthanCo/article/details/126739511