Navigation is one of the important components in Jetpack, used to design and organize App page jumps. Since the official recommendation is to use Framgent to carry the page implementation, the first thing that comes to mind when it comes to Navigation is Fragment. But in fact, Navigation also supports other types of page implementations, such as custom views. This article will introduce the use of custom View in Navigation.
Before the formal introduction, let’s review the basic use of Navigation:
Basic structure of Navigation
The use of Navigation mainly involves the following objects
- Graph
uses XML to design the APP page (Destination) and the jump path between each page. Android Studio provides an editor to edit Graph - NavHost
NavHost is a container used to host all nodes in the graph. Navigation provides a default implementation of NavHost for FragmentNavHostFragment
. It can be understood that all Fragments in graph are ChildFragment. In the custom View scenario introduced today, there is also a need for NavHost implementation for custom View. - Controller
Each NavHost has a Controller, which manages the jump between the nodes in the NavHost - Navigator
Controller implements specific jumps by calling Navigator, and Navigator undertakes specific jump implementations
working principle
Each page in Navigation is a Destination, which can be Fragment, Activity or View. Link the source Destination with the destination Destination through Action, and jump from the current Destination to the destination Destination through Action.
Similar to Launch Activity, when the APP starts, a starting Destination needs to be defined as the home page display.
As mentioned earlier, NavHost has specific implementations for different Destinations, and NavController also has different acquisition methods according to the type of Destination, but they are all similar:
- Fragment.findNavController ()
- View.findNavController()
- Activity.findNavController(viewId: Int)
After getting the Controller, you can navigate(int)
jump through, for example
findNavController().navigate(R.id.action_first_view_to_second_view)
findNavController().navigate(R.id.second_view)
Implementation
The basic composition and working principle of Navigation were introduced above, and then we will enter the topic to realize Navigation based on custom View.
The following needs to be achieved:
- ViewNavigator
- Attributes for ViewNavigator
- ViewDestination
- NavigationHostView
- Graph file
ViewNavigator
Navigation provides a way to customize Navigator: use @Navigator.Name
annotations.
We define a name for screen_view
the Navigator, in the xml Graph can be defined by this name corresponding NavDestination.
NavDestination and Navigator are restricted by generics:Navigator<out NavDestination>
@Navigator.Name("screen_view")
class ViewNavigator(private val container: ViewGroup) : Navigator<ViewDestination>() {
override fun navigate(...) {
...}
override fun createDestination(): ViewDestination = ViewDestination(this)
override fun popBackStack(): Boolean {
...}
}
Attributes for ViewNavigator
Define custom attributes used in Xml for Navigator layoutId
,
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ViewNavigator">
<attr name="layoutId" format="reference" />
</declare-styleable>
</resources>
ViewDestination
@NavDestination.ClassType
Allow us to define our ownNavDestination
@NavDestination.ClassType(ViewGroup::class)
class ViewDestination(navigator: Navigator<out NavDestination>) : NavDestination(navigator) {
@LayoutRes var layoutId: Int = 0
override fun onInflate(context: Context, attrs: AttributeSet) {
super.onInflate(context, attrs)
context.resources.obtainAttributes(attrs, R.styleable.ViewNavigator).apply {
layoutId = getResourceId(R.styleable.ViewNavigator_layoutId, 0)
recycle()
}
}
}
In onInflate
, receive and parse layoutId
the value of the custom attribute
NavigationHostView
Define the implementation of NavHost NavigationHostFrame
, which is mainly used to create Controller, register Navigator type and set Graph for it
class NavigationHostFrame(...) : FrameLayout(...), NavHost {
private val navigationController = NavController(context)
init {
Navigation.setViewNavController(this, navigationController)
navigationController.navigatorProvider.addNavigator(ViewNavigator(this))
navigationController.setGraph(R.navigation.navigation)
}
override fun getNavController() = navigationController
}
Graph file
In the Graph file, by <screen_view/>
defining NavDestination
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_navigation"
app:startDestination="@id/first_screen_view"
tools:ignore="UnusedNavigation">
<screen_view
android:id="@+id/first_screen_view"
app:layoutId="@layout/screen_view_first"
tools:layout="@layout/screen_view_first">
<action
android:id="@+id/action_first_screen_view_to_second_screen_view"
app:destination="@id/second_screen_view"
app:launchSingleTop="true"
app:popUpTo="@+id/first_screen_view"
app:popUpToInclusive="false" />
<action
android:id="@+id/action_first_screen_view_to_last_screen_view"
app:destination="@id/last_screen_view"
app:launchSingleTop="true"
app:popUpTo="@+id/first_screen_view"
app:popUpToInclusive="false" />
</screen_view>
<screen_view
android:id="@+id/second_screen_view"
app:layoutId="@layout/screen_view_second"
tools:layout="@layout/screen_view_second">
<action
android:id="@+id/action_second_screen_view_to_screen_view_third"
app:destination="@id/screen_view_third"
app:launchSingleTop="true"
app:popUpTo="@+id/main_navigation"
app:popUpToInclusive="true" />
</screen_view>
<screen_view
android:id="@+id/last_screen_view"
app:layoutId="@layout/screen_view_last"
tools:layout="@layout/screen_view_last" />
<screen_view
android:id="@+id/screen_view_third"
app:layoutId="@layout/screen_view_third"
tools:layout="@layout/screen_view_third" />
</navigation>
Open the Navigation editor of Android Studio and see the result after Xml:
Setup in Activity
Finally, use this NavigationHostView as a container in the layout of the Activity, and associate NavController with NavHost in the code
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.my.sample.navigation.NavigationHostView
android:id="@+id/main_navigation_host"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
navController = Navigation.findNavController(mainNavigationHost)
Navigation.setViewNavController(mainNavigationHost, navController)
}
Call NavController in onBackPressed` to let each NavDestination support BackPress
override fun onSupportNavigateUp(): Boolean = navController.navigateUp()
override fun onBackPressed() {
if (!navController.popBackStack()) {
super.onBackPressed()
}
}
Conclusion
Navigation based on Fragment provides an out-of-the-box implementation, and at the same time reserves an extensible interface through annotations, which is convenient for developers to customize the implementation, and even enjoy the traversal brought by the Android Studio editor.
In the early days of Fragment's instability, many alternatives to Fragment appeared for UI segmentation. If these frameworks are still used in your project, then you can now happily adapt Navigation for them~