Remove Fragment from MainActivity - android

I was able to make a fragment as below
supportFragmentManager.beginTransaction().replace(R.id.fragment2, ConsumeFragment.newInstance(consumerTitle)).commit()
and my ConsumeFragment Class
class ConsumeFragment : Fragment()
companion object {
const val titleKey = "consumeTitle"
fun newInstance(title: String): ConsumeFragment {
val frag = ConsumeFragment()
val bundle = Bundle()
bundle.putString(titleKey, title)
frag.arguments = bundle
return frag
}
Now I am trying to remove the fragment by clicking the button as below
binding.killConsumeBut.setOnClickListener{
Log.d("XXX", "click KillConsumerButton")
supportFragmentManager.beginTransaction().remove(ConsumeFragment()).commit()}
However, it doesn't remove the fragment. What should I do instead?

Quoting the documentation for remove():
Remove an existing fragment
(emphasis added)
You are creating a new fragment instance via ConsumeFragment(), then are trying to remove it. That is not going to work.
Instead, hold onto the ConsumeFragment instance that you added previously, and use remove() with it. Or, find the fragment in your FragmentManager (e.g., via findFragmentById()), then call remove() with it.

Related

Android FragmentManager and Fragment Result API

How can be that a fragment F which uses the new Fragment Result API to get results from 2 other fragments: A, B gets the result from A but not from B because B has a different parent FragmentManager (and I don't know why) ? How could be something like that ? 2 fragments called in the same way but they end up having same Activity but different FragmentManager ? The function calls are the following:
//THIS DOESN'T WORK. THE LISTENER IS NOT CALLED AFTER THE RESULT IS SET
private fun navigateToItemLocation() {
setFragmentResultListener(REQUEST_LOCATION_KEY) { s: String, bundle: Bundle ->
val locationId = bundle.getParcelable<ParcelUuid>(LOCATION_ID)!!.uuid
viewModel.viewModelScope.launch(Dispatchers.IO) {
val location = LocationRepository().get(locationId)!!
changeItemLocation(location)
}
}
val action = ItemRegistrationPagerHolderDirections.actionNavItemRegistrationPagerToNavStockLocationSelection()
findNavController().navigate(action)
}
//THIS WORKS FINE:
private fun navigateToItemDetails(item: Item2) {
setFragmentResultListener(SELECTED_ITEM_KEY) { s: String, bundle: Bundle ->
val propertySetId = bundle.getParcelable<ParcelUuid>(SELECTED_ITEM_SET_ID)!!.uuid
clearFragmentResultListener(SELECTED_ITEM_KEY)
viewModel.viewModelScope.launch(Dispatchers.IO) {
val repository = PropertySetRepository()
val propertySet = repository.get(propertySetId)!!
val propertySetInfo = ItemFactory.loadPropertySetInfo(propertySet)
withContext(Dispatchers.Main) { setPackageCode(null) }
selectItem(item.item, propertySetInfo, item.description, null)
}
}
val action = ItemRegistrationPagerHolderDirections.actionNavItemRegistrationToNavStockItemDetails(ParcelUuid(item.item.id), true)
findNavController().navigate(action)
}
Both fragments A and B are in a separate Dynamic Feature. The only single problem I have is that when the following function is called:
fun onSelect() {
viewModel.pickedLocation.value = (viewModel.selectedLocation as? LocationExt2?)?.location
val result = bundleOf(Pair(LOCATION_ID, ParcelUuid(viewModel.pickedLocation.value!!.id)))
setFragmentResult(REQUEST_LOCATION_KEY, result)
findNavController().popBackStack()
}
setFragmentResult(REQUEST_LOCATION_KEY, result)
Doesn't produce any result because the FragmentManager is not the same of the calling Fragment. The same method in fragment A which is:
private fun onSetSelected(id: UUID) {
propertySets.removeObservers(viewLifecycleOwner)
adapter.tracker = null
setFragmentResult(SELECTED_ITEM_KEY, bundleOf(Pair(SELECTED_ITEM_SET_ID, ParcelUuid(id))))
findNavController().popBackStack()
}
As a temporarily workaround I replaced the call to Fragment's FragmentManager with Activity.supportFragmentManager.setFragmentResultListener. It works but still I do not understand why fragments A and B behave differently...
Check that fragment where you listen for fragment result and fragment where you set the result are in the same fragment manager.
Common case where this would happen is if you are using Activity.getSupportFragmentManager() or Fragment.getParentFragmentManager() alongside Fragment.getChildFragmentManager().
Check this blog article for the principles and rules with Fragment Result API on medium: https://medium.com/#FrederickKlyk/state-of-the-art-communication-between-fragments-and-their-activity-daa1fe4e014d
Only one listener can be registered for a specific request key.
If more than one listener is registered on the same key, the previous one will be replaced by the newest listener.

Send data from fragment to another when we use dependency injection for fragments

Is there a way for sending data from a fragment to another without create new instance from it ?
I inject my fragment but i faced with a problem that i describe it in below :
We inject our fragments like this :
val fragmentModule= module {
factory { MyFragment() }
}
And this is a function for replace fragment :
fun AppCompatActivity.replaceFragment(fragment: Class<out Fragment>,frameId: Int){
supportFragmentManager
.beginTransaction()
.replace(frameId,fragment,null,null)
.commit()
}
Then i use it like this :
activity.replaceFragment(MyFragment::class.java,R.id.frameLayoutId)
So my question is how can i set argument to fragment in this way and without create new instance of fragment how can we pass a data ?
val bundle=Bundle()
bundle.putString("YourKey","YourData")
val fragment=MyFragment()
fragment.arguments=bundle
without create new instance? NO . Dependency injection framework only takes care of provide the dependencies where needed it does not connect the Component automatically in any way.
With Koin you can pass the Bundle during creation of fragment using a FragmentFactory.
val arguments = Bundle().apply {
putString("key", "value")
}
supportFragmentManager.beginTransaction()
.replace(R.id.container, MyFragment::class.java, arguments)
.commit()
To pass data after the fragment created you have set it in Fragment somehow. There can be following options:-
If you are using MVVM you can use a shared ViewModel to pass the data using LiveData.
Alternatively you can get the Fragment from back stack and call a method on it.
Or you can use a callback interface

Fragments not added to backstack using Navigation Components

Information:
I'm programmatically inserting a NavHostFragment for each feature of the app. Each NavHostFragment has it's own Navigation Graph. Dagger is providing them by using a FragmentFactory specific to each feature. It's a Single Activity structure with MVVM architecture.
Repo: https://github.com/mitchtabian/DaggerMultiFeature/tree/nav-component-backstack-bug
checkout the branch "nav-component-backstack-bug".
The Problem
When navigating into the graph the fragments are not being added to the backstack. The only fragment that's added is whichever one has most recently been visited. So the stack size always stays at one.
Originally I thought it was because I wasn't setting the FragmentFactory to the ChildFragmentManager but that doesn't change anything. See the code snippets below for the relevant code. Or checkout the project and run it. I have logs printing out the fragments currently in the backstack from the ChildFragmentManager and also the SupportFragmentManager. Both have a constant size of 1.
Feature1NavHostFragment.kt
This is one of the custom NavHostFragment's. The create() function in the companion object is how I create them.
class Feature1NavHostFragment
#Inject
constructor(
private val feature1FragmentFactory: Feature1FragmentFactory
): NavHostFragment(){
override fun onAttach(context: Context) {
((activity?.application) as BaseApplication)
.getAppComponent()
.feature1Component()
.create()
.inject(this)
childFragmentManager.fragmentFactory = feature1FragmentFactory
super.onAttach(context)
}
companion object{
const val KEY_GRAPH_ID = "android-support-nav:fragment:graphId"
#JvmStatic
fun create(
feature1FragmentFactory: Feature1FragmentFactory,
#NavigationRes graphId: Int = 0
): Feature1NavHostFragment{
var bundle: Bundle? = null
if(graphId != 0){
bundle = Bundle()
bundle.putInt(KEY_GRAPH_ID, graphId)
}
val result = Feature1NavHostFragment(feature1FragmentFactory)
if(bundle != null){
result.arguments = bundle
}
return result
}
}
}
MainActivity.kt
This is an example of how I create the NavHostFragment's in MainActivity.
val newNavHostFragment = Feature1NavHostFragment.create(
getFeature1FragmentFactory(),
graphId
)
supportFragmentManager.beginTransaction()
.replace(
R.id.main_nav_host_container,
newNavHostFragment,
getString(R.string.NavHostFragmentTag)
)
.setPrimaryNavigationFragment(newNavHostFragment)
.commit()
Feature1MainFragment.kt
And here is an example of how I'm navigating to other fragments in the graph.
btn_go_next.setOnClickListener {
findNavController().navigate(R.id.action_feature1MainFragment_to_feature1NextFragment)
}
Summary
Like I said, in every fragment I'm printing the backstack for both the ChildFragmentManager and the SupportFragmentManager. Both have a constant size of one. It's as if the fragments are being replaced as I navigate into the graph instead of added to the stack.
Anyways, thanks for reading this and I would appreciate any insights.
Looks like a misunderstanding on my part (and a bug, also on my part).
If you loop through the fragments in the childFragmentManager it only ever shows the top-most fragment for some reason.
Example
val navHostFragment = supportFragmentManager
.findFragmentByTag(getString(R.string.NavHostFragmentTag)) as NavHostFragment
val fragments = navHostFragment.childFragmentManager.fragments
for(fragment in fragments){
// Only prints a single fragment, no matter the backstack size
}
However, if you print the backstack size like this, you will get the correct answer.
val navHostFragment = supportFragmentManager
.findFragmentByTag(getString(R.string.NavHostFragmentTag)) as NavHostFragment
val backstackCount = navHostFragment.childFragmentManager.backStackEntryCount
println("backstack count: $backstackCount")
At the end of the day this misunderstanding caused me to believe the fragments were not being added to the backstack. All is good.

Android navigation component: how save fragment state

I use bottomNavigationView and navigation component. Please tell me how I don't destroy the fragment after switching to another tab and return to the old one? For example I have three tabs - A, B, C. My start tab is A. After I navigate to B, then return A. When I return to tab A, I do not want it to be re-created. How do it? Thanks
As per the open issue, Navigation does not directly support multiple back stacks - i.e., saving the state of stack B when you go back to B from A or C since Fragments do not support multiple back stacks.
As per this comment:
The NavigationAdvancedSample is now available at https://github.com/googlesamples/android-architecture-components/tree/master/NavigationAdvancedSample
This sample uses multiple NavHostFragments, one for each bottom navigation tab, to work around the current limitations of the Fragment API in supporting multiple back stacks.
We'll be proceeding with the Fragment API to support multiple back stacks and the Navigation API to plug into it once created, which will remove the need for anything like the NavigationExtensions.kt file. We'll continue to use this issue to track that work.
Therefore you can use the NavigationAdvancedSample approach in your app right now and star the issue so that you get updates for when the underlying issue is resolved and direct support is added to Navigation.
In case you can deal with destroying fragment, but want to save ViewModel, you can scope it into the Navigation Graph:
private val viewModel: FavouritesViewModel by
navGraphViewModels(R.id.mobile_navigation) {
viewModelFactory
}
Read more here
EDIT
As #SpiralDev noted, using Hilt simplifies a bit:
private val viewModel: MainViewModel by
navGraphViewModels(R.id.mobile_navigation) {
defaultViewModelProviderFactory
}
just use navigation component version 2.4.0-alpha01 or above
Update:
Using last version of fragment navigation component, handle fragment states itself. see this sample
Old:
class BaseViewModel : ViewModel() {
val bundleFromFragment = MutableLiveData<Bundle>()
}
class HomeViewModel : BaseViewModel () {
... HomeViewModel logic
}
inside home fragment (tab of bottom navigation)
private var viewModel: HomeViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.bundleFromFragment.observe(viewLifecycleOwner, Observer {
val message = it.getString("ARGUMENT_MESSAGE", "")
binding.edtName.text = message
})
}
override fun onDestroyView() {
super.onDestroyView()
viewModel.bundleFromFragment.value = bundleOf(
"ARGUMENT_MESSAGE" to binding.edtName.text.toString(),
"SCROLL_POSITION" to binding.scrollable.scrollY
)
}
You can do this pattern for all fragments inside bottom navigation
Update 2021
use version 2.4.0-alpha05 or above.
don't use this answer or other etc.
This can be achieved using Fragment show/hide logic.
private val bottomFragmentMap = hashMapOf<Int, Fragment>()
bottomFragmentMap[0] = FragmentA.newInstance()
bottomFragmentMap[1] = FragmentB.newInstance()
bottomFragmentMap[2] = FragmentC.newInstance()
bottomFragmentMap[3] = FragmentD.newInstance()
private fun loadFragment(fragmentIndex: Int) {
val fragmentTransaction = childFragmentManager.beginTransaction()
val bottomFragment = bottomFragmentMap[fragmentIndex]!!
// first time case. Add to container
if (!bottomFragment.isAdded) {
fragmentTransaction.add(R.id.container, bottomFragment)
}
// hide remaining fragments
for ((key, value) in bottomFragmentMap) {
if (key == fragmentIndex) {
fragmentTransaction.show(value)
} else if (value.isVisible) {
fragmentTransaction.hide(value)
}
}
fragmentTransaction.commit()
}
Declare fragment on the activity & create fragment instance on onCreate method, then pass the fragment instance in updateFragment method. Create as many fragment instances as required corresponding to bottom navigation listener item id.
Fragment fragmentA;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home);
fragmentA = new Fragment();
updateFragment(fragmentA);
}
public void updateFragment(Fragment fragment) {
FragmentTransaction transaction =
getSupportFragmentManager().beginTransaction();
transaction.add(R.id.layoutFragment, fragment);
transaction.commit();
}
Furthermore be sure you are using android.support.v4.app.Fragment and calling getSupportFragmentManager()

Get current fragment with ViewPager2

I'm migrating my ViewPager to ViewPager2 since the latter is supposed to solve all the problems of the former. Unfortunately, when using it with a FragmentStateAdapter, I don't find any way to get the currently displayed fragment.
viewPager.getCurrentItem() gives the current displayed index and adapter.getItem(index) generally creates a new Fragment for the current index. Unless keeping a reference to all created fragments in getItem(), I have no idea how to access the currently displayed fragment.
With the old ViewPager, one solution was to call adapter.instantiateItem(index) which would return the fragment at the desired index.
Am I missing something with ViewPager2?
In ViewPager2 the FragmentManager by default have assigned tags to fragments like this:
Fragment in 1st position has a tag of "f0"
Fragment in 2nd position has a tag of "f1"
Fragment in 3rd position has a tag of "f2" and so on... so you can get your fragment's tag and by concatenating the "f" with position of your fragment. To get the current Fragment you can get current position from the viewPager2 position and make your tag like this (For Kotlin):
val myFragment = supportFragmentManager.findFragmentByTag("f" + viewpager.currentItem)
For fragment at a certain position
val myFragment = supportFragmentManager.findFragmentByTag("f" + position)
You can cast the Fragment and always check if it is not null if you are using this technique.
If you host your ViewPager2 in Fragment, use childFragmentManager instead.
REMEMBER
If you have overriden the getItemId(position: Int) in your adapter. Then your case is different. It should be:
val myFragment = supportFragmentManager.findFragmentByTag("f" + your_id_at_that_position)
OR SIMPLY:
val myFragment = supportFragmentManager.findFragmentByTag("f" + adapter.getItemId(position))
If you host your ViewPager2 in Fragment, use childFragmentManager instead of supportFragmentManager.
The solution to find current Fragment by its tag seems the most suitable for me. I've created these extension functions for that:
fun ViewPager2.findCurrentFragment(fragmentManager: FragmentManager): Fragment? {
return fragmentManager.findFragmentByTag("f$currentItem")
}
fun ViewPager2.findFragmentAtPosition(
fragmentManager: FragmentManager,
position: Int
): Fragment? {
return fragmentManager.findFragmentByTag("f$position")
}
If your ViewPager2 host is Activity, use supportFragmentManager
or fragmentManager.
If your ViewPager2 host is Fragment, use childFragmentManager
Note that:
findFragmentAtPosition will work only for Fragments that were initialized in ViewPager2's RecyclerView. Therefore you can get only the positions that are visible + 1.
Lint will suggest you to remove ViewPager2. from fun ViewPager2.findFragmentAtPosition, because you don't use anything from ViewPager2 class. I think it should stay there, because this workaround applies solely to ViewPager2.
I had similar problem when migrating to ViewPager2.
In my case I decided to use parentFragment property (I think you can make it also work for activity) and hope, that ViewPager2 will keep only the current fragment resumed. (i.e. page fragment that was resumed last is the current one.)
So in my main fragment (HostFragment) that contains ViewPager2 view I created following property:
private var _currentPage: WeakReference<MyPageFragment>? = null
val currentPage
get() = _currentPage?.get()
fun setCurrentPage(page: MyPageFragment) {
_currentPage = WeakReference(page)
}
I decided to use WeakReference, so I don't leak inactive Fragment instances
And each of my fragments that I display inside ViewPager2 inherits from common super class MyPageFragment. This class is responsible for registering its instance with host fragment in onResume:
override fun onResume() {
super.onResume()
(parentFragment as HostFragment).setCurrentPage(this)
}
I also used this class to define common interface of paged fragments:
abstract fun someOperation1()
abstract fun someOperation2()
And then I can call them from the HostFragment like this:
currentPage?.someOperation1()
I'm not saying it's a nice solution, but I think it's more elegant than relying on internals of ViewPager's adapter with instantiateItem method that we had to use before.
I was able to get access to current fragment in FragmentStateAdapter using reflection.
Extension function in Kotlin:
fun FragmentStateAdapter.getItem(position: Int): Fragment? {
return this::class.superclasses.find { it == FragmentStateAdapter::class }
?.java?.getDeclaredField("mFragments")
?.let { field ->
field.isAccessible = true
val mFragments = field.get(this) as LongSparseArray<Fragment>
return#let mFragments[getItemId(position)]
}
}
Add Kotlin reflection dependency if needed:
implementation "org.jetbrains.kotlin:kotlin-reflect:1.3.61"
Example call:
val tabsAdapter = viewpager.adapter as FragmentStateAdapter
val currentFragment = tabsAdapter.getItem(viewpager.currentItem)
supportFragmentManager.findFragmentByTag("f" + viewpager.currentItem)
with FragmentStateAdapter in placeFragmentInViewHolder(#NonNull final FragmentViewHolder holder)add Fragment
mFragmentManager.beginTransaction()
.add(fragment, "f" + holder.getItemId())
.setMaxLifecycle(fragment, STARTED)
.commitNow()
May as well post my solution to this - it's the same basic approach as #Almighty 's, except I'm keeping the Fragment weak references in a lookup table in the PagerAdapter:
private class PagerAdapter(fm: FragmentManager, lifecycle: Lifecycle) : FragmentStateAdapter(fm, lifecycle) {
// only store as weak references, so you're not holding discarded fragments in memory
private val fragmentCache = mutableMapOf<Int, WeakReference<Fragment>>()
override fun getItemCount(): Int = tabList.size
override fun createFragment(position: Int): Fragment {
// return the cached fragment if there is one
fragmentCache[position]?.get()?.let { return it }
// no fragment found, time to make one - instantiate one however you
// like and add it to the cache
return tabList[position].fragment.newInstance()
.also { fragmentCache[position] = WeakReference(it) }
.also { Timber.d("Created a fragment! $it") }
}
// not necessary, but I think the code reads better if you
// can use a getter when you want to... try to get an existing thing
fun getFragment(position: Int) = createFragment(position)
}
and then you can call getFragment with the appropriate page number, like adapter.currentPage or whatever.
So basically, the adapter is keeping its own cache of fragments it's created, but with WeakReferences so it's not actually holding onto them, once the components actually using the fragments are done with them, they won't be in the cache anymore. So you can hold a lookup for all the current fragments.
You could have the getter just return the (nullable) result of the lookup, if you wanted. This version obviously creates the fragment if it doesn't already exist, which is useful if you expect it to be there. This can be handy if you're using ViewPager2.OnPageChangeCallback, which will fire with the new page number before the view pager creates the fragment - you can "get" the page, which will create and cache it, and when the pager calls createFragment it should still be in the cache and avoid recreating it.
It's not guaranteed the weak reference won't have been garbage collected between those two moments though, so if you're setting stuff on that fragment instance (rather than just reading something from it, like a title you want to display) be aware of that!
If you want the current Fragment to only perform some action in it, you can use a SharedViewModel which is shared between ViewPager container and its Fragments and pass an Identifier to each fragment and observe to a LiveData in SharedViewModel. Set value of that LiveData to an object which consists of Identifier of the fragment you want to update (i.e. Pair<String, MyData> which String is type of the Identifier). Then inside your observers check if the current emitted Identifer is as same as the fragment's Identifier or not and consume data if it is equal.
Its not as simple as using fragment's tags to find them, But it least you do not need to worry about changes to how ViewPager2 create tag for each fragment.
I had the same problem. I converted from ViewPager to ViewPager2, using FragmentStateAdapter. In my case I have a DetailActivity class (extends AppCompatActivity) which houses the ViewPager2, which is used to page through lists of data (Contacts, Media, etc.) on smaller form-factor devices.
I need to know the currently shown fragment (which is my own class DetailFragment which extends androidx.fragment.app.Fragment), because that class contains the string I use to update the title on the DetailActivity toolbar.
I first started down the road of registering an onPageChangeCallback listener as suggested by some, but I quickly ran into problems:
I initially created tags during the ViewPager2's adapter.createFragment() call as suggested by some with the idea to add the newly created fragment to a Bundle object (using FragmentManager.put()) with that tag. This way I could then save them across config changes. The problem here is that during createFragment(), the fragment isn't actually yet part of the FragmentManager, so the put() calls fail.
The other problem is that if the fundamental idea is to use the onPageSelected() method of the OnPageChangeCallback to find the fragment using the internally generated "f"+position tag names - we again have the same timing issue: The very first time in, onPageSelected() is called PRIOR to the createFragment() call on the adapter - so there are no fragments yet created and added to the FragmentManager, so I can't get a reference to that first fragment using the "f0" tag.
I then tried to find a way where I could save the position passed in to onPageSelected, then try and reference that somewhere else in order to retrieve the fragment after the adapter had made the createFragment() calls - but I could not identify any type of handler within the adapter, the associated recyclerview, the viewpager etc. that allows me to surface the list of fragments that I could then reference that to the position identified within that listener. Strangely, for example, one adapter method that looked very promising was onViewAttachedToWindow() - however it is marked final so can't be overridden (even though the JavaDoc clearly anticipates it being used this way).
So what I ended up doing that worked for me was the following:
In my DetailFragment class, I created an interface that can be implemented by the hosting activity:
public interface DetailFragmentShownListener {
// Allows classes that extend this to update visual items after shown
void onDetailFragmentShown(DetailFragment me);
}
Then I added code within onResume() of DetailFragment to see if the associated activity has implemented the DetailFragmentShownListener interface within this class, and if so I make the callback:
public void onResume() {
super.onResume();
View v = getView();
if (v!=null && v.getViewTreeObserver().isAlive()) {
v.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
v.getViewTreeObserver().removeOnGlobalLayoutListener(this);
// Let our parent know we are laid out
if ( getActivity() instanceof DetailFragmentShownListener ) {
((DetailFragmentShownListener) getActivity()).onDetailFragmentShown(DetailFragment.this);
}
}
});
}
}
Then where I need to know when this fragment is shown (such as within DetailActivity), I implement the interface and when it receives this callback I know that this is the current fragment:
#Override
public void onDetailFragmentShown(DetailFragment me) {
mCurrentFragment = me;
updateToolbarTitle();
}
mCurrentFragment is a property of this class as its used in various other places.
The ViewPagerAdapter is intended to hide all these implementation details which is why there is no straight-forward way to do it.
You could try setting and id or tag on the fragment when you instantiate it in getItem() then use fragmentManager.findFragmentById() or fragmentManager.findFragmentByTag() to retrieve.
Doing it like this, however, is a bit of a code smell. It suggests to me that stuff is being done in the activity when it should be done in the fragment (or elsewhere).
Perhaps there is another approach to achieve what you want but it's hard to give suggestions without knowing why you need to get the current fragment.
Similar to some other answers here, this is what I'm currently using.
I'm not clear on how reliable it is but at least one of these is bound to work 😁.
//Unclear how reliable this is
fun getFragmentAtIndex(index: Int): Fragment? {
return fm.findFragmentByTag("f${getItemId(index)}")
?: fm.findFragmentByTag("f$index")
?: fm.findFragmentById(getItemId(index).toInt())
?: fm.findFragmentById(index)
}
fm is the supportFragmentManager
Solution for get fragment by position:
class ClubPhotoAdapter(
private val dataList: List<MyData>,
fm: FragmentManager,
lifecycle: Lifecycle
) : FragmentStateAdapter(fm, lifecycle) {
private val fragmentMap = mutableMapOf<Int, WeakReference<MyFragment>>()
override fun getItemCount(): Int = dataList.size
override fun createFragment(position: Int): Fragment {
val fragment = MyFragment[dataList.getOrNull(position)]
fragmentMap[position] = WeakReference(fragment)
return fragment
}
fun getItem(position: Int): MyFragment? = fragmentMap[position]?.get()
}
You can get the current fragment from ViewPager2 like following,
adapter.getRegisteredFragment(POSITION);
Sample FragmentStateAdapter,
public class ViewPagerAdapterV2 extends FragmentStateAdapter {
private final FragmentActivity context;
private final HashMap<Integer, Fragment> mapFragments;
public ViewPagerAdapterV2(FragmentActivity fm) {
super(fm);
this.context = fm;
this.mapFragments = new HashMap<>();
}
public Context getContext() {
return context;
}
#Override
public int getItemCount() {
return NUMBER_OF_PAGES;
}
public Fragment getRegisteredFragment(int position) {
return mapFragments.get(position);
}
#NonNull
#Override
public Fragment createFragment(int position) {
MyFragment fragment = new MyFragment();
mapFragments.put(position, fragment);
return fragment;
}
}
I found this works for me even after the activity gets destroyed and re-created.
The key can be any type. Assume you have ViewModel used for your fragment.
fun findFragment(fragmentId: String?): YourFragment? {
fragmentId?: return null
return supportFragmentManager.fragments.filterIsInstance(YourFragment::class.java).find { it.viewModel.yourData.value.id == fragmentId}
}
To avoid finding fragment with the hard-coded tag f$id set internally which might be changed by Google in any future release:
Method 1: filtering the page fragments with the resumed one
Assuming the page fragment of the ViewPager2 is PageFragment, then you can find the current ViewPager fragment in the current fragmentManager fragments and check if it is in the resumed state (currently displayed on the screen)
val fragment = supportFragmentManager.fragments
.find{ it is PageFragment && it.isResumed } as PageFragment
Note: supportFragmentManager should be replaced with childFragmentManager if the ViewPager is a part of a Fragment.
For java (API 24+):
Fragment fragment =
getSupportFragmentManager().getFragments().stream()
.filter(it -> it instanceof PageFragment && it.isResumed())
.collect(Collectors.toList()).stream().findFirst().get();
Method 2: setting some argument to the PageFragment, and filter fragments based on it
Kotlin:
Adapter
class MyAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
FragmentStateAdapter(fragmentManager, lifecycle) {
//.... omitted
override fun createFragment(position: Int): Fragment
= PageFragment.getInstance(position)
}
PageFragment:
class PageFragment : Fragment() {
//.... omitted
companion object {
const val POSITION = "POSITION";
fun getInstance(position: Int): PageFragment {
return PageFragment().apply {
arguments = Bundle().also {
it.putInt(POSITION, position)
}
}
}
}
}
And filter with the position argument to get the needed PageFragment:
val fragment = supportFragmentManager.fragments.firstOrNull {
it is PageFragment && it.arguments?.getInt("POSITION") == id } // id from the viewpager >> for instance `viewPager.currentItem`
Java (API 24+):
Adapter:
class MyAdapter extends FragmentStateAdapter {
// .... omitted
#NonNull
#Override
public Fragment createFragment(int position) {
return PagerFragment.newInstance(position);
}
}
PageFragment:
public class PagerFragment extends Fragment {
// .... omitted
public static Fragment newInstance(int position) {
PagerFragment fragment = new PagerFragment();
Bundle args = new Bundle();
args.putInt("POSITION", position);
fragment.setArguments(args);
return fragment;
}
}
And to get a fragment of a certain viewPager item:
Optional<Fragment> optionalFragment = getSupportFragmentManager().getFragments()
.stream()
.filter(it -> it instanceof PagerFragment && it.getArguments() != null && it.getArguments().getInt("POSITION") == id)
.findFirst();
optionalFragment.ifPresent(fragment -> {
// This is your needed PageFragment
});
Solutions with WeakReference did not worked for me because Android restores the state and createFragment is not always called, so I was always getting nulls.
Here is my try and seems to be woking fine:
val fragment = childFragmentManager.fragments.first {
it.lifecycle.currentState == Lifecycle.State.RESUMED
}
this will iterate over the fragments and return one which is RESUMED, meaning it will be the current selected fragment
I simply use this in my tab fragment
fun currentFragment() = childFragmentManager.fragments.find { it.isResumed }
There is no need to rely on tags. ViewPager2 hides implementation details, but we all know it has an inner recycler so the layout manager would do the trick. We can write something like this:
fun ViewPager2.getCurrentView(): View? {
return (getChildAt(0) as RecyclerView).layoutManager?.getChildAt(currentItem)
}
fun ViewPager2.getCurrentFragment(): Fragment? {
return getCurrentView().findFragment()
}
It would also works for for any given position not only the first one if you want to.
was facing same issue now its solved by adding one object in adapter
class MyViewPager2Adapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) {
private val FRAGMENTS_SIZE = 2
var currentFragmentWeakReference: WeakReference<Fragment>? = null
override fun getItemCount(): Int {
return this.FRAGMENTS_SIZE
}
override fun createFragment(position: Int): Fragment {
when (position) {
0 -> {
currentFragmentWeakReference= MyFirstFragment()
return MyFirstFragment()
}
1 -> {
currentFragmentWeakReference= MySecondFragment()
return MySecondFragment()
}
}
return MyFirstFragment() /for default view
}
}
after creating adapter I registered my Viewpager 2 with ViewPager2.OnPageChangeCallback()
and overrided its method onPageSelected
now simple did this trick to get current fragment
private fun getCurrentFragment() :Fragment?{
val fragment = (binding!!.pager.adapter as MyViewPager2Adapter).currentFragmentWeakReference?.get()
retrun fragment
}
I've only tested this with 2 fragments in ViewPager2
cheers guys , hope this mayhelp you.!

Categories

Resources