[go: nahoru, domu]

Add Experimental APIs for opts out of savedState

Now that navigation supports saving the state for multiple back stacks,
we have made that the default behavior for all of the NavigationUI
helpers. Unfortunately, saving state does not support some deprecated
APIs like retained fragments, which means using them together causes a
crash.

We need to add some experimental APIs that allow developers to disable
saved stated in the short term so that those apps can fix their usages
of deprecated APis and properly save their state.

RelNote: "Added experimental APIs to `NavigationUI` to allow you to opt
out of saving your state."
Test: ./gradlew checkApi
Bug: 188466447

Change-Id: Idf93c093c0f3729148c78c1de9d36a0f10fbb31f
diff --git a/navigation/navigation-ui/api/public_plus_experimental_current.txt b/navigation/navigation-ui/api/public_plus_experimental_current.txt
index 8be4b08..ac4f692 100644
--- a/navigation/navigation-ui/api/public_plus_experimental_current.txt
+++ b/navigation/navigation-ui/api/public_plus_experimental_current.txt
@@ -64,6 +64,7 @@
     method public static boolean navigateUp(androidx.navigation.NavController navController, androidx.customview.widget.Openable? openableLayout);
     method public static boolean navigateUp(androidx.navigation.NavController navController, androidx.navigation.ui.AppBarConfiguration configuration);
     method public static boolean onNavDestinationSelected(android.view.MenuItem item, androidx.navigation.NavController navController);
+    method @androidx.navigation.ui.NavigationUiSaveStateControl public static boolean onNavDestinationSelected(android.view.MenuItem item, androidx.navigation.NavController navController, boolean saveState);
     method public static void setupActionBarWithNavController(androidx.appcompat.app.AppCompatActivity activity, androidx.navigation.NavController navController, androidx.customview.widget.Openable? openableLayout);
     method public static void setupActionBarWithNavController(androidx.appcompat.app.AppCompatActivity activity, androidx.navigation.NavController navController, optional androidx.navigation.ui.AppBarConfiguration configuration);
     method public static void setupActionBarWithNavController(androidx.appcompat.app.AppCompatActivity activity, androidx.navigation.NavController navController);
@@ -74,10 +75,15 @@
     method public static void setupWithNavController(com.google.android.material.appbar.CollapsingToolbarLayout collapsingToolbarLayout, androidx.appcompat.widget.Toolbar toolbar, androidx.navigation.NavController navController, optional androidx.navigation.ui.AppBarConfiguration configuration);
     method public static void setupWithNavController(com.google.android.material.appbar.CollapsingToolbarLayout collapsingToolbarLayout, androidx.appcompat.widget.Toolbar toolbar, androidx.navigation.NavController navController);
     method public static void setupWithNavController(com.google.android.material.navigation.NavigationView navigationView, androidx.navigation.NavController navController);
+    method @androidx.navigation.ui.NavigationUiSaveStateControl public static void setupWithNavController(com.google.android.material.navigation.NavigationView navigationView, androidx.navigation.NavController navController, boolean saveState);
     method public static void setupWithNavController(com.google.android.material.navigation.NavigationBarView navigationBarView, androidx.navigation.NavController navController);
+    method @androidx.navigation.ui.NavigationUiSaveStateControl public static void setupWithNavController(com.google.android.material.navigation.NavigationBarView navigationBarView, androidx.navigation.NavController navController, boolean saveState);
     field public static final androidx.navigation.ui.NavigationUI INSTANCE;
   }
 
+  @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget) public @interface NavigationUiSaveStateControl {
+  }
+
   public final class NavigationViewKt {
     method public static void setupWithNavController(com.google.android.material.navigation.NavigationView, androidx.navigation.NavController navController);
   }
diff --git a/navigation/navigation-ui/build.gradle b/navigation/navigation-ui/build.gradle
index 0333fb6..e0fa19fa 100644
--- a/navigation/navigation-ui/build.gradle
+++ b/navigation/navigation-ui/build.gradle
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
 import static androidx.build.dependencies.DependenciesKt.*
 import androidx.build.LibraryGroups
 import androidx.build.LibraryVersions
@@ -41,6 +43,7 @@
     api("androidx.drawerlayout:drawerlayout:1.1.1")
     api("com.google.android.material:material:1.4.0-beta01")
     implementation("androidx.transition:transition:1.3.0")
+    api("androidx.annotation:annotation-experimental:1.1.0")
 
     androidTestImplementation(project(":internal-testutils-navigation"), {
         exclude group: "androidx.navigation", module: "navigation-common"
@@ -60,3 +63,10 @@
     inceptionYear = "2018"
     description = "Android Navigation-UI"
 }
+
+// Allow usage of Kotlin's @OptIn.
+tasks.withType(KotlinCompile).configureEach {
+    kotlinOptions {
+        freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn"]
+    }
+}
diff --git a/navigation/navigation-ui/src/main/java/androidx/navigation/ui/NavigationUI.kt b/navigation/navigation-ui/src/main/java/androidx/navigation/ui/NavigationUI.kt
index 2192929..2575ece 100644
--- a/navigation/navigation-ui/src/main/java/androidx/navigation/ui/NavigationUI.kt
+++ b/navigation/navigation-ui/src/main/java/androidx/navigation/ui/NavigationUI.kt
@@ -97,6 +97,69 @@
     }
 
     /**
+     * Attempt to navigate to the [NavDestination] associated with the given MenuItem. This
+     * MenuItem should have been added via one of the helper methods in this class.
+     *
+     * Importantly, it assumes the [menu item id][MenuItem.getItemId] matches a valid
+     * [action id][NavDestination.getAction] or [destination id][NavDestination.id] to be
+     * navigated to.
+     *
+     * By default, the back stack will be popped back to the navigation graph's start destination.
+     * Menu items that have `android:menuCategory="secondary"` will not pop the back
+     * stack.
+     *
+     * @param item The selected MenuItem.
+     * @param navController The NavController that hosts the destination.
+     * @param saveState Whether the NavController should save the back stack state. This must
+     * always be `false`: leave this parameter off entirely to use the non-experimental version
+     * of this API, which saves the state by default.
+     *
+     * @return True if the [NavController] was able to navigate to the destination
+     * associated with the given MenuItem.
+     */
+    @NavigationUiSaveStateControl
+    @JvmStatic
+    public fun onNavDestinationSelected(
+        item: MenuItem,
+        navController: NavController,
+        saveState: Boolean
+    ): Boolean {
+        check(!saveState) {
+            "Leave the saveState parameter out entirely to use the non-experimental version of " +
+                "this API, which saves the state by default"
+        }
+        val builder = NavOptions.Builder().setLaunchSingleTop(true)
+        if (
+            navController.currentDestination!!.parent!!.findNode(item.itemId)
+            is ActivityNavigator.Destination
+        ) {
+            builder.setEnterAnim(R.anim.nav_default_enter_anim)
+                .setExitAnim(R.anim.nav_default_exit_anim)
+                .setPopEnterAnim(R.anim.nav_default_pop_enter_anim)
+                .setPopExitAnim(R.anim.nav_default_pop_exit_anim)
+        } else {
+            builder.setEnterAnim(R.animator.nav_default_enter_anim)
+                .setExitAnim(R.animator.nav_default_exit_anim)
+                .setPopEnterAnim(R.animator.nav_default_pop_enter_anim)
+                .setPopExitAnim(R.animator.nav_default_pop_exit_anim)
+        }
+        if (item.order and Menu.CATEGORY_SECONDARY == 0) {
+            builder.setPopUpTo(
+                findStartDestination(navController.graph).id,
+                inclusive = false
+            )
+        }
+        val options = builder.build()
+        return try {
+            // TODO provide proper API instead of using Exceptions as Control-Flow.
+            navController.navigate(item.itemId, null, options)
+            true
+        } catch (e: IllegalArgumentException) {
+            false
+        }
+    }
+
+    /**
      * Handles the Up button by delegating its behavior to the given NavController. This should
      * generally be called from [AppCompatActivity.onSupportNavigateUp].
      *
@@ -426,6 +489,75 @@
     }
 
     /**
+     * Sets up a [NavigationView] for use with a [NavController]. This will call
+     * [onNavDestinationSelected] when a menu item is selected.
+     * The selected item in the NavigationView will automatically be updated when the destination
+     * changes.
+     *
+     * If the [NavigationView] is directly contained with an [Openable] layout,
+     * it will be closed when a menu item is selected.
+     *
+     * Similarly, if the [NavigationView] has a [BottomSheetBehavior] associated with
+     * it (as is the case when using a [com.google.android.material.bottomsheet.BottomSheetDialog]),
+     * the bottom sheet will be hidden when a menu item is selected.
+     *
+     * @param navigationView The NavigationView that should be kept in sync with changes to the
+     * NavController.
+     * @param navController The NavController that supplies the primary and secondary menu.
+     * @param saveState Whether the NavController should save the back stack state. This must
+     * always be `false`: leave this parameter off entirely to use the non-experimental version
+     * of this API, which saves the state by default.
+     *
+     * Navigation actions on this NavController will be reflected in the
+     * selected item in the NavigationView.
+     */
+    @NavigationUiSaveStateControl
+    @JvmStatic
+    public fun setupWithNavController(
+        navigationView: NavigationView,
+        navController: NavController,
+        saveState: Boolean
+    ) {
+        check(!saveState) {
+            "Leave the saveState parameter out entirely to use the non-experimental version of " +
+                "this API, which saves the state by default"
+        }
+        navigationView.setNavigationItemSelectedListener { item ->
+            val handled = onNavDestinationSelected(item, navController, saveState)
+            if (handled) {
+                val parent = navigationView.parent
+                if (parent is Openable) {
+                    parent.close()
+                } else {
+                    val bottomSheetBehavior = findBottomSheetBehavior(navigationView)
+                    if (bottomSheetBehavior != null) {
+                        bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
+                    }
+                }
+            }
+            handled
+        }
+        val weakReference = WeakReference(navigationView)
+        navController.addOnDestinationChangedListener(
+            object : NavController.OnDestinationChangedListener {
+                override fun onDestinationChanged(
+                    controller: NavController,
+                    destination: NavDestination,
+                    arguments: Bundle?
+                ) {
+                    val view = weakReference.get()
+                    if (view == null) {
+                        navController.removeOnDestinationChangedListener(this)
+                        return
+                    }
+                    view.menu.forEach { item ->
+                        item.isChecked = matchDestination(destination, item.itemId)
+                    }
+                }
+            })
+    }
+
+    /**
      * Walks up the view hierarchy, trying to determine if the given View is contained within
      * a bottom sheet.
      */
@@ -494,6 +626,59 @@
     }
 
     /**
+     * Sets up a [NavigationBarView] for use with a [NavController]. This will call
+     * [onNavDestinationSelected] when a menu item is selected. The
+     * selected item in the NavigationBarView will automatically be updated when the destination
+     * changes.
+     *
+     * @param navigationBarView The NavigationBarView ([BottomNavigationView] or
+     * [NavigationRailView])
+     * that should be kept in sync with changes to the NavController.
+     * @param navController The NavController that supplies the primary menu.
+     * @param saveState Whether the NavController should save the back stack state. This must
+     * always be `false`: leave this parameter off entirely to use the non-experimental version
+     * of this API, which saves the state by default.
+     *
+     * Navigation actions on this NavController will be reflected in the
+     * selected item in the NavigationBarView.
+     */
+    @NavigationUiSaveStateControl
+    @JvmStatic
+    public fun setupWithNavController(
+        navigationBarView: NavigationBarView,
+        navController: NavController,
+        saveState: Boolean
+    ) {
+        check(!saveState) {
+            "Leave the saveState parameter out entirely to use the non-experimental version of " +
+                "this API, which saves the state by default"
+        }
+        navigationBarView.setOnItemSelectedListener { item ->
+            onNavDestinationSelected(item, navController, saveState)
+        }
+        val weakReference = WeakReference(navigationBarView)
+        navController.addOnDestinationChangedListener(
+            object : NavController.OnDestinationChangedListener {
+                override fun onDestinationChanged(
+                    controller: NavController,
+                    destination: NavDestination,
+                    arguments: Bundle?
+                ) {
+                    val view = weakReference.get()
+                    if (view == null) {
+                        navController.removeOnDestinationChangedListener(this)
+                        return
+                    }
+                    view.menu.forEach { item ->
+                        if (matchDestination(destination, item.itemId)) {
+                            item.isChecked = true
+                        }
+                    }
+                }
+            })
+    }
+
+    /**
      * Determines whether the given `destId` matches the NavDestination. This handles
      * both the default case (the destination's id matches the given id) and the nested case where
      * the given id is a parent/grandparent/etc of the destination.
diff --git a/navigation/navigation-ui/src/main/java/androidx/navigation/ui/NavigationUiSaveStateControl.kt b/navigation/navigation-ui/src/main/java/androidx/navigation/ui/NavigationUiSaveStateControl.kt
new file mode 100644
index 0000000..724eeb1
--- /dev/null
+++ b/navigation/navigation-ui/src/main/java/androidx/navigation/ui/NavigationUiSaveStateControl.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2021 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.ui
+
+/**
+ * @see NavigationUI
+ */
+@Retention(AnnotationRetention.RUNTIME)
+@Target(AnnotationTarget.FUNCTION)
+@RequiresOptIn(level = RequiresOptIn.Level.WARNING)
+public annotation class NavigationUiSaveStateControl
\ No newline at end of file