├── .gitignore ├── Navigation architecture.odg ├── Navigation_architecture.jpg ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── dirkeisold │ │ └── navigation │ │ ├── MainActivity.kt │ │ ├── common │ │ ├── OnReselectedDelegate.kt │ │ └── Utils.kt │ │ └── usecase │ │ ├── dashboard │ │ └── DashboardFragment.kt │ │ ├── detail │ │ └── DetailFragment.kt │ │ ├── home │ │ └── HomeFragment.kt │ │ └── notifications │ │ └── NotificationFragment.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ ├── ic_arrow_back_black_24dp.xml │ ├── ic_dashboard_black_24dp.xml │ ├── ic_home_black_24dp.xml │ ├── ic_launcher_background.xml │ └── ic_notifications_black_24dp.xml │ ├── layout │ ├── activity_main.xml │ ├── dashboard_fragment.xml │ ├── detail_fragment.xml │ ├── home_fragment.xml │ └── notification_fragment.xml │ ├── menu │ └── navigation.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── navigation │ ├── nav_graph_section_dashboard.xml │ ├── nav_graph_section_home.xml │ └── nav_graph_section_notifications.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradlew ├── gradlew.bat ├── nav_graph.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/* 5 | /.idea/workspace.xml 6 | /.idea/libraries 7 | .DS_Store 8 | /build 9 | 10 | 11 | -------------------------------------------------------------------------------- /Navigation architecture.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deisold/navigation/63254b917e17c06e10cc35239141a864cc56c130/Navigation architecture.odg -------------------------------------------------------------------------------- /Navigation_architecture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deisold/navigation/63254b917e17c06e10cc35239141a864cc56c130/Navigation_architecture.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mastering the Bottom Navigation with the new _Navigation_ Architecture Component 2 | 3 | The demo-app shows the usage of the new _Navigation Architecture Component_ in colaboration with the _Bottom Navigation bar_. 4 | 5 | ## Bottom Navigation 6 | 7 | The _bottom navigation_ was introduced 2 years ago as a material design pattern. The goal was to give the user quick access to 3-5 top-level destinations in an Android app, but an appropriate implementation was missing for long time. 8 | Meanwhile Goole introduced the [Bottom Navigation bar](https://material.io/design/components/bottom-navigation.html#usage) as an implementation. 9 | 10 | The common architectural approach for such a top level navigation would be to make use of Fragments which are added/replaced in a `FrameLayout` serving as a holder in the Activity's view hierarchy. The `FragmentManager` used for dealing with fragments within an Activity only knows _one_ backstack. Building up a backstack for each single view section accessible by the _Bottom Navigation bar_ was impossible. Therefore the "old" pattern behavior was about to remove the whole backstack when the user switched from one section to another. 11 | 12 | But this wasn't the best user experience. Other approaches popped up, like the [Conductor framework](https://github.com/bluelinelabs/Conductor) making it possible to maintain different controller-based backstacks attached to different _Router_'s used in one Activity. 13 | 14 | The "new" pattern behavior is about to maintain the view state a user left in one section when navigating back to it. 15 | 16 | ## Navigation Architecture Component 17 | 18 | With the new [Navigation Architecture Component](https://developer.android.com/topic/libraries/architecture/navigation/) Google introduces a similar concept which uses a `NavHostFragment` hosting a `NavController` operating on a _navigation graph_. 19 | 20 | ``` 21 | 29 | ``` 30 | Placing this xml snippet into your Activity gives you access to it's `NavController` managing the navigation within the `NavHost`. Based on the _navigation graph_ it allows to navigate to another fragment defined as an `action` or to pop the backstack. 31 | 32 | ``` 33 | 37 | 41 | 42 | 46 | 47 | 51 | 52 | 53 | 54 | ``` 55 | 56 | ## Mastering several view sections of the _Bottom Navigation bar_ 57 | 58 | To maintain a fragment backstack for each view section of the _Bottom Navigation bar_ a possible solution would be to use several `NavHostFragments`: 59 | 60 | ``` 61 | 68 | 69 | 77 | 78 | 79 | 86 | 87 | 95 | 96 | 97 | 104 | 105 | 113 | 114 | ``` 115 | Each navigation host (_NavHostFragment_) contains its own `NavController` based on a specific _Navigation Graph_ and maintains its own Fragment backstack. Initially only the first `FrameLayout` containing the home section will be visible to the user. When switching view sections the appropriate `FrameLayout` will be shown to the user, the others hidden. The view state of each view section backed by a `NavHostFragment` and its `NavController` will be maintained and not changed. 116 | 117 | ![A section navigation using the Navigation Architecture Component](Navigation_architecture.jpg) 118 | 119 | ## Android Studio 3.2 120 | 121 | The _Android Studio 3.2_ provides a nice graphical editor for designing a _Navigation Graph_. 122 | 123 | ![Navigation Graph editor: building up a navigation graph](nav_graph.png) 124 | 125 | Finally, Google comes up with a useful architecture component for implementing a _Navigation Graph_ programmatically and visually, too. 126 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion 27 7 | defaultConfig { 8 | applicationId "com.dirkeisold.navigation" 9 | minSdkVersion 24 10 | targetSdkVersion 27 11 | versionCode 1 12 | versionName "1.0" 13 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 14 | } 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | 22 | compileOptions { 23 | sourceCompatibility JavaVersion.VERSION_1_8 24 | targetCompatibility JavaVersion.VERSION_1_8 25 | } 26 | 27 | androidExtensions { 28 | experimental = true 29 | } 30 | } 31 | 32 | def SUPPORT = '27.1.1' 33 | def nav_version = "1.0.0-alpha01" 34 | 35 | dependencies { 36 | implementation fileTree(dir: 'libs', include: ['*.jar']) 37 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 38 | implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" 39 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:0.22.1' 40 | 41 | implementation "com.android.support:support-v4:$SUPPORT" 42 | implementation "com.android.support:support-fragment:$SUPPORT" 43 | implementation "com.android.support:appcompat-v7:$SUPPORT" 44 | implementation "com.android.support:design:$SUPPORT" 45 | 46 | implementation 'com.android.support.constraint:constraint-layout:1.1.0' 47 | 48 | // ARCHITECTURE COMPONENTS 49 | implementation "android.arch.navigation:navigation-fragment-ktx:$nav_version" 50 | // use -ktx for Kotlin 51 | implementation "android.arch.navigation:navigation-ui-ktx:$nav_version" // use -ktx for Kotlin 52 | 53 | } 54 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/dirkeisold/navigation/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.dirkeisold.navigation 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.support.design.widget.BottomNavigationView 6 | import android.support.v4.app.Fragment 7 | import android.support.v7.app.AppCompatActivity 8 | import android.view.View 9 | import android.widget.FrameLayout 10 | import androidx.navigation.NavController 11 | import androidx.navigation.findNavController 12 | import androidx.navigation.fragment.FragmentNavigator 13 | import com.dirkeisold.navigation.common.OnReselectedDelegate 14 | import com.dirkeisold.navigation.common.or 15 | import kotlinx.android.synthetic.main.activity_main.* 16 | 17 | class MainActivity : AppCompatActivity() { 18 | 19 | val sectionHomeWrapper: FrameLayout by lazy { section_home_wrapper } 20 | val sectionDashboardWrapper: FrameLayout by lazy { section_dashboard_wrapper } 21 | val sectionNotificationsWrapper: FrameLayout by lazy { section_notification_wrapper } 22 | 23 | val navHomeController: NavController by lazy { findNavController(R.id.section_home) } 24 | val navHomeFragment: Fragment by lazy { section_home } 25 | val navDashboardController: NavController by lazy { findNavController(R.id.section_dashboard) } 26 | val navDashboardFragment: Fragment by lazy { section_dashboard } 27 | val navNotificationController: NavController by lazy { findNavController(R.id.section_notification) } 28 | val navNotificationFragment: Fragment by lazy { section_notification } 29 | 30 | var currentController: NavController? = null 31 | 32 | private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item -> 33 | var returnValue = false 34 | 35 | when (item.itemId) { 36 | R.id.navigation_home -> { 37 | currentController = navHomeController 38 | 39 | sectionHomeWrapper.visibility = View.VISIBLE 40 | sectionDashboardWrapper.visibility = View.INVISIBLE 41 | sectionNotificationsWrapper.visibility = View.INVISIBLE 42 | returnValue = true 43 | } 44 | R.id.navigation_dashboard -> { 45 | currentController = navDashboardController 46 | 47 | sectionHomeWrapper.visibility = View.INVISIBLE 48 | sectionDashboardWrapper.visibility = View.VISIBLE 49 | sectionNotificationsWrapper.visibility = View.INVISIBLE 50 | returnValue = true 51 | } 52 | R.id.navigation_notifications -> { 53 | currentController = navNotificationController 54 | 55 | sectionHomeWrapper.visibility = View.INVISIBLE 56 | sectionDashboardWrapper.visibility = View.INVISIBLE 57 | sectionNotificationsWrapper.visibility = View.VISIBLE 58 | returnValue = true 59 | } 60 | } 61 | onReselected(item.itemId) 62 | return@OnNavigationItemSelectedListener returnValue 63 | } 64 | 65 | override fun onCreate(savedInstanceState: Bundle?) { 66 | super.onCreate(savedInstanceState) 67 | setContentView(R.layout.activity_main) 68 | 69 | currentController = navHomeController 70 | 71 | navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener) 72 | 73 | sectionHomeWrapper.visibility = View.VISIBLE 74 | sectionDashboardWrapper.visibility = View.INVISIBLE 75 | sectionNotificationsWrapper.visibility = View.INVISIBLE 76 | } 77 | 78 | override fun supportNavigateUpTo(upIntent: Intent) { 79 | currentController?.navigateUp() 80 | } 81 | 82 | override fun onBackPressed() { 83 | currentController 84 | ?.let { if (it.popBackStack().not()) finish() } 85 | .or { finish() } 86 | } 87 | 88 | private fun onReselected(itemId: Int) { 89 | when (itemId) { 90 | R.id.navigation_home -> { 91 | val fragmentClassName = (navHomeController.currentDestination as FragmentNavigator.Destination).fragmentClass.simpleName 92 | 93 | navHomeFragment.childFragmentManager.fragments.asReversed().forEach { 94 | if (it.javaClass.simpleName == fragmentClassName && it is OnReselectedDelegate) { 95 | it.onReselected() 96 | return@forEach 97 | } 98 | } 99 | } 100 | R.id.navigation_dashboard -> { 101 | val fragmentClassName = (navDashboardController.currentDestination as FragmentNavigator.Destination).fragmentClass.simpleName 102 | 103 | navDashboardFragment.childFragmentManager.fragments.asReversed().forEach { 104 | if (it.javaClass.simpleName == fragmentClassName && it is OnReselectedDelegate) { 105 | it.onReselected() 106 | return@forEach 107 | } 108 | } 109 | } 110 | R.id.navigation_notifications -> { 111 | val fragmentClassName = (navNotificationController.currentDestination as FragmentNavigator.Destination).fragmentClass.simpleName 112 | 113 | navNotificationFragment.childFragmentManager.fragments.asReversed().forEach { 114 | if (it.javaClass.simpleName == fragmentClassName && it is OnReselectedDelegate) { 115 | it.onReselected() 116 | return@forEach 117 | } 118 | } 119 | } 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /app/src/main/java/com/dirkeisold/navigation/common/OnReselectedDelegate.kt: -------------------------------------------------------------------------------- 1 | package com.dirkeisold.navigation.common 2 | 3 | interface OnReselectedDelegate { 4 | fun onReselected() 5 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dirkeisold/navigation/common/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.dirkeisold.navigation.common 2 | 3 | import android.support.v4.app.Fragment 4 | import android.support.v7.app.AppCompatActivity 5 | import android.view.View 6 | import android.view.ViewGroup 7 | 8 | object Utils { 9 | } 10 | 11 | fun T?.or(default: T): T = this ?: default 12 | fun T?.or(compute: () -> T): T = this ?: compute() 13 | 14 | fun Fragment.isSectionVisible(): Boolean = (((view?.parent as? ViewGroup)?.parent as? ViewGroup)?.visibility == View.VISIBLE) 15 | 16 | fun Fragment.setupActionBar(title: String, displayHome: Boolean = false) { 17 | (activity as? AppCompatActivity)?.supportActionBar?.apply { 18 | this.title = title 19 | setDisplayShowHomeEnabled(displayHome) 20 | setDisplayHomeAsUpEnabled(displayHome) 21 | } 22 | setHasOptionsMenu(displayHome) 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dirkeisold/navigation/usecase/dashboard/DashboardFragment.kt: -------------------------------------------------------------------------------- 1 | package com.dirkeisold.navigation.usecase.dashboard 2 | 3 | import android.os.Bundle 4 | import android.support.v4.app.Fragment 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import com.dirkeisold.navigation.R 9 | import com.dirkeisold.navigation.common.OnReselectedDelegate 10 | import com.dirkeisold.navigation.common.isSectionVisible 11 | import com.dirkeisold.navigation.common.setupActionBar 12 | 13 | class DashboardFragment : Fragment(), OnReselectedDelegate { 14 | 15 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = 16 | View.inflate(activity, R.layout.dashboard_fragment, null) 17 | 18 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 19 | if (isSectionVisible()) setupActionBar() 20 | } 21 | 22 | private fun setupActionBar() = setupActionBar("Dashboard") 23 | 24 | override fun onReselected() = setupActionBar() 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dirkeisold/navigation/usecase/detail/DetailFragment.kt: -------------------------------------------------------------------------------- 1 | package com.dirkeisold.navigation.usecase.detail 2 | 3 | import android.os.Bundle 4 | import android.support.v4.app.Fragment 5 | import android.view.LayoutInflater 6 | import android.view.MenuItem 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import androidx.navigation.findNavController 10 | import com.dirkeisold.navigation.R 11 | import com.dirkeisold.navigation.common.OnReselectedDelegate 12 | import com.dirkeisold.navigation.common.isSectionVisible 13 | import com.dirkeisold.navigation.common.setupActionBar 14 | 15 | class DetailFragment : Fragment(), OnReselectedDelegate { 16 | 17 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = 18 | View.inflate(activity, R.layout.detail_fragment, null) 19 | 20 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 21 | if (isSectionVisible()) setupActionBar() 22 | } 23 | 24 | private fun setupActionBar() = setupActionBar("Details from " + arguments?.getString("FROM"), true) 25 | 26 | override fun onReselected() = setupActionBar() 27 | 28 | override fun onOptionsItemSelected(menuItem: MenuItem): Boolean { 29 | when (menuItem.itemId) { 30 | android.R.id.home -> { 31 | view?.findNavController()?.navigateUp() 32 | return true 33 | } 34 | } 35 | return super.onOptionsItemSelected(menuItem) 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dirkeisold/navigation/usecase/home/HomeFragment.kt: -------------------------------------------------------------------------------- 1 | package com.dirkeisold.navigation.usecase.home 2 | 3 | import android.os.Bundle 4 | import android.support.v4.app.Fragment 5 | import android.util.Log 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import android.widget.Button 10 | import androidx.navigation.Navigation 11 | import com.dirkeisold.navigation.R 12 | import com.dirkeisold.navigation.common.OnReselectedDelegate 13 | import com.dirkeisold.navigation.common.isSectionVisible 14 | import com.dirkeisold.navigation.common.setupActionBar 15 | import kotlinx.android.synthetic.main.home_fragment.* 16 | 17 | class HomeFragment : Fragment(), OnReselectedDelegate { 18 | 19 | val detailsButton: Button by lazy { details_button } 20 | 21 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = 22 | View.inflate(activity, R.layout.home_fragment, null).apply { Log.d("HomeFragment", "onCreateView") } 23 | 24 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 25 | Log.d("HomeFragment", "onViewCreated") 26 | (view.findViewById