xref: /aosp_15_r20/external/dagger2/javatests/dagger/hilt/android/ViewModelSavedStateOwnerTest.java (revision f585d8a307d0621d6060bd7e80091fdcbf94fe27)
1 /*
2  * Copyright (C) 2022 The Dagger Authors.
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 
17 package dagger.hilt.android;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 import static org.junit.Assert.assertThrows;
21 
22 import android.os.Build;
23 import android.os.Bundle;
24 import androidx.fragment.app.Fragment;
25 import androidx.fragment.app.FragmentActivity;
26 import androidx.annotation.Nullable;
27 import androidx.annotation.OptIn;
28 import androidx.lifecycle.SavedStateHandle;
29 import androidx.lifecycle.ViewModel;
30 import androidx.lifecycle.ViewModelProvider;
31 import androidx.lifecycle.ViewModelStoreOwner;
32 import androidx.navigation.NavController;
33 import androidx.navigation.Navigation;
34 import androidx.test.core.app.ActivityScenario;
35 import androidx.test.ext.junit.runners.AndroidJUnit4;
36 import dagger.hilt.android.lifecycle.ActivityRetainedSavedState;
37 import dagger.hilt.android.lifecycle.HiltViewModel;
38 import dagger.hilt.android.testing.HiltAndroidRule;
39 import dagger.hilt.android.testing.HiltAndroidTest;
40 import dagger.hilt.android.testing.HiltTestApplication;
41 import javax.inject.Inject;
42 import javax.inject.Provider;
43 import org.junit.Rule;
44 import org.junit.Test;
45 import org.junit.runner.RunWith;
46 import org.robolectric.annotation.Config;
47 
48 /** Test that you can use the Hilt ViewModel factory with other owners. */
49 @OptIn(markerClass = UnstableApi.class)
50 @HiltAndroidTest
51 @RunWith(AndroidJUnit4.class)
52 // Robolectric requires Java9 to run API 29 and above, so use API 28 instead
53 @Config(sdk = Build.VERSION_CODES.P, application = HiltTestApplication.class)
54 public class ViewModelSavedStateOwnerTest {
55 
56   @Rule public final HiltAndroidRule rule = new HiltAndroidRule(this);
57 
58   @Test
activityRetainedComponentSaveState_configurationChange_successfullySavedState()59   public void activityRetainedComponentSaveState_configurationChange_successfullySavedState() {
60     try (ActivityScenario<TestActivity> scenario = ActivityScenario.launch(TestActivity.class)) {
61       scenario.onActivity(
62           activity -> {
63             assertThat((String) activity.savedStateHandle.get("argument_key")).isNull();
64             activity.savedStateHandle.set("other_key", "activity_other_key");
65           });
66       scenario.recreate();
67       scenario.onActivity(
68           activity -> {
69             assertThat((String) activity.savedStateHandle.get("argument_key")).isNull();
70             assertThat((String) activity.savedStateHandle.get("other_key"))
71                 .isEqualTo("activity_other_key");
72           });
73     }
74   }
75 
76   @Test
firstTimeAccessToActivityRetainedSaveState_inActivityOnDestroy_fails()77   public void firstTimeAccessToActivityRetainedSaveState_inActivityOnDestroy_fails() {
78     Exception exception =
79         assertThrows(
80             NullPointerException.class,
81             () -> {
82               try (ActivityScenario<ErrorTestActivity> scenario =
83                   ActivityScenario.launch(ErrorTestActivity.class)) {}
84             });
85     assertThat(exception)
86         .hasMessageThat()
87         .contains(
88             "The first access to SavedStateHandle should happen between super.onCreate() and"
89                 + " super.onDestroy()");
90   }
91 
92   @Test
testViewModelSavedState()93   public void testViewModelSavedState() {
94     try (ActivityScenario<TestActivity> scenario = ActivityScenario.launch(TestActivity.class)) {
95       scenario.onActivity(
96           activity -> {
97             NavController navController =
98                 Navigation.findNavController(activity, R.id.nav_host_fragment);
99             TestFragment startFragment = findTestFragment(activity);
100 
101             MyViewModel activityVm =
102                 getViewModel(activity, activity.getDefaultViewModelProviderFactory());
103             MyViewModel fragmentVm =
104                 getViewModel(startFragment, startFragment.getDefaultViewModelProviderFactory());
105             MyViewModel fragmentBackStackVm =
106                 getViewModel(
107                     navController.getBackStackEntry(R.id.start_destination),
108                     startFragment.getDefaultViewModelProviderFactory());
109             MyViewModel navGraphVm =
110                 getViewModel(
111                     navController.getBackStackEntry(R.id.nav_graph),
112                     startFragment.getDefaultViewModelProviderFactory());
113 
114             // The activity shouldn't have any arguments since it was only set on the fragment.
115             assertThat((String) activityVm.savedStateHandle.get("argument_key")).isNull();
116             activityVm.savedStateHandle.set("other_key", "activity_other_key");
117 
118             // The fragment argument (set in the navgraph xml) should be set.
119             assertThat((String) fragmentVm.savedStateHandle.get("argument_key"))
120                 .isEqualTo("fragment_argument");
121             fragmentVm.savedStateHandle.set("other_key", "fragment_other_key");
122 
123             // The back stack entry also has the fragment arguments
124             assertThat((String) fragmentBackStackVm.savedStateHandle.get("argument_key"))
125                 .isEqualTo("fragment_argument");
126             fragmentBackStackVm.savedStateHandle.set("other_key", "fragment_backstack_other_key");
127 
128             // When the nav graph itself is the owner, then there should be no arguments.
129             assertThat((String) navGraphVm.savedStateHandle.get("argument_key")).isNull();
130             navGraphVm.savedStateHandle.set("other_key", "nav_graph_other_key");
131 
132             navController.navigate(R.id.next_destination);
133           });
134 
135       // Now move to the next fragment to compare
136       scenario.onActivity(
137           activity -> {
138             NavController navController =
139                 Navigation.findNavController(activity, R.id.nav_host_fragment);
140 
141             TestFragment nextFragment = findTestFragment(activity);
142 
143             MyViewModel activityVm =
144                 getViewModel(activity, activity.getDefaultViewModelProviderFactory());
145             MyViewModel fragmentVm =
146                 getViewModel(nextFragment, nextFragment.getDefaultViewModelProviderFactory());
147             MyViewModel navGraphVm =
148                 getViewModel(
149                     navController.getBackStackEntry(R.id.nav_graph),
150                     nextFragment.getDefaultViewModelProviderFactory());
151             MyViewModel fragmentBackStackVm =
152                 getViewModel(
153                     navController.getBackStackEntry(R.id.next_destination),
154                     nextFragment.getDefaultViewModelProviderFactory());
155 
156             // The activity still shouldn't have any arguments, but since it is the same
157             // owner (since the activity didn't change), the other key should still be set
158             // from before.
159             assertThat((String) activityVm.savedStateHandle.get("argument_key")).isNull();
160             assertThat((String) activityVm.savedStateHandle.get("other_key"))
161                 .isEqualTo("activity_other_key");
162 
163             // The fragment argument should be set via the navgraph xml again. Also, since
164             // this is a new fragment, the other key should not be set.
165             assertThat((String) fragmentVm.savedStateHandle.get("argument_key"))
166                 .isEqualTo("next_fragment_argument");
167             assertThat((String) fragmentVm.savedStateHandle.get("other_key")).isNull();
168 
169             // Same as using the fragment as the owner.
170             assertThat((String) fragmentBackStackVm.savedStateHandle.get("argument_key"))
171                 .isEqualTo("next_fragment_argument");
172             assertThat((String) fragmentBackStackVm.savedStateHandle.get("other_key")).isNull();
173 
174             // Similar to the activity case, the navgraph is the same so we expect the same
175             // key to be set from before. Arguments should still be missing.
176             assertThat((String) navGraphVm.savedStateHandle.get("argument_key")).isNull();
177             assertThat((String) navGraphVm.savedStateHandle.get("other_key"))
178                 .isEqualTo("nav_graph_other_key");
179           });
180     }
181   }
182 
findTestFragment(FragmentActivity activity)183   private TestFragment findTestFragment(FragmentActivity activity) {
184     return (TestFragment)
185         activity
186             .getSupportFragmentManager()
187             .findFragmentById(R.id.nav_host_fragment)
188             .getChildFragmentManager()
189             .getPrimaryNavigationFragment();
190   }
191 
getViewModel(ViewModelStoreOwner owner, ViewModelProvider.Factory factory)192   private MyViewModel getViewModel(ViewModelStoreOwner owner, ViewModelProvider.Factory factory) {
193     return new ViewModelProvider(owner, factory).get(MyViewModel.class);
194   }
195 
196   @AndroidEntryPoint(FragmentActivity.class)
197   public static class TestActivity extends Hilt_ViewModelSavedStateOwnerTest_TestActivity {
198     @Inject @ActivityRetainedSavedState Provider<SavedStateHandle> provider;
199     SavedStateHandle savedStateHandle;
200 
201     @Override
onCreate(@ullable Bundle savedInstanceState)202     protected void onCreate(@Nullable Bundle savedInstanceState) {
203       super.onCreate(savedInstanceState);
204       savedStateHandle = provider.get();
205       setContentView(R.layout.navigation_activity);
206     }
207   }
208 
209   @AndroidEntryPoint(FragmentActivity.class)
210   public static class ErrorTestActivity
211       extends Hilt_ViewModelSavedStateOwnerTest_ErrorTestActivity {
212     @Inject @ActivityRetainedSavedState Provider<SavedStateHandle> provider;
213 
214     @SuppressWarnings("unused")
215     @Override
onDestroy()216     protected void onDestroy() {
217       super.onDestroy();
218       SavedStateHandle savedStateHandle = provider.get();
219     }
220   }
221 
222   @AndroidEntryPoint(Fragment.class)
223   public static class TestFragment extends Hilt_ViewModelSavedStateOwnerTest_TestFragment {
224     @Override
onCreate(@ullable Bundle savedInstanceState)225     public void onCreate(@Nullable Bundle savedInstanceState) {
226       super.onCreate(savedInstanceState);
227     }
228   }
229 
230   @HiltViewModel
231   static class MyViewModel extends ViewModel {
232     final SavedStateHandle savedStateHandle;
233 
234     @Inject
MyViewModel(SavedStateHandle savedStateHandle)235     MyViewModel(SavedStateHandle savedStateHandle) {
236       this.savedStateHandle = savedStateHandle;
237     }
238   }
239 }
240