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