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.
Related
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.
Let me add more context:
I run bottom view navigation with a ViewPager2. For all 4 of my tab/fragments of my bottom navigation view I have an options menu which I created separately dynamically in each fragment.
Now, when we navigate through the app, it behaves correctly as every options menu is displayed ONLY for their respective fragment.
Problem is: Only when the app is launched, all the options menus from all the the 4 fragments show up on the start destination fragment's tab. BUT, once we swipe and swipe back, only the start destinations options menu is shown on the app bar. As is for every of the other 3 fragment/tabs.
Theory: I think it has something to do with onCreateOptionsMenu which is called when all four fragments are also created to which they share an app bar.
Is anybody familiar with this type of issue? Here is my PagerAdapters code for my ViewPager:
const val F1_PAGE_INDEX = 0
const val F2_PAGE_INDEX = 1
const val F3_PAGE_INDEX = 2
const val F4_PAGE_INDEX = 3
class PagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
/**
* Mapping of the ViewPager page indexes to their respective Fragments
*/
private val tabFragmentsCreators: Map<Int, () -> Fragment> = mapOf(
F1_PAGE_INDEX to { FirstFragment() },
F2_PAGE_INDEX to { SecondFragment() },
F3_PAGE_INDEX to { ThirdFragment() },
F4_PAGE_INDEX to { FourthFragment() }
)
override fun getItemCount() = tabFragmentsCreators.size
override fun createFragment(position: Int): Fragment {
return tabFragmentsCreators[position]?.invoke() ?: throw IndexOutOfBoundsException()
}
}
Here is also my Home View Pager Fragment where I create my bottom Navigation and affect to to the main fragments:
class HomeViewPagerFragment(): Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = FragmentViewPagerBinding.inflate(inflater, container, false)
val viewPager = binding.viewPager
viewPager.isUserInputEnabled = false
viewPager.adapter = PagerAdapter(this)
//Save states of four fragments
viewPager.offscreenPageLimit = 4
(activity as AppCompatActivity).setSupportActionBar(binding.toolbar)
val bottomNavigation = binding.bottomNavView
bottomNavigation.setOnNavigationItemSelectedListener(
BottomNavigationView.OnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.fragment1_destination -> viewPager.currentItem = F1_PAGE_INDEX
R.id.fragment2_destination -> viewPager.currentItem = F2_PAGE_INDEX
R.id.fragment3_destination -> viewPager.currentItem = F3_PAGE_INDEX
R.id.fragment4_destination -> viewPager.currentItem = F4_PAGE_INDEX
}
true
})
return binding.root
}
}
The starting destination (first fragment) is where all the fragment's menu are shown at app start.
Any kind of help is appreciated! Thank you!
Issue: After endless hours, we have found a solution. The issue was directly created when we set the off screen page limit to 4. That causes all the fragments to be created at the same time, thus, obligates the option menus to be shown at launch on the starting destinations fragment since we instructed setHasOptionsMenu(true) in the onCreate of each fragment.
Solution: Simply, set the options menu to true in the onResumeof the fragment to only be called when we swipe to the respective fragment in this manner:
override fun onResume() {
super.onResume()
setHasOptionsMenu(true)
}
I think what you need to do is, make OptionsMenu visible only when that fragment is visible.
Try this: Put this in each fragment
override fun onResume(){
super.onResume()
setHasOptionsMenu(isVisible())
}
This will make the options menu visible only when that fragment is visible. You can make it hidden in onCreateView or in onPause if just onResume doesn't work.
I have two tabs in which I add two different lists of data, both tabs share a single recyclerview, so at my viewpager adapter, I just create a new instance to populate the data
From the View
val allProducts = completeProductList
val productsList = mutableListOf<Products>()
val drinksList = mutableListOf<Products>()
for (product in allProducts) {
if (product.isDrink) {
drinksList.add(product)
} else {
productsList.add(product)
}
}
viewPagerAdapter.productsList = productsList
viewPagerAdapter.drinksList = drinksList
viewPagerAdapter.notifyDataSetChanged()
Adapter
class PagerAdapter(fragmentActivity: FragmentActivity) :
FragmentStateAdapter(fragmentActivity) {
var productsList: MutableList<Product> = arrayListOf()
var drinksList: MutableList<Product> = arrayListOf()
override fun getItemCount(): Int {
return 2
}
override fun createFragment(position: Int): Fragment {
return when(position){
0 -> {
FragmentProducts.newInstance(productsList)
}
else -> {
FragmentProducts.newInstance(drinksList)
}
}
}
}
Then in my FragmentProducts
companion object {
fun newInstance(product: MutableList<Product>) = FragmentProducts().apply {
arguments = Bundle().apply {
putParcelableArrayList(ARG_PROD,ArrayList<Parcelable>(product))
}
}
}
// I get the product list from the adapter, either drinks or normal products
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
productsList = it.getParcelableArrayList<Product>(ARG_PROD)
}
}
// Then I just set it up to the shared recyclerview
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
adapter.productsList = productsList!!
adapter.notifyDataSetChanged()
}
So, the list is displayed correctly, lets say I have two tabs, first tab has 2 items and second tab has 1 item, so, If I click on item 1 at tab 1 I get its id and get the right product, then if I click item 2 on tab 1 it also works, when I swipe to tab 2 and click item 1 it will display correctly again the item, but, if I go back to tab 1 and click item 2 it will throw a IndexOutOfBoundsException, it seems like when swiping back it takes the latest recyclerview data set
I dont know how to fix this to prevent creating a different fragment for tab 2 since they show the same data
I need to know what is happening, it seems that the last FragmentProducts.newInstance(drinksList) is replacing the whole recyclerview at tab 1
StackTrace
java.lang.IndexOutOfBoundsException: Index: 1, Size: 1
at java.util.ArrayList.get(ArrayList.java:411)
at com.StoreAdapter.getItem(StoreAdapter.kt:45)
at com.FragmentProducts.onCartClick(FragmentProducts.kt:65)
at com.StoreAdapter$StoreViewHolder$bind$1.onClick(StoreAdapter.kt:59)
StoreAdapter error at this line
fun getItem(position: Int):Product{
return productList[position]
}
viewPagerAdapter.productsList = productsList
viewPagerAdapter.drinksList = drinksList
viewPagerAdapter.notifyDataSetChanged()
If i didn't understand wrong when you came here your viewPagerAdapter already created and you defined your lists as nonNull in adapter( MutableList< Product >).
fun getItem(position: Int):Product{
return productList[position]
}
For example this block tries to get position 1 when adapter is created and before you set data. It gonna return IndexOutOfBoundsException.
Maybe you must try to move this lists in viewPagerAdapter's constructor.
I have two tabs in which I add two different lists of data, both tabs share a single recyclerview, so at my viewpager adapter, I just create a new instance to populate the data
From the View
val allProducts = completeProductList
val productsList = mutableListOf<Products>()
val drinksList = mutableListOf<Products>()
for (product in allProducts) {
if (product.isDrink) {
drinksList.add(product)
} else {
productsList.add(product)
}
}
viewPagerAdapter.productsList = productsList
viewPagerAdapter.drinksList = drinksList
viewPagerAdapter.notifyDataSetChanged()
Adapter
class PagerAdapter(fragmentActivity: FragmentActivity) :
FragmentStateAdapter(fragmentActivity) {
var productsList: MutableList<Product> = arrayListOf()
var drinksList: MutableList<Product> = arrayListOf()
override fun getItemCount(): Int {
return 2
}
override fun createFragment(position: Int): Fragment {
return when(position){
0 -> {
FragmentProducts.newInstance(productsList)
}
else -> {
FragmentProducts.newInstance(drinksList)
}
}
}
}
Then in my FragmentProducts
companion object {
fun newInstance(product: MutableList<Product>) = FragmentProducts().apply {
arguments = Bundle().apply {
putParcelableArrayList(ARG_PROD,ArrayList<Parcelable>(product))
}
}
}
// I get the product list from the adapter, either drinks or normal products
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
productsList = it.getParcelableArrayList<Product>(ARG_PROD)
}
}
// Then I just set it up to the shared recyclerview
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
adapter.productsList = productsList!!
adapter.notifyDataSetChanged()
}
So, the list is displayed correctly, lets say I have two tabs, first tab has 2 items and second tab has 1 item, so, If I click on item 1 at tab 1 I get its id and get the right product, then if I click item 2 on tab 1 it also works, when I swipe to tab 2 and click item 1 it will display correctly again the item, but, if I go back to tab 1 and click item 2 it will throw a IndexOutOfBoundsException, it seems like when swiping back it takes the latest recyclerview data set
I dont know how to fix this to prevent creating a different fragment for tab 2 since they show the same data
I need to know what is happening, it seems that the last FragmentProducts.newInstance(drinksList) is replacing the whole recyclerview at tab 1
StackTrace
java.lang.IndexOutOfBoundsException: Index: 1, Size: 1
at java.util.ArrayList.get(ArrayList.java:411)
at com.StoreAdapter.getItem(StoreAdapter.kt:45)
at com.FragmentProducts.onCartClick(FragmentProducts.kt:65)
at com.StoreAdapter$StoreViewHolder$bind$1.onClick(StoreAdapter.kt:59)
StoreAdapter error at this line
fun getItem(position: Int):Product{
return productList[position]
}
viewPagerAdapter.productsList = productsList
viewPagerAdapter.drinksList = drinksList
viewPagerAdapter.notifyDataSetChanged()
If i didn't understand wrong when you came here your viewPagerAdapter already created and you defined your lists as nonNull in adapter( MutableList< Product >).
fun getItem(position: Int):Product{
return productList[position]
}
For example this block tries to get position 1 when adapter is created and before you set data. It gonna return IndexOutOfBoundsException.
Maybe you must try to move this lists in viewPagerAdapter's constructor.
I have a Pager Adapter to load server images as like below:
class ImagePagerAdapter(var imageList: List<ProductImageModel>, var fragmentManager: FragmentManager) :
FragmentPagerAdapter(fragmentManager) {
override fun getItem(position: Int): Fragment {
val fragment = ImagePagerFragment()
fragment.arguments = Bundle().apply {
putString(PAGER_IMAGE_URL, imageList[position].image)
}
return fragment
}
override fun getCount(): Int {
return imageList.size
}
}
I want to know any option to addtoBackStack(null) while creating a new Fragment inside the pagerAdapter.
The current code, automatically adding the fragments to the backstack. So it's making a huge issue while the user presses the back button. I have added another logic to filter the fragments in the back press.