【Android】只给个泛型,如何自动初始化ViewModel与ViewBinding?这几种方案值得了解

自动完成VM和VB初始化的几种方案

前言

例如我们的 Activity/Fragment 内部的对象初始化,如果是常规的通用的对象初始化,我们当然可以在基类中就定义了。但是对于一些类似ViewModel,ViewBindig之类的对象初始化,我们需要明确知道是哪一个类型才能初始化的怎么办?

类似我们在具体的页面定义泛型:

kotlin 复制代码
class ProfileActivity : BaseActivity<ProfileViewModel, ActivityProfileBinding>() {}

但是我不想在具体的页面去写这些手动的调用:

scss 复制代码
ViewModelProvider(owner).get(%T::class.java)
%T.inflate(layoutInflater)

或者基类抽象实现:

scala 复制代码
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);
}

具体还是让子类去实现:

scala 复制代码
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);
    }

    // ...
}

可以是可以,但是好麻烦哦,我想只给个泛型,让基类去自动帮我初始化,能不能直接在基类中:

kotlin 复制代码
ViewModelProvider(this).get(VM::class.java)
VB.inflate(inflater)

这样会报错的,因为运行期间泛型会被擦除也无法实例化对应的对象。

那...可如何是好呐。

其实我们想要在基类完成泛型的实例化,我们目前是有两种思路,一种是反射获取到泛型的实例,一种是通过编译器代码生成完成对象的实例创建,其中又分为APT代码生成和ASM字节码插桩两个小分支。

一、使用反射

平常我们的封装就算再简单,我们也需要传入 ViewMiel 的 class 对象,或者 DataBinding::inflate 对象。

kotlin 复制代码
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 ,这是很好的封装方式,性能也好,没用到反射,其实已经很优秀了,你绝对可以使用这种方式封装。

本文我们也是从懒人的角度看,除了这种方式之外我们还能用哪些更"懒"的方式来实现自动的初始化。

这里就得提到反射的作用了。

kotlin 复制代码
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 的实例对象。

此时我们就能达到文章开始的效果:

kotlin 复制代码
class ProfileActivity : BaseActivity<ProfileViewModel, ActivityProfileBinding>() {}

反射的方案有没有缺点?

反射太慢了 可能有些人会脱口而出,其实反射真的慢吗?这不属于本文探讨的范围,随着越多越多的一些对比评测【传送门】大家其实也明白过来,反射其实并没有比正常调用慢多少。

虽然反射需要在运行时动态解析类的元数据,执行安全权限检查,以及进行方法调用,虽然反射调用时,JVM会进行额外的安全检查,增加了性能开销,但是如果调用次数很少基本和正常方法调用区别不大,特别是对于 Android 开发的场景,特别还是这种只调用一次的场景,其实运行速度差别真的不大。

混淆,这才是大问题,反射代码在混淆过程中我们需要额外的注意,因为类和成员的名称可能会被改变。如果不正确配置混淆规则,可能导致在运行时无法正确地通过名称找到相应的类、方法或字段,引发异常。

例如我们混淆打包之后,如果通过反射,必须保证反射的直接对象需要保存不被混淆。

我们注释掉混淆规则

scala 复制代码
# 保持ViewModel和ViewBinding不混淆,否则无法反射自动创建
-keep class * implements androidx.viewbinding.ViewBinding { *; }
-keep class * extends androidx.lifecycle.ViewModel { *; }

然后反编译我们的apk很容易的就能找到为混淆的类:

类型安全与可读性 反射调用减少了编译时类型检查的机会,增加了运行时错误的风险。例如,如果通过反射错误地调用了方法或访问了字段,可能会在运行时引发ClassCastException等异常,并且由于是硬编码不好调试不说,如果被反射方改变了方法那么会增加错误的风险。

二、使用APT代码生成

其实相比ASM的字节码插桩,使用APT生成代码相对简单很多,我们可以生成对应的 ViewBinding 和 ViewModel 的初始化对象。

如果你不会 APT 的代码生成,那么跟着过一遍就回了,下面的代码会给出详细的注释。

我们先定义对应的注解:

less 复制代码
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class AutoInject

我们添加 auto-service 和 kotlinpoet 代码生成器的依赖

arduino 复制代码
    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 定义如下:

kotlin 复制代码
@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"
    }
}

那么我们只需要标记哪些类需要生成对应的文件即可,例如:

kotlin 复制代码
@AutoInject
class ProfileActivity : BaseActivity<ProfileViewModel, ActivityProfileBinding>() {}

生成的代码:

kotlin 复制代码
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)
}

基类中调用:

kotlin 复制代码
    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()
        }
    }

我们同样可以无感的在基类自动创建对应的初始化代码,需要注意的是我同样需要混淆生成的代码

csharp 复制代码
#自定义的自动注入生成类,保护实现
-keep class **.*ViewModelInit { *; }

当然了,理论上我们可以直接在 ASM 字节码插桩生成的代码中直接在onCreate方法中自动调用给 mViewModelmViewBinding 这两个固定的字段赋值,但是这有点"硬编码"的意思了,一旦在基类中修改了这个变量的名字就会导致异常,如果你确保不会变动,其实也可以直接用字节码插桩或者AOP面向切面自动赋值到这两个变量中。

后记

本文详细介绍了常用的三种封装方案,所以这三种方案你更喜欢哪一种?

原始的:

kotlin 复制代码
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
){}

反射:

kotlin 复制代码
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
    }
}

使用

kotlin 复制代码
class ProfileActivity : BaseActivity<ProfileViewModel, ActivityProfileBinding>() {}

APT的使用:

kotlin 复制代码
@AutoInject  //自定义注解,自己定义
class ProfileActivity : BaseActivity<ProfileViewModel, ActivityProfileBinding>() {}

总的来说三种方案各有利弊,都是可以实现的,用哪一种方案完全看自己的意愿。

如果你有其他的更多的更好的实现方式,也希望大家能评论区交流一起学习进步。如果我的文章有错别字,不通顺的,或者代码、注释、有错漏的地方,同学们都可以指出修正。

本文代码都已在文中贴出,内部的代码参考的项目为 【2024年Android项目开发模板开源】

如果感觉本文对你有一点的启发和帮助,还望你能点赞支持一下,你的支持对我真的很重要。

Ok,这一期就此完结。

相关推荐
雨白4 小时前
Jetpack系列(二):Lifecycle与LiveData结合,打造响应式UI
android·android jetpack
kk爱闹5 小时前
【挑战14天学完python和pytorch】- day01
android·pytorch·python
每次的天空7 小时前
Android-自定义View的实战学习总结
android·学习·kotlin·音视频
恋猫de小郭7 小时前
Flutter Widget Preview 功能已合并到 master,提前在体验毛坯的预览支持
android·flutter·ios
断剑重铸之日8 小时前
Android自定义相机开发(类似OCR扫描相机)
android
随心最为安8 小时前
Android Library Maven 发布完整流程指南
android
岁月玲珑8 小时前
【使用Android Studio调试手机app时候手机老掉线问题】
android·ide·android studio
还鮟13 小时前
CTF Web的数组巧用
android
小蜜蜂嗡嗡14 小时前
Android Studio flutter项目运行、打包时间太长
android·flutter·android studio
aqi0014 小时前
FFmpeg开发笔记(七十一)使用国产的QPlayer2实现双播放器观看视频
android·ffmpeg·音视频·流媒体