前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【Android】只给个泛型,如何自动初始化ViewModel与ViewBinding?这几种方案值得了解

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

作者头像
Rouse
发布2024-06-11 19:11:23
2360
发布2024-06-11 19:11:23
举报
文章被收录于专栏:Android补给站

链接:https://juejin.cn/post/7357546247849197606 本文由作者授权发布

前言

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

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

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

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

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

或者基类抽象实现:

代码语言:javascript
复制
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);
}

具体还是让子类去实现:

代码语言:javascript
复制
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);
    }

    // ...
}

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

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

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

那...可如何是好呐。

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

一、使用反射

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

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

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

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

代码语言:javascript
复制
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 的实例对象。

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

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

反射的方案有没有缺点?

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

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

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

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

我们注释掉混淆规则

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

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

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

二、使用APT代码生成

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

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

我们先定义对应的注解:

代码语言:javascript
复制
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class AutoInject

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

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

代码语言:javascript
复制
@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"
    }
}

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

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

生成的代码:

代码语言:javascript
复制
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)
}

基类中调用:

代码语言:javascript
复制
  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()
        }
    }

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

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

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

后记

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

代码语言:javascript
复制
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
){}

反射:

代码语言:javascript
复制
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
    }
}

使用:

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

APT的使用:

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

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

如果你有其他的更多的更好的实现方式,也希望大家能评论区交流一起学习进步。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-05-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Android补给站 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 一、使用反射
  • 二、使用APT代码生成
  • 后记
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档