I'm currently using a ViewPager and TabLayout for a couple of similar fragments. The fragments hosted within the ViewPager all have a RecyclerView. When I swipe more than one page over, the Fragment (I think?) is destroyed. When I swipe back it's recreated.
Upon debugging, I found that the adapter within the Fragment is non null and populated with the data from before. However, once the fragment is visible it no longer displays any entries.
Here's a video of what's going on.
This is a fragment within the ViewPager
class ArtistListFragment : Fragment(), ListView {
override val title: String
get() = "Artists"
private val artistList: RecyclerView by lazy { artist_list }
#Inject lateinit var api: SaddleAPIManager
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View?
= container?.inflate(R.layout.fragment_list_artist)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
artistList.apply {
setHasFixedSize(true)
if (adapter == null) {
adapter = ViewTypeAdapter(mapOf(AdapterConstants.ARTIST to ArtistDelegateAdapter()))
}
layoutManager = LinearLayoutManager(context)
if (savedInstanceState != null && savedInstanceState.containsKey("artist")) {
(adapter as ViewTypeAdapter).setItems(savedInstanceState.getParcelableArrayList<Artist>("artists"))
}
}
(activity?.application as SaddleApplication).apiComponent.inject(this)
refreshData()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelableArrayList(
"artists",
(artistList.adapter as ViewTypeAdapter).items as ArrayList<out Parcelable>
)
}
private fun refreshData() = launch(UI) {
val result = withContext(CommonPool) { api.getArtists() }
when(result) {
is Success -> (artistList.adapter as ViewTypeAdapter).setItems(result.data.results)
is Failure -> Snackbar.make(artistList, result.error.message ?: "Error", Snackbar.LENGTH_LONG).show()
}
}
}
This is the Fragment hosting the ViewPager
class NavigationFragment : Fragment() {
private val viewPager: ViewPager by lazy { pager }
private val tabLayout: TabLayout by lazy { tab_layout }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View?
= container?.inflate(R.layout.fragment_navigation)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewPager.apply {
adapter = NavigationPageAdapter(childFragmentManager)
}
tabLayout.apply {
setupWithViewPager(viewPager)
}
}
}
The adapter I'm using for paging
class NavigationPageAdapter(fragmentManager: FragmentManager) : FragmentStatePagerAdapter(fragmentManager) {
companion object {
const val NUM_PAGES = 4
}
private val pages: List<Fragment> = (0 until NUM_PAGES).map {
when (it) {
0 -> ArtistListFragment()
1 -> AlbumListFragment()
2 -> TrackListFragment()
else -> PlaylistListFragment()
} as Fragment
}
override fun getItem(position: Int): Fragment = pages[position]
override fun getCount(): Int = NUM_PAGES
override fun getPageTitle(position: Int): CharSequence? = (pages[position] as ListView).title
}
I've tried overriding onSaveInstanceState and reading the information from the bundle. It doesn't seem to do anything. The problem seems to actually be the RecyclerView displaying? It's populated with data which is why I'm stumped.
Try to use setOffscreenPageLimit for ViewPager to keep containing fragments as below:
viewPager.setOffscreenPageLimit(NavigationPageAdapter.NUM_PAGES)
I've figured out the problem. While setOffScreenPageLimit(NavigationAdapter.NUM_PAGES) did work, I am cautious to use it because of memory consumption concerns.
As it turns out, storing references to views using lazy is bad practice. Since onCreateView gets called many times in the lifecycle of the ArtistListFragment the same view wasn't being referenced that was currently inflated on the screen.
Removing all lazy instantiated views and accessing them directly with the Android Extensions solved my problem.
Related
Hi folks I have a ViewPager2 with single activity architecture. When I click a button, I swap out the ViewPager2 host fragment with another one using the Jetpack Navigation library.
This calls onDestroyView for the host fragment. When I click back, we are back to onCreateView. How can I return to the ViewPager2 I was looking at, seeing as the host fragment itself is not destroyed?
I believe based on this answer that restoring a ViewPager2 is actually impossible, not sure if this is by design or not. So what is the best practice here, assuming each fragment loads a heavy list, am I supposed to reload all the data every time a user pops the backstack into my viewpager? The only thing I can think of is to have an activity scoped ViewModel which maintains the list of data for each fragment, which sounds ridiculous, imagine if my pages were dynamically generated or I had several recycler views on each fragment....
Here is my attempt, I am trying to do the bare minimum when navigating back, however without assigning the view pager adapter again, I am looking at a blank fragment tab. I don't understand this, the binding has not died, so why is the view pager not capable of restoring my fragment?
OrderTabsFragment.kt
var adapter: TabsPagerAdapter? = null
private var _binding: FragmentOrdersTabsBinding? = null
private val binding get() = _binding!!
private var initted = false
override fun onCreate(savedInstanceState: Bundle?) {
Timber.d("OrderTabsFragment $initted - onCreate $savedInstanceState")
super.onCreate(savedInstanceState)
adapter = TabsPagerAdapter(this, Tabs.values().size)
adapter?.currentTab = Tabs.valueOf(savedInstanceState?.getString(CURRENT_TAB) ?: Tabs.ACTIVE.name)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
Timber.d("OrderTabsFragment $initted - onCreateView $savedInstanceState, _binding=$_binding")
if(_binding == null)
_binding = FragmentOrdersTabsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
Timber.d("OrderTabsFragment $initted - onViewCreated $savedInstanceState")
super.onViewCreated(view, savedInstanceState)
if(!initted) {
initted = true
val viewpager = binding.viewpager
viewpager.adapter = adapter
viewpager.isSaveEnabled = false
binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {}
override fun onTabUnselected(tab: TabLayout.Tab?) {}
override fun onTabReselected(tab: TabLayout.Tab?) {
if (adapter?.currentTab == Tabs.FILTERED) {
showFilterBalloon(tab)
}
}
})
TabLayoutMediator(binding.tabLayout, viewpager) { tab, position ->
when (position) {
0 -> tab.text = getString(R.string.title_active).uppercase(Locale.getDefault())
1 -> tab.text =
getString(R.string.title_scheduled).uppercase(Locale.getDefault())
2 -> tab.text =
getString(R.string.title_complete).uppercase(Locale.getDefault())
}
}.attach()
}
else{
val viewpager = binding.viewpager
viewpager.adapter = adapter //Required otherwise we are looking at a blank fragment tab. The adapter rv was detached and can't be reattached?
viewpager.isSaveEnabled = false //Required otherwise "Expected the adapter to be 'fresh' while restoring state."
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
Timber.d("OrderTabsFragment $initted - onSaveInstanceState")
outState.putString(CURRENT_TAB, adapter?.currentTab?.name)
}
override fun onDestroy() {
super.onDestroy()
Timber.d("OrderTabsFragment $initted - onDestroy")
binding.viewpager.adapter = null
_binding = null
adapter = null
}
enum class Tabs {
ACTIVE, SCHEDULED, COMPLETE, FILTERED
}
Edit:
Here's roughly the same questions coming up in other places 1, 2, 3
Problem:
i have a layout like this:
in the parent fragment i have a searchview responsible for filtering the recyclerviews inside the child fragments.
the child fragments are inside a tablayout viewpager combination.
so the OnQueryTextListener is inside the parent fragment and i have to get an instance of the recyclerviews adapters from child fragments to do the filtering.
what is the proper way to do this?
What I've tried:
i searched and found getChildFramgentManager which returns an instance of the child fragment. but apparently, the fragments should be created dynamically? im not sure how that works with viewpager. so if there is a way to do this with getChildFramgentManager and viewpager please explain.
My code:
everything I've written is the standard procedure and im not that far into the project to add something that changes the outcome so im not gonna take your time by adding the code, but if the code is necessary please say so and i will add it.
In your viewpager's adapter you can store references to child fragments into a list and iterate through child fragments to call a method.
Something like this :
Parent fragment
class ParentFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.parent_fragment, container, false)
val search : SearchView = findViewById(R.id.search)
val pager : ViewPager = findViewById(R.id.pager)
val adapter = ViewPagerAdapter(supportFragmentManager)
pager.adapter = adapter
search.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
adapter.filter(query ?: "")
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
return true
}
})
return view
}
}
Pager adapter
class ViewPagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) {
private var mList : MutableList<ChildFragment> = ArrayList()
init {
mList.add(ChildFragment())
mList.add(ChildFragment())
}
fun filter(text: String) {
mList.forEach {
it.filter(text)
}
}
override fun getItem(position: Int): Fragment {
return mList.get(position)
}
override fun getCount(): Int {
return mList.size
}
}
Child fragment
class ChildFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.child_fragment, container, false)
// get reference to recyclerview, set adapter...
return view
}
fun filter(text: String) {
// do job here
}
}
I have a fragment called MainFragment that contains a ViewPager that contains another fragment called LibraryFragment.
LibraryFragment contains a RecyclerView with a list of items that contain an ImageView. The ImageView's contents are loaded with Coil.
When an item is clicked, LibraryFragment navigates to another fragment called ArtistDetailFragment and uses the ImageView from the item.
The problem is that while the enter transition works fine, the ImageView does not return to the list item when navigating back and only fades away. Ive attached an example below:
Ive tried using postponeEnterTransition() and startPostponedEnterTransition() along with adding a SharedElementCallback but neither have worked that well. I've also ruled out Coil being the issue.
Heres LibraryFragment:
class LibraryFragment : Fragment() {
private val musicModel: MusicViewModel by activityViewModels()
private val libraryModel: LibraryViewModel by activityViewModels()
private lateinit var binding: FragmentLibraryBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentLibraryBinding.inflate(inflater)
binding.libraryRecycler.adapter = ArtistAdapter(
musicModel.artists.value!!,
BindingClickListener { artist, itemBinding ->
navToArtist(artist, itemBinding)
}
)
return binding.root
}
private fun navToArtist(artist: Artist, itemBinding: ItemArtistBinding) {
// When navigation, pass the artistImage of the item as a shared element to create
// the image popup.
findNavController().navigate(
MainFragmentDirections.actionShowArtist(artist.id),
FragmentNavigatorExtras(
itemBinding.artistImage to itemBinding.artistImage.transitionName
)
)
}
}
Heres ArtistDetailFragment:
class ArtistDetailFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = FragmentArtistDetailBinding.inflate(inflater)
sharedElementEnterTransition = TransitionInflater.from(requireContext())
.inflateTransition(android.R.transition.move)
val musicModel: MusicViewModel by activityViewModels()
val artistId = ArtistDetailFragmentArgs.fromBundle(requireArguments()).artistId
// Get the transition name used by the recyclerview ite
binding.artistImage.transitionName = artistId.toString()
binding.artist = musicModel.artists.value?.find { it.id == artistId }
return binding.root
}
}
And heres the RecyclerView Adapter/ViewHolder:
class ArtistAdapter(
private val data: List<Artist>,
private val listener: BindingClickListener<Artist, ItemArtistBinding>
) : RecyclerView.Adapter<ArtistAdapter.ViewHolder>() {
override fun getItemCount(): Int = data.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
ItemArtistBinding.inflate(LayoutInflater.from(parent.context))
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(data[position])
}
// Generic ViewHolder for an artist
inner class ViewHolder(
private val binding: ItemArtistBinding
) : RecyclerView.ViewHolder(binding.root) {
// Bind the view w/new data
fun bind(artist: Artist) {
binding.artist = artist
binding.root.setOnClickListener { listener.onClick(artist, binding) }
// Update the transition name with the new artist's ID.
binding.artistImage.transitionName = artist.id.toString()
binding.executePendingBindings()
}
}
}
EDIT: I used postponeEnterTransition and startPostponedEnterTransition like this:
// LibraryFragment
override fun onResume() {
super.onResume()
postponeEnterTransition()
// Refresh the parent adapter to make the image reappear
binding.libraryRecycler.adapter = artistAdapter
// Do the Pre-Draw listener
binding.libraryRecycler.viewTreeObserver.addOnPreDrawListener {
startPostponedEnterTransition()
true
}
}
This only makes the RecyclerView itself refresh however, the shared element still fades away instead of returning to the RecyclerView item.
Chris Banes says;
You may wonder why we set the OnPreDrawListener on the parent rather than the view itself. Well that is because your view may not actually be drawn, therefore the listener would never fire and the transaction would sit there postponed forever. To work around that we set the listener on the parent instead, which will (probably) be drawn.
https://chris.banes.dev/fragmented-transitions/
try this;
change
// LibraryFragment
override fun onResume() {
super.onResume()
postponeEnterTransition()
// Refresh the parent adapter to make the image reappear
binding.libraryRecycler.adapter = artistAdapter
// Do the Pre-Draw listener
binding.libraryRecycler.viewTreeObserver.addOnPreDrawListener {
startPostponedEnterTransition()
true
}
}
enter code here
to
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
postponeEnterTransition()
binding = FragmentLibraryBinding.inflate(inflater)
binding.libraryRecycler.adapter = ArtistAdapter(
musicModel.artists.value!!,
BindingClickListener { artist, itemBinding ->
navToArtist(artist, itemBinding)
}
)
(view?.parent as? ViewGroup)?.doOnPreDraw {
// Parent has been drawn. Start transitioning!
startPostponedEnterTransition()
}
return binding.root
}
I have a Fragment that contains a ViewPager. When I navigate to this fragment I see the first page as expected. However, if I hit back, and then navigate to that fragment again, I am then presented with a blank white screen...
What I'm noticing in the Layout Inspector is that when I navigate to the ViewPager Fragment the second time, the ViewPager is present, but none of the page fragments are in the hierarchy.
Here is how I set up my viewPager in my ViewPagerFragment:
/**
* Variables
*/
var pageOneFragment = ForgotPasswordInitiateFragment()
var pageTwoFragment = ForgotPasswordSubmitCodeFragment()
var pageThreeFragment = ForgotPasswordSubmitPasswordFragment()
lateinit var mViewPager: ForgotPasswordViewPager
lateinit var mPagerAdapter: ForgotPasswordPagerAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.forgot_password_pager_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
forgotPasswordViewModel = ViewModelProviders.of(this, viewModelFactory)
.get(ForgotPasswordViewModel::class.java)
pageOneFragment.viewPager = this
pageTwoFragment.viewPager = this
pageThreeFragment.viewPager = this
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
activity?.let {
mPagerAdapter = ForgotPasswordPagerAdapter(it.supportFragmentManager)
}
mViewPager = viewPager
mViewPager.adapter = mPagerAdapter
mViewPager.setListeners()
}
override fun onResume() {
super.onResume()
}
inner class ForgotPasswordPagerAdapter(fm: FragmentManager): FragmentPagerAdapter(fm) {
override fun getItem(position: Int): Fragment {
when (position) {
0 -> return pageOneFragment
1 -> return pageTwoFragment
2 -> return pageThreeFragment
else -> throw IllegalStateException("Pager position $position is out of bounds.")
}
}
override fun getCount(): Int {
return 3
}
}
I attempted to set all my mViewPager configurations into the onResume function but the same thing happened.
When does the viewPager actually populate its pages? Because what it seems like to me is they are populated the first time, but not the second....
EDIT:
I debugged the Activity's fragments on the back pressed and noticed that the pages were in that fragment stack, but not the actual viewPager fragment.
You're using
activity?.let {
mPagerAdapter = ForgotPasswordPagerAdapter(it.supportFragmentManager)
}
I.e., using the activity's FragmentManager. That's always the wrong thing to do and the source of your issue. Instead, you need to use the Fragment's childFragmentManager:
mPagerAdapter = ForgotPasswordPagerAdapter(childFragmentManager)
I want to create a dialog which contain's ViewPager inside it which have 3 pages and all pages have different layout structure. I want a solution by that i can set the layout content programmatically . I think this can be done by making fragments for each page but i don't know how to do this.
I go through these answers but i am not getting idea how to use them in my case.
Viewpager in Dialog?
ViewPager in Custom Dialog
ViewPager in simple Dialog
You can try and build your custom dialog through DialogFragment. Consider the XML layout would contain a ViewPager and the code to go about would be:
class PagerDialog : DialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.element_fragment_pager_dialog, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupPager()
}
private fun setupPager() {
val pagerFragment1 = PagerFragment1.newInstance()
val pagerFragment2 = PagerFragment2.newInstance()
val pagerFragment3 = PagerFragment3.newInstance()
viewPager?.adapter = MyFragmentPagerAdapter(childFragmentManager).apply {
adapterReference = object : PageAdapterInterface {
override var fragmentList: List<Fragment> =
mutableListOf(pagerFragment1, pagerFragment2, pagerFragment3)
}
}
}
companion object {
const val tag = "PagerDialog"
}
}
I have used reference to the list because it might cause leaks when not handled correctly. So the PagerAdapterInterface would look like:
interface PageAdapterInterface {
var fragmentList: List<Fragment>
fun getItemCount() = fragmentList.size
#Throws(StackOverflowError::class)
fun getItemAt(index: Int) : Fragment {
if (index >= fragmentList.size) throw StackOverflowError()
return fragmentList[index]
}
}
Your view pager adapter can make use of this reference in manner that is accessing referentially like:
class MyFragmentPagerAdapter(childFragmentManager: FragmentManager) : FragmentStatePagerAdapter(childFragmentManager){
lateinit var adapterReference: PageAdapterInterface
override fun getItem(p0: Int): Fragment = adapterReference.getItemAt(p0)
override fun getCount(): Int = adapterReference.getItemCount()
}
Finally in your Activity or Fragment on create() or onViewCreated() functions respectively, you can initialize the dialog as shown:
class MyActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// use childFragmentManager if the code is
// used within the Fragment
val prev = supportFragmentManager.findFragmentByTag(PagerDialog.tag)
if (prev != null) {
supportFragmentManager.beginTransaction()
.remove(prev)
.addToBackStack(null)
.commit()
}
PagerDialog().show(supportFragmentManager, PagerDialog.tag)
}
}
Note: DialogFragment is deprecated on > API 28 check out https://developer.android.com/reference/android/app/DialogFragment