This article has participated in the “Digitalstar Project” and won a creative gift package to challenge the creative incentive money.

preface

Navigation is one of the most important components of Jetpack to organize your App’s page jumps. Since the official recommendation is to use Framgent to host the page implementation, Navigation first comes to mind with fragments. Navigation is well designed to support any type of page jump, even a custom View.

This article introduces the use of View in Navigation. Before getting to the point, review the basics of Navigation


Basic composition of Navigation

The use of Navigation involves the following concepts:

  • NavGraph: Use XML to design jump paths between APP pages (destinations). Android Studio also provides an editor to edit Graph

  • NavHost: NavHost is a container that hosts all the nodes in the Graph. Navigation provides the default implementation of NavHostFragments for fragments. All fragments in the Graph are childFragments. In the custom View scenario described in this article, you also need to define navHosts for the custom View

  • NavController: Each NavHost has a Controller that handles jumps and fallbacks between nodes in the NavHost

  • Navigator: Controller implements specific jumps by calling the Navigator, which assumes the implementation of the jump logic


Working principle of Navigation

Each page in Navigation is a Destination, which can be a Fragment, Activity, or View. Each Detnation is identified by a unique Dest ID, which can be used to redirect from the current Destination to the Destination by finding the ID in the Action.

Like MainActivity, the APP starts with a start Destination as the home page.

As mentioned earlier, there are implementations of navHosts for different destinations, and navControllers have different methods of obtaining them depending on the type of Destination, but they are all very similar:

  • Fragment.findNavController()
  • View.findNavController()
  • Activity.findNavController(viewId: Int)

After a Controller is obtained, navigate(int) is its method to jump, for example

findNavController().navigate(R.id.action_first_view_to_second_view)
findNavController().navigate(R.id.second_view)
Copy the code

Navigation for View

The basic structure and working principle of Navigation were introduced in the previous section. Next, we will enter the topic to realize Navigation based on custom View.

The following needs to be implemented:

  • ViewNavigator
  • Attributes for ViewNavigator
  • ViewDestination
  • NavigationHostView
  • Graph file

ViewNavigator

Navigation provides a way to customize the Navigator using the @navigator.name annotation. We define a Navigator named Screen_view that we can use to define the corresponding NavDestination in the Graph’s XML.

NavDestination and Navigator are constrained by generics: Navigator

@Navigator.Name("screen_view")
class ViewNavigator(private val container: ViewGroup) : Navigator<ViewDestination>() {

    private val viewStack: Deque<Pair<Int.Int>> = LinkedList()
    private val navigationHost = container as NavigationHostView

    override fun navigate(
        destination: ViewDestination,
        args: Bundle? , navOptions:NavOptions? , navigatorExtras:Extras?). = destination.apply {
        viewStack.push(Pair(destination.id, destination.layoutId))
        replaceView(navigationHost.getViewForId(destination.layoutId))
    }

    private fun replaceView(view: View?).{ view? .let { container.removeAllViews() container.addView(it) } }override fun createDestination(a): ViewDestination = ViewDestination(this)

    override fun popBackStack(a): Boolean = when{ viewStack.isNotEmpty() -> { viewStack.pop() viewStack.peekLast()? .let { replaceView(navigationHost.getViewForId(it.second)) }true
        }
        else -> false
    }
	
	fun NavigationHostView.getViewForId(layoutId: Int) = when (layoutId) {
    	R.layout.screen_view_first -> FirstView(context)
    	R.layout.screen_view_second -> SecondView(context)
    	R.layout.screen_view_third -> ThirdView(context)
    	R.layout.screen_view_last -> LastView(context)
    	else -> null}}Copy the code

findNavController().navigate(…) The jump screen will eventually reach the Navigate method of the ViewNavigator, which does two things:

  • viewStackRecord the rollback of the stack to return to the previous screen
  • replaceViewRealize screen switching

Attributes for ViewNavigator

Define the custom attribute layoutId used in the Xml for Navigator,


      
<resources>

    <declare-styleable name="ViewNavigator">
        <attr name="layoutId" format="reference" />
    </declare-styleable>

</resources>
Copy the code

ViewDestination

ClassType allows us to define our own NavDestination

@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()
        }
    }
}
Copy the code

In the onInflate, the value of the custom property layoutId is received and resolved

NavigationHostView

NavigationHostFrame is used to create the Controller, register the Navigator type for it, 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(a) = navigationController
}
Copy the code

NavGraph

In the Graph file, define the NavDestination with


      
<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>

Copy the code

Open the Navigation editor in Android Studio to view NavGraph:

Setup in Activity

Finally, use the NavigationHostView as a container in the Activity layout and associate the NavController with the NavHost in the code


      
<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>
Copy the code
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)
}
Copy the code

Call NavController in onBackPressed to make each NavDestination support BackPress

override fun onSupportNavigateUp(a): Boolean = navController.navigateUp()
override fun onBackPressed(a) {
      if(! navController.popBackStack()) {super.onBackPressed()
      }
}
Copy the code



The last

Navigation fragment-based Navigation provides an out-of-the-box implementation, while annotations provide an extensible interface for developers to customize the implementation and even enjoy the traversal of Android Studio’s editor.

In the early days of fragmentation, due to its unstable functionality, many companies will develop their own alternatives to fragmentation. If you are still using these frameworks in your project, you can also consider adapting Navigation to them in a similar way

(after)

Please feel free to discuss in the comments section. The nuggets will draw 100 nuggets in the comments section after the diggnation project. See the event article for details