RecyclerView во фрагменте падает при переходе к другому фрагменту - PullRequest
6 голосов
/ 08 июля 2019

Я следил за примерами в Интернете, чтобы создать RecyclerView. Единственное, что я сделал по-другому - это поместить RecyclerView во фрагмент, а не в MainActivity. RecyclerView хорошо отображается вместе с данными. Но когда я перехожу к другому фрагменту, приложение вылетает с исключением, относящимся к 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 ]

2 голосов
/ 15 июля 2019

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

Если вы поместите этот код в onResume () в MainActivity, вы можете увидеть это ясно:

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

Поскольку RecyclerView предназначен для хранения ViewHolders, созданных из данных в Adapter, он все еще сохраняетпеременная called childCount (в данном случае = 3) даже после того, как представление фрагмента Hello добавлено в RecyclerView и удалено из него все ViewHolders.

Когда добавлено новое представление, 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 голос
/ 12 июля 2019

Я бы посоветовал вам внести некоторые изменения, если вы хотите динамически изменить Fragment s внутри Activity.

  1. Если вы хотите изменить фрагменты в деятельности, возьмите FrameLayout в файле макета (main_layout) из MainActivity, как показано ниже:

    <?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 был нулевым в этом экземпляре.

1 голос
/ 11 июля 2019

Метод 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 В вашей текущей реализации вы добавили RecyclerFragment из xml, но когда выпопытайтесь заменить его на 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)
    }
}

Таким образом, ваш предыдущий фрагмент будет удален, а новый будет добавлен.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...