├── .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 | 
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 | 
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