android / views-widgets-samples

Multiple samples showing the best practices in views-widgets on Android.
Apache License 2.0
5.07k stars 3.01k forks source link

When you will resolve all problems with nested fragments in viewpager2?! #257

Closed georrge1994 closed 1 year ago

georrge1994 commented 1 year ago

Too muach bugs, hacks and problems with viewpager2! There is my perfect solution with nested fragments in an another fragment, but I still have huge problems with stability (the fcking detaching adapter from GC). No memory leaks! But nested fragments are alive and go throw fragment-life-circle. Every time when I leave from screen and return back (restarted both fragments) I got one additional instance of NoteListFragment! Previous viewPager1 was not optimized, but it works! You have to retire the person who created this sht!

NotesFragment - Class with viewpager2:

internal class NotesFragment : SearchToolbarFragment<NotesViewModel>(NotesViewModel::class) {
    private val viewBinding by viewBinding(FragmentNotesBinding::bind)
    private lateinit var zoomOutPageTransformer: ZoomOutPageTransformer

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        zoomOutPageTransformer = ZoomOutPageTransformer()
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
        inflater.inflate(R.layout.fragment_notes, container, false)

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewBinding.viewPager2.adapter = NotesViewPagerAdapter(childFragmentManager, viewLifecycleOwner.lifecycle)
        viewBinding.viewPager2.setPageTransformer(zoomOutPageTransformer)
        viewBinding.viewPager2.isSaveEnabled = false
        TabLayoutMediator(viewBinding.tabLayout, viewBinding.viewPager2) { tab, position ->
            tab.text = when (position) {
                0 -> context?.getString(R.string.notes_fragment_tab_title_by_lessons)
                1 -> context?.getString(R.string.notes_fragment_tab_title_own_notes)
                else -> context?.getString(R.string.notes_fragment_tab_title_own_notes)
            }
        }.attach()
    }

    override fun onDestroyView() {
        viewBinding.viewPager2.adapter = null
        super.onDestroyView()
    }
}

NotesViewPagerAdapter:

internal class NotesViewPagerAdapter(
    fragmentManager: FragmentManager,
    viewLifecycle: Lifecycle
) : FragmentStateAdapter(fragmentManager, viewLifecycle) {
    override fun getItemCount() = NUM_PAGES

    override fun createFragment(position: Int): Fragment = when (position) {
        0 -> NoteListFragment.newInstance(NotesTabTypes.BY_LESSONS)
        1 -> NoteListFragment.newInstance(NotesTabTypes.OWN_NOTES)
        else -> NoteListFragment.newInstance(NotesTabTypes.OWN_NOTES)
    }
}

NoteListFragment is inner fragment.

internal class NoteListFragment : NavigationFragment() {
    private val viewBinding by viewBinding(FragmentNoteListBinding::bind)
    private lateinit var adapter: NotesRecyclerViewAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        adapter = NotesRecyclerViewAdapter(requireContext(), noteActions).also { it.setHasStableIds(true) }
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
        inflater.inflate(R.layout.fragment_note_list, container, false)

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewBinding.recyclerView.adapter = adapter
    }

    override fun onDestroyView() {
        adapter.detachRecyclerView()
        viewBinding.recyclerView.adapter = null
        super.onDestroyView()
        println("NoteListFragment onDestroyView")
    }
}
georrge1994 commented 1 year ago

I have resolved my problem with manually removing the old fragments from fragmentManager. I don't reuse old detached adapters, so I don't to have old instances of nested fragments in fragment manager.

    override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
        super.onDetachedFromRecyclerView(recyclerView)
        fragmentManager.beginTransaction().apply {
            fragmentManager.fragments.forEach { fragment ->
                if (fragment is NoteListFragment) {
                    detach(fragment)
                }
            }
        }.commitAllowingStateLoss()
    }

UPDATE: Also I had to add the same code for onAttachedToRecyclerView before super call. Without it I still had a leak by screen rotation.