阅读 526

记录一次ViewModel初始化位置的优化

最近在使用MVVM架构的时候涉及到一个在View层中何处初始化话ViewModel的问题。因为我们的项目中都会有BaseAcitvity,对于MVVM架构的话,我们可以通过泛型指定子类使用的具体的ViweModel,然后BaseActivity抽象出初始化ViewModel的方法,由子类来具体实现。主要代码如下:

public abstract class BaseActivity<VM extends ViewModel> extends AppCompatActivity {
    protected VM viewModel;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(layoutId());
        viewModel = createViewModel();
    }

    abstract VM createViewModel();

    @LayoutRes
    abstract int layoutId();
}

复制代码
public class ActivityHome extends BaseActivity<HomeViewModel> {
    @Override
    HomeViewModel createViewModel() {
        return new ViewModelProvider(this).get(HomeViewModel.class);
    }

    @Override
    int layoutId() {
        return R.layout.activity_home;
    }
}

复制代码
public class HomeViewModel extends AndroidViewModel {

    public HomeViewModel(@NonNull Application application) {
        super(application);
    }
}
复制代码

于是我想到可不可以直接在在BaseActivity里初始化子类的ViewModel,而不用子类每次都要实现这个createViewmodel方法,但是要实现这个效果需要解决两个问题:

首先,第一个问题:如何在BaseActivity中知道子类指定的哪个ViewModel

public class ActivityHome extends BaseActivity

我们在子类中通过泛型指定了HomeViewModel并当作BaseActivity的泛型类的形参传递给BaseActivity,按道理来讲在BaseActivity中是可以拿到自己的泛型信息的,实际上也是可以的: Class类中的getGenericSuperclass()方法可以拿到父类的类型信息,当然也就拿到了泛型信息,因为一个类拥有泛型,那么这个类的类型就是这些泛型决定的(不准确)。

public abstract class BaseActivity<VM extends ViewModel> extends AppCompatActivity {
    private final String TAG = getClass().getSimpleName();
    protected VM viewModel;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(layoutId());
        viewModel = createViewModel();
        Type genericSuperclass = getClass().getGenericSuperclass();
        if (genericSuperclass != null) {
            Log.i(TAG, genericSuperclass.toString());
            // log输出为:ActivityHome: com.allfun.blogssourcecode.one.BaseActivity<com.allfun.blogssourcecode.one.HomeViewModel>
        }
    }

    abstract VM createViewModel();

    @LayoutRes
    abstract int layoutId();
}
复制代码

其实就是当前类ActivityHome初始化的时候调用父类BaseActivity的onCreate方法触发getGenericSuperclass获得BaseActivity的泛型,从log里可以看出,我们看到了ActivityHome在泛型里指定的ViewModel:HomeViewModel。

第二个问题:知道子类指定的ViewModel之后如何得到对应ViewModel的class类(因为需要使用new ViewModelProvider(this).get(HomeViewModel.class)进行ViewModel的实例化)。

通过问题一的getGenericSuperclass()方法拿到父类的泛型信息,返回值为Type,然后我们通过强转将类型强转为泛型类型,再通过getActualTypeArguments()方法拿到泛型类型对应的泛型数组,因为我们这里只有一个泛型VM所以取第一个就是我们需要的HomeViewModel。

但是我们需要的是HomeViewModel的Class对象,而现在得到的是HomeViewModel Type,那么我们找一下Class和Type的关系:

Type继承关系树
由Type的继承关系可以看出,它只有一个实现子类Class,也就是说,所有类型的Type最终都是由一个个Class表示的,那么我们就直接将得到的type强转为Class对象,至此就可以通过ViewModelProvider(this).get(HomeViewModel.class)在BaseActivity中通过泛型直接实例化出子类的ViewModel对象了。 修改一下代码,如下:

public abstract class BaseActivity<VM extends ViewModel> extends AppCompatActivity {
    private final String TAG = getClass().getSimpleName();
    protected VM viewModel;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(layoutId());
        createViewModel();
    }

    private void createViewModel() {
        Type genericSuperclass = getClass().getGenericSuperclass();
        if (genericSuperclass != null) {
            ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;
            Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
            Class homeViewModelClass = (Class) actualTypeArguments[0];
            viewModel = ((VM) new ViewModelProvider(this).get(homeViewModelClass));
        }


    }


    @LayoutRes
    abstract int layoutId();
}
...
...
复制代码

目的是达到了,但是有个问题还是不太明白: java泛型只存在编译期间,运行时就会将泛型擦除,也就是说 ((VM) new ViewModelProvider(this).get(homeViewModelClass))这里的VM会被替换为Object,那问题来了,这样的话是如何完成类型转换的,因为从结果看也顺利转换为HomeViewModel类型了。

其实这里VM确实会被替换为ViewModel(此处因为又extends ViewModel,应该会替换为ViewModel),之所以也能转转换成功,是因为这个Class本身就是HomeViewModel的Class类,这里强转只是为了让编译器(存疑)可以识别这个Class是HomeViewModel的Class,是为了继续下一步的编码,因为我们不强转为VM类型,那么就无法使 VM viewModel = ((VM) new ViewModelProvider(this).get(homeViewModelClass)),因为左边需要一个VM实例,而右边不强转的话只是一个Object,尽管这个Object实际上是VM的实例,我举个例子会比较好理解:

public class Example {
    static class People {

    }

    static class Doctor extends People {

    }

    public static void main(String[] args) {
        Doctor doctor = new Doctor();
        test(doctor);
    }

    static void test(Object object) {
        Class aClass = object.getClass();
        Log.e("test",aClass.toString());
        //log输出为: test: class com.allfun.blogssourcecode.one.Example$Doctor
    }


}
复制代码

又上面这个例子可以知道,一个类实例本身是什么实例化的时候就定下来了,中间可能因为作为参数传递的时候丢失了具体的类型而转化为父类(头Doctor转化为Object)但是在内存里依然是Doctor对象,所以呢类型转化在这里只是一个标记而已,告诉程序这个对象现在当作这种类型来使用,并不会改变对象实际的属性(据说也会有改变原本对象结构的强转,但是在这里这种情况不是)。

所以呢,我认为((VM) new ViewModelProvider(this).get(homeViewModelClass))这里的强转只是为了让编码能进行下去,因为不加(VM)等式不成立会报错。

但是程序实际运行的时候方法体内VM会替换为ViewModel,那么会被强转为ViewModel为不是HomeViewModel不是吗?其实不会,因为Java泛型有这么一种规律:

位于声明一侧的,源码里写了什么到运行时就能看到什么;
位于使用一侧的,源码里写什么到运行时都没了。

protected VM viewModel;这里属于声明一侧,所以viewModel = ((VM) new ViewModelProvider(this).get(homeViewModelClass));运行的时候是赋值给VM类型的viewModel,换成例子中VM就是HomeViewModel,所以最后还是拿到了VM泛型代表的类型实参的对象实例。

其实呢,createViewModel方法可以这样写

 private void createViewModel() {
        Type genericSuperclass = getClass().getGenericSuperclass();
        if (genericSuperclass != null) {
            ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;
            Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
            Class<VM> homeViewModelClass = (Class<VM>) actualTypeArguments[0];
            viewModel = (new ViewModelProvider(this).get(homeViewModelClass));
        }
    }
复制代码