使用ViewBindingPropertyDelegate或Binding、ViewBindingKTX等第三方库,可以简化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委托结合使用,有几种不同的写法,下文来逐一介绍
这种是采用不反射的方式,性能上会比较好,但是在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)
}
}
这里使用到了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()
这一步了
我们也可以采用反射的方式,下面介绍的反射的其中一种写法,这种方式需要在AppCompatActivity()
中传入布局ID
class MainActivity2 : AppCompatActivity(R.layout.activity_main) {
private val binding :ActivityMainBinding by viewBinding()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
}
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文件可以看这里,可以和这里的反射对照着看
这是另外一种反射实现方式的写法,这种的好处是可以使用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)
}
}
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文件可以看这里,可以和这里的反射对照着看
看了这么多,总觉得不够完美,有没有一句代码就解决,不用在AppCompatActivity()
中传入layout布局ID
或不用写setContentView
的方式呢 ?
其实是有的,Binding这个库就支持,其写法如下,无需手动再写setContentView()
class MainActivity : AppCompatActivity() {
private val binding :ActivityMainBinding by viewbind()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
}
其原理也很简单,就是把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)
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));
}
}
看似炫酷的被施了魔法的private val binding :ActivityMainBinding by viewbind()
实现ViewBinding,其实就是用到了Kotlin委托
,并在委托类
的get()
方法中,通过不反射 / 反射 的方式 调用ActivityMainBinding.java
的inflate
或bind
方法,然后,可以将setContentView()
也在此处进行调用,这样,就不用再去写ViewBinding
常规的那些代码了。
具体代码可以下载这个 ViewBinding与Kotlin委托结合使用,原理伪代码,相当于是手写了一个简单的ViewBindingPropertyDelegate和Binding库