1 /* <lambda>null2 * Copyright (C) 2017 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 package androidx.navigation.fragment 17 18 import android.content.Context 19 import android.os.Bundle 20 import android.util.AttributeSet 21 import android.view.LayoutInflater 22 import android.view.View 23 import android.view.ViewGroup 24 import androidx.annotation.CallSuper 25 import androidx.annotation.NavigationRes 26 import androidx.annotation.RestrictTo 27 import androidx.core.content.res.use 28 import androidx.core.os.bundleOf 29 import androidx.fragment.app.DialogFragment 30 import androidx.fragment.app.Fragment 31 import androidx.fragment.app.FragmentContainerView 32 import androidx.navigation.NavController 33 import androidx.navigation.NavHost 34 import androidx.navigation.NavHostController 35 import androidx.navigation.Navigation 36 import androidx.navigation.Navigator 37 import androidx.navigation.plusAssign 38 39 /** 40 * NavHostFragment provides an area within your layout for self-contained navigation to occur. 41 * 42 * NavHostFragment is intended to be used as the content area within a layout resource defining your 43 * app's chrome around it, e.g.: 44 * ``` 45 * <androidx.drawerlayout.widget.DrawerLayout 46 * xmlns:android="http://schemas.android.com/apk/res/android" 47 * xmlns:app="http://schemas.android.com/apk/res-auto" 48 * android:layout_width="match_parent" 49 * android:layout_height="match_parent"> 50 * 51 * <androidx.fragment.app.FragmentContainerView 52 * android:layout_width="match_parent" 53 * android:layout_height="match_parent" 54 * android:id="@+id/my_nav_host_fragment" 55 * android:name="androidx.navigation.fragment.NavHostFragment" 56 * app:navGraph="@navigation/nav_sample" 57 * app:defaultNavHost="true" /> 58 * 59 * <com.google.android.material.navigation.NavigationView 60 * android:layout_width="wrap_content" 61 * android:layout_height="match_parent" 62 * android:layout_gravity="start"/>; 63 * </androidx.drawerlayout.widget.DrawerLayout> 64 * ``` 65 * 66 * Each NavHostFragment has a [NavController] that defines valid navigation within the navigation 67 * host. This includes the [navigation graph][NavGraph] as well as navigation state such as current 68 * location and back stack that will be saved and restored along with the NavHostFragment itself. 69 * 70 * NavHostFragments register their navigation controller at the root of their view subtree such that 71 * any descendant can obtain the controller instance through the [Navigation] helper class's methods 72 * such as [Navigation.findNavController]. View event listener implementations such as 73 * [android.view.View.OnClickListener] within navigation destination fragments can use these helpers 74 * to navigate based on user interaction without creating a tight coupling to the navigation host. 75 */ 76 public open class NavHostFragment : Fragment(), NavHost { 77 internal val navHostController: NavHostController by lazy { 78 val context = 79 checkNotNull(context) { 80 "NavController cannot be created before the fragment is attached" 81 } 82 NavHostController(context).apply { 83 setLifecycleOwner(this@NavHostFragment) 84 setViewModelStore(viewModelStore) 85 onCreateNavHostController(this) 86 savedStateRegistry.consumeRestoredStateForKey(KEY_NAV_CONTROLLER_STATE)?.let { 87 restoreState(it) 88 } 89 savedStateRegistry.registerSavedStateProvider(KEY_NAV_CONTROLLER_STATE) { 90 saveState() ?: Bundle.EMPTY 91 } 92 savedStateRegistry.consumeRestoredStateForKey(KEY_GRAPH_ID)?.let { bundle -> 93 graphId = bundle.getInt(KEY_GRAPH_ID) 94 } 95 savedStateRegistry.registerSavedStateProvider(KEY_GRAPH_ID) { 96 if (graphId != 0) { 97 bundleOf(KEY_GRAPH_ID to graphId) 98 } else { 99 Bundle.EMPTY 100 } 101 } 102 if (graphId != 0) { 103 // Set from onInflate() 104 setGraph(graphId) 105 } else { 106 // See if it was set by NavHostFragment.create() 107 val args = arguments 108 val graphId = args?.getInt(KEY_GRAPH_ID) ?: 0 109 val startDestinationArgs = args?.getBundle(KEY_START_DESTINATION_ARGS) 110 if (graphId != 0) { 111 setGraph(graphId, startDestinationArgs) 112 } 113 } 114 } 115 } 116 private var viewParent: View? = null 117 118 // State that will be saved and restored 119 private var graphId = 0 120 private var defaultNavHost = false 121 122 /** 123 * The [navigation controller][NavController] for this navigation host. This method will return 124 * null until this host fragment's [onCreate] has been called and it has had an opportunity to 125 * restore from a previous instance state. 126 * 127 * @return this host's navigation controller 128 * @throws IllegalStateException if called before [onCreate] 129 */ 130 final override val navController: NavController 131 get() = navHostController 132 133 @CallSuper 134 public override fun onAttach(context: Context) { 135 super.onAttach(context) 136 137 // TODO This feature should probably be a first-class feature of the Fragment system, 138 // but it can stay here until we can add the necessary attr resources to 139 // the fragment lib. 140 if (defaultNavHost) { 141 parentFragmentManager.beginTransaction().setPrimaryNavigationFragment(this).commit() 142 } 143 } 144 145 @CallSuper 146 public override fun onCreate(savedInstanceState: Bundle?) { 147 // We are accessing the NavController here to ensure that it is always created by this point 148 navHostController 149 if (savedInstanceState != null) { 150 if (savedInstanceState.getBoolean(KEY_DEFAULT_NAV_HOST, false)) { 151 defaultNavHost = true 152 parentFragmentManager.beginTransaction().setPrimaryNavigationFragment(this).commit() 153 } 154 } 155 156 // We purposefully run this last as this will trigger the onCreate() of 157 // child fragments, which may be relying on having the NavController already 158 // created and having its state restored by that point. 159 super.onCreate(savedInstanceState) 160 } 161 162 /** 163 * Callback for when the [NavHostController] is created. If you support any custom destination 164 * types, their [Navigator] should be added here to ensure it is available before the navigation 165 * graph is inflated / set. 166 * 167 * This provides direct access to the host specific methods available on [NavHostController] 168 * such as [NavHostController.setOnBackPressedDispatcher]. 169 * 170 * By default, this adds a [DialogFragmentNavigator] and [FragmentNavigator]. 171 * 172 * This is only called once when the navController is called. This will be called in [onCreate] 173 * if the navController has not yet been called. This should not be called directly by 174 * subclasses. 175 * 176 * @param navHostController The newly created [NavHostController] that will be returned by 177 * [getNavController] after 178 */ 179 @Suppress("DEPRECATION") 180 @CallSuper 181 protected open fun onCreateNavHostController(navHostController: NavHostController) { 182 onCreateNavController(navHostController) 183 } 184 185 /** 186 * Callback for when the [NavController][getNavController] is created. If you support any custom 187 * destination types, their [Navigator] should be added here to ensure it is available before 188 * the navigation graph is inflated / set. 189 * 190 * By default, this adds a [DialogFragmentNavigator] and [FragmentNavigator]. 191 * 192 * This is only called once when the navController is called. This will be called in [onCreate] 193 * if the navController has not yet been called. This should not be called directly by 194 * subclasses. 195 * 196 * @param navController The newly created [NavController]. 197 */ 198 @Suppress("DEPRECATION") 199 @CallSuper 200 @Deprecated( 201 """Override {@link #onCreateNavHostController(NavHostController)} to gain 202 access to the full {@link NavHostController} that is created by this NavHostFragment.""" 203 ) 204 protected open fun onCreateNavController(navController: NavController) { 205 navController.navigatorProvider += 206 DialogFragmentNavigator(requireContext(), childFragmentManager) 207 navController.navigatorProvider.addNavigator(createFragmentNavigator()) 208 } 209 210 /** 211 * Create the FragmentNavigator that this NavHostFragment will use. By default, this uses 212 * [FragmentNavigator], which replaces the entire contents of the NavHostFragment. 213 * 214 * This is only called once in [onCreate] and should not be called directly by subclasses. 215 * 216 * @return a new instance of a FragmentNavigator 217 */ 218 @Deprecated("Use {@link #onCreateNavController(NavController)}") 219 protected open fun createFragmentNavigator(): Navigator<out FragmentNavigator.Destination> { 220 return FragmentNavigator(requireContext(), childFragmentManager, containerId) 221 } 222 223 public override fun onCreateView( 224 inflater: LayoutInflater, 225 container: ViewGroup?, 226 savedInstanceState: Bundle? 227 ): View? { 228 val containerView = FragmentContainerView(inflater.context) 229 // When added via XML, this has no effect (since this FragmentContainerView is given the ID 230 // automatically), but this ensures that the View exists as part of this Fragment's View 231 // hierarchy in cases where the NavHostFragment is added programmatically as is required 232 // for child fragment transactions 233 containerView.id = containerId 234 return containerView 235 } 236 237 /** 238 * We specifically can't use [View.NO_ID] as the container ID (as we use 239 * [androidx.fragment.app.FragmentTransaction.add] under the hood), so we need to make sure we 240 * return a valid ID when asked for the container ID. 241 * 242 * @return a valid ID to be used to contain child fragments 243 */ 244 private val containerId: Int 245 get() { 246 val id = id 247 return if (id != 0 && id != View.NO_ID) { 248 id 249 } else R.id.nav_host_fragment_container 250 // Fallback to using our own ID if this Fragment wasn't added via 251 // add(containerViewId, Fragment) 252 } 253 254 public override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 255 super.onViewCreated(view, savedInstanceState) 256 check(view is ViewGroup) { "created host view $view is not a ViewGroup" } 257 Navigation.setViewNavController(view, navHostController) 258 // When added programmatically, we need to set the NavController on the parent - i.e., 259 // the View that has the ID matching this NavHostFragment. 260 if (view.getParent() != null) { 261 viewParent = view.getParent() as View 262 if (viewParent!!.id == id) { 263 Navigation.setViewNavController(viewParent!!, navHostController) 264 } 265 } 266 } 267 268 @CallSuper 269 public override fun onInflate( 270 context: Context, 271 attrs: AttributeSet, 272 savedInstanceState: Bundle? 273 ) { 274 super.onInflate(context, attrs, savedInstanceState) 275 context.obtainStyledAttributes(attrs, androidx.navigation.R.styleable.NavHost).use { navHost 276 -> 277 val graphId = navHost.getResourceId(androidx.navigation.R.styleable.NavHost_navGraph, 0) 278 if (graphId != 0) { 279 this.graphId = graphId 280 } 281 } 282 context.obtainStyledAttributes(attrs, R.styleable.NavHostFragment).use { array -> 283 val defaultHost = array.getBoolean(R.styleable.NavHostFragment_defaultNavHost, false) 284 if (defaultHost) { 285 defaultNavHost = true 286 } 287 } 288 } 289 290 @CallSuper 291 public override fun onSaveInstanceState(outState: Bundle) { 292 super.onSaveInstanceState(outState) 293 if (defaultNavHost) { 294 outState.putBoolean(KEY_DEFAULT_NAV_HOST, true) 295 } 296 } 297 298 public override fun onDestroyView() { 299 super.onDestroyView() 300 viewParent?.let { it -> 301 if (Navigation.findNavController(it) === navHostController) { 302 Navigation.setViewNavController(it, null) 303 } 304 } 305 viewParent = null 306 } 307 308 public companion object { 309 /** */ 310 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 311 public const val KEY_GRAPH_ID: String = "android-support-nav:fragment:graphId" 312 313 /** */ 314 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 315 public const val KEY_START_DESTINATION_ARGS: String = 316 "android-support-nav:fragment:startDestinationArgs" 317 private const val KEY_NAV_CONTROLLER_STATE = 318 "android-support-nav:fragment:navControllerState" 319 private const val KEY_DEFAULT_NAV_HOST = "android-support-nav:fragment:defaultHost" 320 321 /** 322 * Find a [NavController] given a local [Fragment]. 323 * 324 * This method will locate the [NavController] associated with this Fragment, looking first 325 * for a [NavHostFragment] along the given Fragment's parent chain. If a [NavController] is 326 * not found, this method will look for one along this Fragment's 327 * [view hierarchy][Fragment.getView] as specified by [Navigation.findNavController]. 328 * 329 * @param fragment the locally scoped Fragment for navigation 330 * @return the locally scoped [NavController] for navigating from this [Fragment] 331 * @throws IllegalStateException if the given Fragment does not correspond with a [NavHost] 332 * or is not within a NavHost. 333 */ 334 @JvmStatic 335 public fun findNavController(fragment: Fragment): NavController { 336 var findFragment: Fragment? = fragment 337 while (findFragment != null) { 338 if (findFragment is NavHostFragment) { 339 return findFragment.navHostController 340 } 341 val primaryNavFragment = 342 findFragment.parentFragmentManager.primaryNavigationFragment 343 if (primaryNavFragment is NavHostFragment) { 344 return primaryNavFragment.navHostController 345 } 346 findFragment = findFragment.parentFragment 347 } 348 349 // Try looking for one associated with the view instead, if applicable 350 val view = fragment.view 351 if (view != null) { 352 return Navigation.findNavController(view) 353 } 354 355 // For DialogFragments, look at the dialog's decor view 356 val dialogDecorView = (fragment as? DialogFragment)?.dialog?.window?.decorView 357 if (dialogDecorView != null) { 358 return Navigation.findNavController(dialogDecorView) 359 } 360 throw IllegalStateException("Fragment $fragment does not have a NavController set") 361 } 362 363 /** 364 * Create a new NavHostFragment instance with an inflated [NavGraph] resource. 365 * 366 * @param graphResId Resource id of the navigation graph to inflate. 367 * @param startDestinationArgs Arguments to send to the start destination of the graph. 368 * @return A new NavHostFragment instance. 369 */ 370 @JvmOverloads 371 @JvmStatic 372 public fun create( 373 @NavigationRes graphResId: Int, 374 startDestinationArgs: Bundle? = null 375 ): NavHostFragment { 376 var b: Bundle? = null 377 if (graphResId != 0) { 378 b = Bundle() 379 b.putInt(KEY_GRAPH_ID, graphResId) 380 } 381 if (startDestinationArgs != null) { 382 if (b == null) { 383 b = Bundle() 384 } 385 b.putBundle(KEY_START_DESTINATION_ARGS, startDestinationArgs) 386 } 387 val result = NavHostFragment() 388 if (b != null) { 389 result.arguments = b 390 } 391 return result 392 } 393 } 394 } 395