blob: 081ef2982d5cab80d0b66a2eb7966a0db984788d [file] [log] [blame]
/*
* Copyright (C) 2017 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.content.Context
import android.content.ContextWrapper
import android.os.Bundle
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.OnBackPressedDispatcherOwner
import androidx.annotation.CallSuper
import androidx.annotation.NavigationRes
import androidx.annotation.RestrictTo
import androidx.core.content.res.use
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentContainerView
import androidx.navigation.NavController
import androidx.navigation.NavHost
import androidx.navigation.NavHostController
import androidx.navigation.Navigation
import androidx.navigation.Navigator
import androidx.navigation.plusAssign
/**
* NavHostFragment provides an area within your layout for self-contained navigation to occur.
*
* NavHostFragment is intended to be used as the content area within a layout resource
* defining your app's chrome around it, e.g.:
*
* ```
* <androidx.drawerlayout.widget.DrawerLayout
* xmlns:android="http://schemas.android.com/apk/res/android"
* xmlns:app="http://schemas.android.com/apk/res-auto"
* android:layout_width="match_parent"
* android:layout_height="match_parent">
* <androidx.fragment.app.FragmentContainerView
* android:layout_width="match_parent"
* android:layout_height="match_parent"
* android:id="@+id/my_nav_host_fragment"
* android:name="androidx.navigation.fragment.NavHostFragment"
* app:navGraph="@navigation/nav_sample"
* app:defaultNavHost="true" />
* <com.google.android.material.navigation.NavigationView
* android:layout_width="wrap_content"
* android:layout_height="match_parent"
* android:layout_gravity="start"/>;
* </androidx.drawerlayout.widget.DrawerLayout>
* ```
*
* Each NavHostFragment has a [NavController] that defines valid navigation within
* the navigation host. This includes the [navigation graph][NavGraph] as well as navigation
* state such as current location and back stack that will be saved and restored along with the
* NavHostFragment itself.
*
* NavHostFragments register their navigation controller at the root of their view subtree
* such that any descendant can obtain the controller instance through the [Navigation]
* helper class's methods such as [Navigation.findNavController]. View event listener
* implementations such as [android.view.View.OnClickListener] within navigation destination
* fragments can use these helpers to navigate based on user interaction without creating a tight
* coupling to the navigation host.
*/
public open class NavHostFragment : Fragment(), NavHost {
private var navHostController: NavHostController? = null
private var isPrimaryBeforeOnCreate: Boolean? = null
private var viewParent: View? = null
// State that will be saved and restored
private var graphId = 0
private var defaultNavHost = false
/**
* The [navigation controller][NavController] for this navigation host.
* This method will return null until this host fragment's [onCreate]
* has been called and it has had an opportunity to restore from a previous instance state.
*
* @return this host's navigation controller
* @throws IllegalStateException if called before [onCreate]
*/
final override val navController: NavController
get() {
checkNotNull(navHostController) { "NavController is not available before onCreate()" }
return navHostController as NavHostController
}
@CallSuper
public override fun onAttach(context: Context) {
super.onAttach(context)
// TODO This feature should probably be a first-class feature of the Fragment system,
// but it can stay here until we can add the necessary attr resources to
// the fragment lib.
if (defaultNavHost) {
parentFragmentManager.beginTransaction()
.setPrimaryNavigationFragment(this)
.commit()
}
}
@CallSuper
public override fun onCreate(savedInstanceState: Bundle?) {
var context = requireContext()
navHostController = NavHostController(context)
navHostController!!.setLifecycleOwner(this)
while (context is ContextWrapper) {
if (context is OnBackPressedDispatcherOwner) {
navHostController!!.setOnBackPressedDispatcher(
(context as OnBackPressedDispatcherOwner).onBackPressedDispatcher
)
// Otherwise, caller must register a dispatcher on the controller explicitly
// by overriding onCreateNavHostController()
break
}
context = context.baseContext
}
// Set the default state - this will be updated whenever
// onPrimaryNavigationFragmentChanged() is called
navHostController!!.enableOnBackPressed(
isPrimaryBeforeOnCreate != null && isPrimaryBeforeOnCreate as Boolean
)
isPrimaryBeforeOnCreate = null
navHostController!!.setViewModelStore(viewModelStore)
onCreateNavHostController(navHostController!!)
var navState: Bundle? = null
if (savedInstanceState != null) {
navState = savedInstanceState.getBundle(KEY_NAV_CONTROLLER_STATE)
if (savedInstanceState.getBoolean(KEY_DEFAULT_NAV_HOST, false)) {
defaultNavHost = true
parentFragmentManager.beginTransaction()
.setPrimaryNavigationFragment(this)
.commit()
}
graphId = savedInstanceState.getInt(KEY_GRAPH_ID)
}
if (navState != null) {
// Navigation controller state overrides arguments
navHostController!!.restoreState(navState)
}
if (graphId != 0) {
// Set from onInflate()
navHostController!!.setGraph(graphId)
} else {
// See if it was set by NavHostFragment.create()
val args = arguments
val graphId = args?.getInt(KEY_GRAPH_ID) ?: 0
val startDestinationArgs = args?.getBundle(KEY_START_DESTINATION_ARGS)
if (graphId != 0) {
navHostController!!.setGraph(graphId, startDestinationArgs)
}
}
// We purposefully run this last as this will trigger the onCreate() of
// child fragments, which may be relying on having the NavController already
// created and having its state restored by that point.
super.onCreate(savedInstanceState)
}
/**
* Callback for when the [NavHostController] is created. If you
* support any custom destination types, their [Navigator] should be added here to
* ensure it is available before the navigation graph is inflated / set.
*
* This provides direct access to the host specific methods available on
* [NavHostController] such as
* [NavHostController.setOnBackPressedDispatcher].
*
* By default, this adds a [DialogFragmentNavigator] and [FragmentNavigator].
*
* This is only called once in [onCreate] and should not be called directly by
* subclasses.
*
* @param navHostController The newly created [NavHostController] that will be
* returned by [getNavController] after
*/
@Suppress("DEPRECATION")
@CallSuper
protected open fun onCreateNavHostController(navHostController: NavHostController) {
onCreateNavController(navHostController)
}
/**
* Callback for when the [NavController][getNavController] is created. If you
* support any custom destination types, their [Navigator] should be added here to
* ensure it is available before the navigation graph is inflated / set.
*
* By default, this adds a [DialogFragmentNavigator] and [FragmentNavigator].
*
* This is only called once in [onCreate] and should not be called directly by
* subclasses.
*
* @param navController The newly created [NavController].
*/
@Suppress("DEPRECATION")
@CallSuper
@Deprecated(
"""Override {@link #onCreateNavHostController(NavHostController)} to gain
access to the full {@link NavHostController} that is created by this NavHostFragment."""
)
protected open fun onCreateNavController(navController: NavController) {
navController.navigatorProvider +=
DialogFragmentNavigator(requireContext(), childFragmentManager)
navController.navigatorProvider.addNavigator(createFragmentNavigator())
}
@CallSuper
public override fun onPrimaryNavigationFragmentChanged(isPrimaryNavigationFragment: Boolean) {
if (navHostController != null) {
navHostController?.enableOnBackPressed(isPrimaryNavigationFragment)
} else {
isPrimaryBeforeOnCreate = isPrimaryNavigationFragment
}
}
/**
* Create the FragmentNavigator that this NavHostFragment will use. By default, this uses
* [FragmentNavigator], which replaces the entire contents of the NavHostFragment.
*
* This is only called once in [onCreate] and should not be called directly by
* subclasses.
* @return a new instance of a FragmentNavigator
*/
@Deprecated("Use {@link #onCreateNavController(NavController)}")
protected open fun createFragmentNavigator(): Navigator<out FragmentNavigator.Destination> {
return FragmentNavigator(requireContext(), childFragmentManager, containerId)
}
public override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val containerView = FragmentContainerView(inflater.context)
// When added via XML, this has no effect (since this FragmentContainerView is given the ID
// automatically), but this ensures that the View exists as part of this Fragment's View
// hierarchy in cases where the NavHostFragment is added programmatically as is required
// for child fragment transactions
containerView.id = containerId
return containerView
}
/**
* We specifically can't use [View.NO_ID] as the container ID (as we use
* [androidx.fragment.app.FragmentTransaction.add] under the hood),
* so we need to make sure we return a valid ID when asked for the container ID.
*
* @return a valid ID to be used to contain child fragments
*/
private val containerId: Int
get() {
val id = id
return if (id != 0 && id != View.NO_ID) {
id
} else R.id.nav_host_fragment_container
// Fallback to using our own ID if this Fragment wasn't added via
// add(containerViewId, Fragment)
}
public override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
check(view is ViewGroup) { "created host view $view is not a ViewGroup" }
Navigation.setViewNavController(view, navHostController)
// When added programmatically, we need to set the NavController on the parent - i.e.,
// the View that has the ID matching this NavHostFragment.
if (view.getParent() != null) {
viewParent = view.getParent() as View
if (viewParent!!.id == id) {
Navigation.setViewNavController(viewParent!!, navHostController)
}
}
}
@CallSuper
public override fun onInflate(
context: Context,
attrs: AttributeSet,
savedInstanceState: Bundle?
) {
super.onInflate(context, attrs, savedInstanceState)
context.obtainStyledAttributes(
attrs,
androidx.navigation.R.styleable.NavHost
).use { navHost ->
val graphId = navHost.getResourceId(
androidx.navigation.R.styleable.NavHost_navGraph, 0
)
if (graphId != 0) {
this.graphId = graphId
}
}
context.obtainStyledAttributes(attrs, R.styleable.NavHostFragment).use { array ->
val defaultHost = array.getBoolean(R.styleable.NavHostFragment_defaultNavHost, false)
if (defaultHost) {
defaultNavHost = true
}
}
}
@CallSuper
public override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val navState = navHostController!!.saveState()
if (navState != null) {
outState.putBundle(KEY_NAV_CONTROLLER_STATE, navState)
}
if (defaultNavHost) {
outState.putBoolean(KEY_DEFAULT_NAV_HOST, true)
}
if (graphId != 0) {
outState.putInt(KEY_GRAPH_ID, graphId)
}
}
public override fun onDestroyView() {
super.onDestroyView()
viewParent?.let { it ->
if (Navigation.findNavController(it) === navHostController) {
Navigation.setViewNavController(it, null)
}
}
viewParent = null
}
public companion object {
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public const val KEY_GRAPH_ID: String = "android-support-nav:fragment:graphId"
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public const val KEY_START_DESTINATION_ARGS: String =
"android-support-nav:fragment:startDestinationArgs"
private const val KEY_NAV_CONTROLLER_STATE =
"android-support-nav:fragment:navControllerState"
private const val KEY_DEFAULT_NAV_HOST = "android-support-nav:fragment:defaultHost"
/**
* Find a [NavController] given a local [Fragment].
*
* This method will locate the [NavController] associated with this Fragment,
* looking first for a [NavHostFragment] along the given Fragment's parent chain.
* If a [NavController] is not found, this method will look for one along this
* Fragment's [view hierarchy][Fragment.getView] as specified by
* [Navigation.findNavController].
*
* @param fragment the locally scoped Fragment for navigation
* @return the locally scoped [NavController] for navigating from this [Fragment]
* @throws IllegalStateException if the given Fragment does not correspond with a
* [NavHost] or is not within a NavHost.
*/
@JvmStatic
public fun findNavController(fragment: Fragment): NavController {
var findFragment: Fragment? = fragment
while (findFragment != null) {
if (findFragment is NavHostFragment) {
return findFragment.navHostController as NavController
}
val primaryNavFragment = findFragment.parentFragmentManager
.primaryNavigationFragment
if (primaryNavFragment is NavHostFragment) {
return primaryNavFragment.navHostController as NavController
}
findFragment = findFragment.parentFragment
}
// Try looking for one associated with the view instead, if applicable
val view = fragment.view
if (view != null) {
return Navigation.findNavController(view)
}
// For DialogFragments, look at the dialog's decor view
val dialogDecorView = (fragment as? DialogFragment)?.dialog?.window?.decorView
if (dialogDecorView != null) {
return Navigation.findNavController(dialogDecorView)
}
throw IllegalStateException("Fragment $fragment does not have a NavController set")
}
/**
* Create a new NavHostFragment instance with an inflated [NavGraph] resource.
*
* @param graphResId Resource id of the navigation graph to inflate.
* @param startDestinationArgs Arguments to send to the start destination of the graph.
* @return A new NavHostFragment instance.
*/
@JvmOverloads
@JvmStatic
public fun create(
@NavigationRes graphResId: Int,
startDestinationArgs: Bundle? = null
): NavHostFragment {
var b: Bundle? = null
if (graphResId != 0) {
b = Bundle()
b.putInt(KEY_GRAPH_ID, graphResId)
}
if (startDestinationArgs != null) {
if (b == null) {
b = Bundle()
}
b.putBundle(KEY_START_DESTINATION_ARGS, startDestinationArgs)
}
val result = NavHostFragment()
if (b != null) {
result.arguments = b
}
return result
}
}
}