导航到另一个片段时,片段中的RecyclerView崩溃

时间:2019-07-08 18:53:22

标签: android android-fragments kotlin android-recyclerview

我一直在网上跟踪创建RecyclerView的示例。我唯一不同的是将RecyclerView放入Fragment中,而不是将其放入MainActivity中。 RecyclerView可以很好地显示数据。但是,当我导航到另一个Fragment时,应用程序崩溃并显示与RecyclerView相关的异常:

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

这是一个复制的最小示例:

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>

菜单:

<?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)
    }
}

你好:

<?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"/>

此实现是否存在问题?如何在片段中使用RecyclerView?

3 个答案:

答案 0 :(得分:2)

@Jeel和@Birju向您展示了使用片段的正确方法,但是如果您想更深入地了解您的实现为何无效的情况,我还是留下我的答案。

原因:

首先,查看main_layout:

<ConstraintLayout>

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

</ConstraintLayout>

main_layout在MainActivity中膨胀时,<fragment>元素将简单地由RecyclerFragment的布局(也称为recycler_list布局)中包含的任何元素替换。

因此main_layout实际上将变为:

<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>

如果将这些代码放在MainActivity的onResume()中,则可以清楚地看到它:

    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}

因此,当您呼叫此行时:

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

实际上,它把RecyclerView作为容器视图组,并将HelloFragment布局中的所有内容添加到RecyclerView

作为证据,您可以在 FragmentManager 类中查看以下几行:


// 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

由于RecyclerView旨在保留从适配器中的数据创建的ViewHolders,因此即使在Hello的片段视图添加到RecyclerView内并删除了所有ViewHolders之后,它仍然保留名为 childCount 的变量(在这种情况下,= 3)从它。

添加新视图后,RecyclerView调度新布局,然后调用名为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;
            }

如您所见,由于所有ViewHolders均已删除,因此holder will be null和NPE将在行if (holder.shouldIgnore()) {处抛出

感谢您阅读冗长的答案!

答案 1 :(得分:1)

方法1

RecyclerView布局中,请勿将RecyclerFragment用作父项。像这样将其包装在LinearLayout中:

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>

方法2 在您当前的实现中,您已经从xml中添加了RecyclerFragment,但是当您尝试将其替换为HelloFragment时,它不会被替换,而是会在其上方或下方添加新的片段。

要正确实施此操作,应从活动的RecyclerFragment方法中添加onCreate,如下所示,并将其从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>

当您要替换选项项目上的片段时,请执行以下操作:

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)
    }
}

这样,您之前的片段将被删除,新的片段将被添加。

答案 2 :(得分:1)

如果您想在Fragment内部动态更改Activity,我建议您稍作改动。

  1. 要更改活动中的片段时,请在MainActivity的布局文件(main_layout)中获取FrameLayout,如下所示:

    <?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">
    
        <FrameLayout
                android:id="@+id/fragment_container"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"/>
    </android.support.constraint.ConstraintLayout>
    
  2. 然后在MainActivity中以编程方式替换片段,最初,我们将RecyclerFragment加载到fragment_container中。

    class MainActivity : AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.main_layout)
            supportFragmentManager.beginTransaction()
                .replace(R.id.fragment_container, RecyclerFragment())
    
        }
    }
    
  3. ViewGroup片段提供RecyclerFragment (可选,但建议)

    <?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">
    
       <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:layout_width="0dp"
                android:layout_height="0dp"
                android:orientation="vertical"
                app:layoutManager="android.support.v7.widget.LinearLayoutManager"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />
    </android.support.constraint.ConstraintLayout>
    

现在,运行您的项目,一切将正常运行。


关于您的崩溃:

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

之所以NullPointerException是因为当您导航到下一个片段HelloFragment时,RecyclerFragment进入分离状态,并且对象RecyclerView当时为 null 实例。