Repository: moallemi/MultiNavHost Branch: master Commit: 7fba1c11d546 Files: 41 Total size: 48.4 KB Directory structure: gitextract_dii09b2p/ ├── .gitignore ├── README.md ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── me/ │ │ └── moallemi/ │ │ └── multinavhost/ │ │ └── ExampleInstrumentedTest.kt │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── me/ │ │ │ └── moallemi/ │ │ │ └── multinavhost/ │ │ │ ├── MainActivity.kt │ │ │ ├── fragments/ │ │ │ │ ├── BaseFragment.kt │ │ │ │ ├── DashboardFragment.kt │ │ │ │ ├── HomeFragment.kt │ │ │ │ ├── NotificationsFragment.kt │ │ │ │ └── PageFragment.kt │ │ │ └── navigation/ │ │ │ ├── TabHistory.kt │ │ │ └── TabManager.kt │ │ └── res/ │ │ ├── drawable/ │ │ │ ├── ic_dashboard_black_24dp.xml │ │ │ ├── ic_home_black_24dp.xml │ │ │ ├── ic_launcher_background.xml │ │ │ └── ic_notifications_black_24dp.xml │ │ ├── drawable-v24/ │ │ │ └── ic_launcher_foreground.xml │ │ ├── layout/ │ │ │ ├── activity_main.xml │ │ │ ├── fragment_dashboard.xml │ │ │ ├── fragment_home.xml │ │ │ ├── fragment_notifications.xml │ │ │ └── fragment_page.xml │ │ ├── menu/ │ │ │ └── navigation.xml │ │ ├── mipmap-anydpi-v26/ │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ ├── navigation/ │ │ │ └── navigation_graph_main.xml │ │ └── values/ │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test/ │ └── java/ │ └── me/ │ └── moallemi/ │ └── multinavhost/ │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .idea *.iml .gradle /local.properties /.idea/caches/build_file_checksums.ser /.idea/libraries /.idea/modules.xml /.idea/workspace.xml .DS_Store /build /captures .externalNativeBuild ================================================ FILE: README.md ================================================ # MultiNavHost This sample provides an approach to separate back stack history for each tab in Bottom Navigation View using Android Navigation Architecture Component Bottom Navigation View gives the user quick access to 3-5 top-level destinations in an Android app. The common architectural approach for such a top level navigation which is provided by the Android navigation component is that activity only knows one backstack. But in some cases you need to have different back stack history for each tab in bottom navigation view like Instagram app. This sample app shows the usage of the new Navigation Architecture Component in collaboration with the Bottom Navigation view with separate back stack history for each tab. As you know when you are using Android Navigation Component you have to use a NavHostFragment as a container for your fragments: ```xml ``` Because `Navigation` class in navigation components use just one back stack for each graph, you have to use multiple `NavHostFragment` with a **single** navigation graph. So your main xml layout should look like this: ```xml ``` To avoid creating multiple navigation_graph.xml files, we use only `navigation_graph_main` file and every destination and action must be defined here. ```xml ``` There is no need to define the `app:startDestination` in the navigation graph. We need different start destination for each tab. So we handle start destination in `TabManager` class. ```kotlin private val startDestinations = mapOf( R.id.navigation_home to R.id.homeFragment, R.id.navigation_dashboard to R.id.dashboardFragment, R.id.navigation_notifications to R.id.notificationsFragment ) ``` For further information please review the sample app code. ================================================ FILE: app/.gitignore ================================================ /build ================================================ FILE: app/build.gradle ================================================ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: "androidx.navigation.safeargs.kotlin" android { compileSdkVersion 28 defaultConfig { applicationId "me.moallemi.multinavhost" minSdkVersion 19 targetSdkVersion 28 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'com.google.android.material:material:1.0.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha3' implementation 'androidx.core:core-ktx:1.0.1' implementation "android.arch.navigation:navigation-fragment-ktx:1.0.0-rc02" implementation "android.arch.navigation:navigation-ui-ktx:1.0.0-rc02" testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' } ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: app/src/androidTest/java/me/moallemi/multinavhost/ExampleInstrumentedTest.kt ================================================ package me.moallemi.multinavhost import androidx.test.InstrumentationRegistry import androidx.test.runner.AndroidJUnit4 import org.junit.Test import org.junit.runner.RunWith import org.junit.Assert.* /** * Instrumented test, which will execute on an Android device. * * See [testing documentation](http://d.android.com/tools/testing). */ @RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { @Test fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getTargetContext() assertEquals("me.moallemi.multinavhost", appContext.packageName) } } ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/me/moallemi/multinavhost/MainActivity.kt ================================================ package me.moallemi.multinavhost import android.content.Intent import android.os.Bundle import android.view.MenuItem import androidx.appcompat.app.AppCompatActivity import com.google.android.material.bottomnavigation.BottomNavigationView import kotlinx.android.synthetic.main.activity_main.* import me.moallemi.multinavhost.navigation.TabManager class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemSelectedListener { private val tabManager: TabManager by lazy { TabManager(this) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) if (savedInstanceState == null) { tabManager.currentController = tabManager.navHomeController if (intent.containsDeepLink()) { handleDeepLink() } } bottomNavigationView.setOnNavigationItemSelectedListener(this) } override fun onSaveInstanceState(outState: Bundle?) { super.onSaveInstanceState(outState) tabManager.onSaveInstanceState(outState) } override fun onRestoreInstanceState(savedInstanceState: Bundle?) { super.onRestoreInstanceState(savedInstanceState) tabManager.onRestoreInstanceState(savedInstanceState) } override fun supportNavigateUpTo(upIntent: Intent) { tabManager.supportNavigateUpTo(upIntent) } override fun onBackPressed() { tabManager.onBackPressed() } override fun onNavigationItemSelected(menuItem: MenuItem): Boolean { tabManager.switchTab(menuItem.itemId) return true } override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) setIntent(intent) if (this.intent.containsDeepLink()) { handleDeepLink() } } private fun handleDeepLink() { intent.data?.pathSegments?.also { deepLinkPathSegments -> when(deepLinkPathSegments.firstOrNull()?.trim()) { "dashboard" -> R.id.navigation_dashboard "home" -> R.id.navigation_home "notifications" -> R.id.navigation_notifications "pages" -> { tabManager.currentController?.navigate(NavigationGraphMainDirections.actionGlobalPageFragment(getPageNumberFromSegments(deepLinkPathSegments), "PageFragment")) null } else -> null }?.also { tabManager.switchTab(it) bottomNavigationView.menu.findItem(it).isChecked = true } } } private fun getPageNumberFromSegments(deepLinkPathSegments: List): Int = if (deepLinkPathSegments.size < 2) 0 else deepLinkPathSegments[1].toIntOrNull() ?: 0 private fun Intent.containsDeepLink(): Boolean = action == Intent.ACTION_VIEW && data != null } ================================================ FILE: app/src/main/java/me/moallemi/multinavhost/fragments/BaseFragment.kt ================================================ package me.moallemi.multinavhost.fragments import androidx.fragment.app.Fragment abstract class BaseFragment : Fragment() ================================================ FILE: app/src/main/java/me/moallemi/multinavhost/fragments/DashboardFragment.kt ================================================ package me.moallemi.multinavhost.fragments import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.navigation.findNavController import kotlinx.android.synthetic.main.fragment_dashboard.buttonNextPage import me.moallemi.multinavhost.NavigationGraphMainDirections import me.moallemi.multinavhost.R class DashboardFragment : BaseFragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_dashboard, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) buttonNextPage.setOnClickListener { val action = NavigationGraphMainDirections.actionGlobalPageFragment(1, "DashboardFragment") view.findNavController().navigate(action) } } } ================================================ FILE: app/src/main/java/me/moallemi/multinavhost/fragments/HomeFragment.kt ================================================ package me.moallemi.multinavhost.fragments import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.navigation.findNavController import kotlinx.android.synthetic.main.fragment_home.buttonNextPage import me.moallemi.multinavhost.NavigationGraphMainDirections import me.moallemi.multinavhost.R class HomeFragment : BaseFragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_home, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) buttonNextPage.setOnClickListener { val action = NavigationGraphMainDirections.actionGlobalPageFragment(1, "HomeFragment") view.findNavController().navigate(action) } } } ================================================ FILE: app/src/main/java/me/moallemi/multinavhost/fragments/NotificationsFragment.kt ================================================ package me.moallemi.multinavhost.fragments import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.navigation.findNavController import kotlinx.android.synthetic.main.fragment_notifications.buttonNextPage import me.moallemi.multinavhost.NavigationGraphMainDirections import me.moallemi.multinavhost.R class NotificationsFragment : BaseFragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_notifications, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) buttonNextPage.setOnClickListener { val action = NavigationGraphMainDirections.actionGlobalPageFragment(1, "NotificationsFragment") view.findNavController().navigate(action) } } } ================================================ FILE: app/src/main/java/me/moallemi/multinavhost/fragments/PageFragment.kt ================================================ package me.moallemi.multinavhost.fragments import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.navigation.findNavController import androidx.navigation.fragment.navArgs import kotlinx.android.synthetic.main.fragment_page.buttonNextPage import kotlinx.android.synthetic.main.fragment_page.message import me.moallemi.multinavhost.NavigationGraphMainDirections import me.moallemi.multinavhost.R class PageFragment : BaseFragment() { private val fragmentArgs: PageFragmentArgs by navArgs() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_page, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) message.text = "Page number ${fragmentArgs.pageNumber}, Parent: ${fragmentArgs.pageParent}" buttonNextPage.setOnClickListener { view.findNavController().navigate( NavigationGraphMainDirections.actionGlobalPageFragment(fragmentArgs.pageNumber + 1, "PageFragment") ) } } } ================================================ FILE: app/src/main/java/me/moallemi/multinavhost/navigation/TabHistory.kt ================================================ package me.moallemi.multinavhost.navigation import java.io.Serializable import java.util.ArrayList class TabHistory : Serializable { private val stack: ArrayList = ArrayList() private val isEmpty: Boolean get() = stack.size == 0 val size: Int get() = stack.size fun push(entry: Int) { stack.add(entry) } fun popPrevious(): Int { var entry = -1 if (!isEmpty) { entry = stack[stack.size - 2] stack.removeAt(stack.size - 2) } return entry } fun clear() { stack.clear() } } ================================================ FILE: app/src/main/java/me/moallemi/multinavhost/navigation/TabManager.kt ================================================ package me.moallemi.multinavhost.navigation import android.content.Intent import android.os.Bundle import android.view.View import androidx.core.view.isInvisible import androidx.navigation.NavController import androidx.navigation.findNavController import kotlinx.android.synthetic.main.activity_main.bottomNavigationView import kotlinx.android.synthetic.main.activity_main.dashboardTabContainer import kotlinx.android.synthetic.main.activity_main.homeTabContainer import kotlinx.android.synthetic.main.activity_main.notificationsTabContainer import me.moallemi.multinavhost.MainActivity import me.moallemi.multinavhost.R class TabManager(private val mainActivity: MainActivity) { private val startDestinations = mapOf( R.id.navigation_home to R.id.homeFragment, R.id.navigation_dashboard to R.id.dashboardFragment, R.id.navigation_notifications to R.id.notificationsFragment ) private var currentTabId: Int = R.id.navigation_home var currentController: NavController? = null private var tabHistory = TabHistory().apply { push(R.id.navigation_home) } val navHomeController: NavController by lazy { mainActivity.findNavController(R.id.homeTab).apply { graph = navInflater.inflate(R.navigation.navigation_graph_main).apply { startDestination = startDestinations.getValue(R.id.navigation_home) } } } private val navDashboardController: NavController by lazy { mainActivity.findNavController(R.id.dashboardTab).apply { graph = navInflater.inflate(R.navigation.navigation_graph_main).apply { startDestination = startDestinations.getValue(R.id.navigation_dashboard) } } } private val navNotificationsController: NavController by lazy { mainActivity.findNavController(R.id.notificationsTab).apply { graph = navInflater.inflate(R.navigation.navigation_graph_main).apply { startDestination = startDestinations.getValue(R.id.navigation_notifications) } } } private val homeTabContainer: View by lazy { mainActivity.homeTabContainer } private val dashboardTabContainer: View by lazy { mainActivity.dashboardTabContainer } private val notificationsTabContainer: View by lazy { mainActivity.notificationsTabContainer } fun onSaveInstanceState(outState: Bundle?) { outState?.putSerializable(KEY_TAB_HISTORY, tabHistory) } fun onRestoreInstanceState(savedInstanceState: Bundle?) { savedInstanceState?.let { tabHistory = it.getSerializable(KEY_TAB_HISTORY) as TabHistory switchTab(mainActivity.bottomNavigationView.selectedItemId, false) } } fun supportNavigateUpTo(upIntent: Intent) { currentController?.navigateUp() } fun onBackPressed() { currentController?.let { if (it.currentDestination == null || it.currentDestination?.id == startDestinations.getValue(currentTabId)) { if (tabHistory.size > 1) { val tabId = tabHistory.popPrevious() switchTab(tabId, false) mainActivity.bottomNavigationView.menu.findItem(tabId)?.isChecked = true } else { mainActivity.finish() } } it.popBackStack() } ?: run { mainActivity.finish() } } fun switchTab(tabId: Int, addToHistory: Boolean = true) { currentTabId = tabId when (tabId) { R.id.navigation_home -> { currentController = navHomeController invisibleTabContainerExcept(homeTabContainer) } R.id.navigation_dashboard -> { currentController = navDashboardController invisibleTabContainerExcept(dashboardTabContainer) } R.id.navigation_notifications -> { currentController = navNotificationsController invisibleTabContainerExcept(notificationsTabContainer) } } if (addToHistory) { tabHistory.push(tabId) } } private fun invisibleTabContainerExcept(container: View) { homeTabContainer.isInvisible = true dashboardTabContainer.isInvisible = true notificationsTabContainer.isInvisible = true container.isInvisible = false } companion object { private const val KEY_TAB_HISTORY = "key_tab_history" } } ================================================ FILE: app/src/main/res/drawable/ic_dashboard_black_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_home_black_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_launcher_background.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_notifications_black_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable-v24/ic_launcher_foreground.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_main.xml ================================================ ================================================ FILE: app/src/main/res/layout/fragment_dashboard.xml ================================================