The recent use of the MVVM architecture has involved the question of where to initialize the ViewModel in the View layer. Since there will be BaseAcitvity in our project, in the case of MVVM architecture, we can generalize the ViweModel to be used by the subclass, and then the BaseActivity abstractions the method of initializing the ViewModel to be implemented by the subclass. The main code is as follows:

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();
}

Copy the code
public class ActivityHome extends BaseActivity<HomeViewModel> { @Override HomeViewModel createViewModel() { return new ViewModelProvider(this).get(HomeViewModel.class); } @Override int layoutId() { return R.layout.activity_home; }}Copy the code
public class HomeViewModel extends AndroidViewModel { public HomeViewModel(@NonNull Application application) { super(application); }}Copy the code

It occurred to me that I could initialize the subclass ViewModel directly in the BaseActivity instead of the subclass implementing the createViewmodel method every time, but there were two problems to do this:

First, the first question: how do I know in BaseActivity which ViewModel a subclass specifies

public class ActivityHome extends BaseActivity

We specify HomeViewModel via generics in our subclass and pass it to BaseActivity as a parameter to the BaseActivity’s generic class. It is possible to get its generic information in BaseActivity, and it is possible: The getGenericSuperclass() method in the Class Class gets the type information of the parent Class, and of course the generics information, since a Class has generics, the type of the Class is determined by those generics.

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 output is :ActivityHome: com.allfun.blogssourcecode.one.BaseActivity<com.allfun.blogssourcecode.one.HomeViewModel> } } abstract VM createViewModel(); @LayoutRes abstract int layoutId(); }Copy the code

When ActivityHome initializes, it calls the onCreate method of its BaseActivity parent and triggers getGenericSuperclass to get a generic BaseActivity. As you can see from the log, We see the ViewModel specified by ActivityHome in the generic: HomeViewModel.

Second question: Once you know the ViewModel specified by the subclass, how to get the class class of the corresponding ViewModel (because you need to instantiate the ViewModel using new ViewModelProvider(this).get(homeViewModel.class)).

GetGenericSuperclass () gets the parent’s generic information and returns Type. Then we force the Type to a generic Type using a strong cast, and getActualTypeArguments() gets the generic array of the generic Type. Because we only have one generic VM here so taking the first one is the HomeViewModel that we need.

But we need the Class object of HomeViewModel, and now we have HomeViewModel Type, so let’s find the relation between Class and Type:Class = Class; Class = Class; Class = Class; Class = Class; At this point, you can instantiate the ViewModel object of the subclass directly by generics in BaseActivity via ViewModelProvider(this).get(homeViewModel.class). Modify the code as follows:

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(); }... .Copy the code

The purpose was achieved, but there was a question that was still unclear: Java generics only exist at compile time, and the runtime will erase the generics, that is, ((VM) new ViewModelProvider(this).get(homeViewModelClass)) where the VM will be replaced with Object, and the problem is, How does that do the conversion, because it turns out it’s a good conversion to HomeViewModel.

The VM is actually going to be replaced with a ViewModel, and it works because the Class itself is the Class of HomeViewModel, We’re doing this just so that the compiler (doubtfully) can recognize this Class as the Class of HomeViewModel in order to continue coding, because we’re not forcing it to be VM, We can’t make VM viewModel = ((VM) new ViewModelProvider(this).get(homeViewModelClass)), because we need a VM instance on the left, The right-hand side is just an Object, even though the Object is actually an instance of a VM. Let me give you an example to make it easier to understand:

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()); / / the log output is: test: class com. Allfun. Blogssourcecode.. One Example $Doctor}}Copy the code

And the example above, can know what is a class instance is instantiated when it settled, may because when passed as a parameter is lost among the specific type and into the parent class (head Doctor into Object) is still a Doctor but in memory Object, so type conversion here are just a tag, Telling the program that the object is now being used as this type does not change the actual properties of the object (it is said that there are strong transformations that change the structure of the original object, but that is not the case here).

So, I think ((VM) new ViewModelProvider(this).get(homeViewModelClass)) is just strong enough to keep coding going, because if you don’t add (VM) the equation won’t be set up, you’ll get an error.

But when the program actually runs, the VM in the method body will be replaced by the ViewModel, so it will be forcibly converted to ViewModel not HomeViewModel, right? No, because Java generics have a rule:

On the declaration side, what is written in the source code is visible at runtime;

Whatever is written in the source code on the usage side is lost at runtime.

protected VM viewModel; This is the declaration side, so viewModel = ((VM) new ViewModelProvider(this).get(homeViewModelClass)); This is run by assigning to the viewModel of the VM type, which in this case is HomeViewModel, so you still end up with an object instance of the type argument represented by the VM generic.

Actually, the createViewModel method could look something like this

 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));
        }
    }
Copy the code