How to handle back button when using Nested Fragments in Android - android

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()
}
}
})

Related

LiveData stops observing when fragment is destroyed

I have couple of fragments (lets say A,B and C) replacing each other. In fragment A, there is a recyclerView which should be updated as an observer receives changes in a liveData. But the observer does not receive updates when I move to fragment B because fragment A is already destroyed.
I have changed the the lifeCycle owner from viewLifeCycleOwner to this (fragment) but still the same result.
Here is some snippet of my code:
class MainActivity: AppCompatActivity() {
val fragments = listOf(fragmentA(), fragmentB(), fragmentC())
private fun moveToFragment(index: Int) {
if (fragments.isNotEmpty()) {
viewModel.selectedPage = index
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragment_container, fragments[index])
transaction.commit()
}
}
}
class FragmentA(): Fragment{
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
fragmentAViewModel.listOfThings.observe(viewLifecycleOwner) { things ->
things?.let {
when (things.status) {
Status.SUCCESS -> {
thingsAdapter.submitList(things.data)
}
else -> {
thingsAdapter.submitList(emptyList())
}
}
}
}
}
I know that "replace" destroys the fragments but I also tried using HIDE/SHOW fragments but this pattern overlays fragments on top of each other and I wasn't able to get it fixed.
My question is what the best practice is here to make sure that the observer/recyclerView is already updated before going to the fragment. Otherwise the recyclerView will have old values and they change in few milliseconds after I go the fragmentA. The most obvious situation is when the recyclerView is supposed to be empty but when I go to fragmentA items are still there for a moment before they disappear.

Fragment popped call OnViewCreated after PopBackStack

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)
}
}

Access parent activity views from fragment in tablayout?

I am using TabLayout with ViewPager. I have 3 fragments and 3 tabs. I want to access the FloatingActionButton in activitymain.xml file.
In every fragment I write a code like this:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val fab :View= (requireActivity() as MainActivity).findViewById(R.id.fab_main)
fab.setOnClickListener {
Toast.makeText(context,"Fragment1",Toast.LENGTH_SHORT).show()
}
}
But when I click the FloatingActionButton in every fragment it gives me the same Toast message which is related to the last fragment. I want to perform different actions in every fragment when I clicked the button.
You could use FragmentStatePagerAdapter for tab function
class TabAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
..........
}
It will be make Fragment call onResume when the fragment is active.
So you can like this in Fragment
override fun onResume() {
super.onResume()
val fab :View= (requireActivity() as MainActivity).findViewById(R.id.fab_main)
fab.setOnClickListener {
Toast.makeText(context,"Fragment1",Toast.LENGTH_SHORT).show()
}
}

Button lose listener after fragment replace

I have the weirdest bug on Kotlin, and after two days of trying I finally asking for help.
The problem is simple : I have two fragment and one activity, the first fragment A is a form, with a validate button, when I click on validate, the fragment B replace the fragment A, and if I press back, the fragment A show up again with the form filled.
My problem is that after the fragment is shown again, I can click on the button but the listener is not call, so I can't go to the fragment B again. The strange thing is that the other listener are properly working, so I'm thinking it's because the previous fragment is catching the onClick, but idk what to do. Here is some code :
ViewUtils :
fun addFragment(activity: Activity, fragment: androidx.fragment.app.Fragment, container: Int) {
val fragmentManager = (activity as AppCompatActivity).supportFragmentManager
val pendingTransaction = fragmentManager.beginTransaction().add(container, fragment, fragment.javaClass.name)
pendingTransaction.commitAllowingStateLoss()
}
fun replaceFragment(manager: FragmentManager, fragment: androidx.fragment.app.Fragment, container: Int) {
if (fragment.isAdded) return
val pendingTransaction = mangaer.beginTransaction()
pendingTransaction.replace(container, fragment, fragment.javaClass.name)
pendingTransaction.commitAllowingStateLoss()
}
fun removeFragment(activity: Activity, fragment: Fragment) {
val manager = (activity as AppCompatActivity).supportFragmentManager
val trans = manager.beginTransaction()
trans.remove(fragment)
trans.commit()
manager.popBackStack()
}
Activity :
fun displayFragmentA() {
ViewUtils.replaceFragment(supportFragmentManager, FragmentA,
R.id.fragmentLayout)
}
fun FragmentB() {
ViewUtils.replaceFragment(supportFragmentManager, FragmentB,
R.id.fragmentLayout)
}
Fragment A
class AFragment : BaseFragment(), AContract.View {
companion object {
#JvmStatic
fun newInstance(): AFragment {
val fragment = AFragment()
return fragment
}
}
#Inject
lateinit var APresenter: AContract.Presenter<AContract.View>
//end region
//region lifecycle
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_A_layout, container, false)
}
override fun onResume() {
super.onResume()
button_validate.setOnClickListener {
presenter.goToNextStep()
}
}
override fun onAttach(context: Context) {
AndroidSupportInjection.inject(this)
super.onAttach(context)
}
The listener was set in the onViewCreated but I tried moving it to onResume (didn't change anything)
Fragment B code is not important I think, but I can add it if it helps.
Any help is welcome, I really don't know what's going on, the replace/add methods were there before I came to the project, they are not perfect but they are working elsewhere on the project.
I try using breakpoint, the button is not null but we never enter the listener.
Edit : I tried on 3 differents devices, I don't have the bug with a Sony Android 9, but with Huawei et One plus 6 Android 10, the problem persist ..
Ok so after asking to a lot of people, the only solution I found is not using kotlin.synthetic, and using findById instead :
view.findViewById<Button>(R.id.button_validate.setOnClickListener

Finishing and removing fragment and going back to the MainActivity

Android Studio 3.4
I have a simple stock forecast app that uses single activity called ForecastActivity and 3 fragments called LoadingFragment, ForecastFragment, and RetryFragment
The RetryFragment will be added first using the following code in the
`ForecastActivity`:
private fun startRetryFragment() {
fragmentManager?.let {
val fragmentTransaction = it.beginTransaction()
fragmentTransaction.replace(R.id.forecastActivityContainer, RetryFragment(), "RetryFragment")
fragmentTransaction.commit()
}
}
I have a button in the RetryFragment that will start the ForecastActivity again.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btnRetry.setOnClickListener {
startActivity(Intent(activity, ForecastActivity::class.java))
}
}
Then the LoadingFragment will start using this code in the ForecastActvity
private fun startLoadingFragment() {
fragmentManager?.let {
val fragmentTransaction = it.beginTransaction()
fragmentTransaction.replace(R.id.forecastActivityContainer, LoadingFragment(), "LoadingFragment")
fragmentTransaction.commit()
}
}
Then after loading has completed the ForecatActivity will start the ForecastFragment
private fun startForecastFragment() {
fragmentManager?.let {
val fragmentTransaction = it.beginTransaction()
fragmentTransaction.replace(R.id.forecastActivityContainer, RetryFragment(), "ForecastFragment")
fragmentTransaction.commit()
}
In my ForecastActivity I am trying to remove the fragments from the backstack, basically, from here I just want to finish the app. However, as I didn't add any to the backstack I don't expect the count to greater than zero. However, I was wondering how can I end the app and prevent the RetryFragment displaying again?
override fun onBackPressed() {
fragmentManager?.let {
if(it.backStackEntryCount > 0) {
it.popBackStackImmediate()
}
else {
super.onBackPressed()
}
}
}
However, when I click the back from when I am on the ForecastFragment I expect the application to finish. However, the RetryFragment is displayed and then I have to click the back button again to end the app.
Is there something wrong doing this in the RetryFragment
Basically, I just want to go back to the ForecastActivity from the RetryFragment to start again.
startActivity(Intent(activity, ForecastActivity::class.java))
Many thanks for any suggestions.
I think your issue is that you're re-launching the ForecastActivity again from the RetryFragment. If you restructure your code so that the RetryFragment can trigger a retry function in ForecastActivity, you could just replace the RetryFragment with the next appropriate fragment instead of calling startActivity() to create a new ForecastActivity.

Categories

Resources