How to handle backstack between fragments and viewpagers? - android

I am using a ViewPager having 2 pages. First page of that ViewPager is MainFragment. MainFragment has ViewPager as bottomNavigationView. Each page has a FragmentContainerView and By default they contains HomeFragment, CommunityFragment and NotificationFragment respectively.
This is the Source Code and this is the APK of the project. So you can test it and improve it easily.
Now if i am in HomeFragment and I click on a profile button so It transact to ProfileFragment and from there setting and so on. And on clicking on back button it get back perfectly one-by-one. But it does not happens same with other FragmentContainerView. Even they get back directly to the parent fragment. Overall i am unable to handle the backstack between different ViewPagers and fragments.
To avoid the confusion of FragmentContainers i transact it like this
val containerId = (view?.parent as ViewGroup).id
activity?.supportFragmentManager?.beginTransaction()?.add(containerId, profileFragment)?.addToBackStack(null)?.commit()
Now the handling of BackPressed() in MainActivity is here
if (view_pager_main_activity?.currentItem == 0)
{
if (view_pager_main_fragment?.currentItem == 0)
{
val recyclerView = findViewById<RecyclerView>(R.id.recycler_view_home)
val appbarHome = findViewById<AppBarLayout>(R.id.appbar_home)
val layoutManager = recyclerView?.layoutManager as LinearLayoutManager
when {
layoutManager.findFirstCompletelyVisibleItemPosition() == 0 -> {
super.onBackPressed()
}
supportFragmentManager.backStackEntryCount != 0 -> {
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
}
else -> {
layoutManager.scrollToPositionWithOffset(0, 0)
appbarHome.setExpanded(true)
//recyclerView.smoothScrollToPosition(0)
}
}
}
else
{
// Otherwise, select the previous step.
view_pager_main_fragment?.setCurrentItem(view_pager_main_fragment.currentItem - 1, false)
}
}
else
{
// Otherwise, select the previous step.
view_pager_main_activity?.currentItem = view_pager_main_activity.currentItem - 1
}

I checked your code and the problem was the usage of
supportFragmentManager.popBackStack(null,FragmentManager.POP_BACK_STACK_INCLUSIVE)
If you read the description of the flag POP_BACK_STACK_INCLUSIVE it says:
If set, and the name or ID of a back stack entry has been supplied,
then all matching entries will be consumed until one that doesn't
match is found or the bottom of the stack is reached. Otherwise, all
entries up to but not including that entry will be removed.
So it was removing the multiple entries at once.
The reason it worked for Home screen was that it never reached that case. It always went in the first case of:
layoutManager.findFirstCompletelyVisibleItemPosition() == 0 -> {
super.onBackPressed()
}
And that's what you exactly need for your case just call super.onBackPressed() when you want to pop only one fragment at once. So your final code becomes like this:
In your MainActivity's onBackPressed()
override fun onBackPressed() {
if (view_pager_main_activity?.currentItem == 0) {
if (view_pager_main_fragment?.currentItem == 0) {
val recyclerView = findViewById<RecyclerView>(R.id.listRecyclerView)
val appbarHome = findViewById<AppBarLayout>(R.id.appbar_home)
val layoutManager = recyclerView?.layoutManager as LinearLayoutManager
when {
layoutManager.findFirstCompletelyVisibleItemPosition() == 0 -> {
super.onBackPressed()
}
//This is never reached. It can be removed
supportFragmentManager.backStackEntryCount != 0 -> {
supportFragmentManager.popBackStack(
null,
FragmentManager.POP_BACK_STACK_INCLUSIVE
)
}
else -> {
layoutManager.scrollToPositionWithOffset(0, 0)
appbarHome.setExpanded(true)
//recyclerView.smoothScrollToPosition(0)
}
}
} else if (supportFragmentManager.backStackEntryCount != 0) {
super.onBackPressed()
} else {
// Otherwise, select the previous step.
view_pager_main_fragment?.setCurrentItem(
view_pager_main_fragment.currentItem - 1,
false
)
}
} else if (supportFragmentManager.backStackEntryCount != 0) {
super.onBackPressed()
} else {
// Otherwise, select the previous step.
view_pager_main_activity?.currentItem = view_pager_main_activity.currentItem - 1
}
}
Edit 1: From the comments
I want to clear the backstack when bottom nav selection changes.
If you want to clear the selection when you change the bottom navigation selection changes then you've to set a listener and clear the back stack in your MainFragment as following:
view?.bottomNavView?.setOnNavigationItemSelectedListener { item ->
//check if item is being re-selected then just return and don't do anything
if (bottomNavView.selectedItemId == item.itemId){
return#setOnNavigationItemSelectedListener false
}
//if the bottom nav item selection changes then clear the back stack of previous tab
parentFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
when (item.itemId) {
R.id.homeFragment -> viewPager2.setCurrentItem(0, false)
R.id.searchFragment -> viewPager2.setCurrentItem(1, false)
R.id.notificationsFragment -> viewPager2.setCurrentItem(2, false)
}
true
}
And the rest can remain same. Try this out and do let me know.

Related

Is it possible to set first coming page of viewPager2 with Intent in Kotlin?

I use ViewPager2 in Kotlin.
And according to the data value of DB, I change viewPager's page index like below:
in DiaryActivity:
diaryDB
.document("${userId}_${writeTime}")
.get()
.addOnCompleteListener { task ->
if (task.isSuccessful) {
val document = task.result
if(document != null) {
if (document.exists()) {
viewPager.currentItem = 1
} else {
viewPager.currentItem = 0
}
}
} else {
viewPager.currentItem = 0
}
}
But When I call DiaryActivity from other Activity by Intent, (If data value needs to go currentItem 1) It goes first to currentItem = 0 and slides to currentItem = 1
I don't want screen shows first 0 and slides to 1, I want screen firstly shows 1.
Is it possible with viewPager2?
This should do the trick
viewPager.setCurrentItem(1, false)
//smoothScroll – true to smoothly scroll to the new item, false to transition immediately

Clear state for fragment when using bottom navigation

We have implemented bottom navigation as described here:
https://developer.android.com/guide/navigation/navigation-ui#bottom_navigation
https://medium.com/androiddevelopers/navigation-multiple-back-stacks-6c67ba41952f
We are using navigation version 2.4.1, which supports multiple backstacks out of the box. This saves fragment state so that in navigating from main fragment A -> B -> C -> B using the bottomnav, state of fragment B is saved and restored upon return. This is as intended and much requested behaviour.
However, for one of the fragments in our bottomnav menu, I would like the possibility to NOT save the state. This is due to some confusing behaviour when navigating using talkback. Is there a way in the navigation framework to set a flag to not save state for a single fragment? Or any other way to programmatically clear savedstate without actually doing so "manually" by resetting the UI elements in fragment onDestroy/onResume or similar?
What I did was just use the same androidx.navigation.ui.NavigationUI.setupWithNavController logic but change the saveState and other logic specific to my use case. You could apply this when navigating to one specific fragment.
this.findViewById<BottomNavigationView>(R.id.bottom_navigation).apply {
setOnItemSelectedListener { item ->
val builder = NavOptions.Builder().setLaunchSingleTop(true)
val destinationId = item.itemId
item.isChecked = true
if (
navController.currentDestination!!.parent!!.findNode(item.itemId)
is ActivityNavigator.Destination
) {
builder.setEnterAnim(R.anim.nav_default_enter_anim)
.setExitAnim(R.anim.nav_default_exit_anim)
.setPopEnterAnim(R.anim.nav_default_pop_enter_anim)
.setPopExitAnim(R.anim.nav_default_pop_exit_anim)
} else {
builder.setEnterAnim(R.animator.nav_default_enter_anim)
.setExitAnim(R.animator.nav_default_exit_anim)
.setPopEnterAnim(R.animator.nav_default_pop_enter_anim)
.setPopExitAnim(R.animator.nav_default_pop_exit_anim)
}
if (item.order and Menu.CATEGORY_SECONDARY == 0) {
builder.setPopUpTo(
navController.graph.findStartDestination().id,
inclusive = false,
saveState = false
)
}
val options = builder.build()
return#setOnItemSelectedListener try {
navController.navigate(destinationId, null, options)
// Return true only if the destination we've navigated to matches the MenuItem
(navController.currentDestination?.id ?: false) == destinationId
} catch (e: IllegalArgumentException) {
false
}
}
// Do nothing on reselect
setOnItemReselectedListener {}
val weakReference = WeakReference(this)
navController.addOnDestinationChangedListener(
object : NavController.OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
// Hide BottomNavigationView from top level fragments
if (topLevelDestinations.any { it == destination.id }) {
this#apply.visibility = View.VISIBLE
} else this#apply.visibility = View.GONE
// Highlight item in BottomNavigationView
val view = weakReference.get()
if (view == null) {
navController.removeOnDestinationChangedListener(this)
return
}
view.menu.forEach { item ->
if (destination.id == item.itemId) {
item.isChecked = true
}
}
}
})
}

Jetpack Compose Navigation - Bottom Nav Multiple Back Stack - View Model Scoping Issue

So I have two tabs, Tab A and Tab B. Each tab has its own back stack. I implemented the multiple back stack navigation using code in this google docs
val navController = rememberNavController()
Scaffold(
bottomBar = {
BottomNavigation {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
items.forEach { screen ->
BottomNavigationItem(
icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
label = { Text(stringResource(screen.resourceId)) },
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
onClick = {
navController.navigate(screen.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}
)
}
}
}
) {
NavHost(navController, startDestination = A1.route) {
composable(A1.route) {
val viewModelA1 = hiltViewModel()
A1(viewModelA1)
}
composable(A2.route) {
val viewModelA2 = hiltViewModel()
A2(viewModelA2)
}
composable(A3.route) {
val viewModelA3 = hiltViewModel()
A3(viewModelA3)
}
}
}
Tab A has 3 screens (Screen A1 -> Screen A2 -> Screen A3). I use the hiltViewModel() function to instantiate the view model and I invoked it inside the composable() block for each screen
The issue is when I'm navigating from A1 to A2 to A3 and then when I change tab to Tab B, the view model for Screen A2 seems like it's being disposed (onCleared is called). So when I go back to Tab A displaying Screen A3 then hit back to Screen A2, the view model for A2 is instantiated again (init block is called again). What I wanted to achieve is to retain the view model for A2 for this flow until I back out of A2.
Is this even possible?
This seems like a bug when you click on the next navigation item too fast, while the current view appear transition is not yet finished. This is a known issue, please star it to bring more attention.
Meanwhile you can wait current screen transition to finish before navigating to the next one. To do so you can check visibleEntries variable and navigate only after it contains only single item.
Also I think that current documentation provide not the best example of bottom navigation, because if you're not on a start destination screen, pressing back button will bring you back to the start destination, when I expect the view to be dismissed. So I've changed how you navigate too, if you're fine with the documentation behaviour, you can replace content of fun navigate() with your own.
val navController = rememberNavController()
var waitEndAnimationJob by remember { mutableStateOf<Job?>(null)}
Scaffold(
bottomBar = {
BottomNavigation {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
val scope = rememberCoroutineScope()
items.forEach { screen ->
fun navigate() {
navController.navigate(screen.route) {
val navigationRoutes = items
.map(Screen::route)
val firstBottomBarDestination = navController.backQueue
.firstOrNull { navigationRoutes.contains(it.destination.route) }
?.destination
// remove all navigation items from the stack
// so only the currently selected screen remains in the stack
if (firstBottomBarDestination != null) {
popUpTo(firstBottomBarDestination.id) {
inclusive = true
saveState = true
}
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}
BottomNavigationItem(
icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
label = { Text(stringResource(screen.resourceId)) },
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
onClick = {
// if we're already waiting for an other screen to start appearing
// we need to cancel that job
waitEndAnimationJob?.cancel()
if (navController.visibleEntries.value.count() > 1) {
// if navController.visibleEntries has more than one item
// we need to wait animation to finish before starting next navigation
waitEndAnimationJob = scope.launch {
navController.visibleEntries
.collect { visibleEntries ->
if (visibleEntries.count() == 1) {
navigate()
waitEndAnimationJob = null
cancel()
}
}
}
} else {
// otherwise we can navigate instantly
navigate()
}
}
)
}
}
}
) { innerPadding ->
// ...
}
Found the root cause of this. I was using these dependencies and they don't seem to go together.
androidx.hilt:hilt-navigation-compose:1.0.0-alpha03
androidx.navigation:navigation-compose:2.4.0-alpha10"
I removed the navigation:navigation-compose dependency and it seemed to work fine now.

How to prevent recreation of Fragments while navigating back and forth?

I have set of Fragments navigates inside activity. While I called findFragmentByTag() the fragments onCreateView() and onViewCreated() are called again and the data is reset to normal. how to prevent the recreation of fragment?
You can have look on the code of the advanced navigation in android samples
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()
.attach(selectedFragment)
.setPrimaryNavigationFragment(selectedFragment)
.apply {
// Detach all other Fragments
graphIdToTagMap.forEach { _, fragmentTagIter ->
if (fragmentTagIter != newlySelectedItemTag) {
detach(fragmentManager.findFragmentByTag(firstFragmentTag)!!)
}
}
}
.addToBackStack(firstFragmentTag)
.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)
.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
}
this example code you can find the full code here
https://github.com/android/architecture-components-samples/tree/master/NavigationAdvancedSample

How to remove duplicate entries from FragmentManager?

I have a simple activity with a BottomNavigationView. I'm using fragments to implement the contents of the activity for the different pages.
When the user presses the back button, it's supposed to go back to the previously looked at page. The problem is, when you repeatedly switch back and forth between the pages (fragments), this entire history is recorded. Take this example:
A -> B -> A -> B -> C -> A -> C
Pressing the back button would result in the reverse, but instead I want this behaviour (I noticed it in the Instagram app):
C -> A -> B -> Exit App
So every fragment should only have one entry in the backstack. How do I do this? I do I remove the previous transactions for a fragment from the stack?
Is this at all possible using a FragmentManager? Or do I have to implement my own?
My Activity with the BottomNavigationView:
class ActivityOverview : AppCompatActivity() {
// Listener for BottomNavigationView
private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.navigation_home -> {
// "Home" menu item pressed
setActiveFragment(resources.getString(R.string.tag_fragment_home))
return#OnNavigationItemSelectedListener true
}
R.id.navigation_dashboard -> {
// "Dashboard" menu item pressed
return#OnNavigationItemSelectedListener true
}
R.id.navigation_settings -> {
// "Settings" menu item pressed
setActiveFragment(resources.getString(R.string.tag_fragment_settings))
return#OnNavigationItemSelectedListener true
}
}
false
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_overview)
navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener)
navigation.menu.findItem(R.id.navigation_home).setChecked(true)
// Set initial fragment
setActiveFragment(resources.getString(R.string.tag_fragment_home))
}
override fun onBackPressed() {
// > 1 so initial fragment addition isn't removed from stack
if (fragmentManager.backStackEntryCount > 1) {
fragmentManager.popBackStack()
} else {
finish()
}
}
// Update displayed fragment
fun setActiveFragment(tag: String) {
val fragment = if (fragmentManager.findFragmentByTag(tag) != null) {
// Fragment is already initialized
if (fragmentManager.findFragmentByTag(tag).isVisible) {
// Fragment is visible already, don't add another transaction
null
} else {
// Fragment is not visible, add transaction
fragmentManager.findFragmentByTag(tag)
}
} else {
// Fragment is not initialized yet
when (tag) {
resources.getString(R.string.tag_fragment_home) -> FragmentHome()
resources.getString(R.string.tag_fragment_settings) -> FragmentSettings()
else -> null
}
}
if (fragment != null) {
val transaction = fragmentManager.beginTransaction()
transaction.replace(R.id.container_fragment, fragment, tag)
transaction.addToBackStack(null)
transaction.commit()
}
}
}
At this point I'm pretty sure it doesn't work with FragmentManager, so I created a class to implement a stack that doesn't allow duplicates:
class NoDuplicateStack<T> {
val stack: MutableList<T> = mutableListOf()
val size: Int
get() = stack.size
// Push element onto the stack
fun push(p: T) {
val index = stack.indexOf(p)
if (index != -1) {
stack.removeAt(index)
}
stack.add(p)
}
// Pop upper element of stack
fun pop(): T? {
if (size > 0) {
return stack.removeAt(stack.size - 1)
} else {
return null
}
}
// Look at upper element of stack, don't pop it
fun peek(): T? {
if (size > 0) {
return stack[stack.size - 1]
} else {
return null
}
}
}
I then integrated this class into my activity:
class ActivityOverview : AppCompatActivity() {
val fragmentsStack = NoDuplicateStack<String>()
val fragmentHome = FragmentHome()
val fragmentSettings = FragmentSettings()
val fragmentHistory = FragmentHistory()
// Listener for BottomNavigationView
private val mOnNavigationItemSelectedListener = ...
override fun onCreate(savedInstanceState: Bundle?) {
...
}
override fun onBackPressed() {
if (fragmentsStack.size > 1) {
// Remove current fragment from stack
fragmentsStack.pop()
// Get previous fragment from stack and set it again
val newTag = fragmentsStack.pop()
if (newTag != null) {
setActiveFragment(newTag)
}
} else {
finish()
}
}
// Update displayed fragment
fun setActiveFragment(tag: String) {
val fragment = when (tag) {
resources.getString(R.string.tag_fragment_home) -> fragmentHome
resources.getString(R.string.tag_fragment_settings) -> fragmentSettings
resources.getString(R.string.tag_fragment_history) -> fragmentHistory
else -> null
}
if (fragment != null && !fragment.isVisible) {
fragmentManager.beginTransaction()
.replace(R.id.container_fragment, fragment, tag)
.commit()
fragmentsStack.push(tag)
}
}
}
I also faced the same problem, I did this which uses the system stack
val totalFragments = supportFragmentManager.backStackEntryCount
if (totalFragments != 0) {
val removed = supportFragmentManager.getBackStackEntryAt(totalFragments - 1)
poppedFragments.add(removed.name!!)
for (idx in totalFragments - 1 downTo 0) {
val fragment = supportFragmentManager.getBackStackEntryAt(idx)
if (!poppedFragments.contains(fragment.name)) {
supportFragmentManager.popBackStack(fragment.name, 0)
return
}
}
finish()
return
}
super.onBackPressed()
and then added this while launching the fragment
if (poppedFragments.contains(tag)) {
poppedFragments.remove(tag)
}

Categories

Resources