1 /* 2 * Copyright 2019 The Android Open Source Project 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 androidx.navigation.dynamicfeatures 18 19 import android.content.Context 20 import android.os.Bundle 21 import android.util.AttributeSet 22 import androidx.annotation.RestrictTo 23 import androidx.core.content.withStyledAttributes 24 import androidx.navigation.NavBackStackEntry 25 import androidx.navigation.NavDestination 26 import androidx.navigation.NavGraph 27 import androidx.navigation.NavGraphNavigator 28 import androidx.navigation.NavOptions 29 import androidx.navigation.Navigator 30 import androidx.navigation.NavigatorProvider 31 32 /** 33 * Navigator for graphs in dynamic feature modules. 34 * 35 * This class handles navigating to a progress destination when the installation of a dynamic 36 * feature module is required. By default, the progress destination set by 37 * [installDefaultProgressDestination] will be used, but this can be overridden by setting the 38 * `app:progressDestinationId` attribute in your navigation XML file. 39 */ 40 @Navigator.Name("navigation") 41 public class DynamicGraphNavigator( 42 private val navigatorProvider: NavigatorProvider, 43 private val installManager: DynamicInstallManager 44 ) : NavGraphNavigator(navigatorProvider) { 45 46 /** @return The progress destination supplier if any is set. */ 47 internal var defaultProgressDestinationSupplier: (() -> NavDestination)? = null 48 private set 49 50 internal val destinationsWithoutDefaultProgressDestination = mutableListOf<DynamicNavGraph>() 51 52 /** 53 * Navigate to a destination. 54 * 55 * In case the destination module is installed the navigation will trigger directly. Otherwise 56 * the dynamic feature module is requested and navigation is postponed until the module has 57 * successfully been installed. 58 */ navigatenull59 override fun navigate( 60 entries: List<NavBackStackEntry>, 61 navOptions: NavOptions?, 62 navigatorExtras: Extras? 63 ) { 64 for (entry in entries) { 65 navigate(entry, navOptions, navigatorExtras) 66 } 67 } 68 navigatenull69 private fun navigate( 70 entry: NavBackStackEntry, 71 navOptions: NavOptions?, 72 navigatorExtras: Extras? 73 ) { 74 val destination = entry.destination 75 val extras = if (navigatorExtras is DynamicExtras) navigatorExtras else null 76 if (destination is DynamicNavGraph) { 77 val moduleName = destination.moduleName 78 if (moduleName != null && installManager.needsInstall(moduleName)) { 79 installManager.performInstall(entry, extras, moduleName) 80 return 81 } 82 } 83 super.navigate( 84 listOf(entry), 85 navOptions, 86 if (extras != null) extras.destinationExtras else navigatorExtras 87 ) 88 } 89 90 /** 91 * Create a destination for the [DynamicNavGraph]. 92 * 93 * @return The created graph. 94 */ createDestinationnull95 override fun createDestination(): DynamicNavGraph { 96 return DynamicNavGraph(this, navigatorProvider) 97 } 98 99 /** 100 * Installs the default progress destination to this graph via a lambda. This supplies a 101 * [NavDestination] to use when the actual destination is not installed at navigation time. 102 * 103 * This **must** be called before you call [androidx.navigation.NavController.setGraph] to 104 * ensure that all [DynamicNavGraph] instances have the correct progress destination installed 105 * in [onRestoreState]. 106 * 107 * @param progressDestinationSupplier The default progress destination supplier. 108 */ installDefaultProgressDestinationnull109 public fun installDefaultProgressDestination( 110 progressDestinationSupplier: () -> NavDestination 111 ) { 112 this.defaultProgressDestinationSupplier = progressDestinationSupplier 113 } 114 115 /** 116 * Navigates to a destination after progress is done. 117 * 118 * @return The destination to navigate to if any. 119 */ navigateToProgressDestinationnull120 internal fun navigateToProgressDestination( 121 dynamicNavGraph: DynamicNavGraph, 122 progressArgs: Bundle? 123 ) { 124 var progressDestinationId = dynamicNavGraph.progressDestination 125 if (progressDestinationId == 0) { 126 progressDestinationId = installDefaultProgressDestination(dynamicNavGraph) 127 } 128 129 val progressDestination = 130 dynamicNavGraph.findNode(progressDestinationId) 131 ?: throw IllegalStateException( 132 "The progress destination id must be set and " + 133 "accessible to the module of this navigator." 134 ) 135 val navigator = 136 navigatorProvider.getNavigator<Navigator<NavDestination>>( 137 progressDestination.navigatorName 138 ) 139 val entry = state.createBackStackEntry(progressDestination, progressArgs) 140 navigator.navigate(listOf(entry), null, null) 141 } 142 143 /** 144 * Install the default progress destination 145 * 146 * @return The [NavDestination#getId] of the newly added progress destination 147 */ installDefaultProgressDestinationnull148 private fun installDefaultProgressDestination(dynamicNavGraph: DynamicNavGraph): Int { 149 val progressDestinationSupplier = defaultProgressDestinationSupplier 150 checkNotNull(progressDestinationSupplier) { 151 "You must set a default progress destination " + 152 "using DynamicNavGraphNavigator.installDefaultProgressDestination or " + 153 "pass in an DynamicInstallMonitor in the DynamicExtras.\n" + 154 "Alternatively, when using NavHostFragment make sure to swap it with " + 155 "DynamicNavHostFragment. This will take care of setting the default " + 156 "progress destination for you." 157 } 158 val progressDestination = progressDestinationSupplier.invoke() 159 dynamicNavGraph.addDestination(progressDestination) 160 dynamicNavGraph.progressDestination = progressDestination.id 161 return progressDestination.id 162 } 163 onSaveStatenull164 override fun onSaveState(): Bundle? { 165 // Return a non-null Bundle to get a callback to onRestoreState 166 return Bundle.EMPTY 167 } 168 onRestoreStatenull169 override fun onRestoreState(savedState: Bundle) { 170 super.onRestoreState(savedState) 171 val iterator = destinationsWithoutDefaultProgressDestination.iterator() 172 while (iterator.hasNext()) { 173 val dynamicNavGraph = iterator.next() 174 installDefaultProgressDestination(dynamicNavGraph) 175 iterator.remove() 176 } 177 } 178 179 /** The [NavGraph] for dynamic features. */ 180 public class DynamicNavGraph( 181 @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 182 internal val navGraphNavigator: DynamicGraphNavigator, 183 @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 184 internal val navigatorProvider: NavigatorProvider 185 ) : NavGraph(navGraphNavigator) { 186 187 internal companion object { 188 189 /** 190 * Get the [DynamicNavGraph] for a supplied [NavDestination] or throw an exception if 191 * it's not a [DynamicNavGraph]. 192 */ getOrThrownull193 internal fun getOrThrow(destination: NavDestination): DynamicNavGraph { 194 return destination.parent as? DynamicNavGraph 195 ?: throw IllegalStateException( 196 "Dynamic destinations must be part of a DynamicNavGraph.\n" + 197 "You can use DynamicNavHostFragment, which will take care of " + 198 "setting up the NavController for Dynamic destinations.\n" + 199 "If you're not using Fragments, you must set up the " + 200 "NavigatorProvider manually." 201 ) 202 } 203 } 204 205 /** The dynamic feature's module name. */ 206 public var moduleName: String? = null 207 208 /** 209 * Resource id of progress destination. This will be preferred over any default progress 210 * destination set by [installDefaultProgressDestination]. 211 */ 212 public var progressDestination: Int = 0 213 onInflatenull214 override fun onInflate(context: Context, attrs: AttributeSet) { 215 super.onInflate(context, attrs) 216 context.withStyledAttributes(attrs, R.styleable.DynamicGraphNavigator) { 217 moduleName = getString(R.styleable.DynamicGraphNavigator_moduleName) 218 progressDestination = 219 getResourceId(R.styleable.DynamicGraphNavigator_progressDestination, 0) 220 if (progressDestination == 0) { 221 navGraphNavigator.destinationsWithoutDefaultProgressDestination.add( 222 this@DynamicNavGraph 223 ) 224 } 225 } 226 } 227 equalsnull228 override fun equals(other: Any?): Boolean { 229 if (this === other) return true 230 if (other == null || other !is DynamicNavGraph) return false 231 return super.equals(other) && 232 moduleName == other.moduleName && 233 progressDestination == other.progressDestination 234 } 235 hashCodenull236 override fun hashCode(): Int { 237 var result = super.hashCode() 238 result = 31 * result + moduleName.hashCode() 239 result = 31 * result + progressDestination.hashCode() 240 return result 241 } 242 } 243 } 244