主动完结VM和VB初始化的几种计划
前言
例如咱们的 Activity/Fragment 内部的目标初始化,假如是惯例的通用的目标初始化,咱们当然能够在基类中就界说了。可是关于一些相似ViewModel,ViewBindig之类的目标初始化,咱们需求清晰知道是哪一个类型才干初始化的怎么办?
相似咱们在具体的页面界说泛型:
class ProfileActivity : BaseActivity<ProfileViewModel, ActivityProfileBinding>() {}
可是我不想在具体的页面去写这些手动的调用:
ViewModelProvider(owner).get(%T::class.java)
%T.inflate(layoutInflater)
或许基类笼统完结:
public abstract class BaseActivity<VM extends ViewModel, VB extends ViewBinding> extends AppCompatActivity {
protected VM viewModel;
protected VB binding;
protected abstract Class<VM> getViewModelClass();
protected abstract VB inflateViewBinding(LayoutInflater inflater);
}
具体仍是让子类去完结:
public class ProfileActivity extends BaseActivity<ProfileViewModel, ActivityProfileBinding> {
@Override
protected Class<ProfileViewModel> getViewModelClass() {
return ProfileViewModel.class;
}
@Override
protected ActivityProfileBinding inflateViewBinding(LayoutInflater inflater) {
return ActivityProfileBinding.inflate(inflater);
}
// ...
}
能够是能够,可是好麻烦哦,我想只给个泛型,让基类去主动帮我初始化,能不能直接在基类中:
ViewModelProvider(this).get(VM::class.java)
VB.inflate(inflater)
这样会报错的,由于运转期间泛型会被擦除也无法实例化对应的目标。
那…可怎么是好呐。
其实咱们想要在基类完结泛型的实例化,咱们目前是有两种思路,一种是反射获取到泛型的实例,一种是经过编译器代码生成完结目标的实例创立,其中又分为APT代码生成和ASM字节码插桩两个小分支。
一、运用反射
往常咱们的封装就算再简略,咱们也需求传入 ViewMiel 的 class 目标,或许 DataBinding::inflate 目标。
abstract class BaseVDBActivity<VM : ViewModel,VB : ViewBinding>(
private val vmClass: Class<VM>, private val vb: (LayoutInflater) -> VB,
) : AppCompatActivity() {
}
class MainActivity : BaseVDBActivity<ActivityMainBinding, MainViewModel>(
ActivityMainBinding::inflate,
MainViewModel::class.java
){}
相似于上面这种写法,界说了泛型目标,在经过构造传入对应的Class目标和函数目标。咱们才干在基类中正常的初始化 ViewModel 和 ViewBinding ,这是很好的封装办法,性能也好,没用到反射,其完成已很优秀了,你肯定能够运用这种办法封装。
本文咱们也是从懒人的视点看,除了这种办法之外咱们还能用哪些更“懒”的办法来完结主动的初始化。
这里就得说到反射的作用了。
abstract class BaseActivity<VM : ViewModel, VB : ViewBinding> : AppCompatActivity() {
protected lateinit var viewModel: VM
protected lateinit var binding: VB
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = getViewModelInstance()
binding = getViewBindingInstance(layoutInflater)
setContentView(binding.root)
}
private fun getViewModelInstance(): VM {
val superClass = javaClass.genericSuperclass as ParameterizedType
val vmClass = (superClass.actualTypeArguments[0] as Class<VM>).kotlin
return ViewModelProvider(this, ViewModelProvider.NewInstanceFactory()).get(vmClass.java)
}
private fun getViewBindingInstance(inflater: LayoutInflater): VB {
val superClass = javaClass.genericSuperclass as ParameterizedType
val vbClass = superClass.actualTypeArguments[1] as Class<VB>
val method = vbClass.getMethod("inflate", LayoutInflater::class.java)
return method.invoke(null, inflater) as VB
}
}
咱们指定第一个泛型为ViewModel,第二个泛型为ViewBinding,那么咱们就能找到当时类的泛型目标的class,更进一步咱们甚至能经过反射调用它的办法得到 VB 的实例目标。
此时咱们就能到达文章开始的效果:
class ProfileActivity : BaseActivity<ProfileViewModel, ActivityProfileBinding>() {}
反射的计划有没有缺陷?
反射太慢了或许有些人会信口开河,其实反射真的慢吗?这不属于本文讨论的规模,跟着越多越多的一些对比评测【传送门】咱们其实也明白过来,反射其实并没有比正常调用慢多少。
尽管反射需求在运转时动态解析类的元数据,履行安全权限查看,以及进行办法调用,尽管反射调用时,JVM会进行额定的安全查看,添加了性能开支,可是假如调用次数很少根本和正常办法调用差异不大,特别是关于 Android 开发的场景,特别仍是这种只调用一次的场景,其实运转速度不同真的不大。
混杂,这才是大问题,反射代码在混杂进程中咱们需求额定的留意,由于类和成员的名称或许会被改动。假如不正确配置混杂规矩,或许导致在运转时无法正确地经过名称找到相应的类、办法或字段,引发反常。
例如咱们混杂打包之后,假如经过反射,必须确保反射的直接目标需求保存不被混杂。
咱们注释掉混杂规矩
# 保持ViewModel和ViewBinding不混杂,否则无法反射主动创立
-keep class * implements androidx.viewbinding.ViewBinding { *; }
-keep class * extends androidx.lifecycle.ViewModel { *; }
然后反编译咱们的apk很简略的就能找到为混杂的类:
类型安全与可读性 反射调用减少了编译时类型查看的时机,添加了运转时过错的危险。例如,假如经过反射过错地调用了办法或访问了字段,或许会在运转时引发ClassCastException等反常,并且由于是硬编码不好调试不说,假如被反射方改动了办法那么会添加过错的危险。
二、运用APT代码生成
其实比较ASM的字节码插桩,运用APT生成代码相对简略许多,咱们能够生成对应的 ViewBinding 和 ViewModel 的初始化目标。
假如你不会 APT 的代码生成,那么跟着过一遍就回了,下面的代码会给出具体的注释。
咱们先界说对应的注解:
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class AutoInject
咱们添加 auto-service 和 kotlinpoet 代码生成器的依靠
implementation 'com.squareup:kotlinpoet:1.4.0'
compileOnly "com.google.auto.service:auto-service:1.0.1"
kapt 'com.google.auto.service:auto-service:1.0.1'
auto-service
是一种用于生成APT(Annotation Processing Tool)代码的计划之一。APT是Java编译器供给的一个东西,用于在编译期间处理注解,并生成相应的代码。
auto-service
是一个Google开源的库,它简化了运用APT生成代码的进程。它供给了一个注解@AutoService
和一个笼统类AutoService
,经过在完结类上添加@AutoService
注解,并继承AutoService
笼统类,能够主动生成用于注册该完结类的META-INF/services
文件。
在你的代码中,你运用了auto-service
库,并运用@AutoService
注解和AutoService
笼统类来主动生成META-INF/services
文件,用于注册你的注解处理器。这样,当你的项目构建时,编译器会主动调用APT并生成相应的代码。
kotlinpoet 是一个用于生成 Kotlin 代码的库,由 Square 公司开发。KotlinPoet 经过供给一个强壮的 DSL(领域特定语言)来协助开发者编程地构建 Kotlin 源文件。这个库特别合适那些需求主动生成 Kotlin 代码的场景,比方编写编译时注解处理器(Annotation Processors)或是其他需求生成 Kotlin 代码的东西。
两者经常被一同运用,尤其是在创立编译时注解处理器时,当你编写一个注解处理器来处理注解时,或许会用到 KotlinPoet 来生成一些 Kotlin 代码,同时用 AutoService 来注册注解处理器,使得在编译时能够被 javac 东西主动发现和调用。这样能够大大简化注解处理器的开发进程,使得开发者更专心于处理注解的逻辑,而不是服务文件的细节。
本场景的 Processor 界说如下:
@Suppress("unused")
@AutoService(Processor::class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedOptions(AutoInjectAnnotationProcessor.KAPT_KOTLIN_GENERATED_OPTION_NAME)
class AutoInjectAnnotationProcessor : AbstractProcessor() {
override fun getSupportedAnnotationTypes(): Set<String> = setOf(
AutoInject::class.java.canonicalName,
)
override fun process(annotations: MutableSet<out TypeElement>?, roundEnv: RoundEnvironment): Boolean {
roundEnv.getElementsAnnotatedWith(AutoInject::class.java).forEach { element ->
if (element.kind != ElementKind.CLASS) {
processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, "@AutoInject can only be applied to classes.")
return true
}
val typeElement = element as TypeElement
generateCodeForViewModel(element, typeElement)
}
return true
}
private fun generateCodeForViewModel(element: Element, typeElement: TypeElement) {
val className = element.simpleName.toString()
val pack = processingEnv.elementUtils.getPackageOf(element).toString()
val fileName = "${className}ViewModelInit"
val superClassType = (element as TypeElement).superclass //泛型指定在父类上
val typeArguments = (superClassType as? DeclaredType)?.typeArguments ?: emptyList() //获取到所有的泛型
if (typeArguments.isEmpty()) return // 假如没有泛型参数,则不生成代码
val viewModelName = typeArguments[0].asTypeName().toString() // 第一个泛型参数总是用于ViewModel
val typeSpecBuilder = TypeSpec.classBuilder(fileName) // 生成的首要类
.addModifiers(KModifier.PUBLIC) // 指定类是公有的
// 添加办法provideViewModel
typeSpecBuilder.addFunction(
FunSpec.builder("provideViewModel") // 办法名
.addModifiers(KModifier.PUBLIC) // 指定办法是公有的
.addParameter("owner", ClassName("androidx.lifecycle", "ViewModelStoreOwner")) // 参数
.returns(ClassName(pack, viewModelName)) // 回来类型
.addStatement("return ViewModelProvider(owner).get(%T::class.java)", ClassName(pack, viewModelName)) // 具体的办法
.build()
)
// 假如有第二个泛型参数,则生成provideViewBinding办法
if (typeArguments.size > 1) {
val viewBindingName = typeArguments[1].asTypeName().toString()
typeSpecBuilder.addFunction(
FunSpec.builder("provideViewBinding") // 办法名
.addModifiers(KModifier.PUBLIC) // 指定办法是公有的
.addParameter("layoutInflater", ClassName("android.view", "LayoutInflater")) // 参数
.returns(ClassName(pack, viewBindingName)) // 回来类型
.addStatement("return %T.inflate(layoutInflater)", ClassName(pack, viewBindingName)) // 具体的办法
.build()
)
}
val fileSpec = FileSpec.builder(pack, fileName)
.addImport("androidx.lifecycle", "ViewModelProvider") //指定导入类
.addImport("android.view", "LayoutInflater")
.addType(typeSpecBuilder.build())
.build()
val kaptKotlinGeneratedDir = processingEnv.options[KAPT_KOTLIN_GENERATED_OPTION_NAME]
fileSpec.writeTo(File(kaptKotlinGeneratedDir, "$fileName.kt"))
}
companion object {
const val KAPT_KOTLIN_GENERATED_OPTION_NAME = "kapt.kotlin.generated"
}
}
那么咱们只需求标记哪些类需求生成对应的文件即可,例如:
@AutoInject
class ProfileActivity : BaseActivity<ProfileViewModel, ActivityProfileBinding>() {}
生成的代码:
public class ProfileActivityViewModelInit {
public fun provideViewModel(owner: ViewModelStoreOwner): com.newki.profile.mvi.vm.ProfileViewModel
= ViewModelProvider(owner).get(com.newki.profile.mvi.vm.ProfileViewModel::class.java)
public fun provideViewBinding(layoutInflater: LayoutInflater):
com.newki.profile.databinding.ActivityProfileBinding =
com.newki.profile.databinding.ActivityProfileBinding.inflate(layoutInflater)
}
基类中调用:
protected open fun createViewBinding() {
try {
val currentPackageName = this::class.java.`package`?.name
val className = currentPackageName + "." + this::class.java.simpleName + "ViewModelInit"
val generatedClass = Class.forName(className)
val method = generatedClass.getDeclaredMethod("provideViewBinding", LayoutInflater::class.java)
val generatedClassInstance = generatedClass.getDeclaredConstructor().newInstance()
_binding = method.invoke(generatedClassInstance, layoutInflater) as VB
} catch (e: Exception) {
e.printStackTrace()
}
}
protected open fun createViewModel(): VM {
try {
val currentPackageName = this::class.java.`package`?.name
val className = currentPackageName + "." + this::class.java.simpleName + "ViewModelInit"
val generatedClass = Class.forName(className)
val method = generatedClass.getDeclaredMethod("provideViewModel", ViewModelStoreOwner::class.java)
val generatedClassInstance = generatedClass.getDeclaredConstructor().newInstance()
return method.invoke(generatedClassInstance, this) as VM
} catch (e: Exception) {
e.printStackTrace()
}
}
咱们同样能够无感的在基类主动创立对应的初始化代码,需求留意的是我同样需求混杂生成的代码
#自界说的主动注入生成类,保护完结
-keep class **.*ViewModelInit { *; }
当然了,理论上咱们能够直接在 ASM 字节码插桩生成的代码中直接在onCreate办法中主动调用给 mViewModel
和 mViewBinding
这两个固定的字段赋值,可是这有点”硬编码”的意思了,一旦在基类中修改了这个变量的名字就会导致反常,假如你确保不会变动,其实也能够直接用字节码插桩或许AOP面向切面主动赋值到这两个变量中。
跋文
本文具体介绍了常用的三种封装计划,所以这三种计划你更喜爱哪一种?
原始的:
abstract class BaseActivity<VM : ViewModel,VB : ViewBinding>(
private val vmClass: Class<VM>, private val vb: (LayoutInflater) -> VB,
) : AppCompatActivity() {
}
class ProfileActivity : BaseActivity<ActivityProfileBinding, ProfileViewModel>(
ActivityProfileBinding::inflate,
ProfileViewModel::class.java
){}
反射:
abstract class BaseActivity<VM : ViewModel,VB : ViewBinding>(){
//...
private fun getViewModelInstance(): VM {
val superClass = javaClass.genericSuperclass as ParameterizedType
val vmClass = (superClass.actualTypeArguments[0] as Class<VM>).kotlin
return ViewModelProvider(this, ViewModelProvider.NewInstanceFactory()).get(vmClass.java)
}
private fun getViewBindingInstance(inflater: LayoutInflater): VB {
val superClass = javaClass.genericSuperclass as ParameterizedType
val vbClass = superClass.actualTypeArguments[1] as Class<VB>
val method = vbClass.getMethod("inflate", LayoutInflater::class.java)
return method.invoke(null, inflater) as VB
}
}
运用
class ProfileActivity : BaseActivity<ProfileViewModel, ActivityProfileBinding>() {}
APT的运用:
@AutoInject //自界说注解,自己界说
class ProfileActivity : BaseActivity<ProfileViewModel, ActivityProfileBinding>() {}
总的来说三种计划各有利弊,都是能够完结的,用哪一种计划完全看自己的志愿。
假如你有其他的更多的更好的完结办法,也期望咱们能评论区沟通一同学习进步。假如我的文章有错别字,不通畅的,或许代码、注释、有讹夺的当地,同学们都能够指出修正。
本文代码都已在文中贴出,内部的代码参考的项目为 【2024年Android项目开发模板开源】 。
假如感觉本文对你有一点的启示和协助,还望你能点赞
支撑一下,你的支撑对我真的很重要。
Ok,这一期就此完结。