Skip to content

Commit

Permalink
Updates to ViewModel support to use the new CreationExtras.
Browse files Browse the repository at this point in the history
- Fixes issues with keyed ViewModels.
- Fixes issues with using the ViewModel factory with a navigation entry as owner. Now the lazy function hiltNavGraphViewModels() provided by androidx.hilt:hilt-navigation-fragment is no longer needed.
- Updates deps on fragment to 1.5.0 and lifecycle to 2.5.0

Fixes #3232.
Issue #2152.

RELNOTES=Fixes #3232 and #2152 (without hiltNavGraphViewModels).
PiperOrigin-RevId: 449844449
  • Loading branch information
Chang-Eric authored and Dagger Team committed Jul 11, 2022
1 parent a7f8dac commit 9a3ac21
Show file tree
Hide file tree
Showing 19 changed files with 319 additions and 34 deletions.
2 changes: 1 addition & 1 deletion BUILD
Expand Up @@ -101,7 +101,7 @@ android_library(
name = "android_local_test_exports",
exports = [
# TODO(bcorso): see if we can remove jsr250 dep from autovalue to prevent this.
"@javax_annotation_jsr250_api", # For @Generated
"@maven//:javax_annotation_jsr250_api", # For @Generated
"@maven//:org_robolectric_shadows_framework", # For ActivityController
"@maven//:androidx_lifecycle_lifecycle_common", # For Lifecycle.State
"@maven//:androidx_activity_activity", # For ComponentActivity
Expand Down
25 changes: 14 additions & 11 deletions WORKSPACE
Expand Up @@ -64,9 +64,9 @@ local_repository(

http_archive(
name = "google_bazel_common",
sha256 = "8b6aebdc095c8448b2f6a72bb8eae4a563891467e2d20c943f21940b1c444e38",
strip_prefix = "bazel-common-3d0e5005cfcbee836e31695d4ab91b5328ccc506",
urls = ["https://github.com/google/bazel-common/archive/3d0e5005cfcbee836e31695d4ab91b5328ccc506.zip"],
sha256 = "60a9aebe25f476646f61c041d1679a9b21076deffbd51526838c7f24d6468ac0",
strip_prefix = "bazel-common-227a23a508a2fab0fa67ffe2d9332ae536a40edc",
urls = ["https://github.com/google/bazel-common/archive/227a23a508a2fab0fa67ffe2d9332ae536a40edc.zip"],
)

load("@google_bazel_common//:workspace_defs.bzl", "google_common_workspace_rules")
Expand Down Expand Up @@ -161,9 +161,9 @@ kt_register_toolchains()
# Load Maven dependencies
#############################

RULES_JVM_EXTERNAL_TAG = "2.7"
RULES_JVM_EXTERNAL_TAG = "4.2"

RULES_JVM_EXTERNAL_SHA = "f04b1466a00a2845106801e0c5cec96841f49ea4e7d1df88dc8e4bf31523df74"
RULES_JVM_EXTERNAL_SHA = "cd1a77b7b02e8e008439ca76fd34f5b07aecb8c752961f9640dea15e9e5ba1ca"

http_archive(
name = "rules_jvm_external",
Expand Down Expand Up @@ -199,13 +199,16 @@ maven_install(
artifacts = [
"androidx.annotation:annotation:1.1.0",
"androidx.appcompat:appcompat:1.3.1",
"androidx.activity:activity:1.3.1",
"androidx.fragment:fragment:1.3.6",
"androidx.lifecycle:lifecycle-common:2.3.1",
"androidx.lifecycle:lifecycle-viewmodel:2.3.1",
"androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.1",
"androidx.activity:activity:1.5.0",
"androidx.fragment:fragment:1.5.0",
"androidx.lifecycle:lifecycle-common:2.5.0",
"androidx.lifecycle:lifecycle-viewmodel:2.5.0",
"androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.0",
"androidx.multidex:multidex:2.0.1",
"androidx.savedstate:savedstate:1.0.0",
"androidx.navigation:navigation-common:2.5.0",
"androidx.navigation:navigation-fragment:2.5.0",
"androidx.navigation:navigation-runtime:2.5.0",
"androidx.savedstate:savedstate:1.2.0",
"androidx.test:monitor:1.4.0",
"androidx.test:core:1.4.0",
"androidx.test.ext:junit:1.1.3",
Expand Down
Expand Up @@ -24,6 +24,7 @@
import androidx.lifecycle.SavedStateHandle;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.viewmodel.CreationExtras;
import androidx.savedstate.SavedStateRegistryOwner;
import dagger.Module;
import dagger.hilt.EntryPoint;
Expand Down Expand Up @@ -78,7 +79,7 @@ public HiltViewModelFactory(
this.hiltViewModelKeys = hiltViewModelKeys;
this.delegateFactory = delegateFactory;
this.hiltViewModelFactory =
new AbstractSavedStateViewModelFactory(owner, defaultArgs) {
new AbstractSavedStateViewModelFactory() {
@NonNull
@Override
@SuppressWarnings("unchecked")
Expand All @@ -102,6 +103,17 @@ protected <T extends ViewModel> T create(
};
}

@NonNull
@Override
public <T extends ViewModel> T create(
@NonNull Class<T> modelClass, @NonNull CreationExtras extras) {
if (hiltViewModelKeys.contains(modelClass.getName())) {
return hiltViewModelFactory.create(modelClass, extras);
} else {
return delegateFactory.create(modelClass, extras);
}
}

@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
Expand Down
2 changes: 1 addition & 1 deletion javatests/dagger/android/processor/BUILD
Expand Up @@ -35,7 +35,7 @@ GenJavaTests(
"//third_party/java/guava/collect",
"//third_party/java/junit",
"//third_party/java/truth",
"@androidsdk//:platforms/android-30/android.jar",
"@androidsdk//:platforms/android-31/android.jar",
"@maven//:androidx_activity_activity",
"@maven//:androidx_fragment_fragment",
"@maven//:androidx_lifecycle_lifecycle_common",
Expand Down
4 changes: 4 additions & 0 deletions javatests/dagger/hilt/android/AndroidManifest.xml
Expand Up @@ -65,6 +65,10 @@
android:name=".ViewModelScopedTest$TestActivity"
android:exported="false"
tools:ignore="MissingClass"/>
<activity
android:name=".ViewModelSavedStateOwnerTest$TestActivity"
android:exported="false"
tools:ignore="MissingClass"/>
<activity
android:name=".ViewModelWithBaseTest$TestActivity"
android:exported="false"
Expand Down
42 changes: 42 additions & 0 deletions javatests/dagger/hilt/android/BUILD
Expand Up @@ -18,6 +18,17 @@ load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library")

package(default_visibility = ["//:src"])

android_library(
name = "resources",
manifest = "AndroidManifest.xml",
resource_files = glob(["res/**"]),
deps = [
"@maven//:androidx_navigation_navigation_common",
"@maven//:androidx_navigation_navigation_fragment",
"@maven//:androidx_navigation_navigation_runtime",
],
)

# Checks that multiple test roots can be compiled together. This library
# only compiles the sources. they are tested in the android_local_tests.
android_library(
Expand Down Expand Up @@ -576,6 +587,37 @@ android_local_test(
],
)

android_local_test(
name = "ViewModelSavedStateOwnerTest",
srcs = ["ViewModelSavedStateOwnerTest.java"],
manifest = "AndroidManifest.xml",
manifest_values = {
"minSdkVersion": "14",
},
deps = [
":resources",
"//:android_local_test_exports",
"//:dagger_with_compiler",
"//java/dagger/hilt:install_in",
"//java/dagger/hilt/android:android_entry_point",
"//java/dagger/hilt/android:package_info",
"//java/dagger/hilt/android/lifecycle",
"//java/dagger/hilt/android/scopes",
"//java/dagger/hilt/android/testing:hilt_android_test",
"//third_party/java/jsr330_inject",
"//third_party/java/truth",
"@maven//:androidx_activity_activity",
"@maven//:androidx_fragment_fragment",
"@maven//:androidx_lifecycle_lifecycle_common",
"@maven//:androidx_lifecycle_lifecycle_viewmodel",
"@maven//:androidx_lifecycle_lifecycle_viewmodel_savedstate",
"@maven//:androidx_navigation_navigation_common",
"@maven//:androidx_navigation_navigation_fragment",
"@maven//:androidx_navigation_navigation_runtime",
"@maven//:junit_junit",
],
)

android_local_test(
name = "ViewModelWithBaseTest",
srcs = ["ViewModelWithBaseTest.java"],
Expand Down
183 changes: 183 additions & 0 deletions javatests/dagger/hilt/android/ViewModelSavedStateOwnerTest.java
@@ -0,0 +1,183 @@
/*
* Copyright (C) 2022 The Dagger Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package dagger.hilt.android;

import static com.google.common.truth.Truth.assertThat;

import android.os.Build;
import android.os.Bundle;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.annotation.Nullable;
import androidx.lifecycle.SavedStateHandle;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelStoreOwner;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import androidx.test.core.app.ActivityScenario;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import dagger.hilt.android.lifecycle.HiltViewModel;
import dagger.hilt.android.testing.HiltAndroidRule;
import dagger.hilt.android.testing.HiltAndroidTest;
import dagger.hilt.android.testing.HiltTestApplication;
import javax.inject.Inject;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;

/** Test that you can use the Hilt ViewModel factory with other owners. */
@HiltAndroidTest
@RunWith(AndroidJUnit4.class)
// Robolectric requires Java9 to run API 29 and above, so use API 28 instead
@Config(sdk = Build.VERSION_CODES.P, application = HiltTestApplication.class)
public class ViewModelSavedStateOwnerTest {

@Rule public final HiltAndroidRule rule = new HiltAndroidRule(this);

@Test
public void testViewModelSavedState() {
try (ActivityScenario<TestActivity> scenario = ActivityScenario.launch(TestActivity.class)) {
scenario.onActivity(
activity -> {
NavController navController =
Navigation.findNavController(activity, R.id.nav_host_fragment);
TestFragment startFragment = findTestFragment(activity);

MyViewModel activityVm =
getViewModel(activity, activity.getDefaultViewModelProviderFactory());
MyViewModel fragmentVm =
getViewModel(startFragment, startFragment.getDefaultViewModelProviderFactory());
MyViewModel fragmentBackStackVm =
getViewModel(
navController.getBackStackEntry(R.id.start_destination),
startFragment.getDefaultViewModelProviderFactory());
MyViewModel navGraphVm =
getViewModel(
navController.getBackStackEntry(R.id.nav_graph),
startFragment.getDefaultViewModelProviderFactory());

// The activity shouldn't have any arguments since it was only set on the fragment.
assertThat((String) activityVm.savedStateHandle.get("argument_key")).isNull();
activityVm.savedStateHandle.set("other_key", "activity_other_key");

// The fragment argument (set in the navgraph xml) should be set.
assertThat((String) fragmentVm.savedStateHandle.get("argument_key"))
.isEqualTo("fragment_argument");
fragmentVm.savedStateHandle.set("other_key", "fragment_other_key");

// The back stack entry also has the fragment arguments
assertThat((String) fragmentBackStackVm.savedStateHandle.get("argument_key"))
.isEqualTo("fragment_argument");
fragmentBackStackVm.savedStateHandle.set("other_key", "fragment_backstack_other_key");

// When the nav graph itself is the owner, then there should be no arguments.
assertThat((String) navGraphVm.savedStateHandle.get("argument_key")).isNull();
navGraphVm.savedStateHandle.set("other_key", "nav_graph_other_key");

navController.navigate(R.id.next_destination);
});

// Now move to the next fragment to compare
scenario.onActivity(
activity -> {
NavController navController =
Navigation.findNavController(activity, R.id.nav_host_fragment);

TestFragment nextFragment = findTestFragment(activity);

MyViewModel activityVm =
getViewModel(activity, activity.getDefaultViewModelProviderFactory());
MyViewModel fragmentVm =
getViewModel(nextFragment, nextFragment.getDefaultViewModelProviderFactory());
MyViewModel navGraphVm =
getViewModel(
navController.getBackStackEntry(R.id.nav_graph),
nextFragment.getDefaultViewModelProviderFactory());
MyViewModel fragmentBackStackVm =
getViewModel(
navController.getBackStackEntry(R.id.next_destination),
nextFragment.getDefaultViewModelProviderFactory());

// The activity still shouldn't have any arguments, but since it is the same
// owner (since the activity didn't change), the other key should still be set
// from before.
assertThat((String) activityVm.savedStateHandle.get("argument_key")).isNull();
assertThat((String) activityVm.savedStateHandle.get("other_key"))
.isEqualTo("activity_other_key");

// The fragment argument should be set via the navgraph xml again. Also, since
// this is a new fragment, the other key should not be set.
assertThat((String) fragmentVm.savedStateHandle.get("argument_key"))
.isEqualTo("next_fragment_argument");
assertThat((String) fragmentVm.savedStateHandle.get("other_key")).isNull();

// Same as using the fragment as the owner.
assertThat((String) fragmentBackStackVm.savedStateHandle.get("argument_key"))
.isEqualTo("next_fragment_argument");
assertThat((String) fragmentBackStackVm.savedStateHandle.get("other_key")).isNull();

// Similar to the activity case, the navgraph is the same so we expect the same
// key to be set from before. Arguments should still be missing.
assertThat((String) navGraphVm.savedStateHandle.get("argument_key")).isNull();
assertThat((String) navGraphVm.savedStateHandle.get("other_key"))
.isEqualTo("nav_graph_other_key");
});
}
}

private TestFragment findTestFragment(FragmentActivity activity) {
return (TestFragment)
activity
.getSupportFragmentManager()
.findFragmentById(R.id.nav_host_fragment)
.getChildFragmentManager()
.getPrimaryNavigationFragment();
}

private MyViewModel getViewModel(ViewModelStoreOwner owner, ViewModelProvider.Factory factory) {
return new ViewModelProvider(owner, factory).get(MyViewModel.class);
}

@AndroidEntryPoint(FragmentActivity.class)
public static class TestActivity extends Hilt_ViewModelSavedStateOwnerTest_TestActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.navigation_activity);
}
}

@AndroidEntryPoint(Fragment.class)
public static class TestFragment extends Hilt_ViewModelSavedStateOwnerTest_TestFragment {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}

@HiltViewModel
static class MyViewModel extends ViewModel {
final SavedStateHandle savedStateHandle;

@Inject
MyViewModel(SavedStateHandle savedStateHandle) {
this.savedStateHandle = savedStateHandle;
}
}
}
14 changes: 11 additions & 3 deletions javatests/dagger/hilt/android/ViewModelScopedTest.java
Expand Up @@ -53,7 +53,13 @@ public void testViewModelScopeInFragment() {
activity -> {
TestFragment fragment =
(TestFragment) activity.getSupportFragmentManager().findFragmentByTag("tag");
assertThat(fragment.vm.one.bar).isEqualTo(fragment.vm.two.bar);
// Check that the scoped bar is the same instance within the same view model.
assertThat(fragment.vm1.one.bar).isEqualTo(fragment.vm1.two.bar);
assertThat(fragment.vm2.one.bar).isEqualTo(fragment.vm2.two.bar);

// Check that the keyed viewmodels are separate by checking that the bar instances
// are different, and hence have different components.
assertThat(fragment.vm1.one.bar).isNotEqualTo(fragment.vm2.one.bar);
});
}
}
Expand All @@ -76,12 +82,14 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {

@AndroidEntryPoint(Fragment.class)
public static class TestFragment extends Hilt_ViewModelScopedTest_TestFragment {
MyViewModel vm;
MyViewModel vm1;
MyViewModel vm2;

@Override
public void onCreate(@Nullable Bundle bundle) {
super.onCreate(bundle);
vm = new ViewModelProvider(this).get(MyViewModel.class);
vm1 = new ViewModelProvider(this).get("foo", MyViewModel.class);
vm2 = new ViewModelProvider(this).get("bar", MyViewModel.class);
}
}

Expand Down
2 changes: 1 addition & 1 deletion javatests/dagger/hilt/android/processor/internal/BUILD
Expand Up @@ -24,7 +24,7 @@ compiler_test(
compiler_deps = [
"//java/dagger/hilt/android:hilt_android_app",
"//java/dagger/hilt/android:android_entry_point",
"@androidsdk//:platforms/android-30/android.jar",
"@androidsdk//:platforms/android-31/android.jar",
"@maven//:androidx_annotation_annotation",
],
deps = [
Expand Down

0 comments on commit 9a3ac21

Please sign in to comment.