I have a main activity which has a frame and two buttons. Clicking on each button displays a fragment.
App Imageļ¼
Code:
package com.example.activitylifecycle
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Button
import androidx.fragment.app.Fragment
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val firstFragment = FirstFragment()
val secondFragment = SecondFragment()
supportFragmentManager.beginTransaction().apply {
replace(R.id.flFragment, firstFragment)
commit()
}
val btnFragmentOne = findViewById<Button>(R.id.btnFragment1)
val btnFragmentTwo = findViewById<Button>(R.id.btnFragment2)
btnFragmentOne.setOnClickListener {
supportFragmentManager.beginTransaction().apply {
replace(R.id.flFragment, firstFragment)
addToBackStack(null)
commit()
}
}
btnFragmentTwo.setOnClickListener {
supportFragmentManager.beginTransaction().apply {
replace(R.id.flFragment, secondFragment)
addToBackStack(null)
commit()
}
}
}
}
I am using addToBackStack() function to make sure that when someone has pressed the fragment2 button and after that they press back button the fragment1 is shown. It's working perfectly.
But when I press the fragment2 button 5 times. I have to press back button 5 times to get to fragment1.
How can I check if the fragment is already in the stack before adding it again?
There is one way, that before creating some kind of fragment it is better to check if this fragment is active and visible on the screen. If it is active, a new fragment of that class will not be created. That may solve your problem
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val firstFragment = FirstFragment()
val secondFragment = SecondFragment()
//First Fragment class name entered as Tag
supportFragmentManager.beginTransaction().apply {
replace(R.id.flFragment, firstFragment, firstFragment.javaClass.name)
commit()
}
val btnFragmentOne = findViewById<Button>(R.id.btnFragment1)
val btnFragmentTwo = findViewById<Button>(R.id.btnFragment2)
btnFragmentOne.setOnClickListener {
val fragment1 =
supportFragmentManager.findFragmentByTag(FirstFragment::class.java.name) as FirstFragment?
// Checking is First Fragment is not active
if (!(fragment1 != null && fragment1.isVisible)) {
supportFragmentManager.beginTransaction().apply {
//First Fragment class name entered as Tag
replace(R.id.flFragment, firstFragment, firstFragment.javaClass.name)
addToBackStack(null)
commit()
}
}
}
btnFragmentTwo.setOnClickListener {
val fragment2 =
supportFragmentManager.findFragmentByTag(SecondFragment::class.java.name) as SecondFragment?
// Checking is Second Fragment is not active
if (!(fragment2 != null && fragment2.isVisible)) {
supportFragmentManager.beginTransaction().apply {
//Second Fragment class name entered as Tag
replace(R.id.flFragment, secondFragment, secondFragment.javaClass.name)
addToBackStack(null)
commit()
}
}
}
}
Hope that will help to you
When you add it to the stack, it gets added everytime you press the button.
So if you press fragment2button 5 times, it gets added 5 times to the stack, and you need to press backbutton 5 times to remove all 5 of those from the stack and get back to fragment1
You need to edit the code such that if the fragment already exists, then it shouldn't be added to the stack again
Related
i have 1 Activity with 3 Fragments. (A, B and C). So,
Activity -> FragmentContainerView with fragment A
<androidx.fragment.app.FragmentContainerView
android:id="#+id/host_fragment"
android:name="cl.gersard.shoppingtracking.ui.product.list.ListProductsFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:tag="ListProductsFragment" />
Fragment A has a button to go to Fragment B
Fragment A -> Fragment B (with addToBackStack)
Then, i go to from Fragment B to Fragment C
Fragment B -> Fragment C (without addToBackStack)
i need when i save a item in Fragment C, come back to Fragment A, so i dont use addToBackStack.
The problem is when in Fragment C i use
requireActivity().supportFragmentManager.popBackStack()
or
requireActivity().onBackPressed()
the Fragment A appears but the method OnViewCreated in Fragment C is called so execute a validations that i have in that Fragment C.
I need from Fragment C come back to Fragment A without calling OnViewCreated of Fragment C
Code of interest
MainActivity
fun changeFragment(fragment: Fragment, addToBackStack: Boolean) {
val transaction = supportFragmentManager.beginTransaction()
.replace(R.id.host_fragment, fragment,fragment::class.java.simpleName)
if (addToBackStack) transaction.addToBackStack(null)
transaction.commit()
}
Fragment A (ListProductsFragment)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
observeLoading()
observeProducts()
viewModel.fetchProducts()
viewBinding.btnEmptyProducts.setOnClickListener { viewModel.fetchProducts() }
viewBinding.fabAddPurchase.setOnClickListener { addPurchase() }
}
private fun addPurchase() {
(requireActivity() as MainActivity).changeFragment(ScanFragment.newInstance(),true)
}
Fragment B (ScanFragment)
override fun barcodeDetected(barcode: String) {
if (processingBarcode.compareAndSet(false, true)) {
(requireActivity() as MainActivity).changeFragment(PurchaseFragment.newInstance(barcode), false)
}
}
Fragment C (PurchaseFragment)
private fun observePurchaseState() {
viewModel.purchasesSaveState.observe(viewLifecycleOwner, { purchaseState ->
when (purchaseState) {
is PurchaseSaveState.Error -> TODO()
is PurchaseSaveState.Loading -> manageProgress(purchaseState.isLoading)
PurchaseSaveState.Success -> {
Toast.makeText(requireActivity(), getString(R.string.purchase_saved_successfully), Toast.LENGTH_SHORT).show()
requireActivity().supportFragmentManager.popBackStack()
}
}
})
}
The full code is here https://github.com/gersard/PurchaseTracking
OK, I think I see your issue. You are conditionally adding things to the backstack which put the fragment manager in a weird state.
Issue
You start on Main, add the Scanner to the back stack, but not the Product. So when you press back, you're popping the Scanner off the stack but the Product stays around in the FragmentManager. This is why get a new instance each and every time you scan and go back. Why this is happening is not clear to me - seems like maybe an Android bug? You are replacing fragments so it's odd that extra instances are building up.
One Solution
Change your changeFragment implementation to conditionally pop the stack instead of conditionally adding things to it.
fun changeFragment(fragment: Fragment, popStack: Boolean) {
if (keepStack) supportFragmentManager.popBackStack()
val transaction = supportFragmentManager.beginTransaction()
.replace(R.id.host_fragment, fragment,fragment::class.java.simpleName)
transaction.addToBackStack(null) // Always add the new fragment so "back" works
transaction.commit()
}
Then invert your current logic that calls changeFragment:
private fun addPurchase() {
// Pass false to not pop the main activity
(requireActivity() as MainActivity)
.changeFragment(ScanFragment.newInstance(), false)
}
And ...
override fun barcodeDetected(barcode: String) {
if (processingBarcode.compareAndSet(false, true)) {
// Pass true to pop the barcode that's currently there
(requireActivity() as MainActivity)
.changeFragment(PurchaseFragment.newInstance(barcode), true)
}
}
I have a Home fragment with multiple buttons and when I click the Contact button, another fragment is opened. Inside this fragment I have two child fragments and two buttons, and I can switch between those child fragments using those buttons. The problem is when I press the Back Button, it switches back between child fragments and only after that it goes back to Home fragment, but I want to directly go back to Home Fragment.
This is how I'm opening the child fragments:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val contactsListFragment = ContactsListFragment()
val groupsListFragment = GroupsListFragment()
activity?.title = getString(R.string.contacts_and_groups)
openChildFragment(contactsListFragment)
binding.contactsButton.setOnClickListener {
openChildFragment(contactsListFragment)
}
binding.groupsButton.setOnClickListener {
openChildFragment(groupsListFragment)
}
}
private fun openChildFragment(fragment: Fragment) {
val childFragmentManager = childFragmentManager
val transaction: FragmentTransaction = childFragmentManager.beginTransaction()
transaction.replace(binding.contactsGroupsFl.id, fragment)
transaction.addToBackStack(null)
transaction.commit()
}
If anyone can help me with this issue would be great. Thanks!
I think you should add a listener for the back button in both of your fragments so that you can clear all the backstack when it's pressed. Something like this :
requireActivity().onBackPressedDispatcher.addCallback(
viewLifecycleOwner,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
for (i: Int in 1..parentFragmentManager.backStackEntryCount) {
parentFragmentManager.popBackStack()
}
}
})
In my main activity I have bottom navigation bar. Each button opens a different fragment.My code in main activity looks like this
class MainActivity : AppCompatActivity() {
private val homeFragment = HomeFragment()
private val calendarFragment = CalendarFragment()
private val addFragment = AddFragment()
private val plannerFragment = PlannerFragment()
private val profileFragment = ProfileFragment()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
replaceFragment(homeFragment)
nav_view.setOnNavigationItemSelectedListener{
when(it.itemId){
R.id.homeButton -> replaceFragment(homeFragment)
R.id.calendarButton -> replaceFragment(calendarFragment)
R.id.addButton -> replaceFragment(addFragment)
R.id.plannerButton -> replaceFragment(plannerFragment)
R.id.profileButton -> replaceFragment(profileFragment)
}
true
}
}
private fun replaceFragment(fragment: Fragment){
if (fragment!=null){
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragmentContainer, fragment)
transaction.commit()
}
}
}
In profileFragment I have a button that opens a new activity called EditProfile. In that activity I have a button called goBackToProfileButton
I want to set a listener that will go back to mainactivity, but I want profileFragment to be open not the default fragment which is homeFragment.
goBackToProfileButton.setOnClickListener {
val intent = Intent(this,MainActivity::class.java)
startActivity(intent)
}
For now my code looks like this
You need to add some bundle data to your intent with information which fragment should be started
https://stackoverflow.com/a/819427/11538132
And then when you receive this bundle data then you can manually set your ** profileFragment**
You can add TAG while fragment transaction.
private fun replaceFragment(fragment: Fragment){
if (fragment!=null){
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragmentContainer, fragment, "FragmentTag")
transaction.commit()
}
}
While calling the first time to replace a fragment, find your fragment by tag.
//replaceFragment(homeFragment)
val fragment = supportFragmentManager.findFragmentByTag("FragmentTag")
if(fragment !=null){
replaceFragment(fragment)
} else{
replaceFragment(homeFragment)
}
if findFragmentByTag return null means no fragment was added. So add Home Fragment. If not null , it will update with last fragment added.
You could use fragment.startActivityForResult() and call the ProfileFragment from the MainActivity once it receives the onActivityResult() callback.
I have an activity with two fragments added to it using :
fun addFragment(fragment: Fragment){
private val fragmentManager = supportFragmentManager
val fragmentTag = fragment::class.simpleName.toString()
val ft = fragmentManager.beginTransaction()
ft.add(R.id.fragments_container, fragment,fragmentTag)
ft.addToBackStack(fragmentTag)
ft.commit()
}
I add two fragments A and B to activity with this order:
A ---> B
when i press back button on phone it return from B to A as expected
but the problem is that when i call activity's onBackPressed method when clicking on a view for example:
imgBack.setOnClickListener {
onBackPressed()
}
it does not work like when i press back button on the phone
it returns to fragment A but not showing fragment A views as expected.
onBackPressed:
override fun onBackPressed() {
if (fragmentManager.backStackEntryCount > 1) {
fragmentManager.popBackStack()
} else {
finish()
}
}
if you imgBack are in fragmentB call
imgBack.setOnClickListener {
activity?.onBackPressed()
}
There is a huge bug I am trying to figure out in my latest project using Kotlin. So how the app works is that it has a Bottom Navigation Activity, and on click on the icons on the Bottom menu, it replaces the container with the respected fragment. On onCreate of the activity, I instantiate all the different fragments with a function defined in a companionObject within the fragments, which returns itself (kind of like a static factory method in java):
override fun onCreate(savedInstanceState: Bundle?) {
{...}
navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener)
fragment1 = Fragment1.newInstance()
fragment2 = Fragment2.newInstance()
}
Then I have this switch to replace the container to the respected fragment on click:
private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.f1 -> {
replaceFragment(fragment1)
return#OnNavigationItemSelectedListener true
}
R.id.f2 -> {
replaceFragment(fragment2)
return#OnNavigationItemSelectedListener true
}
}
false
}
{...}
fun replaceFragment(destFragment: Fragment) {
supportFragmentManager
.beginTransaction()
.replace(R.id.container, destFragment)
.addToBackStack(destFragment.toString())
.commit()
}
The fragments are communicating. If you where to click on both fragments, and thereby cause their lifecycles to execute, the code works fine. However, if you do changes in frag1, invoking a communication between the fragments, without having clicked on frag2 in advance, the app crashes. The reason is this line in frag2:
fun saveList(){
val sharedpref : SharedPreferences = context!!.getSharedPreferences("sharedPrefs", MODE_PRIVATE)
{...}
}
Turns out that the context is null, if the user has never invoked the frag2 lifecycle by clicking it. Any ideas to how to fix this problem?
Use Activity context for these case
fun saveList() {
activity?.let {
val sharedpref: SharedPreferences =
it.getSharedPreferences("sharedPrefs", MODE_PRIVATE)
{ }
}
}