androidkotlinandroid-viewpagerfragmentpageradapter

java.lang.IllegalStateException: Fragment already added: MovieFragment


got error fragment already added on my MyPagerAdapter class which extends FragmentPagerAdapter

this is my error logs

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: id.cahyowhy.tmdbmovieimplementation, PID: 8228
    java.lang.IllegalStateException: Fragment already added: MovieFragment{df79747} (01cf33b8-9eb3-4dce-9519-384ca7dd7570) id=0x7f08014e android:switcher:2131231054:0}
        at androidx.fragment.app.FragmentStore.addFragment(FragmentStore.java:67)
        at androidx.fragment.app.FragmentManager.addFragment(FragmentManager.java:1563)
        at androidx.fragment.app.BackStackRecord.executeOps(BackStackRecord.java:405)
        at androidx.fragment.app.FragmentManager.executeOps(FragmentManager.java:2167)
        at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:1990)
        at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1945)
        at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1847)
        at androidx.fragment.app.FragmentManager$4.run(FragmentManager.java:413)
        at android.os.Handler.handleCallback(Handler.java:873)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:193)
        at android.app.ActivityThread.main(ActivityThread.java:6669)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)

this is my pageradapter code

class MyPagerAdapter(fm: FragmentManager) :
    FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {

    private val pages = listOf(
        MovieFragment.newInstance(MovieListType.POPULAR),
        MovieFragment.newInstance(MovieListType.TOP_RATED),
        MovieFragment.newInstance(MovieListType.UPCOMING)
    )

    override fun getItem(position: Int): Fragment {
        return pages[position]
    }

    override fun getCount(): Int {
        return pages.size
    }

    override fun getPageTitle(position: Int): CharSequence? {
        return when (position) {
            0 -> "Popular"
            1 -> "Top Rated"
            else -> "Up Coming"
        }
    }
}

this is my movie fragment code

package id.cahyowhy.tmdbmovieimplementation.ui.activity.fragment

import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import id.cahyowhy.tmdbmovieimplementation.databinding.MoviesFragmentResultBinding
import id.cahyowhy.tmdbmovieimplementation.ui.adapter.ItemTypeFactoryImpl
import id.cahyowhy.tmdbmovieimplementation.ui.adapter.VisitableRecyclerAdapter
import id.cahyowhy.tmdbmovieimplementation.ui.adapter.viewholder.ErrorStateItem
import id.cahyowhy.tmdbmovieimplementation.ui.adapter.viewholder.MovieItem
import id.cahyowhy.tmdbmovieimplementation.ui.base.BaseFragment
import id.cahyowhy.tmdbmovieimplementation.ui.base.BaseViewItem
import id.cahyowhy.tmdbmovieimplementation.ui.util.ext.observe
import org.koin.android.viewmodel.ext.android.viewModel

open class MovieFragment(
    private val movieListType: MovieListType
) : BaseFragment() {

    private val viewModel by viewModel<MovieFragmentViewModel>()

    private lateinit var binding: MoviesFragmentResultBinding

    private val movieAdapter by lazy {
        VisitableRecyclerAdapter(
            ItemTypeFactoryImpl(),
            ::onItemClicked
        )
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = MoviesFragmentResultBinding.inflate(
            inflater,
            container,
            false
        )

        return binding.root
    }

    override fun onResume() {
        super.onResume()
        viewModel.loadData(0, movieListType)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        with(binding) {
            recyclerView.adapter = movieAdapter
            recyclerView.setHasFixedSize(true)

            swipeRefresh.setOnRefreshListener { viewModel.loadData(0, movieListType) }
        }

        super.onViewCreated(view, savedInstanceState)
    }

    override fun observeChange() {
        observe(viewModel.loading, ::handleLoading)
        observe(viewModel.movies, ::onDataLoaded)
        observe(viewModel.toastMessage, ::showSnackbarMessage)
    }

    private fun handleLoading(status: Boolean) {
        binding.swipeRefresh.isRefreshing = status
    }

    private fun onDataLoaded(items: List<BaseViewItem>) {
        movieAdapter.submitList(items)
    }

    private fun onItemClicked(viewItem: BaseViewItem, view: View) {
        when (viewItem) {
            is MovieItem -> {
                Log.d("MovieItem", "MovieItem Click: ${viewItem.title}")
            }
            is ErrorStateItem -> {
                viewModel.loadData(0, movieListType)
            }
        }
    }

    companion object {
        fun newInstance(movieListType: MovieListType): MovieFragment =
            MovieFragment(movieListType)
    }
}

and this is when i called from MainActivity.kt

class MainActivity : BaseActivity() {
    private lateinit var binding: ActivityMainBinding;

    ....

    fun initView() {
        with(binding) {
            viewPager.adapter = MyPagerAdapter(supportFragmentManager)
            viewPagerTab.setupWithViewPager(viewPager)
        }
    }

can anyone solve this.. thanks..


Solution

  • The problem is MyFragment is a class and the FragmentManager is something that you get by calling supportFragmentManager. This is provided to you by the system. For the fragment manager to be able to add new fragments to the back stack, the Fragments provided to it should be unique.

    Now I do not know what your use case is but calling MyFragment.newInstance() does not provide you unique classes as they are just new instances of the same MyFragment class. The error log is stating that.

    To get around this you can create three different Fragment classes like PopularMoviesFragment, TopRatedMoviesFragment and UpcomingMoviesFragment and then add them to your list on the adapter class.

    I know it seems like a lot of work but this way you have three different fragments that shows data for three different cases of Movies and you can have additional logic in them. And you will also not hit java.lang.IllegalStateException: Fragment already added: MovieFragment error

    Your Adapter code where you call:

     private val pages = listOf(
            MovieFragment.newInstance(MovieListType.POPULAR),
            MovieFragment.newInstance(MovieListType.TOP_RATED),
            MovieFragment.newInstance(MovieListType.UPCOMING)
        )
    

    should now look like:

     private val pages = listOf(
            PopularMoviesFragment.newInstance(MovieListType.POPULAR),
            TopRatedMoviesFragment.newInstance(MovieListType.TOP_RATED),
            UpcomingMoviesFragment.newInstance(MovieListType.UPCOMING)
        )