Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

RecyclerView in Fragment crashes when navigating to another Fragment

I've been following examples on the web to create a RecyclerView. The only thing I made different is to put the RecyclerView in a Fragment instead of having it in the MainActivity. The RecyclerView displays nicely with the data. But when I navigate to another Fragment the App crashes with an Exception relating to RecyclerView:

java.lang.NullPointerException: Attempt to invoke virtual method 'boolean android.support.v7.widget.RecyclerView$ViewHolder.shouldIgnore()' on a null object reference

Here is a minimal example to reproduce:

MainActivity:

class MainActivity : AppCompatActivity() {

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

main_layout:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
        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 class="package.RecyclerFragment"
              android:id="@+id/fragment"
              app:layout_constraintTop_toTopOf="parent"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"/>

</android.support.constraint.ConstraintLayout>

RecyclerFragment:

class RecyclerFragment : Fragment() {

    private val data = listOf("Moscow", "Washington")

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        setHasOptionsMenu(true)
        val view = inflater.inflate(R.layout.recycler_list, container, false)
        view.findViewById<RecyclerView>(R.id.list)?.apply {
            adapter = RecyclerAdapter(data)
        }
        return view
    }

    override fun onCreateOptionsMenu(menu: Menu?, menuInflater: MenuInflater) {
        menuInflater.inflate(R.menu.menu, menu)
    }

    override fun onOptionsItemSelected(item: MenuItem?): Boolean {
        return when (item?.itemId) {
            R.id.navigate -> {
                fragmentManager?.beginTransaction()?.replace(R.id.fragment, HelloFragment())?.commit()
                true
            }
            else -> super.onOptionsItemSelected(item)
        }
    }
}

recycler_list:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.RecyclerView
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/list"
        android:orientation="vertical"
        app:layoutManager="android.support.v7.widget.LinearLayoutManager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

RecyclerAdapter:

class RecyclerAdapter(private val data: List<String>):
    RecyclerView.Adapter<RecyclerAdapter.ViewHolder>() {

    inner class ViewHolder(val view: CardView): RecyclerView.ViewHolder(view)

    override fun onCreateViewHolder(root: ViewGroup, viewType: Int): ViewHolder {
        val listItem = LayoutInflater.from(root.context)
            .inflate(R.layout.list_item, root, false) as CardView
        return ViewHolder(listItem)
    }

    override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
        viewHolder.view.findViewById<TextView>(R.id.text).text = data[position]
    }

    override fun getItemCount() = data.size
}

list_item:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_height="wrap_content"
        android:layout_width="match_parent"
        android:layout_margin="5sp">
    <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:padding="20sp"
            android:textSize="20sp"
            android:id="@+id/text"/>
</android.support.v7.widget.CardView>

menu:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
    <item android:id="@+id/navigate"
          android:title="Navigate"
          app:showAsAction="ifRoom"/>
</menu>

HelloFragment:

class HelloFragment : Fragment() {

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.hello, container, false)
    }
}

hello:

<?xml version="1.0" encoding="utf-8"?>
<TextView android:text="Hello"
          android:textSize="30sp"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          xmlns:android="http://schemas.android.com/apk/res/android"/>

Is there something wrong with this implementation? How do you use RecyclerView in a Fragment?

like image 394
user1785730 Avatar asked Jul 08 '19 18:07

user1785730


2 Answers

 @Jeel and @Birju showed you the right way to use fragment, but I still leave my answer in case you want to understand deeper why your implementation doesn't work.

Reason:

First, looking into main_layout:

<ConstraintLayout>

    <fragment class="package.RecyclerFragment"
              android:id="@+id/fragment"
     ... />

</ConstraintLayout>

When main_layout is inflated in MainActivity, <fragment> element is simply replaced by whatever included inside RecyclerFragment's layout a.k.a recycler_list layout.

So main_layout will actually become:

<ConstraintLayout>

    <android.support.v7.widget.RecyclerView
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/list"
        android:orientation="vertical"
        app:layoutManager="android.support.v7.widget.LinearLayoutManager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</ConstraintLayout>

If you put these code in onResume() in MainActivity, you can see it clearly:

    override fun onResume() {
        super.onResume()
        val parent = findViewById<ConstraintLayout>(R.id.constraint) 
        val numChild = parent.childCount
        val childView = parent.getChildAt(0)
        Log.d("Parent", parent.toString())
        Log.d("NumChild", numChild.toString())
        Log.d("ChildView", childView.toString())
        return
    }

    // Log
    D/Parent: androidx.constraintlayout.widget.ConstraintLayout{a6e5545 V.E...... ......I. 0,0-0,0 #7f07004d app:id/constraint}
    D/NumChild: 1
    D/ChildView: androidx.recyclerview.widget.RecyclerView{753849a VFED..... ......I. 0,0-0,0 #7f070060 app:id/fragment}

Therefore, when you call this line:

fragmentManager?.beginTransaction()?.replace(R.id.fragment, HelloFragment())?.commit()

It actually took RecyclerView as the container view group and add whatever in HelloFragment's layout into RecyclerView

For evidence, you can take a look at these lines in FragmentManager class:


// mContainerId here is R.id.fragment in your layout

container = (ViewGroup) mContainer.onFindViewById(f.mContainerId);

// Now container is RecyclerView

...

f.performCreateView(f.performGetLayoutInflater(
                                    f.mSavedFragmentState), container, f.mSavedFragmentState)

// After this line, f.mView is the view inside hello.xml

...

container.addView(f.mView);

// Add layout in hello.xml into RecyclerView

Because RecyclerView is designed to hold ViewHolders created from data in Adapter, it still keep a variable called childCount (= 3 in this case) even after Hello's fragment view is added inside RecyclerView and removed all ViewHolders from it.

When new view added, RecyclerView dispatch new layout, which then call a function named findMinMaxChildLayoutPositions()

private void findMinMaxChildLayoutPositions(int[] into) {

        final int count = mChildHelper.getChildCount();
        ...
        for (int i = 0; i < count; ++i) {
            final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
            if (holder.shouldIgnore()) {
                continue;
            }

As you can see, because all ViewHolders have been removed, holder will be null and NPE will be thrown when it comes to line if (holder.shouldIgnore()) {

Thank you for reading this long answer!

like image 186
Miller Go Dev Avatar answered Oct 19 '22 08:10

Miller Go Dev


Method 1

Just don't use RecyclerView as the parent in RecyclerFragment layout. Wrap it in LinearLayout like this:

recycler_list.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />

</LinearLayout>

Method 2 In your current implementation you have added RecyclerFragment from xml but when you try to replace it with HelloFragment, it won't get replaced, instead the new fragment will be added over it or below it.

To implement this properly, you should add RecyclerFragment from your activity's onCreate method like below and remove it from xml:

MainActivity

class MainActivity : AppCompatActivity() {

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

        supportFragmentManager.beginTransaction()
            .add(R.id.root,RecyclerFragment())
            .commit()
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/root"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

</androidx.constraintlayout.widget.ConstraintLayout>

And when you want to replace fragment on options item click do like this:

override fun onOptionsItemSelected(item: MenuItem): Boolean {
    return when (item.itemId) {
        R.id.navigate -> {
            requireActivity().supportFragmentManager.beginTransaction().replace(R.id.root, HelloFragment(), "Hello")
                .commit()
            true
        }
        else -> super.onOptionsItemSelected(item)
    }
}

This way, your previous fragment will be removed and new one will be added.

like image 1
Birju Vachhani Avatar answered Oct 19 '22 06:10

Birju Vachhani