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.
Related
I have a viewpager2 with FragmentStateAdapter() adapter inside it. I also have a tab layout with 4 tabs. I use a single fragment for all tab. that is named AllOrdersTab.
in my architecture I just send different value to load different API data to AllOrdersTab fragment.
when each tab layout is selected , a fragment created and works fine for the first time for all 4 fragments. after that if I swipe back to previous tab it is not created or refreshed again.
I want to recreate the fragment or a way to call API again when swiping between tabs. I also read this page.
FragmentStateAdapter not recreating currentFragment after notifyDataSetChanged
I tried to do this. so I decided to create 4 instance of Allorderstab() fragment. but never work for me because I guess hash codes of fragments are same.
ViewPager2 Adapter:
class ViewPagerOrdersAdapter(fm: FragmentManager,val listFragments:MutableList<Fragment>, viewlifecycler: Lifecycle) : FragmentStateAdapter(fm, viewlifecycler)
{
override fun getItemCount(): Int
{
return listFragments.size
}
override fun createFragment(position: Int): Fragment {
val args = Bundle()
when (position) {
1 -> {
args.putString("KEY_ID", "inProgress")
listFragments[position].arguments = args
return listFragments[position]
}
2 -> {
args.putString("KEY_ID", "cancel")
listFragments[position].arguments = args
return listFragments[position]
}
3 ->
{
args.putString("KEY_ID","deliver")
listFragments[position].arguments=args
return listFragments[position]
}
else -> {
args.putString("KEY_ID", "all")
listFragments[position].arguments = args
return listFragments[position]
}
}
}
override fun getItemId(position: Int): Long {
return listFragments[position].hashCode().toLong()
}
override fun containsItem(itemId: Long): Boolean {
return listFragments.find {it.id.hashCode().toLong() == itemId } != null
}
}
Here I created 4 instance of Allorderstab() Fragment.
Set ViewPager2 Adapter
MainFragment:
val fragments:MutableList<Fragment> = mutableListOf(Allorderstab(), Allorderstab(), Allorderstab(), Allorderstab())
vp.setAdapter(ViewPagerOrdersAdapter(this.childFragmentManager,fragments, lifecycle))
AllOrderstab Fragment:
override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?, savedInstanceState: Bundle?): View? {
val bundle = arguments
bundle?.let {
val myStatus = bundle.getString("KEY_ID")
myStatus?.let{
//getting history of each tab orders - calling API
myviewModel.getOrdersHistory(tempTn,myStatus)
}
}
}
All in all I don't know how to refresh while swiping between tabs . if I have a single fragment for all 4 tabs.
For those who wasted a day for this problem like me , wanting to call API each time for refreshing data, Move your code to onResume function. fortunately it is run each time your fragment visible.
AllOrderstab Fragment:
override fun onResume() {
val bundle = arguments
bundle?.let {
val myStatus = bundle.getString("KEY_ID")
myStatus?.let{
//getting history of all orders
myviewModel.getOrdersHistory(tempTn,myStatus)
}
}
super.onResume()
}
You have 4 different instances of the same tab. If you need to refresh the tabs everytime the user navigates to your tab you need to add a PageChangeListener to your tabs view. Then whenever you change the page you need to notify the fragment that it has come to foreground you can do so by calling a method on Allorderstab class and then refreshing the data from this method.
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'm using Navigation Components, I have Fragment A and Fragment B, from Fragment A I send an object to Fragment B with safe args and navigate to it.
override fun onSelectableItemClick(position:Int,product:Product) {
val action = StoreFragmentDirections.actionNavigationStoreToOptionsSelectFragment(product,position)
findNavController().navigate(action)
}
Now, after some logic in my Fragment B , I want to deliver that data to Fragment A again, which I use
btn_add_to_cart.setOnClickListener {button ->
findNavController().previousBackStackEntry?.savedStateHandle?.set("optionList",Pair(result,product_position))
findNavController().popBackStack()
}
Then in Fragment A, I catch up this data with
findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<Pair<MutableList<ProductOptions>,Int>>("optionList")
?.observe(viewLifecycleOwner, Observer {
storeAdapter.updateProductOptions(it.second,it.first)
})
Now, this is working fine, but if I go from Fragment A to Fragment B and press the back button, the observer above fires again duplicating my current data, is there a way to just fire this observer when I only press the btn_add_to_cart button from Fragment B ?
You use this extenstion:
fun <T> Fragment.getResult(key: String = "key") =
findNavController().currentBackStackEntry?.savedStateHandle?.get<T>(key)
fun <T> Fragment.getResultLiveData(key: String = "key"): MutableLiveData<T>? {
viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_DESTROY) {
findNavController().previousBackStackEntry?.savedStateHandle?.remove<T>(key)
}
})
return findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<T>(key)
}
fun <T> Fragment.setResult(key: String = "key", result: T) {
findNavController().previousBackStackEntry?.savedStateHandle?.set(key, result)
}
Example:
FragmentA -> FragmentB
Fragment B need to set the result of the TestModel.class
ResultTestModel.class
data class ResultTestModel(val id:String?, val name:String?)
Fragment A:
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// ...
getNavigationResultLiveData<PassengerFragmentResultNavigationModel>(
"UNIQUE_KEY")?.observe(viewLifecycleOwner) { result ->
Log.i("-->","${result.id} and ${result.name}")
}
//...
}
Fragment B: set data and call popBackStack.
ResultTestModel(id = "xyz", name = "Rasoul")
setNavigationResult(key = "UNIQUE_KEY", result = resultNavigation)
findNavController().popBackStack()
Facing same issue
Resolve this by removing old data from savedStateHandle live data
Inside Fragment B :
button?.setOnClickListener {
findNavController().previousBackStackEntry?.savedStateHandle?.set(key, data)
findNavController().popBackStack()
}
Inside Fragment A:
Here is key to remove the old data by using live data remove method and it should be after view created like in onViewCreated method of fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<String>(key)?.observe(viewLifecycleOwner) {
result(it)
findNavController().currentBackStackEntry?.savedStateHandle?.remove<String>(key)
}
}
Update :
I have created Extension for this for better usage
fun <T> Fragment.setBackStackData(key: String, data: T, doBack: Boolean = false) {
findNavController().previousBackStackEntry?.savedStateHandle?.set(key, data)
if (doBack)
findNavController().popBackStack()
}
fun <T> Fragment.getBackStackData(key: String, singleCall : Boolean= true , result: (T) -> (Unit)) {
findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<T>(key)
?.observe(viewLifecycleOwner) {
result(it)
//if not removed then when click back without set data it will return previous data
if(singleCall) findNavController().currentBackStackEntry?.savedStateHandle?.remove<T>(key)
}
}
Calling inside fragment be like
While setting data in fragment B
var user : User = User(data) // Make sure this is parcelable or serializable
setBackStackData("key",user,true)
While getting data inside fragment A
getBackStackData<User>("key",true) { it ->
}
Thanks to This Guy
It is not clear from your code where your last piece of code is called - where you add an Observer to LiveData. I am guessing it is inside one of the methods onResume() or onViewStateRestored() or any other lifecycle callback which is called again whenever you return to Fragment A from Fragment B. If that is the case, then you are adding a new observer to the LiveData and any observer of a LiveData receives an instant update for the current value.
Move that piece of code to one of the callbacks methods which is called only once during the lifecycle of a fragment.
https://stackoverflow.com/a/66111168/8354145
This answer should help in this case. Use SingleLiveEvent.
Otherwise in these cases, maybe using a shared view model (might be scoped to the nav graph) however you won't need to use savedStateHandle.
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)
{ }
}
}
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.