Forked from gotev/NavigationBottomBarSectionsStateKeeperWorkaround.kt
Created
March 2, 2022 09:53
-
-
Save zakayothuku/40a5784efc03e7ccd20c9bdfc32345fc to your computer and use it in GitHub Desktop.
Revisions
-
gotev revised this gist
Jan 19, 2020 . 1 changed file with 5 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -188,6 +188,11 @@ class MainActivity : AppCompatActivity() { override fun onSupportNavigateUp() = navSectionsStateKeeper.onSupportNavigateUp() override fun onBackPressed() { if (!navSectionsStateKeeper.onSupportNavigateUp()) super.onBackPressed() } } ``` -
gotev revised this gist
Jan 18, 2020 . 1 changed file with 5 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -7,7 +7,11 @@ When you switch to a different section and then you come back, you will be exact ## Disclaimer The solution described here is a temporary workaround with code taken from [NavigationAdvancedSample](https://github.com/android/architecture-components-samples/blob/master/NavigationAdvancedSample/app/src/main/java/com/example/android/navigationadvancedsample/NavigationExtensions.kt), while waiting for JetPack Navigation official multiple backstack support. Check [this tweet](https://twitter.com/alexgt89/status/1218525799252406273?s=09) IMHO it's better than a fully handmade custom solution from scratch without JetPack Navigation: - you can use nav graphs, view models, safe args, deep links - looking at mid/long range period in a project's maintenance, this is going in the same direction as JetPack however take this as a study and experimental material, being sure you understand it fully before applying it to a real production project. I followed the principle of: -
gotev revised this gist
Jan 18, 2020 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -7,7 +7,7 @@ When you switch to a different section and then you come back, you will be exact ## Disclaimer The solution described here is a temporary workaround with code taken from [NavigationAdvancedSample](https://github.com/android/architecture-components-samples/blob/master/NavigationAdvancedSample/app/src/main/java/com/example/android/navigationadvancedsample/NavigationExtensions.kt), while waiting for JetPack Navigation official multiple backstack support. Check [this tweet](https://twitter.com/alexgt89/status/1218525799252406273?s=09) IMHO it's better than a fully handmade custom solution from scratch without JetPack Navigation (you can use nav graphs, view models, safe args, deep links), looking at mid/long range period in a project's maintenance, however take this as a study and experimental material, being sure you understand it fully before applying it to a real production project. I followed the principle of: -
gotev revised this gist
Jan 18, 2020 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -57,7 +57,7 @@ Copy: - `NavigationBottomBarSectionsStateKeeperWorkaround.kt` - `NavigationWorkaroundExtensions.kt` -> import your project's R to resolve animations imports in your project (both files are in this gist after the README). In your activity layout you need to have: - `androidx.fragment.app.FragmentContainerView` -
gotev revised this gist
Jan 18, 2020 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -15,7 +15,7 @@ I followed the principle of: [That's an interesting article](https://programmingisterrible.com/post/139222674273/write-code-that-is-easy-to-delete-not-easy-to) since this will be useless once official support in JetPack Navigation is implemented and you want to keep your codebase as clean as possible. >USE THIS AT YOUR OWN RISK. FEEL FREE TO COMMENT IF YOU HAVE SUGGESTIONS FOR IMPROVEMENT OR TO REPORT SOME REAL SCENARIOS WHERE YOU USED IT AND THE OUTCOMES, WHICH CAN SERVE ALSO AS FEEDBACK TO PEOPLE WORKING ON JETPACK NAVIGATION. -
gotev revised this gist
Jan 18, 2020 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -15,7 +15,7 @@ I followed the principle of: [That's an interesting article](https://programmingisterrible.com/post/139222674273/write-code-that-is-easy-to-delete-not-easy-to) since this will be useless once official support in JetPack Navigation is implemented. >USE THIS AT YOUR OWN RISK. FEEL FREE TO COMMENT IF YOU HAVE SUGGESTIONS FOR IMPROVEMENT OR TO REPORT SOME REAL SCENARIOS WHERE YOU USED IT AND THE OUTCOMES, WHICH CAN SERVE ALSO AS FEEDBACK TO PEOPLE WORKING ON JETPACK NAVIGATION. -
gotev revised this gist
Jan 18, 2020 . 1 changed file with 4 additions and 4 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -5,9 +5,9 @@ This mimicks what you can achieve in iOS by using a root `UITabBarController` wi When you switch to a different section and then you come back, you will be exactly at the last position you were in that section. ## Disclaimer The solution described here is a temporary workaround with code taken from [NavigationAdvancedSample](https://github.com/android/architecture-components-samples/blob/master/NavigationAdvancedSample/app/src/main/java/com/example/android/navigationadvancedsample/NavigationExtensions.kt), while waiting for JetPack Navigation official multiple backstack support. Check [this tweet](https://twitter.com/alexgt89/status/1218525799252406273?s=09) IMHO it's better than a fully handmade custom solution from scratch without JetPack Navigation, looking at mid/long range period in a project's maintenance, however take this as a study and experimental material, being sure you understand it fully before applying it to a real production project. I followed the principle of: @@ -17,7 +17,7 @@ I followed the principle of: however, >USE THIS AT YOUR OWN RISK. FEEL FREE TO COMMENT IF YOU HAVE SUGGESTIONS FOR IMPROVEMENT OR TO REPORT SOME REAL SCENARIOS WHERE YOU USED IT AND THE OUTCOMES, WHICH CAN SERVE ALSO AS FEEDBACK TO PEOPLE WORKING ON JETPACK NAVIGATION. ## License: Apache 2.0 ``` @@ -55,7 +55,7 @@ implementation "com.google.android.material:material:1.1.0-rc01" Copy: - `NavigationBottomBarSectionsStateKeeperWorkaround.kt` - `NavigationWorkaroundExtensions.kt` -> import your project's R to resolve animations imports in your project. -
gotev created this gist
Jan 18, 2020 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,60 @@ package jetpack.navigation.workaround import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import androidx.navigation.NavController import androidx.navigation.ui.setupActionBarWithNavController import com.google.android.material.bottomnavigation.BottomNavigationView import java.lang.ref.WeakReference class NavigationBottomBarSectionsStateKeeperWorkaround( activity: AppCompatActivity, private val navHostContainerID: Int, private val navGraphIds: List<Int>, private val bottomNavigationViewID: Int ) { private var currentNavController: LiveData<NavController>? = null private val activityRef = WeakReference(activity) fun onCreate(savedInstanceState: Bundle?) { if (savedInstanceState == null) { setupBottomNavigationBar() } // Else, need to wait for onRestoreInstanceState } fun onRestoreInstanceState(savedInstanceState: Bundle?) { // Now that BottomNavigationBar has restored its instance state // and its selectedItemId, we can proceed with setting up the // BottomNavigationBar with Navigation setupBottomNavigationBar() } /** * Called on first creation and when restoring state. */ private fun setupBottomNavigationBar() { val activity = activityRef.get() ?: return val bottomNavigationView = activity.findViewById<BottomNavigationView>(bottomNavigationViewID) val controller = bottomNavigationView.setupWithNavController( navGraphIds = navGraphIds, fragmentManager = activity.supportFragmentManager, containerId = navHostContainerID, intent = activity.intent ) // Whenever the selected controller changes, setup the action bar. controller.observe(activity, Observer { navController -> activity.setupActionBarWithNavController(navController) }) currentNavController = controller } fun onSupportNavigateUp() = currentNavController?.value?.navigateUp() ?: false } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,271 @@ package jetpack.navigation.workaround // Copied from https://github.com/android/architecture-components-samples/blob/master/NavigationAdvancedSample/app/src/main/java/com/example/android/navigationadvancedsample/NavigationExtensions.kt // google navigation workaround stuff until multiple back stacks are available /* * Copyright 2019, The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import android.content.Intent import android.util.SparseArray import androidx.core.util.forEach import androidx.core.util.set import androidx.fragment.app.FragmentManager import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.navigation.NavController import androidx.navigation.fragment.NavHostFragment import com.google.android.material.bottomnavigation.BottomNavigationView /** * Manages the various graphs needed for a [BottomNavigationView]. * * This sample is a workaround until the Navigation Component supports multiple back stacks. */ fun BottomNavigationView.setupWithNavController( navGraphIds: List<Int>, fragmentManager: FragmentManager, containerId: Int, intent: Intent ): LiveData<NavController> { // Map of tags val graphIdToTagMap = SparseArray<String>() // Result. Mutable live data with the selected controlled val selectedNavController = MutableLiveData<NavController>() var firstFragmentGraphId = 0 // First create a NavHostFragment for each NavGraph ID navGraphIds.forEachIndexed { index, navGraphId -> val fragmentTag = getFragmentTag( index ) // Find or create the Navigation host fragment val navHostFragment = obtainNavHostFragment( fragmentManager, fragmentTag, navGraphId, containerId ) // Obtain its id val graphId = navHostFragment.navController.graph.id if (index == 0) { firstFragmentGraphId = graphId } // Save to the map graphIdToTagMap[graphId] = fragmentTag // Attach or detach nav host fragment depending on whether it's the selected item. if (this.selectedItemId == graphId) { // Update livedata with the selected graph selectedNavController.value = navHostFragment.navController attachNavHostFragment( fragmentManager, navHostFragment, index == 0 ) } else { detachNavHostFragment( fragmentManager, navHostFragment ) } } // Now connect selecting an item with swapping Fragments var selectedItemTag = graphIdToTagMap[this.selectedItemId] val firstFragmentTag = graphIdToTagMap[firstFragmentGraphId] var isOnFirstFragment = selectedItemTag == firstFragmentTag // When a navigation item is selected setOnNavigationItemSelectedListener { item -> // Don't do anything if the state is state has already been saved. if (fragmentManager.isStateSaved) { false } else { val newlySelectedItemTag = graphIdToTagMap[item.itemId] if (selectedItemTag != newlySelectedItemTag) { // Pop everything above the first fragment (the "fixed start destination") fragmentManager.popBackStack( firstFragmentTag, FragmentManager.POP_BACK_STACK_INCLUSIVE ) val selectedFragment = fragmentManager.findFragmentByTag(newlySelectedItemTag) as NavHostFragment // Exclude the first fragment tag because it's always in the back stack. if (firstFragmentTag != newlySelectedItemTag) { // Commit a transaction that cleans the back stack and adds the first fragment // to it, creating the fixed started destination. fragmentManager.beginTransaction() .setCustomAnimations( R.anim.nav_default_enter_anim, R.anim.nav_default_exit_anim, R.anim.nav_default_pop_enter_anim, R.anim.nav_default_pop_exit_anim ) .attach(selectedFragment) .setPrimaryNavigationFragment(selectedFragment) .apply { // Detach all other Fragments graphIdToTagMap.forEach { _, fragmentTagIter -> if (fragmentTagIter != newlySelectedItemTag) { detach(fragmentManager.findFragmentByTag(firstFragmentTag)!!) } } } .addToBackStack(firstFragmentTag) .setReorderingAllowed(true) .commit() } selectedItemTag = newlySelectedItemTag isOnFirstFragment = selectedItemTag == firstFragmentTag selectedNavController.value = selectedFragment.navController true } else { false } } } // Optional: on item reselected, pop back stack to the destination of the graph setupItemReselected(graphIdToTagMap, fragmentManager) // Handle deep link setupDeepLinks(navGraphIds, fragmentManager, containerId, intent) // Finally, ensure that we update our BottomNavigationView when the back stack changes fragmentManager.addOnBackStackChangedListener { if (!isOnFirstFragment && !fragmentManager.isOnBackStack(firstFragmentTag)) { this.selectedItemId = firstFragmentGraphId } // Reset the graph if the currentDestination is not valid (happens when the back // stack is popped after using the back button). selectedNavController.value?.let { controller -> if (controller.currentDestination == null) { controller.navigate(controller.graph.id) } } } return selectedNavController } private fun BottomNavigationView.setupDeepLinks( navGraphIds: List<Int>, fragmentManager: FragmentManager, containerId: Int, intent: Intent ) { navGraphIds.forEachIndexed { index, navGraphId -> val fragmentTag = getFragmentTag( index ) // Find or create the Navigation host fragment val navHostFragment = obtainNavHostFragment( fragmentManager, fragmentTag, navGraphId, containerId ) // Handle Intent if (navHostFragment.navController.handleDeepLink(intent) && selectedItemId != navHostFragment.navController.graph.id ) { this.selectedItemId = navHostFragment.navController.graph.id } } } private fun BottomNavigationView.setupItemReselected( graphIdToTagMap: SparseArray<String>, fragmentManager: FragmentManager ) { setOnNavigationItemReselectedListener { item -> val newlySelectedItemTag = graphIdToTagMap[item.itemId] val selectedFragment = fragmentManager.findFragmentByTag(newlySelectedItemTag) as NavHostFragment val navController = selectedFragment.navController // Pop the back stack to the start destination of the current navController graph navController.popBackStack( navController.graph.startDestination, false ) } } private fun detachNavHostFragment( fragmentManager: FragmentManager, navHostFragment: NavHostFragment ) { fragmentManager.beginTransaction() .detach(navHostFragment) .commitNow() } private fun attachNavHostFragment( fragmentManager: FragmentManager, navHostFragment: NavHostFragment, isPrimaryNavFragment: Boolean ) { fragmentManager.beginTransaction() .attach(navHostFragment) .apply { if (isPrimaryNavFragment) { setPrimaryNavigationFragment(navHostFragment) } } .commitNow() } private fun obtainNavHostFragment( fragmentManager: FragmentManager, fragmentTag: String, navGraphId: Int, containerId: Int ): NavHostFragment { // If the Nav Host fragment exists, return it val existingFragment = fragmentManager.findFragmentByTag(fragmentTag) as NavHostFragment? existingFragment?.let { return it } // Otherwise, create it and return it. val navHostFragment = NavHostFragment.create(navGraphId) fragmentManager.beginTransaction() .add(containerId, navHostFragment, fragmentTag) .commitNow() return navHostFragment } private fun FragmentManager.isOnBackStack(backStackName: String): Boolean { val backStackCount = backStackEntryCount for (index in 0 until backStackCount) { if (getBackStackEntryAt(index).name == backStackName) { return true } } return false } private fun getFragmentTag(index: Int) = "bottomNavigation#$index" This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,190 @@ # Android JetPack Navigation + Material BottomNavigationView keeping sections navigation state This mimicks what you can achieve in iOS by using a root `UITabBarController` with a `UINavigationController` for each section. When you switch to a different section and then you come back, you will be exactly at the last position you were in that section. ## Disclaimer The solution described here is a temporary workaround with code taken from [NavigationAdvancedSample](https://github.com/android/architecture-components-samples/blob/master/NavigationAdvancedSample/app/src/main/java/com/example/android/navigationadvancedsample/NavigationExtensions.kt), while waiting for JetPack Navigation official multiple backstack support. Check: https://twitter.com/alexgt89/status/1218525799252406273?s=09 IMHO it's better than a fully handmade custom solution from scratch, looking at mid/long range period in a project's maintenance. I followed the principle of: >Write code that's easy to delete, not easy to extend [That's an interesting article](https://programmingisterrible.com/post/139222674273/write-code-that-is-easy-to-delete-not-easy-to) however, >USE IT AT YOUR OWN RISK. FEEL FEEL FREE TO COMMENT IF YOU HAVE SUGGESTIONS FOR IMPROVEMENT OR TO REPORT SOME REAL SCENARIOS WHERE YOU USED IT AND THE OUTCOMES. ## License: Apache 2.0 ``` Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ``` ## Recipe Create a new project or open an existing one. You need the following dependencies: ```groovy def lifecycle_version = "2.2.0-rc03" implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version" def nav_version = "2.2.0-rc04" implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" implementation "androidx.navigation:navigation-ui-ktx:$nav_version" // Support implementation "androidx.appcompat:appcompat:1.1.0" implementation "androidx.constraintlayout:constraintlayout:2.0.0-beta4" implementation "com.google.android.material:material:1.1.0-rc01" ``` Copy: - `NavigationBottomBarSectionsStateKeeperWorkaround.kt` - `NavigationWorkaroundExtensions.kt` in your project. In your activity layout you need to have: - `androidx.fragment.app.FragmentContainerView` - `com.google.android.material.bottomnavigation.BottomNavigationView` Example: `activity_main.xml`: ```xml <?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" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <com.google.android.material.appbar.AppBarLayout android:id="@+id/appbar" android:layout_width="match_parent" android:layout_height="wrap_content" android:animateLayoutChanges="true" android:theme="@style/AppTheme.AppBarOverlay" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toTopOf="parent"> <androidx.appcompat.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" app:popupTheme="@style/AppTheme.PopupOverlay" /> </com.google.android.material.appbar.AppBarLayout> <androidx.fragment.app.FragmentContainerView android:id="@+id/nav_host_fragment" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="0dp" app:defaultNavHost="true" app:layout_constraintBottom_toTopOf="@id/bottom_navigation" app:layout_constraintTop_toBottomOf="@id/appbar" /> <com.google.android.material.bottomnavigation.BottomNavigationView android:id="@+id/bottom_navigation" style="@style/Widget.MaterialComponents.BottomNavigationView.Colored" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:menu="@menu/main_bottombar" /> </androidx.constraintlayout.widget.ConstraintLayout> ``` Then, create the menu which contains the bottom bar items. Example: `main_bottombar.xml` ```xml <?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/nav_section1" android:icon="@android:drawable/ic_menu_call" android:title="Section 1" /> <item android:id="@+id/nav_section2" android:icon="@android:drawable/ic_menu_add" android:title="Section 2" /> <item android:id="@+id/nav_section3" android:icon="@android:drawable/ic_menu_camera" android:title="Section 3" /> </menu> ``` Create one nav graph for each section in the tab bar. In this example, the 3 nav graphs must be called: ``` nav_section1 nav_section2 nav_section3 ``` because for the link between the bottom bar and the sections to work, `main_bottombar menu IDs must match navigation graph IDs`. Populate each section with your navigation items and don't forget to set every `startDestination`. now in your `MainActivity.kt` add: ```kotlin class MainActivity : AppCompatActivity() { private val navSectionsStateKeeper by lazy { NavigationBottomBarSectionsStateKeeperWorkaround( activity = this, navHostContainerID = R.id.nav_host_fragment, navGraphIds = listOf( R.navigation.nav_section1, R.navigation.nav_section2, R.navigation.nav_section3 ), bottomNavigationViewID = R.id.bottom_navigation ) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) setSupportActionBar(toolbar) supportActionBar?.apply { setDisplayHomeAsUpEnabled(true) setDisplayShowHomeEnabled(true) } navSectionsStateKeeper.onCreate(savedInstanceState) } override fun onRestoreInstanceState(savedInstanceState: Bundle) { super.onRestoreInstanceState(savedInstanceState) navSectionsStateKeeper.onRestoreInstanceState(savedInstanceState) } override fun onSupportNavigateUp() = navSectionsStateKeeper.onSupportNavigateUp() } ``` Run and enjoy :tada: