1/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package androidx.lifecycle;
18
19import android.app.Application;
20
21import androidx.annotation.MainThread;
22import androidx.annotation.NonNull;
23
24import java.lang.reflect.InvocationTargetException;
25
26/**
27 * An utility class that provides {@code ViewModels} for a scope.
28 * <p>
29 * Default {@code ViewModelProvider} for an {@code Activity} or a {@code Fragment} can be obtained
30 * from {@link androidx.lifecycle.ViewModelProviders} class.
31 */
32@SuppressWarnings("WeakerAccess")
33public class ViewModelProvider {
34
35    private static final String DEFAULT_KEY =
36            "androidx.lifecycle.ViewModelProvider.DefaultKey";
37
38    /**
39     * Implementations of {@code Factory} interface are responsible to instantiate ViewModels.
40     */
41    public interface Factory {
42        /**
43         * Creates a new instance of the given {@code Class}.
44         * <p>
45         *
46         * @param modelClass a {@code Class} whose instance is requested
47         * @param <T>        The type parameter for the ViewModel.
48         * @return a newly created ViewModel
49         */
50        @NonNull
51        <T extends ViewModel> T create(@NonNull Class<T> modelClass);
52    }
53
54    private final Factory mFactory;
55    private final ViewModelStore mViewModelStore;
56
57    /**
58     * Creates {@code ViewModelProvider}, which will create {@code ViewModels} via the given
59     * {@code Factory} and retain them in a store of the given {@code ViewModelStoreOwner}.
60     *
61     * @param owner   a {@code ViewModelStoreOwner} whose {@link ViewModelStore} will be used to
62     *                retain {@code ViewModels}
63     * @param factory a {@code Factory} which will be used to instantiate
64     *                new {@code ViewModels}
65     */
66    public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
67        this(owner.getViewModelStore(), factory);
68    }
69
70    /**
71     * Creates {@code ViewModelProvider}, which will create {@code ViewModels} via the given
72     * {@code Factory} and retain them in the given {@code store}.
73     *
74     * @param store   {@code ViewModelStore} where ViewModels will be stored.
75     * @param factory factory a {@code Factory} which will be used to instantiate
76     *                new {@code ViewModels}
77     */
78    public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
79        mFactory = factory;
80        this.mViewModelStore = store;
81    }
82
83    /**
84     * Returns an existing ViewModel or creates a new one in the scope (usually, a fragment or
85     * an activity), associated with this {@code ViewModelProvider}.
86     * <p>
87     * The created ViewModel is associated with the given scope and will be retained
88     * as long as the scope is alive (e.g. if it is an activity, until it is
89     * finished or process is killed).
90     *
91     * @param modelClass The class of the ViewModel to create an instance of it if it is not
92     *                   present.
93     * @param <T>        The type parameter for the ViewModel.
94     * @return A ViewModel that is an instance of the given type {@code T}.
95     */
96    @NonNull
97    @MainThread
98    public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
99        String canonicalName = modelClass.getCanonicalName();
100        if (canonicalName == null) {
101            throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
102        }
103        return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
104    }
105
106    /**
107     * Returns an existing ViewModel or creates a new one in the scope (usually, a fragment or
108     * an activity), associated with this {@code ViewModelProvider}.
109     * <p>
110     * The created ViewModel is associated with the given scope and will be retained
111     * as long as the scope is alive (e.g. if it is an activity, until it is
112     * finished or process is killed).
113     *
114     * @param key        The key to use to identify the ViewModel.
115     * @param modelClass The class of the ViewModel to create an instance of it if it is not
116     *                   present.
117     * @param <T>        The type parameter for the ViewModel.
118     * @return A ViewModel that is an instance of the given type {@code T}.
119     */
120    @NonNull
121    @MainThread
122    public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
123        ViewModel viewModel = mViewModelStore.get(key);
124
125        if (modelClass.isInstance(viewModel)) {
126            //noinspection unchecked
127            return (T) viewModel;
128        } else {
129            //noinspection StatementWithEmptyBody
130            if (viewModel != null) {
131                // TODO: log a warning.
132            }
133        }
134
135        viewModel = mFactory.create(modelClass);
136        mViewModelStore.put(key, viewModel);
137        //noinspection unchecked
138        return (T) viewModel;
139    }
140
141    /**
142     * Simple factory, which calls empty constructor on the give class.
143     */
144    public static class NewInstanceFactory implements Factory {
145
146        @SuppressWarnings("ClassNewInstance")
147        @NonNull
148        @Override
149        public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
150            //noinspection TryWithIdenticalCatches
151            try {
152                return modelClass.newInstance();
153            } catch (InstantiationException e) {
154                throw new RuntimeException("Cannot create an instance of " + modelClass, e);
155            } catch (IllegalAccessException e) {
156                throw new RuntimeException("Cannot create an instance of " + modelClass, e);
157            }
158        }
159    }
160
161    /**
162     * {@link Factory} which may create {@link AndroidViewModel} and
163     * {@link ViewModel}, which have an empty constructor.
164     */
165    public static class AndroidViewModelFactory extends ViewModelProvider.NewInstanceFactory {
166
167        private static AndroidViewModelFactory sInstance;
168
169        /**
170         * Retrieve a singleton instance of AndroidViewModelFactory.
171         *
172         * @param application an application to pass in {@link AndroidViewModel}
173         * @return A valid {@link AndroidViewModelFactory}
174         */
175        @NonNull
176        public static AndroidViewModelFactory getInstance(@NonNull Application application) {
177            if (sInstance == null) {
178                sInstance = new AndroidViewModelFactory(application);
179            }
180            return sInstance;
181        }
182
183        private Application mApplication;
184
185        /**
186         * Creates a {@code AndroidViewModelFactory}
187         *
188         * @param application an application to pass in {@link AndroidViewModel}
189         */
190        public AndroidViewModelFactory(@NonNull Application application) {
191            mApplication = application;
192        }
193
194        @NonNull
195        @Override
196        public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
197            if (AndroidViewModel.class.isAssignableFrom(modelClass)) {
198                //noinspection TryWithIdenticalCatches
199                try {
200                    return modelClass.getConstructor(Application.class).newInstance(mApplication);
201                } catch (NoSuchMethodException e) {
202                    throw new RuntimeException("Cannot create an instance of " + modelClass, e);
203                } catch (IllegalAccessException e) {
204                    throw new RuntimeException("Cannot create an instance of " + modelClass, e);
205                } catch (InstantiationException e) {
206                    throw new RuntimeException("Cannot create an instance of " + modelClass, e);
207                } catch (InvocationTargetException e) {
208                    throw new RuntimeException("Cannot create an instance of " + modelClass, e);
209                }
210            }
211            return super.create(modelClass);
212        }
213    }
214}
215