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)
}

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

private val binding :ActivityMainBinding by viewbind()

这看上像是被施了魔法,很神奇,实际内部到底做了什么事呢 ?
市面上的相关库,将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)
    }
}

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)
    }
}

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")
    }
}

然后进行使用,就可以省略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)
    }
}

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")
    }
}

可以看到,其内部,主要是先通过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)
    }
}

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
    }
}

可以看到,其内部,主要是先通过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)
    }
}

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)

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));
  }
}

7. 总结

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

8. 本文代码下载

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

猜你喜欢

转载自blog.csdn.net/EthanCo/article/details/126739511