3

In my application I'm trying to create 2 fragments with ViewPagers. The fragments have 3 tabs/pages each with RecyclerViews and they are supposed to be backed by a database, so I'm keeping them in a List.

class MainActivity : AppCompatActivity() {    
    val fragments = listOf(SwipeFragment(), SwipeFragment())

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        supportFragmentManager.beginTransaction().replace(R.id.placeholder, fragments[0]).commit()
        button1.setOnClickListener { 
            supportFragmentManager.beginTransaction().replace(R.id.placeholder, fragments[0]).commit()
        }
        button2.setOnClickListener {
            supportFragmentManager.beginTransaction().replace(R.id.placeholder, fragments[1]).commit()
        }
    }
}

The problem is that after navigating away from the SwipeFragment and going back, the view seems empty. Then, if you navigate to e.g. the leftmost page, the rightmost one "appears" (when you go to it). This results in the middle page staying empty. (the reason for it being that FragmentStatePagerAdapter keeps only the current page and 2 adjacent ones by default. The middle one doesn't get refreshed in a layout with 3 tabs - I tried it also with 5 tabs and I'm able to bring all of them back by going back and forth).

After some debugging I saw that the fragments that represent pages don't get removed from the FragmentManager, but the main SwipeFragment is. I can't get my head around how FragmentManager really works, even after reading almost all of the source code.

If I were able to somehow safely remove the fragments from the FragmentManager, it may work.
I've tried some techniques for saving state, but the framework does that automatically (which might actually be the cause of this problem).
I'm just a beginner in Android, so there's probably a better way to do this anyway. I'll leave the rest of the files here for reference.


activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.example.test.MainActivity">

    <FrameLayout
        android:id="@+id/placeholder"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="0"
        android:orientation="horizontal">

        <Button
            android:id="@+id/button1"
            style="?android:buttonBarButtonStyle"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="button1" />

        <Button
            android:id="@+id/button2"
            style="?android:buttonBarButtonStyle"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="button2" />
    </LinearLayout>
</LinearLayout>

SwipeFragment.kt

class SwipeFragment : Fragment() {

    // TODO: set up and keep the database here

    override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {

        val view = inflater!!.inflate(R.layout.fragment_swipe, container, false)
        view.tab_layout.setupWithViewPager(view.pager)
        view.pager.adapter = DummyPagerAdapter(activity.supportFragmentManager, index)

        return view
    }
}

fragment_swipe.xml

<android.support.v4.view.ViewPager 
    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/pager"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.test.SwipeFragment">

    <android.support.design.widget.TabLayout
        android:id="@+id/tab_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

</android.support.v4.view.ViewPager>

ItemListFragment.kt

class ItemListFragment() : Fragment() {

    // Or keep the database here?
    // Probably not the best idea though

    override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {

        val view = inflater!!.inflate(R.layout.fragment_item_list, container, false)
        if (view is RecyclerView) {
            view.layoutManager = LinearLayoutManager(view.context)
            view.adapter = MyItemRecyclerViewAdapter(DummyContent.ITEMS)
        }

        return view
    }
}

DummyPagerAdapter.kt

class DummyPagerAdapter(manager: FragmentManager, val parentIndex: Int) :
        FragmentStatePagerAdapter(manager) {

    override fun getItem(position: Int): Fragment = ItemListFragment()
    override fun getPageTitle(position: Int): CharSequence = "${ position + 1 }"
    override fun getCount(): Int = 3
}

And a basic implementation of RecyclerView.Adapter generated by Android Studio

MyItemRecyclerViewAdapter.kt

class MyItemRecyclerViewAdapter(val values: List<DummyItem>) : 
        RecyclerView.Adapter<MyItemRecyclerViewAdapter.ViewHolder>() {
    ...
}

fragment_item_list.xml

<android.support.v7.widget.RecyclerView 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/list"
    android:name="com.example.test.ItemListFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutManager="LinearLayoutManager"
    tools:context="com.example.test.ItemListFragment"
    tools:listitem="@layout/fragment_item" />
Jakub Dąbek
  • 981
  • 1
  • 7
  • 15

2 Answers2

7

You should use childFragmentManager instead of activity.supportFragmentManager inside SwipeFragment.

view.pager.adapter = DummyPagerAdapter(childFragmentManager, index)

The difference between getSupportFragmentManager() and getChildFragmentManager() is discussed here.

Basically, the difference is that Fragment's now have their own internal FragmentManager that can handle Fragments. The child FragmentManager is the one that handles Fragments contained within only the Fragment that it was added to. The other FragmentManager is contained within the entire Activity.

BakaWaii
  • 5,602
  • 3
  • 23
  • 36
  • That's also one of the things I tried when searching for an answer, but the application keeps crashing. Here's the [stack trace](https://pastebin.com/3eKjrH7z). The main `FragmentManager` tries getting the state from its saved fragment array for some reason I can't figure out how to circumvent. – Jakub Dąbek Dec 16 '17 at 04:01
  • @JakubDąbek Is it possible for you to use `FragmentPagerAdapter` instead of `FragmentStatePagerAdapter` as suggested [here](https://stackoverflow.com/questions/18747975/difference-between-fragmentpageradapter-and-fragmentstatepageradapter)? I don't really know the reason of that error but it seems that it is caused by using `FragmentStatePagerAdapter`. – BakaWaii Dec 16 '17 at 06:38
  • FYR, this [post](https://stackoverflow.com/questions/45665709/using-fragmentstatepageadapter-to-retain-position-for-fragment-with-viewpager) seems to have the same problem. – BakaWaii Dec 16 '17 at 07:20
0

In my case childFragmentManager didn't help. I think we can use a pattern "Observer" to update existing fragments of ViewPager with new data when return from another fragment.

CoolMind
  • 16,738
  • 10
  • 131
  • 165