Fragment replace() operating differently to remove() + add() - android

According to the Android docs:
replace() is essentially the same as calling remove(Fragment) for all
currently added fragments that were added with the same
containerViewId and then add(int, Fragment, String) with the same
arguments given here.
However my code says otherwise.
My app has 1 activity and multiple fragments. It also has a BottomNavigationView with 3 tabs (Options, Game, Leaderboards).
When the MainActivity is initialised, 3 fragments are added to the container FrameLayout MainActivity. Immediately after being added, 2 fragments are hidden which leaves 1 shown on the screen (the opening fragment).
MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
val transaction = supportFragmentManager.beginTransaction()
transaction.add(R.id.fragment_layout, firstFragment, "Opening")
transaction.add(R.id.fragment_layout, OptionsFragment(), "Options")
transaction.add(R.id.fragment_layout,LeaderboardsFragment(), "Leaderboards")
transaction.commitNow()
val transaction2 = supportFragmentManager.beginTransaction()
val options: androidx.fragment.app.Fragment = supportFragmentManager.findFragmentByTag("Options")!!
val leaderboards: androidx.fragment.app.Fragment = supportFragmentManager.findFragmentByTag("Leaderboards")!!
transaction2.hide(options)
transaction2.hide(leaderboards)
transaction2.commitNow()
}
From here on out, each fragment that isn't OptionsFragment() or LeaderboardsFragment() is swapped out via replace().
OpeningFragment.kt
transaction?.replace(R.id.fragment_layout, playerDetailsFragment, "Add Player")
transaction?.commit()
gameString = "Add Player"
OptionsFragment() and LeaderboardsFragment() are put into view by being selected from the bottom nav, which then hides every other fragment except for the one selected (e.g if Options is selected from bottom nav, then every fragment is hidden except for OptionsFragment()).
MainActivity.kt
override fun onNavigationItemSelected(item: MenuItem): Boolean {
val fragmentTags = arrayListOf("Opening", "Leaderboards", "Options", "Add Player", "Question", "Location", "Game Over")
val transaction = supportFragmentManager.beginTransaction()
val selectedFragmentTag = when (item.itemId) {
R.id.action_leaderboards -> "Leaderboards"
R.id.action_options -> "Options"
else -> {
gameString
}
}
// Selected Fragment
val selectedFragment: androidx.fragment.app.Fragment? = supportFragmentManager.findFragmentByTag(selectedFragmentTag)
for (fragment_tag in fragmentTags){
// Hide every Fragment that has been added and isn't the selected Fragment
val fragment = supportFragmentManager.findFragmentByTag(fragment_tag)
if (fragment != null && fragment != selectedFragment) {
transaction.hide(fragment)
}
}
transaction.show(selectedFragment!!)
transaction.commit()
return true
}
This method doesn't work. However - if I use remove() and add() instead of replace(), then it works fine:
OpeningFragment.kt
val opening = fragmentManager?.findFragmentByTag("Opening")
transaction?.remove(opening!!)
transaction?.add(R.id.fragment_layout, playerDetailsFragment, "Add Player")
transaction?.commit()
gameString = "Add Player"
Any idea why this is?

For me replace works exactly as docs says. It removes all fragments in given container id and then adds new fragment with specified tag.
Your optional code removes only one fragment that you desire, not all of them, therefore it's not identical to replace function.

Related

Issue with backstack and bottomnav in kotlin

I have a bottom nav with 4 fragments Home, Following, Notification, and Profile, there is no issue with the bottom navigation on backstack , but now for eg from profile fragment I jumped to a fragment called edit_profile which is not a part of the bottom nav and when press back I want that it should go back to the profile fragment but the backstack is taking me from edit_profile to directly home fragment
here is a recording link
I recently change my project from java to kotlin and I'm a beginner in kotlin
i really like the navigation of Pinterest and Instagram
Note:- All this code is automatically changed to kotlin (with some
changes done manually ) , this issue was also with java and not after migrating to kotlin , Also if you want more reference of the code
please tell me i will update the question
Code
MainActivity.kt // Bottom Nav
class MainActivity : AppCompatActivity() {
var bottomNavigationView: BottomNavigationView? = null
var integerDeque: Deque<Int> = ArrayDeque(3)
var flag = true
#RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
setContentView(R.layout.activity_main)
val window = this.window
window.statusBarColor = this.resources.getColor(R.color.black)
bottomNavigationView = findViewById(R.id.bottom_navigation_view)
integerDeque.push(R.id.nav_home)
loadFragments(Home_Fragment())
bottomNavigationView!!.selectedItemId = R.id.nav_home
bottomNavigationView!!.setOnNavigationItemSelectedListener(
BottomNavigationView.OnNavigationItemSelectedListener { item: MenuItem ->
val id = item.itemId
if (integerDeque.contains(id)) {
if (id == R.id.nav_home) {
integerDeque.size
if (flag) {
integerDeque.addFirst(R.id.nav_home)
flag = false
}
}
integerDeque.remove(id)
}
integerDeque.push(id)
loadFragments(getFragment(item.itemId))
false
}
)
}
#SuppressLint("NonConstantResourceId")
private fun getFragment(itemId: Int): Fragment {
when (itemId) {
R.id.nav_home -> {
bottomNavigationView!!.menu.getItem(0).isChecked = true
return Home_Fragment()
}
R.id.nav_following -> {
bottomNavigationView!!.menu.getItem(1).isChecked = true
return Following_Fragment()
}
R.id.nav_notification -> {
bottomNavigationView!!.menu.getItem(2).isChecked = true
return Notification_Fragment()
}
R.id.nav_profile -> {
bottomNavigationView!!.menu.getItem(3).isChecked = true
return Profile_Fragment()
}
}
bottomNavigationView!!.menu.getItem(0).isChecked = true
return Home_Fragment()
}
private fun loadFragments(fragment: Fragment?) {
if (fragment != null) {
supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, fragment, fragment.javaClass.simpleName)
.commit()
}
}
override fun onBackPressed() {
integerDeque.pop()
if (!integerDeque.isEmpty()) {
loadFragments(getFragment(integerDeque.peek()))
} else {
finish()
}
}
Edit_Profile.kt // from this fragment i want to go back to the last fragment which should be the profile fragment
class Edit_Profile : Fragment() {
private var profilePhoto: CircleImageView? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_edit_profile, container, false)
profilePhoto = view.findViewById(R.id.circleImageView)
initImageLoader()
setProfileImage()
val imageView = view.findViewById<ImageView>(R.id.backArrow)
imageView.setOnClickListener {
val newCase: Fragment = Profile_Fragment()
assert(fragmentManager != null)
val transaction = requireFragmentManager().beginTransaction()
transaction.replace(R.id.fragment_container, newCase)
transaction.addToBackStack(Profile_Fragment.toString())
transaction.commit()
}
return view
}
Edit
added a part of the transaction from Profile Fragment to Edit Profile
ProfileFragment.kt
editProfileButton!!.setOnClickListener(View.OnClickListener { v: View? ->
val edit_profile: Fragment = Edit_Profile()
requireActivity().getSupportFragmentManager()
.beginTransaction()
.add(R.id.fragment_container, edit_profile,"TAG")
.addToBackStack("TAG")
.commit()
})
Now you are managing the back stack through the integerDeque array.
When you go to a new BottomNavigationView fragment; you added its id to the array if it doesn't already exist.
When you pop up the back stack; the fragment at the top is kicked off the array.
But since you pushed all those ids in the bottomNavigationView.setOnItemSelectedListener callback; then the integerDeque array only contains BottomNavigationView fragments ids.
And as the Edit_Profile fragment is not a part of BottomNavigationView fragments, then it won't be added/popped off the queue. Instead when you try to popup the back stack whenever the Edit_Profile fragment is shown; the normal behavior you manage in the onBackPressed() continues and the Profile_Fragment id will pop up from the queue making you return to the preceding fragment (Home_Fragment) in your mentioned example.
A little fix to this is to consider adding an id into the queue when you transact to Edit_Profile fragment so that this id is popped off the queue resulting in back to Profile_Fragment fragment.
You can do that with the fragment's id in order to make sure it's unique:
editProfileButton!!.setOnClickListener(View.OnClickListener { v: View? ->
val edit_profile: Fragment = Edit_Profile()
requireActivity().getSupportFragmentManager()
.beginTransaction()
.add(R.id.fragment_container, edit_profile,"TAG")
.addToBackStack("TAG")
.commit()
(requireActivity() as MainActivity).integerDeque.push(id) // <<<< pushing id to the queue
})
This should fix your problem.
Side tips:
Use setOnItemSelectedListener instead of setOnNavigationItemSelectedListener on the BNV as the latter is deprecated.
Return true instead of false from setOnItemSelectedListener callback as this should consume the event and mark the BNV as selected.
In Edit_Profile transaction replace the fragment instead of adding it with add as already the container is consumed; and this would make you avoid overlapping fragments in the container.
In onBackPressed(); you'd replace loadFragments(..) with bottomNavigationView.selectedItemId = integerDeque.peek(); this could be lighter to reuse the same fragment instead of redoing the transaction.
Usually I follow this pattern
Where I add HomeF in main container which includes all bottom nav tab, and all bottom nav tab will open in home container, and those fragment which are not part of bottom nav will open in main container. I generally add(not replace) all the fragments in main container and set add to back stack , so that if user goes from profile (home_container) to something in main container , while backstack we can pop the top fragment and user will be seeing profile.

Navigation's back stack is lost when navigation is nested

I have a navigation which looks like this
Frag1 -> Frag2 -> Frag3
Inside Frag2 there is a NavHostFragment with its own navigation
InnerFrag1 -> InnerFrag2
If I do this
Navigate to Frag2
Navigate to InnerFrag2 inside Frag2
Navigate to Frag3
Go back
then I'll see InnerFrag2 inside Frag2, when I press back normally I would go from InnerFrag2 to InnerFrag1 inside Frag2 but now it's going to Frag1 instead.
Here is my navigation handling inside Frag2
private val backPressedCallback = OnBackPressedCallback {
navHostFragment.navController.navigateUp()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
requireActivity().addOnBackPressedCallback(backPressedCallback)
}
override fun onDestroyView() {
activity?.removeOnBackPressedCallback(backPressedCallback)
super.onDestroyView()
}
private val navHostFragment: NavHostFragment
get() = childFragmentManager.findFragmentById(R.id.innerNavHostFragment) as NavHostFragment
When going back to Frag2 the fragment in the nav host is the correct one, but navigating back moves away from Frag2 because inner nav host's back stack is lost. Can I persist it somehow or fix it some other way?
EDIT: actually when going from Frag3 to Frag2 I see InnerFrag1 inside, the both look alike, that's why going back at this point brings me back to Frag1
EDIT2: I found my problem, I inflate Frag2s navigation from code in onViewCreated like this
val navHostFragment = (frag2NavHostFragment as? NavHostFragment) ?: return
val inflater = navHostFragment.navController.navInflater
val graph = inflater.inflate(navigationId)
navHostFragment.navController.graph = graph
setting it in xml makes it work, I still need to set it from code somehow, Frag2 chooses which navigation to use depending on its arguments
Now my question changes from Navigation's back stack is lost to How to preserve NavHostFragment's state when settings it's graph from code
You can now handle onBackPress on fragments. In your fragment just add this in onViewCreated method.
val navController = Navigation.findNavController(view)
requireActivity().onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
navController.popBackStack(R.id.fragmentWhereYouWantToGo, false)
}
})
I would also give a check to app:popUpTo , app:popUpToInclusive or singleTop XML attributes to the fragments inside your Frag2
After looking into this for a little, original question doesn't make much sense, I'd delete it but it got 2 upvotes ¯\_(ツ)_/¯
I solved my problem by adding a check before inflating graph, so that NavHostFragment's graph is set only if it doesn't already have one.
try {
navHostFragment.navController.graph
} catch (e: IllegalStateException) {
val inflater = navHostFragment.navController.navInflater
val graph = inflater.inflate(navigationId)
navHostFragment.navController.graph = graph
}
NavController.getGraph doesn't return null, instead it throws IllegalStateException, hence the weird check

Fragment is blank on second load

App's navigation happens through a BottomNavigationView.
On the 1st position, I have a fragment which has button to navigate to another fragment.The first time it loads and shows everything. The, I click on 1st position on BotttomNavView again, we going back to parent fragment (bound to 1st position of BottomNavigationView). But from this point, if I click the button and open the fragment, it is always blank.
Here is how my fragment management looks like:
fun navigateTo(
context: Context,
fragment: BaseFragment,
navigatable: Navigatable,
addToBackStack: Boolean
) {
val activity = ContextUtil.getActivityFromContext(context)
val tag = fragment.javaClass.toString()
val fragmentManager = (activity as BaseActivity).supportFragmentManager
val transaction = fragmentManager.beginTransaction()
if (fragmentManager.findFragmentByTag(fragment.javaClass.toString()) == null) {
transaction.add(R.id.container, fragment, tag).hide(fragment)
}
val activeFragment = findVisibleFragment(context)
transaction
.apply {
if (activeFragment != null) {
hide(activeFragment)
}
}
.show(fragment)
.apply {
if (addToBackStack) {
this.addToBackStack(tag)
}
}
.commit()
navigatable.afterScreenTransition(fragment).invoke()
}
I did an experiment and found that onAttach() gets called in my pager fragment, but all the views are null, onCreateView() not called.
So after further experimenting, I did supportFragmentManager.popBackStack() before opening the 1st fragment. Now opening the child fragment is not blank.

Put a fragment in a specific container when you have several copies

I have a pager adapter with 4 copies of the same fragment. Each of them has the same fragment container that has to be filled with another fragment when a specific view is clicked in its fragment. I do that with
getFragmentManager().beginTransaction().replace(R.id.gear_change_container,fragment).commit();
but that always put the fragment in the container of the fragment in the first tab.
How can I refer specifically to the container within the tab the click is made?
Here is an example how I would replace a fragment inside a fragment using Kotlin extension.
fun Fragment.replaceFragment(fragment: Fragment?, frameId: Int, addToBackStack: Boolean = true) {
fragment?.let {
childFragmentManager.beginTransaction()
.apply {
if (addToBackStack) addToBackStack(null)
}
.replace(frameId, fragment)
.commit()
}
}
Hope this will help.

Android: BottomNavigationView first fragment switch is super delayed

Attempting to use a bottom nav view, the standard view provided by android. The first time I click on any fragment, it's a SUPER delayed UI reaction time (about 2 seconds until the ripple, item selection update, and new fragment show)
It's only the first time I switch to any fragment, after that, it behaves as expected.
I found a similar question already on here, but there were zero suggestions or answers. See that post here
Find below the logic I user for switching the fragments.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
bottom_nav_bar.inflateMenu(R.menu.bottom_nav_bar_menu)
bottom_nav_bar.selectedItemId = R.id.nav_summary
bottom_nav_bar.setOnNavigationItemSelectedListener { menuItem ->
when (menuItem.itemId) {
R.id.nav_1-> startFragment1()
R.id.nav_2 -> startFragment2()
else -> startFragment3()
}
true
}
}
fun startFragment1() = replaceFragment(Fragment1(), "TAG1")
fun startFragment2() = replaceFragment(Fragment2(), "TAG2")
fun startFragment3() = replaceFragment(Fragment3(), "TAG3")
private fun replaceFragment(fragment: Fragment, fragmentTag: String) {
fragmentManager.beginTransaction()
.setCustomAnimations(R.animator.fade_in, R.animator.fade_out)
.replace(R.id.fragment_container, fragment, fragmentTag)
.commit()
}
Use
supportfragmentmanager
instead of just normal FragmentManager. It’s smoother. Of course you have to change to imports to v4 Fragment but after that it should work better. I was having all kinds of weird stuff happen till I made that switch.

Categories

Resources