主动完结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)

这样会报错的,由于运转期间泛型会被擦除也无法实例化对应的目标。

那…可怎么是好呐。

【Android】只给个泛型,怎么主动初始化ViewModel与ViewBinding?这几种计划值得了解

其实咱们想要在基类完结泛型的实例化,咱们目前是有两种思路,一种是反射获取到泛型的实例,一种是经过编译器代码生成完结目标的实例创立,其中又分为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 开发的场景,特别仍是这种只调用一次的场景,其实运转速度不同真的不大。

混杂,这才是大问题,反射代码在混杂进程中咱们需求额定的留意,由于类和成员的名称或许会被改动。假如不正确配置混杂规矩,或许导致在运转时无法正确地经过名称找到相应的类、办法或字段,引发反常。

例如咱们混杂打包之后,假如经过反射,必须确保反射的直接目标需求保存不被混杂。

咱们注释掉混杂规矩

# 保持ViewModelViewBinding不混杂,否则无法反射主动创立
-keep class * implements androidx.viewbinding.ViewBinding { *; }
-keep class * extends androidx.lifecycle.ViewModel { *; }

然后反编译咱们的apk很简略的就能找到为混杂的类:

【Android】只给个泛型,怎么主动初始化ViewModel与ViewBinding?这几种计划值得了解

类型安全与可读性 反射调用减少了编译时类型查看的时机,添加了运转时过错的危险。例如,假如经过反射过错地调用了办法或访问了字段,或许会在运转时引发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>() {}

生成的代码:

【Android】只给个泛型,怎么主动初始化ViewModel与ViewBinding?这几种计划值得了解

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办法中主动调用给 mViewModelmViewBinding 这两个固定的字段赋值,可是这有点”硬编码”的意思了,一旦在基类中修改了这个变量的名字就会导致反常,假如你确保不会变动,其实也能够直接用字节码插桩或许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,这一期就此完结。

【Android】只给个泛型,怎么主动初始化ViewModel与ViewBinding?这几种计划值得了解