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