A multi back stack android navigation
MIT License - Copyright (c) 2020 Abanoub Milad Nassief Hanna
abanoubcs@gmail.com
@Linkedin
@Github
Add to project level build.gradle
allprojects {
repositories {
maven { url "https://jitpack.io" }
}
}
Add to app level build.gradle
dependencies {
implementation 'com.github.abanoubmilad:labyrinth:0.4'
}
class ExampleMultiNavActivity : AppCompatActivity(), INavHolder {
lateinit var labyrinth: Labyrinth
override fun getINav() = labyrinth
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.example_multi_nav_activity)
labyrinth = Builder(
viewModelStoreOwner = this,
lifecycleOwner = this,
fragmentManager = supportFragmentManager,
/*
* @ResId of FragmentContainerView that exists in your layout
* type: androidx.fragment.app.FragmentContainerView
*/
fragmentContainerId = R.id.nav_host_container,
/*
* BottomNavigationView that exists in your layout
* type: com.google.android.material.bottomnavigation.BottomNavigationView
*/
bottomNavigationView = bottom_nav,
/*
* list of lazy fragment initializer where each initializer
* represents the back stack root fragment
*/
rootTabFragmentsInitializer = listOf(
{ Welcome() },
{ Leaderboard() },
{ Register() }
),
/*
* map of each menu item id to it's corresponding stack index
* if a menu item should not map to a stack do not pass it
*
* if a menu ResId maps to a stack index, once clicked by user
* the module will navigate to the corresponding stack and display it
* onNavTabSelected callback will be fired
*
* if a menu ResId does not map to a stack index, once clicked by user
* onNonNavTabSelected callback will be fired
*/
menuItemIdToRootTabFragmentIndexMap = hashMapOf(
R.id.home to 0,
R.id.list to 1,
R.id.form to 2
)).apply {
/*
* OPTIONAL CUSTOMIZATIONS
*/
/*
* initial selected tab
*/
defaultSelectedTabIndex = 0
/*
* if enabled, clicking same tab wil reset the current stack to its root fragment
* else, clicking same tab will behave as clicking back i.e pop current stack
*/
resetOnSameTabClickEnabled = true
/*
* if enabled, the fragments will survive any configuration change
* i.e fragments will be maintained even if activity is recreated
* this functionality is achieve using a view model
*/
saveStateEnabled = false
/*
* fragments pushed into current tab
* will be retained as long as their current tab is the selected one (active tab)
*
* when tab is switched you can choose to keep the non active stack fragments in memory
* so that when you switch back you will get the same instances
* OR destroy the non active stack fragments and recreate them (with same bundle) when their tab is selected again
*
* if enabled, non active tab (any tab that is not selected) fragments will be retained in memory
* if disabled, non active tab fragments will be destroyed and recreated
* with their previous bundle when the tab is selected again
*
* For better performance it's suggested to keep this disabled (false)
* so fragments of other tab can be destroyed and release memory resources
*
*/
retainNonActiveTabFragmentsEnabled = false
/*
* if enabled, tabs history will be maintained
*/
tabHistoryEnabled = true
/*
* onNavTabSelected: ((menItemId: Int) -> Unit)?
* a callback fired when a bottom menu tab is clicked (nav tab)
* i.e when a bottom menu tab that corresponds to a back stack is clicked
*
* a bottom menu tab corresponds to a stack if
* its exists in the menuItemIdToRootTabFragmentIndexMap
*
*/
onNavTabSelected = null
/*
* onNonNavTabSelected: ((menItemId: Int) -> Unit)?
* a callback fired when a bottom menu tab is clicked (non nav tab)
* i.e when a bottom menu tab that does not correspond to a back stack is clicked
*
* a bottom menu tab does not correspond to a stack if
* its id does exist in the menuItemIdToRootTabFragmentIndexMap
*
*/
onNonNavTabSelected = null
}.build()
}
override fun onDestroy() {
/*
* labyrinth should release it's resources
*/
labyrinth.onDestroy()
super.onDestroy()
}
override fun onBackPressed() {
/*
* if shouldCallSuperOnBackPressed is true it means that labyrinth
* can not move any more back i.e both back stack and tabs history are empty
*/
if (labyrinth.shouldCallSuperOnBackPressed())
super.onBackPressed()
}
}
/*
*
* Note: using a fragment that does not extend NavFragment will also work
* but then you wont have the onVisible() callback since the fragment view wont be retained
* i.e it will behave same as setting shouldSaveState = false
* where the view will be created each time
* (but of course you will still have the multi stack behaviour as expected)
*
*/
class About1 : NavFragment() {
/*
* open val shouldSaveState = false
*
* if set to true:
*
* the fragment will retain its view
* (leak canary will complain about this as a leak, but it's a false positive one)
* i.e the view will survive back stack operations unless the fragment itself is dismissed
*
* in this case:
*
* buildRootView() is guaranteed to be called once only
* onCreated() is guaranteed to be called once only
* this callback is suitable for doing your view initializations
*
* onVisible() called is guaranteed to be called at least once
* as it will be called each time the fragment becomes visible again on ui
* if you use view model, you need to observe the view model inside the
* the onVisible() not the onCreated()
*
* example:
* fragment A is added to backs tack -> A.buildRootView() -> A.onCreated()-> A.onVisible() -> fragment B
* is added on top of A -> .... fragment B is dismissed -> A.onVisible() -> .......
*
* if set to false:
*
* the fragment will not retain its view
* i.e the view will be destroyed during the back stack operations
*
* in this case:
*
* buildRootView(), onCreated() and onVisible()
* will be called in order, each time the fragment becomes visible on ui
* whether it's the first time for this fragment or it's already restored from the back stack
*
* buildRootView() should be used to inflate the view as usual
* onCreated() should be used for view initializations
* onVisible() becomes useless in this case since it's always called with onCreated()
* but it can be used for view model observations this is helpful for
* better code separations and in case you decide to switch shouldSaveState to true
* as shouldSaveState = true requires view model observation to be done in onVisible()
*
*
* example:
* fragment A is added to backs tack -> A.buildRootView() -> A.onCreated()-> A.onVisible() -> fragment B
* is added on top of A -> .... fragment B is dismissed -> A.buildRootView() -> A.onCreated()-> A.onVisible() .......
*
*
*/
override val shouldSaveState = true
/*
* must override to specify how to build the fragment view
* this is really useful if you gonna have custom view initializations
* like data binding for example.
*
* example:
*
* override fun buildRootView(inflater: LayoutInflater, container: ViewGroup?): View {
* val binding: ProfileFragmentBinding =
* DataBindingUtil.inflate(inflater, layoutId, container, false)
* binding.viewModel = viewModel
* binding.lifecycleOwner = this
* return binding.root
* }
*
*/
override fun buildRootView(
inflater: LayoutInflater,
container: ViewGroup?
): View {
return inflater.inflate(R.layout.fragment_about1, container, false)
}
override fun onCreated() {
}
override fun onVisible() {
}
}
class ExampleSingleNavActivity : AppCompatActivity(), INavHolder {
lateinit var labyrinth: LabyrinthSingle
override fun getINav() = labyrinth
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.example_single_nav_activity)
labyrinth = BuilderSingle(
viewModelStoreOwner = this,
lifecycleOwner = this,
fragmentManager = supportFragmentManager,
fragmentContainerId = R.id.nav_host_container
).apply {
saveStateEnabled = true
/*
*
* if enabled, bottom stack fragments will be retained in memory
* if disabled, bottom stack fragments will be destroyed and recreated
* with their previous bundle when they become at top of stack again
*
* For better performance it's suggested to keep this disabled (false)
* so fragments of bottom stack can be destroyed and release memory resources
*
*/
retainNonActiveFragmentsEnabled = false
}.build()
labyrinth.navigate(About1())
}
override fun onBackPressed() {
if (labyrinth.shouldCallSuperOnBackPressed())
super.onBackPressed()
}
}