什么!我竟然用反射初始化ViewBidning和ViewModel!

4,394 阅读7分钟

我为什么要用反射初始化ViewBinding

作为程序员,当然是能少写代码就少写代码。能CV的我绝不动手。哈哈哈。我想很多程序员都是这么想的吧。

当然我们开发中都会去封装一个BaseActivityBaseFragment,不就是抽离通用,减少具体页面代码的编写麻~~

在没有接触ViewModelViewBinding的时候,大多数的Base类封装差不多都是这样。

abstract class BaseActivity : AppCompatActivity(){

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(getLayoutId())
        iniView()
        initData()
    }

    abstract fun getLayoutId(): Int

    abstract fun iniView()

    abstract fun initData()
}

但是当你接触了ViewBinding的时候Base类的封装不得不要对Layout的初始化做出修改

abstract class BaseActivity : AppCompatActivity(){

    val viewBinding:ViewBinding by lazy {
        initViewBinding()
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(viewBinding.root)
    }
    abstract fun initViewBinding():ViewBinding
}

public class MainActivity extends BaseActivity {
    @NotNull
    @Override
    public ViewBinding initViewBinding() {
        return ActivityMainBinding.inflate(LayoutInflater.from(this));
    }
}

哎,我还是要手写ActivityMainBinding.inflate(LayoutInflater.from(this));这么一大串代码,好烦啊😣。还要实现一个方法,本来就臃肿的Activity又多了5行代码。

基于能懒则懒的原则我决定改造这臃肿的代码。

反射初始化ViewBinding

我起初的设想是这样的,我的具体Activity只告诉BaseActivity类我用的是哪个ViewBinding,由BaseActivity类帮我初始化这个ViewBinding。在我考虑了一会后,我想到,我把具体的ViewBinding的类作为泛型交给BaseActivity这样它不就知道我要用哪个页面布局了吗,就跟传统的返回布局Id一样。

于是我就这样设想了我的BaseActivity应该张这个样子:

abstract class BaseActivity<T :ViewBinding> : AppCompatActivity(){

    val viewBinding:T by lazy {
        initViewBinding()
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(viewBinding.root)
    }
    
    fun initViewBinding():T{
        return 返回T类型的ViewBinding的实现
    }
}

这样我的MainActivity代码就变得格外的清爽了。

那么我们的fun initViewBinding():T就开始我们的反射工作吧。

反射要注意什么

这里的代码我开始用Java去写,因为用Java的话看起来更易于阅读。

我先讲一下我当时准备反射时思考的几个问题:

1.我的类中有几个泛型(因为实际开发中不可能只有一种泛型 BaseActivity<VB:ViewBinding,VM:ViewModel>

2.哪个泛型是我需要的(我只要ViewBinding的实现子类类型)

3.Activity多继承时最终的类没有泛型怎么办 ?

4.代码混淆该怎么处理?

编写反射代码

在思考过上面几个问题后,我掏出了我的又长又大的键盘,开始在上面敲代码了。

在思路清晰了之后,写代码就是快,欻欻欻不一会我就把这个泛型方法写完了!

public <V extends ViewBinding> V reflexViewBinding(Class<?> aClass, LayoutInflater from) {

try {

    Type genericSuperclass = aClass.getGenericSuperclass();
   
    if (genericSuperclass instanceof ParameterizedType) {
    
        Type[] actualTypeArguments = ((ParameterizedType) Objects.requireNonNull(genericSuperclass)).getActualTypeArguments();
        
        for (Type actualTypeArgument : actualTypeArguments) {
        
            Class<?> tClass;
            try {
                tClass = (Class<?>) actualTypeArgument;
            } catch (Exception e) {
                continue;
            }
            
            if (ViewBinding.class.isAssignableFrom(tClass)) {
                Method inflate = tClass.getMethod("inflate", LayoutInflater.class);
                return (V) inflate.invoke(null, from);
            }
            
        }
    }
    
    return reflexViewBinding(aClass.getSuperclass(), from);
} catch (Exception e) {
    e.printStackTrace();
    throw new RuntimeException(e.getMessage(), e);
}

}

首先通过当前的class类得到它的创建父类,拿到父类所需要传入的泛型组。

为什么是父类呢,因为具体实现类自己不需要泛型啊。如果你的类不是最终的还要对BaseActivity在进行一次封装的话,也是要让其子类传入这个ViewBinding的实现类泛型的。

在得到actualTypeArguments这个泛型组之后,我们对这组数据进行循环遍历,获取每一个泛型,并将其强转成Class<?>!

这里是Class<?>为什么不是Class<V>呢。因为我们也不知道,这泛型到底是什么类型,如果你知道这个泛型的位置,当且仅当你只有一个泛型的时候,你可以用Class<V>,但为了灵活使用?会更好一些。

你看这里我还进行了强转失败的处理,让它直接跳过,进行下一次循环。这一步是非常必要的。

我们拿到了tClass反射类之后,就要判断它是不是ViewBinding的实现子类了。用isAssignableFrom()就能判断出是不是了。

如果是,那么我们就去反射实现类中的inflate方法,并将LayoutInflater交给它。这样我们就初始化完成了。

但是你看到我在最后又return reflexViewBinding(aClass.getSuperclass(), from);直接就是个递归了。

为什么这么做呢。在开发中呢,如果你只是基于BaseActivity<VB : Viewinding>又封装了一个通用页面的类就像这样👇

public abstract class ListActivity extends BaseActivity<ActivityMainBinding> {
    @Override
    public void initViews() {
        RecyclerView.Adapter adapter = getAdapter();
    }
    protected abstract RecyclerView.Adapter getAdapter();
}
class UserInfoActivity extends ListActivity {
    @Override
    protected RecyclerView.Adapter getAdapter() {
        return null;
    }
}

这个类做通用的RecyclerView的配置。它的具体UserInfoActivity并不需要传递ViewBinding泛型给ListActivity,那么在反射的第一步就直接就有问题了。

Type genericSuperclass = aClass.getGenericSuperclass();
//XXX不会通过判断
if (genericSuperclass instanceof ParameterizedType){
    ....
}

基于上面的场景,这里拿到的genericSuperclass是个空的。那么下面的逻辑就全部不执行了。

然后就来到了我递归return的地方。获取当前将当前Class的父Class传入进去,进行同样的判断初始化逻辑。这样一套流程就打通了。

你没有泛型参数,我就去找你的父亲,如果你的父亲也没有泛型参数,我就去找你的父亲的父亲。这样肯定能找到我想要的泛型类型,

最后进行初始化操作,要是真找不到,那就是你这个类根本就没有继承BaseActivity直接给你报错就行了。

可以看一下ViewBinding的初始化不止这一种方式,我们可以基于当前这个方法,扩展其他的方法,原理一样,效果也一样。

又有人说反射效率不高的问题了,我就知道会有人这么说,不反射也行,老老实实写那重复的初始化代码吧😔。

我的代码混淆了!

在开发中肯定常常用到代码混淆。反射和混淆恰恰很冲突。

那么只能在混淆配置文件加一下规则了:


-keep public interface androidx.viewbinding.ViewBinding
-keep class * implements androidx.viewbinding.ViewBinding{
    *;
}

#你的Activity继承的AppCompatActivity,就是AppCompatActivity
-keep class androidx.appcompat.app.AppCompatActivity
-keep class * extends androidx.appcompat.app.AppCompatActivity
#不加也行,默认不混淆Fragment
-keep class androidx.fragment.app.Fragment
-keep class * extends androidx.fragment.app.Fragment

ViewModel的反射初始化

当我写完ViewBinding的反射初始化之后,我突然发现代码中的ViewModel初始化也是个废手的大户。

ViewModel的初始化要写这么又长的一段代码,我又受不了:

MyViewModel viewModel  =  new ViewModelProvider(owner).get(MyViewModel.class);

好家伙,我直接按照ViewModel的反射思路也来一套ViewModel的初始化吧!

这里就不贴这个代码了,因为几乎一样的代码逻辑。

如果大家有兴趣可以去GitHub上看一下这个仓库NavigationDemo这个项目是我之前写:

《起初Jetpack Navigation把我逼疯了,可是后来真香》

《LiveData巧妙封装,我再也不怕Navigation重建Fragment啦!》

这两篇文章用到的示例Demo里面Fragment就是用了反射初始化ViewModel,如果有兴趣大家可以去看一下。

其他的ViewBinding封装方式

Kotlin中用扩展函数的方式, 给ViewBinding扩展一个initViewActivity实现这个initView,这样就能在Activity中直接使用ViewBinding中的具体控件变量了,也真的是非常的方便,不过初始化的方式也是可以用这种反射的方式进行。但Java没办法扩展函数这么搞了。

反射初始化到底好不好。其实我感觉能给自己开发省很多写代码时间,而且ViewModel的初始化本身也是反射完成的,我只是在外部又给他反射了一下。无伤大雅。而且通过反射,泛型传递,整个Activity代码变得很清晰明了,用的哪个布局,哪个ViewModel,直接在泛型中就能看到,再也不用全局去找initLayout返回的什么布局id,或是全局搜索找initViewBinding,initViewModel返回的是什么实现。

总之我感觉我的这种方式还是挺好的,如果大家有更好的方式,或者有优化提升的空间的话,也请大家多多评论讨论一下。