diff options
author | Ian Lake <ilake@google.com> | 2019-04-24 15:37:02 -0700 |
---|---|---|
committer | Ian Lake <ilake@google.com> | 2019-04-25 15:41:55 -0700 |
commit | d5a1c250e6e3471297f64e116a46d1a3a5c9db6c (patch) | |
tree | 1db65893105d62fb924fd660378d8a0c405cfa42 | |
parent | a831c66f1be7a98d259e5bf123d656c87b9a0e87 (diff) | |
download | support-d5a1c250e6e3471297f64e116a46d1a3a5c9db6c.tar.gz |
Use OnBackPressedCallback in NavController
Replace the brittle OnBackStackChangedListener
with an OnBackPressedCallback registered at the
NavController level which works on all Navigators.
It also helps fix a timing issue where the state
of the FragmentNavigator isn't updated after a Fragment
is popped off the stack, leading to a mismatch in
expected actions available if the newly visible Fragment
calls navigate() in a lifecycle method.
Test: updated tests, new OnBackPressedTest
BUG: 111598096
Change-Id: Id679ea6ac9b238240649656fc0796552d6191757
17 files changed, 254 insertions, 406 deletions
diff --git a/navigation/common/api/restricted_2.1.0-alpha03.txt b/navigation/common/api/restricted_2.1.0-alpha03.txt index 37d70c254db..da4f6cc18fe 100644 --- a/navigation/common/api/restricted_2.1.0-alpha03.txt +++ b/navigation/common/api/restricted_2.1.0-alpha03.txt @@ -1,17 +1 @@ // Signature format: 3.0 -package androidx.navigation { - - public abstract class Navigator<D extends androidx.navigation.NavDestination> { - method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public final void addOnNavigatorBackPressListener(androidx.navigation.Navigator.OnNavigatorBackPressListener); - method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public final void dispatchOnNavigatorBackPress(); - method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) protected void onBackPressAdded(); - method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) protected void onBackPressRemoved(); - method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public final void removeOnNavigatorBackPressListener(androidx.navigation.Navigator.OnNavigatorBackPressListener); - } - - @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static interface Navigator.OnNavigatorBackPressListener { - method public void onPopBackStack(androidx.navigation.Navigator); - } - -} - diff --git a/navigation/common/api/restricted_current.txt b/navigation/common/api/restricted_current.txt index 37d70c254db..da4f6cc18fe 100644 --- a/navigation/common/api/restricted_current.txt +++ b/navigation/common/api/restricted_current.txt @@ -1,17 +1 @@ // Signature format: 3.0 -package androidx.navigation { - - public abstract class Navigator<D extends androidx.navigation.NavDestination> { - method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public final void addOnNavigatorBackPressListener(androidx.navigation.Navigator.OnNavigatorBackPressListener); - method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public final void dispatchOnNavigatorBackPress(); - method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) protected void onBackPressAdded(); - method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) protected void onBackPressRemoved(); - method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public final void removeOnNavigatorBackPressListener(androidx.navigation.Navigator.OnNavigatorBackPressListener); - } - - @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static interface Navigator.OnNavigatorBackPressListener { - method public void onPopBackStack(androidx.navigation.Navigator); - } - -} - diff --git a/navigation/common/src/main/java/androidx/navigation/Navigator.java b/navigation/common/src/main/java/androidx/navigation/Navigator.java index 979e8d6d0d9..46627005005 100644 --- a/navigation/common/src/main/java/androidx/navigation/Navigator.java +++ b/navigation/common/src/main/java/androidx/navigation/Navigator.java @@ -25,11 +25,9 @@ import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.RestrictTo; import java.lang.annotation.Retention; import java.lang.annotation.Target; -import java.util.concurrent.CopyOnWriteArrayList; /** * Navigator defines a mechanism for navigating within an app. @@ -67,9 +65,6 @@ public abstract class Navigator<D extends NavDestination> { String value(); } - private final CopyOnWriteArrayList<OnNavigatorBackPressListener> mOnBackPressListeners = - new CopyOnWriteArrayList<>(); - /** * Construct a new NavDestination associated with this Navigator. * @@ -133,67 +128,6 @@ public abstract class Navigator<D extends NavDestination> { } /** - * @hide - */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - protected void onBackPressAdded() { - } - - /** - * @hide - */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - protected void onBackPressRemoved() { - } - - /** - * @hide - */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - public final void addOnNavigatorBackPressListener( - @NonNull OnNavigatorBackPressListener listener) { - boolean added = mOnBackPressListeners.add(listener); - if (added && mOnBackPressListeners.size() == 1) { - onBackPressAdded(); - } - } - - /** - * @hide - */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - public final void removeOnNavigatorBackPressListener( - @NonNull OnNavigatorBackPressListener listener) { - boolean removed = mOnBackPressListeners.remove(listener); - if (removed && mOnBackPressListeners.isEmpty()) { - onBackPressRemoved(); - } - } - - /** - * @hide - */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - public final void dispatchOnNavigatorBackPress() { - for (OnNavigatorBackPressListener listener : mOnBackPressListeners) { - listener.onPopBackStack(this); - } - } - - /** - * @hide - */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - public interface OnNavigatorBackPressListener { - /** - * This method is called after the Navigator navigates to a new destination. - * - * @param navigator - */ - void onPopBackStack(@NonNull Navigator navigator); - } - - /** * Interface indicating that this class should be passed to its respective * {@link Navigator} to enable Navigator specific behavior. */ diff --git a/navigation/fragment/src/androidTest/AndroidManifest.xml b/navigation/fragment/src/androidTest/AndroidManifest.xml index 5e6b71dc9e7..03b4dc38715 100644 --- a/navigation/fragment/src/androidTest/AndroidManifest.xml +++ b/navigation/fragment/src/androidTest/AndroidManifest.xml @@ -19,6 +19,7 @@ package="androidx.navigation.fragment"> <uses-sdk android:targetSdkVersion="${target-sdk-version}"/> <application> + <activity android:name="androidx.navigation.fragment.test.NavigationActivity"/> <activity android:name="androidx.navigation.fragment.XmlNavigationActivity" /> <activity android:name="androidx.navigation.fragment.DynamicNavigationActivity" /> <activity android:name="androidx.navigation.fragment.EmbeddedXmlActivity" /> diff --git a/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt b/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt index b65f205d6f8..9bad25b147e 100644 --- a/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt +++ b/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt @@ -23,7 +23,6 @@ import androidx.fragment.app.FragmentFactory import androidx.fragment.app.FragmentManager import androidx.lifecycle.Lifecycle import androidx.navigation.NavOptions -import androidx.navigation.Navigator import androidx.navigation.fragment.test.EmptyFragment import androidx.navigation.fragment.test.R import androidx.test.annotation.UiThreadTest @@ -40,9 +39,6 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito.mock -import org.mockito.Mockito.verify -import org.mockito.Mockito.verifyNoMoreInteractions @LargeTest @RunWith(AndroidJUnit4::class) @@ -179,45 +175,6 @@ class FragmentNavigatorTest { @UiThreadTest @Test - fun testNavigateWithPopUpToThenPopWithFragmentManager() { - val fragmentNavigator = FragmentNavigator(emptyActivity, fragmentManager, R.id.container) - val backPressListener = mock(Navigator.OnNavigatorBackPressListener::class.java) - fragmentNavigator.addOnNavigatorBackPressListener(backPressListener) - val destination = fragmentNavigator.createDestination() - destination.id = INITIAL_FRAGMENT - destination.className = EmptyFragment::class.java.name - - // Push initial fragment - assertThat(fragmentNavigator.navigate(destination, null, null, null)) - .isEqualTo(destination) - fragmentManager.executePendingTransactions() - - // Push a second fragment - destination.id = SECOND_FRAGMENT - assertThat(fragmentNavigator.navigate(destination, null, null, null)) - .isEqualTo(destination) - fragmentManager.executePendingTransactions() - - // Pop and then push third fragment, simulating popUpTo to initial. - val success = fragmentNavigator.popBackStack() - assertWithMessage("FragmentNavigator should popBackStack successfully") - .that(success) - .isTrue() - destination.id = THIRD_FRAGMENT - assertThat(fragmentNavigator.navigate(destination, null, - NavOptions.Builder().setPopUpTo(INITIAL_FRAGMENT, false).build(), null)) - .isEqualTo(destination) - fragmentManager.executePendingTransactions() - - // Now pop the Fragment - val popped = fragmentManager.popBackStackImmediate() - assertTrue("FragmentNavigator should return true when popping the third fragment", popped) - verify(backPressListener).onPopBackStack(fragmentNavigator) - verifyNoMoreInteractions(backPressListener) - } - - @UiThreadTest - @Test fun testSingleTopInitial() { val fragmentNavigator = FragmentNavigator(emptyActivity, fragmentManager, R.id.container) val destination = fragmentNavigator.createDestination() @@ -249,8 +206,6 @@ class FragmentNavigatorTest { @Test fun testSingleTop() { val fragmentNavigator = FragmentNavigator(emptyActivity, fragmentManager, R.id.container) - val backPressListener = mock(Navigator.OnNavigatorBackPressListener::class.java) - fragmentNavigator.addOnNavigatorBackPressListener(backPressListener) val destination = fragmentNavigator.createDestination() destination.className = EmptyFragment::class.java.name @@ -463,47 +418,8 @@ class FragmentNavigatorTest { @UiThreadTest @Test - fun testPopWithFragmentManager() { - val fragmentNavigator = FragmentNavigator(emptyActivity, fragmentManager, R.id.container) - val backPressListener = mock(Navigator.OnNavigatorBackPressListener::class.java) - fragmentNavigator.addOnNavigatorBackPressListener(backPressListener) - val destination = fragmentNavigator.createDestination() - destination.id = INITIAL_FRAGMENT - destination.className = EmptyFragment::class.java.name - - // First push an initial Fragment - assertThat(fragmentNavigator.navigate(destination, null, null, null)) - .isEqualTo(destination) - fragmentManager.executePendingTransactions() - val fragment = fragmentManager.findFragmentById(R.id.container) - assertNotNull("Fragment should be added", fragment) - - // Now push the Fragment that we want to pop - destination.id = SECOND_FRAGMENT - assertThat(fragmentNavigator.navigate(destination, null, null, null)) - .isEqualTo(destination) - fragmentManager.executePendingTransactions() - val replacementFragment = fragmentManager.findFragmentById(R.id.container) - assertNotNull("Replacement Fragment should be added", replacementFragment) - assertTrue("Replacement Fragment should be the correct type", - replacementFragment is EmptyFragment) - assertEquals("Replacement Fragment should be the primary navigation Fragment", - replacementFragment, fragmentManager.primaryNavigationFragment) - - // Now pop the Fragment - fragmentManager.popBackStackImmediate() - verify(backPressListener).onPopBackStack(fragmentNavigator) - assertEquals("Fragment should be the primary navigation Fragment after pop", - fragment, fragmentManager.primaryNavigationFragment) - verifyNoMoreInteractions(backPressListener) - } - - @UiThreadTest - @Test - fun testDeepLinkPopWithFragmentManager() { + fun testDeepLinkPop() { val fragmentNavigator = FragmentNavigator(emptyActivity, fragmentManager, R.id.container) - val backPressListener = mock(Navigator.OnNavigatorBackPressListener::class.java) - fragmentNavigator.addOnNavigatorBackPressListener(backPressListener) val destination = fragmentNavigator.createDestination() destination.id = INITIAL_FRAGMENT destination.className = EmptyFragment::class.java.name @@ -528,21 +444,18 @@ class FragmentNavigatorTest { replacementFragment, fragmentManager.primaryNavigationFragment) // Now pop the Fragment - fragmentManager.popBackStackImmediate() - verify(backPressListener).onPopBackStack(fragmentNavigator) + fragmentNavigator.popBackStack() + fragmentManager.executePendingTransactions() val fragment = fragmentManager.findFragmentById(R.id.container) assertEquals("Fragment should be the primary navigation Fragment after pop", fragment, fragmentManager.primaryNavigationFragment) - verifyNoMoreInteractions(backPressListener) } @UiThreadTest @Test - fun testDeepLinkPopWithFragmentManagerWithSaveState() { + fun testDeepLinkPopWithSaveState() { var fragmentNavigator = FragmentNavigator(emptyActivity, fragmentManager, R.id.container) - val backPressListener = mock(Navigator.OnNavigatorBackPressListener::class.java) - fragmentNavigator.addOnNavigatorBackPressListener(backPressListener) val destination = fragmentNavigator.createDestination() destination.id = INITIAL_FRAGMENT destination.className = EmptyFragment::class.java.name @@ -568,19 +481,16 @@ class FragmentNavigatorTest { // Create a new FragmentNavigator, replacing the previous one val savedState = fragmentNavigator.onSaveState() - fragmentNavigator.removeOnNavigatorBackPressListener(backPressListener) fragmentNavigator = FragmentNavigator(emptyActivity, fragmentManager, R.id.container) fragmentNavigator.onRestoreState(savedState) - fragmentNavigator.addOnNavigatorBackPressListener(backPressListener) // Now pop the Fragment - fragmentManager.popBackStackImmediate() - verify(backPressListener).onPopBackStack(fragmentNavigator) + fragmentNavigator.popBackStack() + fragmentManager.executePendingTransactions() val fragment = fragmentManager.findFragmentById(R.id.container) assertEquals("Fragment should be the primary navigation Fragment after pop", fragment, fragmentManager.primaryNavigationFragment) - verifyNoMoreInteractions(backPressListener) } @UiThreadTest @@ -588,8 +498,6 @@ class FragmentNavigatorTest { fun testNavigateThenPopAfterSaveState() { var fragmentNavigator = FragmentNavigator(emptyActivity, fragmentManager, R.id.container) - val backPressListener = mock(Navigator.OnNavigatorBackPressListener::class.java) - fragmentNavigator.addOnNavigatorBackPressListener(backPressListener) val destination = fragmentNavigator.createDestination() destination.id = INITIAL_FRAGMENT destination.className = EmptyFragment::class.java.name @@ -618,11 +526,9 @@ class FragmentNavigatorTest { // Create a new FragmentNavigator, replacing the previous one val savedState = fragmentNavigator.onSaveState() - fragmentNavigator.removeOnNavigatorBackPressListener(backPressListener) fragmentNavigator = FragmentNavigator(emptyActivity, fragmentManager, R.id.container) fragmentNavigator.onRestoreState(savedState) - fragmentNavigator.addOnNavigatorBackPressListener(backPressListener) // Now push a third fragment after the state save destination.id = THIRD_FRAGMENT @@ -637,22 +543,18 @@ class FragmentNavigatorTest { replacementFragment, fragmentManager.primaryNavigationFragment) // Now pop the Fragment - fragmentManager.popBackStackImmediate() - verify(backPressListener).onPopBackStack(fragmentNavigator) + fragmentNavigator.popBackStack() + fragmentManager.executePendingTransactions() fragment = fragmentManager.findFragmentById(R.id.container) assertEquals("Fragment should be the primary navigation Fragment after pop", fragment, fragmentManager.primaryNavigationFragment) - - verifyNoMoreInteractions(backPressListener) } @UiThreadTest @Test - fun testMultipleNavigateFragmentTransactionsThenPopWithFragmentManager() { + fun testMultipleNavigateFragmentTransactionsThenPop() { val fragmentNavigator = FragmentNavigator(emptyActivity, fragmentManager, R.id.container) - val backPressListener = mock(Navigator.OnNavigatorBackPressListener::class.java) - fragmentNavigator.addOnNavigatorBackPressListener(backPressListener) val destination = fragmentNavigator.createDestination() destination.className = EmptyFragment::class.java.name @@ -665,19 +567,16 @@ class FragmentNavigatorTest { fragmentNavigator.navigate(destination, null, null, null) // Now pop the Fragment - val popped = fragmentManager.popBackStackImmediate() + val popped = fragmentNavigator.popBackStack() + fragmentManager.executePendingTransactions() assertTrue("FragmentNavigator should return true when popping the third fragment", popped) - verify(backPressListener).onPopBackStack(fragmentNavigator) - verifyNoMoreInteractions(backPressListener) } @UiThreadTest @Test - fun testMultiplePopFragmentTransactionsThenPopWithFragmentManager() { + fun testMultiplePopFragmentTransactionsThenPop() { val fragmentNavigator = FragmentNavigator(emptyActivity, fragmentManager, R.id.container) - val backPressListener = mock(Navigator.OnNavigatorBackPressListener::class.java) - fragmentNavigator.addOnNavigatorBackPressListener(backPressListener) val destination = fragmentNavigator.createDestination() destination.className = EmptyFragment::class.java.name @@ -696,10 +595,9 @@ class FragmentNavigatorTest { fragmentNavigator.popBackStack() fragmentNavigator.popBackStack() - val popped = fragmentManager.popBackStackImmediate() + val popped = fragmentNavigator.popBackStack() + fragmentManager.executePendingTransactions() assertTrue("FragmentNavigator should return true when popping the third fragment", popped) - verify(backPressListener).onPopBackStack(fragmentNavigator) - verifyNoMoreInteractions(backPressListener) } } diff --git a/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/OnBackPressedTest.kt b/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/OnBackPressedTest.kt new file mode 100644 index 00000000000..e3dc4c6e39a --- /dev/null +++ b/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/OnBackPressedTest.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * 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 androidx.navigation.fragment + +import android.os.Bundle +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.test.EmptyFragment +import androidx.navigation.fragment.test.NavigationActivity +import androidx.navigation.fragment.test.R +import androidx.test.annotation.UiThreadTest +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import androidx.test.rule.ActivityTestRule +import com.google.common.truth.Truth.assertWithMessage +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@MediumTest +@RunWith(AndroidJUnit4::class) +class OnBackPressedTest { + + @get:Rule + var activityRule = ActivityTestRule(NavigationActivity::class.java) + + @UiThreadTest + @Test + fun testOnBackPressedOnRoot() { + val activity = activityRule.activity + val navController = activity.navController + navController.setGraph(R.navigation.nav_simple) + activity.onBackPressed() + assertWithMessage("onBackPressed() should finish the activity on the root") + .that(activity.isFinishing) + .isTrue() + } + + @UiThreadTest + @Test + fun testOnBackPressedAfterNavigate() { + val activity = activityRule.activity + val navController = activity.navController + navController.setGraph(R.navigation.nav_simple) + navController.navigate(R.id.empty_fragment) + + activity.onBackPressed() + assertWithMessage("onBackPressed() should trigger NavController.popBackStack()") + .that(navController.currentDestination?.id) + .isEqualTo(R.id.start_fragment) + } + + @UiThreadTest + @Test + fun testOnBackPressedWithChildBackStack() { + val activity = activityRule.activity + val navHostFragment = activity.supportFragmentManager.primaryNavigationFragment + as NavHostFragment + val navHostFragmentManager = navHostFragment.childFragmentManager + val navController = navHostFragment.navController + navController.setGraph(R.navigation.nav_simple) + navController.navigate(R.id.child_back_stack_fragment) + navHostFragmentManager.executePendingTransactions() + + val currentFragment = navHostFragmentManager.primaryNavigationFragment + as ChildBackStackFragment + assertWithMessage("Current Fragment should have a child Fragment by default") + .that(currentFragment.childFragment) + .isNotNull() + + activity.onBackPressed() + assertWithMessage("onBackPressed() should not trigger NavController when there is a " + + "child back stack") + .that(navController.currentDestination?.id) + .isEqualTo(R.id.child_back_stack_fragment) + assertWithMessage("Child Fragment should be popped") + .that(currentFragment.childFragment) + .isNull() + } +} + +class ChildBackStackFragment : EmptyFragment() { + val childFragment get() = childFragmentManager.findFragmentByTag("child") + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + childFragmentManager.beginTransaction() + .add(Fragment(), "child") + .addToBackStack(null) + .commit() + } +}
\ No newline at end of file diff --git a/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/test/EmptyFragment.kt b/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/test/EmptyFragment.kt index 8ab7d3f97fe..094edf20872 100644 --- a/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/test/EmptyFragment.kt +++ b/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/test/EmptyFragment.kt @@ -23,7 +23,7 @@ import android.view.ViewGroup import android.widget.FrameLayout import androidx.fragment.app.Fragment -class EmptyFragment : Fragment() { +open class EmptyFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, diff --git a/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/test/NavigationActivity.kt b/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/test/NavigationActivity.kt new file mode 100644 index 00000000000..1669f10c585 --- /dev/null +++ b/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/test/NavigationActivity.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * 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 androidx.navigation.fragment.test + +import androidx.fragment.app.FragmentActivity +import androidx.navigation.findNavController + +class NavigationActivity : FragmentActivity(R.layout.navigation_activity) { + val navController get() = findNavController(R.id.nav_host) +}
\ No newline at end of file diff --git a/navigation/fragment/src/androidTest/res/navigation/nav_simple.xml b/navigation/fragment/src/androidTest/res/navigation/nav_simple.xml new file mode 100644 index 00000000000..95f2c5790e3 --- /dev/null +++ b/navigation/fragment/src/androidTest/res/navigation/nav_simple.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2019 The Android Open Source Project + + 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. + --> + +<navigation xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/nav_simple" + app:startDestination="@+id/start_fragment"> + <fragment + android:id="@+id/start_fragment" + android:name="androidx.navigation.fragment.test.EmptyFragment"/> + <fragment + android:id="@+id/empty_fragment" + android:name="androidx.navigation.fragment.test.EmptyFragment"/> + <fragment + android:id="@+id/child_back_stack_fragment" + android:name="androidx.navigation.fragment.ChildBackStackFragment"/> +</navigation>
\ No newline at end of file diff --git a/navigation/fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.java b/navigation/fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.java index 9c125546562..da02a88e23e 100644 --- a/navigation/fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.java +++ b/navigation/fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.java @@ -39,7 +39,6 @@ import androidx.navigation.NavigatorProvider; import java.util.ArrayDeque; import java.util.Collections; -import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; @@ -62,54 +61,9 @@ public class FragmentNavigator extends Navigator<FragmentNavigator.Destination> private static final String KEY_BACK_STACK_IDS = "androidx-nav-fragment:navigator:backStackIds"; private final Context mContext; - @SuppressWarnings("WeakerAccess") /* synthetic access */ - final FragmentManager mFragmentManager; + private final FragmentManager mFragmentManager; private final int mContainerId; - @SuppressWarnings("WeakerAccess") /* synthetic access */ - ArrayDeque<Integer> mBackStack = new ArrayDeque<>(); - @SuppressWarnings("WeakerAccess") /* synthetic access */ - boolean mIsPendingBackStackOperation = false; - - /** - * A fragment manager back stack change listener used to detect a fragment being popped due to - * onBackPressed(). - * - * Since a back press is a pop in the FragmentManager not caused by this navigator a flag is - * used to identify operations by this navigator. If the flag is ON, this listener doesn't do - * anything since the change in the fragment manager's back stack was caused by the navigator. - * The flag is reset once this navigator's back stack matches the fragment manager's back stack. - * If the flag is OFF then a change in the back stack was not caused by this navigator, it is - * then appropriate to check if a fragment was popped to dispatch navigator events. - * - * Note that onBackPressed() invokes popBackStackImmediate(), meaning pending transactions - if - * any - before the pop will be executed causing this listener to be called one or more times - * until the flag is reset. Finally, the pop due to pressing back occurs, at which it is - * appropriate to dispatch a navigator popped event. - */ - private final FragmentManager.OnBackStackChangedListener mOnBackStackChangedListener = - new FragmentManager.OnBackStackChangedListener() { - - @Override - public void onBackStackChanged() { - // If we have pending operations made by us then consume this change, otherwise - // detect a pop in the back stack to dispatch callback. - if (mIsPendingBackStackOperation) { - mIsPendingBackStackOperation = !isBackStackEqual(); - return; - } - - // The initial Fragment won't be on the back stack, so the - // real count of destinations is the back stack entry count + 1 - int newCount = mFragmentManager.getBackStackEntryCount() + 1; - if (newCount < mBackStack.size()) { - // Handle cases where the user hit the system back button - while (mBackStack.size() > newCount) { - mBackStack.removeLast(); - } - dispatchOnNavigatorBackPress(); - } - } - }; + private ArrayDeque<Integer> mBackStack = new ArrayDeque<>(); public FragmentNavigator(@NonNull Context context, @NonNull FragmentManager manager, int containerId) { @@ -118,16 +72,6 @@ public class FragmentNavigator extends Navigator<FragmentNavigator.Destination> mContainerId = containerId; } - @Override - protected void onBackPressAdded() { - mFragmentManager.addOnBackStackChangedListener(mOnBackStackChangedListener); - } - - @Override - protected void onBackPressRemoved() { - mFragmentManager.removeOnBackStackChangedListener(mOnBackStackChangedListener); - } - /** * {@inheritDoc} * <p> @@ -154,7 +98,6 @@ public class FragmentNavigator extends Navigator<FragmentNavigator.Destination> mFragmentManager.popBackStack( generateBackStackName(mBackStack.size(), mBackStack.peekLast()), FragmentManager.POP_BACK_STACK_INCLUSIVE); - mIsPendingBackStackOperation = true; } // else, we're on the first Fragment, so there's nothing to pop from FragmentManager mBackStack.removeLast(); return true; @@ -204,6 +147,7 @@ public class FragmentNavigator extends Navigator<FragmentNavigator.Destination> * asynchronously, so the new Fragment is not instantly available * after this call completes. */ + @SuppressWarnings("deprecation") /* Using instantiateFragment for forward compatibility */ @Nullable @Override public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args, @@ -217,7 +161,6 @@ public class FragmentNavigator extends Navigator<FragmentNavigator.Destination> if (className.charAt(0) == '.') { className = mContext.getPackageName() + className; } - //noinspection deprecation needed to maintain forward compatibility final Fragment frag = instantiateFragment(mContext, mFragmentManager, className, args); frag.setArguments(args); @@ -259,12 +202,10 @@ public class FragmentNavigator extends Navigator<FragmentNavigator.Destination> generateBackStackName(mBackStack.size(), mBackStack.peekLast()), FragmentManager.POP_BACK_STACK_INCLUSIVE); ft.addToBackStack(generateBackStackName(mBackStack.size(), destId)); - mIsPendingBackStackOperation = true; } isAdded = false; } else { ft.addToBackStack(generateBackStackName(mBackStack.size() + 1, destId)); - mIsPendingBackStackOperation = true; isAdded = true; } if (navigatorExtras instanceof Extras) { @@ -336,40 +277,6 @@ public class FragmentNavigator extends Navigator<FragmentNavigator.Destination> } /** - * Checks if this FragmentNavigator's back stack is equal to the FragmentManager's back stack. - */ - @SuppressWarnings("WeakerAccess") /* synthetic access */ - boolean isBackStackEqual() { - int fragmentBackStackCount = mFragmentManager.getBackStackEntryCount(); - // Initial fragment won't be on the FragmentManager's back stack so +1 its count. - if (mBackStack.size() != fragmentBackStackCount + 1) { - return false; - } - - // From top to bottom verify destination ids match in both back stacks/ - Iterator<Integer> backStackIterator = mBackStack.descendingIterator(); - int fragmentBackStackIndex = fragmentBackStackCount - 1; - while (backStackIterator.hasNext() && fragmentBackStackIndex >= 0) { - int destId = backStackIterator.next(); - try { - int fragmentDestId = getDestId(mFragmentManager - .getBackStackEntryAt(fragmentBackStackIndex--) - .getName()); - if (destId != fragmentDestId) { - return false; - } - } catch (NumberFormatException e) { - throw new IllegalStateException("Invalid back stack entry on the " - + "NavHostFragment's back stack - use getChildFragmentManager() " - + "if you need to do custom FragmentTransactions from within " - + "Fragments created via your navigation graph."); - } - } - - return true; - } - - /** * NavDestination specific to {@link FragmentNavigator} */ @NavDestination.ClassType(Fragment.class) diff --git a/navigation/fragment/src/main/java/androidx/navigation/fragment/NavHostFragment.java b/navigation/fragment/src/main/java/androidx/navigation/fragment/NavHostFragment.java index c289b8a9169..8a344e96a18 100644 --- a/navigation/fragment/src/main/java/androidx/navigation/fragment/NavHostFragment.java +++ b/navigation/fragment/src/main/java/androidx/navigation/fragment/NavHostFragment.java @@ -202,8 +202,12 @@ public class NavHostFragment extends Fragment implements NavHost { public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); final Context context = requireContext(); + // TODO Fix Fragments to register their OnBackPressedCallback eagerly + getChildFragmentManager(); mNavController = new NavController(context); + mNavController.setHostLifecycleOwner(this); + mNavController.setHostOnBackPressedDispatcherOwner(requireActivity()); mNavController.setHostViewModelStore(getViewModelStore()); onCreateNavController(mNavController); diff --git a/navigation/runtime/api/2.1.0-alpha03.txt b/navigation/runtime/api/2.1.0-alpha03.txt index 2fd3e0f1c35..8fe3dc47333 100644 --- a/navigation/runtime/api/2.1.0-alpha03.txt +++ b/navigation/runtime/api/2.1.0-alpha03.txt @@ -68,6 +68,8 @@ package androidx.navigation { method @CallSuper public void setGraph(@NavigationRes int, android.os.Bundle?); method @CallSuper public void setGraph(androidx.navigation.NavGraph); method @CallSuper public void setGraph(androidx.navigation.NavGraph, android.os.Bundle?); + method public void setHostLifecycleOwner(androidx.lifecycle.LifecycleOwner); + method public void setHostOnBackPressedDispatcherOwner(androidx.activity.OnBackPressedDispatcherOwner); method public void setHostViewModelStore(androidx.lifecycle.ViewModelStore); field public static final String KEY_DEEP_LINK_INTENT = "android-support-nav:controller:deepLinkIntent"; } diff --git a/navigation/runtime/api/current.txt b/navigation/runtime/api/current.txt index 2fd3e0f1c35..8fe3dc47333 100644 --- a/navigation/runtime/api/current.txt +++ b/navigation/runtime/api/current.txt @@ -68,6 +68,8 @@ package androidx.navigation { method @CallSuper public void setGraph(@NavigationRes int, android.os.Bundle?); method @CallSuper public void setGraph(androidx.navigation.NavGraph); method @CallSuper public void setGraph(androidx.navigation.NavGraph, android.os.Bundle?); + method public void setHostLifecycleOwner(androidx.lifecycle.LifecycleOwner); + method public void setHostOnBackPressedDispatcherOwner(androidx.activity.OnBackPressedDispatcherOwner); method public void setHostViewModelStore(androidx.lifecycle.ViewModelStore); field public static final String KEY_DEEP_LINK_INTENT = "android-support-nav:controller:deepLinkIntent"; } diff --git a/navigation/runtime/build.gradle b/navigation/runtime/build.gradle index 3a02683f2a4..d21d9e69e7c 100644 --- a/navigation/runtime/build.gradle +++ b/navigation/runtime/build.gradle @@ -33,6 +33,7 @@ android { dependencies { api(project(":navigation:navigation-common")) + api(project(":activity")) api(project(":lifecycle:lifecycle-viewmodel")) androidTestImplementation(project(":navigation:navigation-testing")) diff --git a/navigation/runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt b/navigation/runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt index bfbf4ef9b66..bab44e2e330 100644 --- a/navigation/runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt +++ b/navigation/runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt @@ -505,52 +505,6 @@ class NavControllerTest { } @Test - fun testNavigateFromNestedThenNavigatorInstigatedPop() { - val navController = createNavController() - navController.setGraph(R.navigation.nav_nested_start_destination) - val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java) - assertEquals(R.id.nested_test, navController.currentDestination?.id ?: 0) - assertEquals(1, navigator.backStack.size) - - navController.navigate(R.id.second_test) - assertEquals(R.id.second_test, navController.currentDestination?.id ?: 0) - assertEquals(2, navigator.backStack.size) - - // A Navigator can pop a destination off its own back stack - // then inform the NavController via dispatchOnNavigatorNavigated - navigator.backStack.removeLast() - val newDestination = navigator.current.first - assertNotNull(newDestination) - navigator.dispatchOnNavigatorBackPress() - assertEquals(R.id.nested_test, navController.currentDestination?.id ?: 0) - assertEquals(1, navigator.backStack.size) - } - - @Test - fun testNavigateNestedPopUpToThenNavigatorInstigatedPop() { - val navController = createNavController() - navController.setGraph(R.navigation.nav_nested_start_destination) - val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java) - assertEquals(R.id.nested_test, navController.currentDestination?.id ?: 0) - assertEquals(1, navigator.backStack.size) - - navController.navigate(R.id.pop_forward) - assertEquals(R.id.nested_second_test, navController.currentDestination?.id ?: 0) - assertEquals(1, navigator.backStack.size) - - // A Navigator can pop a destination off its own back stack - // then inform the NavController via dispatchOnNavigatorNavigated - navigator.backStack.removeLast() - navigator.dispatchOnNavigatorBackPress() - assertWithMessage("The last destination should be popped off the stack") - .that(navController.currentDestination) - .isNull() - assertWithMessage("TestNavigator should not nothing on its back stack") - .that(navigator.backStack.size) - .isEqualTo(0) - } - - @Test fun testNavigateThenNavigateWithPop() { val navController = createNavController() navController.setGraph(R.navigation.nav_simple) diff --git a/navigation/runtime/src/main/java/androidx/navigation/NavController.java b/navigation/runtime/src/main/java/androidx/navigation/NavController.java index 976374e4177..b5e664c7074 100644 --- a/navigation/runtime/src/main/java/androidx/navigation/NavController.java +++ b/navigation/runtime/src/main/java/androidx/navigation/NavController.java @@ -25,12 +25,16 @@ import android.os.Bundle; import android.os.Parcelable; import android.util.Log; +import androidx.activity.OnBackPressedCallback; +import androidx.activity.OnBackPressedDispatcher; +import androidx.activity.OnBackPressedDispatcherOwner; import androidx.annotation.CallSuper; import androidx.annotation.IdRes; import androidx.annotation.NavigationRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.TaskStackBuilder; +import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.ViewModelStore; import java.util.ArrayDeque; @@ -87,57 +91,10 @@ public class NavController { @SuppressWarnings("WeakerAccess") /* synthetic access */ final Deque<NavBackStackEntry> mBackStack = new ArrayDeque<>(); + private LifecycleOwner mLifecycleOwner; private NavControllerViewModel mViewModel; - private final NavigatorProvider mNavigatorProvider = new NavigatorProvider() { - @Nullable - @Override - public Navigator<? extends NavDestination> addNavigator(@NonNull String name, - @NonNull Navigator<? extends NavDestination> navigator) { - Navigator<? extends NavDestination> previousNavigator = - super.addNavigator(name, navigator); - if (previousNavigator != navigator) { - if (previousNavigator != null) { - previousNavigator.removeOnNavigatorBackPressListener(mOnBackPressListener); - } - navigator.addOnNavigatorBackPressListener(mOnBackPressListener); - } - return previousNavigator; - } - }; - - @SuppressWarnings("WeakerAccess") /* synthetic access */ - final Navigator.OnNavigatorBackPressListener mOnBackPressListener = - new Navigator.OnNavigatorBackPressListener() { - @Override - public void onPopBackStack(@NonNull Navigator navigator) { - // Find what destination just got popped - NavDestination lastFromNavigator = null; - Iterator<NavBackStackEntry> iterator = mBackStack.descendingIterator(); - while (iterator.hasNext()) { - NavDestination destination = iterator.next().getDestination(); - Navigator currentNavigator = getNavigatorProvider().getNavigator( - destination.getNavigatorName()); - if (currentNavigator == navigator) { - lastFromNavigator = destination; - break; - } - } - if (lastFromNavigator == null) { - throw new IllegalArgumentException("Navigator " + navigator - + " reported pop but did not have any destinations" - + " on the NavController back stack"); - } - // Pop all intervening destinations from other Navigators off the - // back stack - popBackStackInternal(lastFromNavigator.getId(), false); - // Now record the pop operation that we were sent - if (!mBackStack.isEmpty()) { - mBackStack.removeLast(); - } - dispatchOnDestinationChanged(); - } - }; + private final NavigatorProvider mNavigatorProvider = new NavigatorProvider(); private final CopyOnWriteArrayList<OnDestinationChangedListener> mOnDestinationChangedListeners = new CopyOnWriteArrayList<>(); @@ -1032,6 +989,52 @@ public class NavController { } /** + * Sets the host's {@link LifecycleOwner}. + * + * @param owner The {@link LifecycleOwner} associated with the containing {@link NavHost}. + * @see #setHostOnBackPressedDispatcherOwner(OnBackPressedDispatcherOwner) + */ + public void setHostLifecycleOwner(@NonNull LifecycleOwner owner) { + mLifecycleOwner = owner; + } + + /** + * Sets the host's {@link OnBackPressedDispatcherOwner}. If set, NavController will + * register a {@link OnBackPressedCallback} to handle system Back button events. + * <p> + * If you have not explicitly called {@link #setHostLifecycleOwner(LifecycleOwner)}, + * the owner you pass here will be used as the {@link LifecycleOwner} for registering + * the {@link OnBackPressedCallback}. + * + * @param owner The {@link OnBackPressedDispatcherOwner} associated with the containing + * {@link NavHost}. + * @see #setHostLifecycleOwner(LifecycleOwner) + */ + public void setHostOnBackPressedDispatcherOwner(@NonNull OnBackPressedDispatcherOwner owner) { + if (mLifecycleOwner == null) { + mLifecycleOwner = owner; + } + OnBackPressedDispatcher dispatcher = owner.getOnBackPressedDispatcher(); + dispatcher.addCallback(mLifecycleOwner, new OnBackPressedCallback(true) { + @Override + public boolean isEnabled() { + int destinationCount = 0; + for (NavBackStackEntry entry : mBackStack) { + if (!(entry.getDestination() instanceof NavGraph)) { + destinationCount++; + } + } + return destinationCount > 1; + } + + @Override + public void handleOnBackPressed() { + popBackStack(); + } + }); + } + + /** * Sets the host's ViewModelStore used by the NavController to store ViewModels at the * navigation graph level. This is required to call {@link #getViewModelStore} and * should generally be called for you by your {@link NavHost}. diff --git a/navigation/runtime/src/main/java/androidx/navigation/NavHost.java b/navigation/runtime/src/main/java/androidx/navigation/NavHost.java index a6b59d5ed9c..c8bdc194c32 100644 --- a/navigation/runtime/src/main/java/androidx/navigation/NavHost.java +++ b/navigation/runtime/src/main/java/androidx/navigation/NavHost.java @@ -19,7 +19,10 @@ package androidx.navigation; import android.os.Bundle; import android.view.View; +import androidx.activity.OnBackPressedDispatcherOwner; import androidx.annotation.NonNull; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.ViewModelStore; /** * A host is a single context or container for navigation via a {@link NavController}. @@ -29,6 +32,17 @@ import androidx.annotation.NonNull; * <li>Handle {@link NavController#saveState() saving} and * {@link NavController#restoreState(Bundle) restoring} their controller's state</li> * <li>Call {@link Navigation#setViewNavController(View, NavController)} on their root view</li> + * <li>Route system Back button events to the NavController either by manually calling + * {@link NavController#popBackStack()} or by calling + * {@link NavController#setHostOnBackPressedDispatcherOwner(OnBackPressedDispatcherOwner)} + * when constructing the NavController.</li> + * </ul> + * Optionally, a navigation host should consider calling: + * <ul> + * <li>Call {@link NavController#setHostLifecycleOwner(LifecycleOwner)} to associate the + * NavController with a specific Lifecycle.</li> + * <li>Call {@link NavController#setHostViewModelStore(ViewModelStore)} to enable usage of + * {@link NavController#getViewModelStore(int)} and navigation graph scoped ViewModels.</li> * </ul> */ public interface NavHost { |