ViewPager activates its fragments before usage - android

I'm using a ViewPager to slide between different fragments as tabs. I noticed that just by creating the adapter, the onCreateView function inside each fragment is being called immediately, which is no good since I would like to run the code inside these functions at a specific given time.
My code in MainActivity:
private var profileTab: TabLayout.Tab? = null
private var swipeTab: TabLayout.Tab? = null
private var matchesTab: TabLayout.Tab? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
profileTab = navigationTabs.newTab()
swipeTab = navigationTabs.newTab()
matchesTab = navigationTabs.newTab()
navigationTabs.addTab(profileTab!!)
navigationTabs.addTab(swipeTab!!)
navigationTabs.addTab(matchesTab!!)
val adapter = PagerAdapter(supportFragmentManager, navigationTabs.tabCount)
fragmentContainer.adapter = adapter
My code of PagerAdapter.kt:
class PagerAdapter (fm: FragmentManager, private val mNoOfTabs: Int) : FragmentStatePagerAdapter(fm, mNoOfTabs) {
override fun getItem(position: Int): Fragment {
when (position) {
0 -> {
return ProfileFragment()
}
1 -> {
return SwipeFragment()
}
2 -> {
return MatchesFragment()
}
}
return null!!
}
override fun getCount() = mNoOfTabs
}
And in activity_main.xml (if it matters):
<com.google.android.material.tabs.TabLayout
android:id="#+id/navigationTabs"
android:layout_width="match_parent"
android:layout_height="#dimen/navigation_height"
android:background="#drawable/navigation_shadow"
app:layout_constraintTop_toTopOf="parent"
app:tabIndicator="#null"
app:tabMinWidth="#dimen/navigation_height"
app:tabRippleColor="#null" />
<!-- // the fragment will be displayed here:-->
<androidx.viewpager.widget.ViewPager
android:id="#+id/fragmentContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="100dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="#+id/navigationTabs"/>
Is this the default behavior? Can it be prevented, so that the fragments won't be initiated immediately?

ViewPager will initialize two fragments minimally.
ViewPager allows us to set setOffscreenPageLimit(), but it require at least 1, so it will initialize first and next fragment.
Read more here ViewPager.setOffscreenPageLimit(0) doesn't work as expected
Try to invoke code in fragments by setting onPageChangedListener() in MainActivity, so you can execute code when user swiped/opened fragment.

ViewPager in Android by default, loads 2 more fragments with current fragment. That is one previous fragment and one next fragment for smooth animation. And you can not alter this default functionality.
One hack to achieve what you want is to use setUserVisibleHint method of fragment. this will called whenever fragments gets visible. But I am not recommending you to use this method

Related

How to perform certain operations in a fragment inside a view pager before the activity is destroyed

My activity layout is as follows
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:orientation="vertical"
android:id="#+id/fragment_container"
android:layout_height="match_parent">
<androidx.viewpager2.widget.ViewPager2
android:id="#+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.tabs.TabLayout
android:id="#+id/inbox_tab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="#style/TabLayoutStyle"
/>
</androidx.viewpager2.widget.ViewPager2>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
The adapter is given below
class InboxAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
override fun getItemCount(): Int = 2
override fun createFragment(position: Int): Fragment =
InboxFragment().apply {
arguments = Bundle().apply {
// Our object is just an integer :-P
putInt(InboxFragment.CURRENT_TAB, position)
}
}
}
The fragment layout consists of a single recycler view which is created, and intialised in onCreateView method.
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="LinearLayoutManager"/>
My activity makes a call to API endpoint to get two sets of lists representing a combination of read, and unread notifications, each of which is populated in the recycler view of the particular fragment.
The activity stores the value in two sets of live data instances and passes the data between the two fragments like the tutorial suggests. This is working well.
ISSUE
I have to send a PUT request to the backend in which the request body consists of list of unread notifications for the fragment which is current in the foreground.
when the user switches tabs
when the user leaves the screen
For the first option, I can do something like this in my fragment as follows:
override fun onPause() {
// Aggregates the list of unread notifications, and submits a PUT request.
// Updates the view model live data backing list as well to mark as read.
// View Model is initialised as per the tutorial
inboxViewModel.markNotificationsAsRead(arguments?.getInt(CURRENT_TAB, -1) ?: 0)
super.onPause()
}
This fails for the 2nd use case when the user leaves the screen, thereby destroying the activity. In this case, both the fragments would be paused before their view is destroyed. This results in both the unread notifications in both the list to be marked as read.
Consider the use case in which the user is on the first fragment (which is loaded by default), then leaves the inbox screen to navigate to another screen which managed by another activity, then I want just the unread notifications displayed in the first fragment to be marked as read.
Since the 2nd fragment was not loaded, the unread notifications that would have been displayed in the 2nd fragment should not be marked as read.
How can I achieve this?
Mark the message as read when fragment be visible. So you don't need to mark whether the message was read or not when the Activity is destroyed.
Or adding a public field to fragment to tag whether fragment has shown. Then according this tag to call inboxViewModel.markNotificationsAsRead.
How to determine if the fragment is displayed
------------------------------------20210-11-12------------------------
Demo code snippet
interface ICheckFragment {
fun markShown()
fun hasShown(): Boolean
}
// in your adapter
xxxAdapter: FragmentPagerAdapter(fm) {
private val fragments = mutableListOf<Fragment>()
init {
val fragment = PlaceholderFragment.newInstance(1)
val fragment2 = PlaceholderFragment.newInstance(2)
fragments.add(fragment)
fragments.add(fragment2)
}
override fun getItem(position: Int): Fragment {
return fragments[position]
}
}
// in your activity code
tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
// Please notice that this callback not be called at first when viewpager shown.
// You need mark fragment at position 0 shown manually
override fun onTabSelected(tab: TabLayout.Tab?) {
val position = tab?.position ?: 0
val fragment = sectionsPagerAdapter.getItem(position)
if (fragment is ICheckFragment) {
fragment.markShown()
}
}
override fun onTabUnselected(tab: TabLayout.Tab?) = Unit
override fun onTabReselected(tab: TabLayout.Tab?) = Unit
})
val fragment0 = sectionsPagerAdapter.getItem(0)
if (fragment is ICheckFragment) {
fragment.markShown()
}
// in your fragment
class xxxFragment: Fragment(), ICheckFragment {
...
private var hasShown = false
override fun markShown() {
hasShown = true
}
override fun hasShown(): Boolean {
return hasShown
}
override fun onPause() {
super.onPause()
// do your logic
if (hasShown) {
} else {
}
}
}

PagingDataAdapter stops loading after fragment is removed and added back

I am presenting a PagingSource returned by Room ORM on a PagingDataAdapter.
The RecyclerView is present on a Fragment -- I have two such fragments. When they are switched, they stop loading the items on next page and only placehodlers are shown on scrolling.
Please view these screen captures if it isn't clear what I mean--
When I scroll without switching fragments, all the items are loaded
When I switch Fragments before scrolling all the way down, the adapter stops loading new items
Relevant pieces of code (please ask if you would like to see some other part/file) -
The Fragment:
private lateinit var recyclerView: RecyclerView
private val recyclerAdapter = CustomersAdapter(this)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView = view.findViewById(R.id.recycler_view)
recyclerView.adapter = recyclerAdapter
recyclerView.layoutManager = LinearLayoutManager(context)
viewLifecycleOwner.lifecycleScope.launch {
viewModel.customersFlow.collectLatest { pagingData ->
recyclerAdapter.submitData(pagingData)
}
}
}
View model-
class CustomersListViewModel(application: Application, private val debtOnly: Boolean): ViewModel() {
private val db = AppDatabase.instance(application)
private val customersDao = db.customersDao()
val customersFlow = Pager(PagingConfig(20)) {
if (debtOnly)
customersDao.getAllDebt()
else
customersDao.getAll()
}.flow.cachedIn(viewModelScope)
}
After I went through your code, I found the problem FragmentTransaction.replace function and flow.cachedIn(viewModelScope)
When the activity calls the replace fragment function, the CustomerFragment will be destroyed and its ViewModel will also be destroyed (the viewModel.onCleared() is triggered) so this time cachedIn(viewModelScope) is also invalid.
I have 3 solutions for you
Solution 1: Remove .cachedIn(viewModelScope)
Note that this is only a temporary solution and is not recommended.
Because of this, instances of fragments still exist on the activity but the fragments had destroyed (memory is still leaking).
Solution 2: Instead of using the FragmentTransaction.replace function in the Main activity, use the FragmentTransaction.add function:
It does not leak memory and can still use the cachedIn function. Should be used when the activity has few fragments and the fragment's view is not too complicated.
private fun switchNavigationFragment(navId: Int) {
when (navId) {
R.id.nav_customers -> {
switchFragment(allCustomersFragment, "Customer")
}
R.id.nav_debt -> {
switchFragment(debtCustomersFragment, "DebtCustomer")
}
}
}
private fun switchFragment(fragment: Fragment, tag: String) {
val existingFragment = supportFragmentManager.findFragmentByTag(tag)
supportFragmentManager.commit {
supportFragmentManager.fragments.forEach {
if (it.isVisible && it != fragment) {
hide(it)
}
}
if (existingFragment != fragment) {
add(R.id.fragment_container, fragment, tag)
.disallowAddToBackStack()
} else {
show(fragment)
}
}
}
Solution 3: Using with Navigation Component Jetpack
This is the safest solution.
It can be created using Android Studio's template or some of the following articles.
Navigation UI
A safer way to collect flows
I tried solution 1 and 2 and here is the result:

ViewPager2 selectCurrentItem - select Tab, but place wrong Fragment inside this tab

I have basic ViewPager2 with Tablayout - and in each page I have different fragments.
When I need to open this view NOT from first (default) tab I'm doing smth like this:
viewPager.currentItem = selectedTabPosition
This code select tab, but inside it is opening fragment from first tab! Only when I'm selecting the tabs by tapping on it - I can see the right fragment in each tabs.
I've also try to select with Tablayout like this:
tabLayout.getTabAt(position)?.select()
But this code doesn't help and also work with this bug.
Also I've try to set viewPager.currentItem with post / postDelay - but this doesn't work too.
Maybe I've lost something? Or it is a bug in ViewPager2 ?
(EDIT - ViewPager code)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupPagerAdapter()
}
private fun setupPagerAdapter() {
val adapter = MainDocumentScreenPagerAdapter(this)
binding?.viewPager?.letUnit {
it.adapter = adapter
binding?.tabsPagerView?.attachViewPager(requireContext(), it, adapter)
// set tab
it.currentItem = params.pageType.ordinal
}
Adapter code
class MainDocumentScreenPagerAdapter (fragment: Fragment) : ViewPager2TitleAdapter(fragment) {
override fun getItemCount(): Int = DocumentPageType.values().size
override fun createFragment(position: Int): Fragment {
val pageType = DocumentPageType.values().firstOrNull { it.ordinal == position } ?: throw IllegalStateException()
val params = DocumentListFragment.createParams(pageType)
return DocumentListFragment.newInstance(params)
}
override fun getPageTitle(position: Int): Int? {
return when (position) {
DocumentPageType.ALL.ordinal -> DocumentPageType.ALL.title
DocumentPageType.SIGN.ordinal -> DocumentPageType.SIGN.title
DocumentPageType.ACCEPT.ordinal -> DocumentPageType.ACCEPT.title
DocumentPageType.CONFIRM.ordinal -> DocumentPageType.CONFIRM.title
DocumentPageType.REJECT.ordinal -> DocumentPageType.REJECT.title
else -> null
}
}
Where ViewPager2TitleAdapter is :
abstract class ViewPager2TitleAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
abstract fun getPageTitle(position: Int): Int?
DocumentListFragment inside it create view, based on param object.
I've also try to create adapter inside OnCreate - but it doesn't affect this case.
The last, but not least - when I've try to open tab, which is out from screen (I've scrollable tabs) - viewPager open selected tab with correct fragment on it.. So, the problem occurs only when I've try to open first 4 tabs (look at the image), which is showing on screen. Bit starting from 5 and next tabs - has selected correct.
So, the decision is in this line of code:
it.setCurrentItem(params.pageType.ordinal, false)
but I was doing it like this:
it.currentItem = params.pageType.ordinal
boolean false make magic in this case - It disable smooth scroll. I've got it from this answer about ViewPager2:
https://stackoverflow.com/a/67319847/4809482
I think an easier more reliable fix is to defer to next run cycle instead of unsecure delay e.g
viewPager.post {
viewPager.setCurrentItem(1, true)
}

Android Navigation Component back button not working

I'm using the Navigation Component in android where I have set 6 fragments initially. The problem is when I added a new fragment (ProfileFragment).
When I navigate to this new fragment from the start destination, pressing the native back button does not pop the current fragment off. Instead, it just stays to the fragment I'm in - the back button does nothing.
Here's my navigation.xml:
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/dashboard_navigation"
app:startDestination="#id/dashboardFragment"
>
<fragment
android:id="#+id/dashboardFragment"
android:name="com.devssocial.localodge.ui.dashboard.ui.DashboardFragment"
android:label="DashboardFragment"
>
<action
android:id="#+id/action_dashboardFragment_to_newPostFragment"
app:destination="#id/newPostFragment"
app:enterAnim="#anim/slide_in_up"
app:exitAnim="#anim/slide_out_down"
app:popEnterAnim="#anim/slide_in_up"
app:popExitAnim="#anim/slide_out_down"
/>
<action
android:id="#+id/action_dashboardFragment_to_notificationsFragment"
app:destination="#id/notificationsFragment"
app:enterAnim="#anim/slide_in_up"
app:exitAnim="#anim/slide_out_down"
app:popEnterAnim="#anim/slide_in_up"
app:popExitAnim="#anim/slide_out_down"
/>
<action
android:id="#+id/action_dashboardFragment_to_mediaViewer"
app:destination="#id/mediaViewer"
app:enterAnim="#anim/slide_in_up"
app:exitAnim="#anim/slide_out_down"
app:popEnterAnim="#anim/slide_in_up"
app:popExitAnim="#anim/slide_out_down"
/>
<action
android:id="#+id/action_dashboardFragment_to_postDetailFragment"
app:destination="#id/postDetailFragment"
app:enterAnim="#anim/slide_in_up"
app:exitAnim="#anim/slide_out_down"
app:popEnterAnim="#anim/slide_in_up"
app:popExitAnim="#anim/slide_out_down"
/>
====================== HERE'S THE PROFILE ACTION ====================
<action
android:id="#+id/action_dashboardFragment_to_profileFragment"
app:destination="#id/profileFragment"
app:enterAnim="#anim/slide_in_up"
app:exitAnim="#anim/slide_out_down"
app:popEnterAnim="#anim/slide_in_up"
app:popExitAnim="#anim/slide_out_down"
/>
=====================================================================
</fragment>
<fragment
android:id="#+id/profileFragment"
android:name="com.devssocial.localodge.ui.profile.ui.ProfileFragment"
android:label="fragment_profile"
tools:layout="#layout/fragment_profile"
/>
</navigation>
In the image above, the highlighted arrow (in the left) is the navigation action I'm having troubles with.
In my Fragment code, I'm navigating as follows:
findNavController().navigate(R.id.action_dashboardFragment_to_profileFragment)
The other navigation actions are working as intended. But for some reason, this newly added fragment does not behave as intended.
There are no logs showing when I navigate to ProfileFragment and when I press the back button.
Am I missing something? or is there anything wrong with my action/fragment configurations?
EDIT:
I do not do anything in ProfileFragment. Here's the code for it:
class ProfileFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_profile, container, false)
}
}
And my activity xml containing the nav host:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="#+id/dashboard_navigation"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:navGraph="#navigation/dashboard_navigation"
app:defaultNavHost="true"/>
</FrameLayout>
if you are using setupActionBarWithNavController in Navigation Component such as:
setupActionBarWithNavController(findNavController(R.id.fragment))
then also override and config this methods in your main activity:
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.fragment)
return navController.navigateUp() || super.onSupportNavigateUp()
}
My MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupActionBarWithNavController(findNavController(R.id.fragment))
}
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.fragment)
return navController.navigateUp() || super.onSupportNavigateUp()
}
}
For anyone using LiveData in a previous Fragment which is a Home Fragment, whenever you go back to the previous Fragment by pressing back button the Fragment is starting to observe the data and because ViewModel survives this operation it immediately emits the last emitted value which in my case opens the Fragment from which I pressed the back button, that way it looks like the back button is not working the solution for this is using something that emits data only once. I used this :
class SingleLiveData<T> : MutableLiveData<T>() {
private val pending = AtomicBoolean()
/**
* Adds the given observer to the observers list within the lifespan of the given
* owner. The events are dispatched on the main thread. If LiveData already has data
* set, it will be delivered to the observer.
*
* #param owner The LifecycleOwner which controls the observer
* #param observer The observer that will receive the events
* #see MutableLiveData.observe
*/
#MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner, Observer { t ->
if (pending.compareAndSet(true, false)) {
observer.onChanged(t)
}
})
}
/**
* Sets the value. If there are active observers, the value will be dispatched to them.
*
* #param value The new value
* #see MutableLiveData.setValue
*/
#MainThread
override fun setValue(value: T?) {
pending.set(true)
super.setValue(value)
}
This problem happened to me while using MutableLiveData to navigate between fragments and was observing the live data object at more than one fragment.
I solved it by observing the live data object one time only or by using SingleLiveEvent instead of MutableLiveData. So If you're having the same scenario here, try to observe the live data object one time only or use SingleLiveEvent.
You can use this following for the Activity
onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
onBackPressed()
// if you want onBackPressed() to be called as normal afterwards
}
}
)
For the fragment, It will be needed requireActivity() along with Callback
requireActivity().onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
requireActivity().onBackPressed()
// if you want onBackPressed() to be called as normal afterwards
}
}
)
If you have a Button or something else to perform an action then you can use
this.findNavController().popBackStack()
You need to set the MutableLiveData to null once the navigation is done.
For example
private val _name = MutableLiveData<String>()
val name: LiveData<String>
get() = _name
fun printName(){
_name.value = "John"
}
fun navigationComplete(){
_name.value = null
}
Now say you are observing the "name" in your fragment and you are doing some navigation once the name is John then should be like that:
viewModel.name.observe(viewLifecycleOwner, Observer { name ->
when (name) {
"John" -> {
this.findNavController() .navigate(BlaBlaFragmentDirections.actionBlaBlaFragmentToBlaBlaFragment())
viewModel.navigationComplete()
}
}
})
Now your back button will be working without a single problem.
Some data are almost used only once, like a Snackbar message or navigation event therefore you must tell set the value to null once done used.
The problem is that the value in _name remains true and itโ€™s not possible to go back to previous fragment.
If you use Moxy or similar libs, checkout the strategy when you navigate from one fragment to second.
I had the same issue when strategy was AddToEndSingleStrategy.
You need change it to SkipStrategy.
interface ZonesListView : MvpView {
#StateStrategyType(SkipStrategy::class)
fun navigateToChannelsList(zoneId: String, zoneName: String)
}
Call onBackPressed in OnCreateView
private fun onBackPressed() {
requireActivity().onBackPressedDispatcher.addCallback(this) {
//Do something
}
}
For everyone who is using LiveData for setting navigation ids, there's no need to use SingleLiveEvent. You can just set the destinationId as null after you set its initial value.
For instance if you want to navigate from Fragment A to B.
ViewModel A:
val destinationId = MutableLiveData<Int>()
fun onNavigateToFragmentB(){
destinationId.value = R.id.fragmentB
destinationId.value = null
}
This will still trigger the Observer in the Fragment and will do the navigation.
Fragment A
viewModel.destinationId.observe(viewLifecycleOwner, { destinationId ->
when (destinationId) {
R.id.fragmentB -> navigateTo(destinationId)
}
})
The Simplest Answer for your problem (If it has something to do with fragments - Bottom navigation) could be
To set defaultNavHost = "false"
From Official Documentation it says->
Let's say you have 3 fragments set for Bottom Navigation, then setting
"defaultNavHost = true" will make fragment A acts like a parent, so when user clicks on back button in fragment 3 , it comes to fragment 1 instead of closing the activity (Bottom Navigation as Example).
Your XML should look like this, if you wanna just press back and close the activity from any fragment you are in.
<fragment
android:id="#+id/fragmentContainerView"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="#+id/bottom_nav"
app:defaultNavHost="false"
app:navGraph="#navigation/visit_summary_navigation" />
Set the MutableLiveData to false after navigation
Put this code in your ViewModel.kt
private val _eventNextFragment = MutableLiveData<Boolean>()
val eventNextFragment: LiveData<Boolean>
get() = _eventNextFragment
fun onNextFragment() {
_eventNextFragment.value = true
}
fun onNextFragmentComplete(){
_eventNextFragment.value = false
}
Let's say you want to navigate to another fragment, you'll call the onNextFragmentComplete method from the viewModel immediately after navigating action.
Put this code in your Fragment.kt
private fun nextFragment() {
val action = actionFirstFragmentToSecondFragment()
NavHostFragment.findNavController(this).navigate(action)
viewModel.onNextFragmentComplete()
}
I had faced the same issue due to the below "run blocking" code block. So don't use it if not necessary.

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