Nikola Toshev
Nikola Toshev

Reputation: 135

Architecture Components: How does the ViewModelProvider know which constructor to call?

I've been reading about the new Architecture Components that were introduced to Android and I cannot figure out how this works:

ViewModelProviders.of(Activity).get(Class)

Initially I thought that it calls the default constructor and returns a ViewModel object that you then instantiate with eg. an init() method as per

public class UserProfileViewModel extends ViewModel {
    private String userId;
    private User user;

    public void init(String userId) {
        this.userId = userId;
    }
    public User getUser() {
        return user;
    }
}

Snippet taken from the guide: https://developer.android.com/topic/libraries/architecture/guide.html

However, later on in the guide there is this snippet:

public class UserProfileViewModel extends ViewModel {
    private LiveData<User> user;
    private UserRepository userRepo;

    @Inject // UserRepository parameter is provided by Dagger 2
    public UserProfileViewModel(UserRepository userRepo) {
        this.userRepo = userRepo;
    }

    public void init(String userId) {
        if (this.user != null) {
            // ViewModel is created per Fragment so
            // we know the userId won't change
            return;
        }
        user = userRepo.getUser(userId);
    }

So how does the ViewModelProvider know to call the provided constructor? Or it sees that there is only 1 constructor and calls that? For example if there were 2 constructors what would happen?

I tried digging through the code and what I found was:

@Override
        public <T extends ViewModel> T create(Class<T> modelClass) {
            if (AndroidViewModel.class.isAssignableFrom(modelClass)) {
                //noinspection TryWithIdenticalCatches
                try {
                    return modelClass.getConstructor(Application.class).newInstance(mApplication);
                } catch (NoSuchMethodException e) {
                    throw new RuntimeException("Cannot create an instance of " + modelClass, e);
                } catch (IllegalAccessException e) {
                    throw new RuntimeException("Cannot create an instance of " + modelClass, e);
                } catch (InstantiationException e) {
                    throw new RuntimeException("Cannot create an instance of " + modelClass, e);
                } catch (InvocationTargetException e) {
                    throw new RuntimeException("Cannot create an instance of " + modelClass, e);
                }
            }
            return super.create(modelClass);
        }

Inside of the DefaultFactory class inside ViewModelProviders.java. However, this confused me even further. How does getConstructor(Application.class) even work when ViewModel objects do not have a constructor taking an Application as an argument?

Upvotes: 3

Views: 2766

Answers (1)

Nikola Despotoski
Nikola Despotoski

Reputation: 50538

In the snippet there is a condition that checks if modelClass is of type AndroidViewModel (inherits ViewModel) which constructor takes Application parameter. This is more like exclusive case that spares the Factory of looking up for constructor matching specific parameters. This provider looks up for constructor matching provider arguments when creating it:

public class ViewModelParameterizedProvider {

    private AtomicBoolean set = new AtomicBoolean(false);

    private ViewModelStore viewModelStore = null;


    static ViewModelParameterizedProvider getProvider() {
        return new ViewModelParameterizedProvider();
    }

    @MainThread
    public static ViewModelProvider ofSupportFragment(Fragment fragment, Object... params) {
        return getProvider().of(fragment).with(params);
    }

    @MainThread
    public static ViewModelProvider ofActivity(FragmentActivity fragmentActivity, Object... params) {
        return getProvider().of(fragmentActivity).with(params);
    }

    @MainThread
    public static ViewModelProvider ofFragment(android.app.Fragment fragment, Object... params) {
        return getProvider().of(fragment).with(params);
    }

    private ViewModelParameterizedProvider of(Fragment fragment) {
        checkForPreviousTargetsAndSet();
        viewModelStore = ViewModelStores.of(fragment);
        return this;
    }

    private ViewModelParameterizedProvider of(android.app.Fragment fragment) {
        FragmentActivity fragAct = (FragmentActivity) fragment.getActivity();
        return of(fragAct);
    }

    private ViewModelParameterizedProvider of(FragmentActivity activity) {
        checkForPreviousTargetsAndSet();
        viewModelStore = ViewModelStores.of(activity);
        return this;
    }


    private ViewModelProvider with(Object... constructorParams) {
        return new ViewModelProvider(viewModelStore, parametrizedFactory(constructorParams));
    }


    private void checkForPreviousTargetsAndSet() {
        if (set.get()) {
            throw new IllegalArgumentException("ViewModelStore already has been set. Create new instance.");
        }
        set.set(true);
    }

    private ViewModelProvider.Factory parametrizedFactory(Object... constructorParams) {
        return new ParametrizedFactory(constructorParams);
    }


    private final class ParametrizedFactory implements ViewModelProvider.Factory {
        private final Object[] mConstructorParams;

        ParametrizedFactory(Object... constructorParams) {
            mConstructorParams = constructorParams;
        }

        @Override
        public <T extends ViewModel> T create(Class<T> modelClass) {
            if (modelClass == null) {
                throw new IllegalArgumentException("Target ViewModel class can not be null")
            }
            Log.w("ParametrizedFactory", "Don't use callbacks or Context parameters in order to avoid leaks!!")
            try {
                if (mConstructorParams == null || mConstructorParams.length == 0) {
                    return modelClass.newInstance();
                } else {
                    Class<?>[] classes = new Class<?>[mConstructorParams.length];
                    for (int i = 0; i < mConstructorParams.length; i++) {
                        classes[i] = mConstructorParams[i].getClass();
                    }
                    return modelClass.getConstructor(classes).newInstance(mConstructorParams);
                }
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
            return null;
        }
    }
}

Here is kotlin version. Here is more read on the subject

Upvotes: 5

Related Questions