Search code examples
androidandroid-recyclerviewandroid-dialogfragment

RecyclerView Selection together with dialog leads to crash. getAdapterPostion() on null object


in my app that I am currently developing I have a fragment in a viewpager that displays a RecyclerView which is filled by some data from a database. In this RecyclerView I have implemented ItemSelection with the recyclerview-selection library together with an actionmode. This alone is working fine. However I also have a FloatingActionButton which opens a dialog where the user can add a new entrance to the database which would also be displayed in the recyclerview. The dialog opens successful but when the user clicks the edittext it crashes with the following error message:

2020-08-16 16:45:36.413 12939-12939/com.nilswinking.kochbuch2 E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.me.myapp, PID: 12939
    java.lang.NullPointerException: Attempt to invoke virtual method 'int androidx.recyclerview.widget.RecyclerView$ViewHolder.getAdapterPosition()' on a null object reference
        at androidx.recyclerview.selection.StableIdKeyProvider.onDetached(StableIdKeyProvider.java:90)
        at androidx.recyclerview.selection.StableIdKeyProvider$1.onChildViewDetachedFromWindow(StableIdKeyProvider.java:69)
        at androidx.recyclerview.widget.RecyclerView.dispatchChildDetached(RecyclerView.java:7546)
        at androidx.recyclerview.widget.RecyclerView.removeDetachedView(RecyclerView.java:4349)
        at androidx.recyclerview.widget.RecyclerView$LayoutManager.removeAndRecycleScrapInt(RecyclerView.java:9243)
        at androidx.recyclerview.widget.RecyclerView.dispatchLayoutStep3(RecyclerView.java:4207)
        at androidx.recyclerview.widget.RecyclerView.dispatchLayout(RecyclerView.java:3862)
        at androidx.recyclerview.widget.RecyclerView.onLayout(RecyclerView.java:4404)
        at android.view.View.layout(View.java:22844)
        at android.view.ViewGroup.layout(ViewGroup.java:6389)
        at androidx.constraintlayout.widget.ConstraintLayout.onLayout(ConstraintLayout.java:1915)
        at android.view.View.layout(View.java:22844)
        at android.view.ViewGroup.layout(ViewGroup.java:6389)
        at androidx.viewpager.widget.ViewPager.onLayout(ViewPager.java:1775)
        at android.view.View.layout(View.java:22844)
        at android.view.ViewGroup.layout(ViewGroup.java:6389)
        at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1829)
        at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1673)
        at android.widget.LinearLayout.onLayout(LinearLayout.java:1582)
        at android.view.View.layout(View.java:22844)
        at android.view.ViewGroup.layout(ViewGroup.java:6389)
        at androidx.coordinatorlayout.widget.CoordinatorLayout.layoutChild(CoordinatorLayout.java:1213)
        at androidx.coordinatorlayout.widget.CoordinatorLayout.onLayoutChild(CoordinatorLayout.java:899)
        at androidx.coordinatorlayout.widget.CoordinatorLayout.onLayout(CoordinatorLayout.java:919)
        at android.view.View.layout(View.java:22844)
        at android.view.ViewGroup.layout(ViewGroup.java:6389)
        at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
        at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
        at android.view.View.layout(View.java:22844)
        at android.view.ViewGroup.layout(ViewGroup.java:6389)
        at androidx.coordinatorlayout.widget.CoordinatorLayout.layoutChild(CoordinatorLayout.java:1213)
        at androidx.coordinatorlayout.widget.CoordinatorLayout.onLayoutChild(CoordinatorLayout.java:899)
        at androidx.coordinatorlayout.widget.CoordinatorLayout.onLayout(CoordinatorLayout.java:919)
        at android.view.View.layout(View.java:22844)
        at android.view.ViewGroup.layout(ViewGroup.java:6389)
        at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1829)
        at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1673)
        at android.widget.LinearLayout.onLayout(LinearLayout.java:1582)
        at android.view.View.layout(View.java:22844)
        at android.view.ViewGroup.layout(ViewGroup.java:6389)
        at androidx.coordinatorlayout.widget.CoordinatorLayout.layoutChild(CoordinatorLayout.java:1213)
        at androidx.coordinatorlayout.widget.CoordinatorLayout.onLayoutChild(CoordinatorLayout.java:899)
        at androidx.coordinatorlayout.widget.CoordinatorLayout.onLayout(CoordinatorLayout.java:919)
        at android.view.View.layout(View.java:22844)
        at android.view.ViewGroup.layout(ViewGroup.java:6389)
        at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
        at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
        at android.view.View.layout(View.java:22844)
        at android.view.ViewGroup.layout(ViewGroup.java:6389)
        at androidx.appcompat.widget.ActionBarOverlayLayout.onLayout(ActionBarOverlayLayout.java:446)
        at android.view.View.layout(View.java:22844)
        at android.view.ViewGroup.layout(ViewGroup.java:6389)
        at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
2020-08-16 16:45:36.416 12939-12939/com.me.myapp E/AndroidRuntime:     at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
        at android.view.View.layout(View.java:22844)
        at android.view.ViewGroup.layout(ViewGroup.java:6389)
        at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1829)
        at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1673)
        at android.widget.LinearLayout.onLayout(LinearLayout.java:1582)
        at android.view.View.layout(View.java:22844)
        at android.view.ViewGroup.layout(ViewGroup.java:6389)
        at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
        at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
        at com.android.internal.policy.DecorView.onLayout(DecorView.java:784)
        at android.view.View.layout(View.java:22844)
        at android.view.ViewGroup.layout(ViewGroup.java:6389)
        at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:3470)
        at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2938)
        at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1952)
        at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:8171)
        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:972)
        at android.view.Choreographer.doCallbacks(Choreographer.java:796)
        at android.view.Choreographer.doFrame(Choreographer.java:731)
        at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:957)
        at android.os.Handler.handleCallback(Handler.java:938)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

Dialog:

class AddBookDialog : DialogFragment() {

    private val TAG = AddBookDialog::class.java.simpleName

    private lateinit var listener: AddBookDialogInterface

    private lateinit var editTextName: EditText
    private lateinit var title: TextView

    private var id: String? = null

    interface AddBookDialogInterface {
        fun addKochbuch(name: String, id: String? = null): Boolean
    }

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        return activity?.let {
            // Use the Builder class for convenient dialog construction
            val builder = AlertDialog.Builder(it, R.style.AppTheme_AlertDialog)
            val inflater = requireActivity().layoutInflater;

            val view = inflater.inflate(R.layout.add_book_dialog, null)

            editTextName = view.findViewById(R.id.editTextName)
//            editTextNameLayout = view.findViewById(R.id.editTextNameLayout)
            title = view.findViewById(R.id.title)

            arguments?.let {
                it.getString(nameParam).let {
                    editTextName.setText(it)
                }
                id = it.getString(idParam)
            }

            editTextName.setOnKeyListener(View.OnKeyListener { v, keyCode, event ->
                if (keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_UP) {
                    add()
                    return@OnKeyListener true
                }
                false
            })


            builder.setView(view)
                .setPositiveButton(
                    "Hinzufügen"
                ) { dialog, id ->
                    add()
                }
                .setNegativeButton(
                    "Abbrechen"
                ) { dialog, id ->
                    // User cancelled the dialog
                }
            // Create the AlertDialog object and return it
            val dialog: AlertDialog = builder.create()
            dialog.setOnShowListener {
                val button: Button = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
                button.setOnClickListener {add()}
            }

            dialog
        } ?: throw IllegalStateException("Activity cannot be null")
    }


    override fun onAttach(context: Context) {
        super.onAttach(context)
        // Verify that the host activity implements the callback interface
        try {
            // Instantiate the NoticeDialogListener so we can send events to the host
            listener = context as AddBookDialogInterface
        } catch (e: ClassCastException) {
            // The activity doesn't implement the interface, throw exception
            throw ClassCastException((context.toString() +
                    " must implement AddBookDialogInterface"))
        }
    }

    private fun add() {
        val name = editTextName.text.toString()
        listener.addKochbuch(name, id).let {
            if (it)
                dismiss()
        }
    }

    companion object {
        fun newInstance(kochbuch: Kochbuch? = null) =
            AddBookDialog().apply {
                arguments = Bundle().apply {
                    putString(nameParam, kochbuch?.name)
                    putString(idParam, kochbuch?.id)
                }
            }

    }
}

Dialog layout:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/title"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="16dp"
        android:text="Erstelle ein neues Kochbuch"
        android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
        android:textColor="@color/material_on_background_emphasis_high_type"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.google.android.material.textfield.TextInputLayout
        android:id="@+id/editTextNameLayout"
        style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="16dp"
        android:layout_marginBottom="16dp"
        android:hint="Name deines Kochbuches"
        android:textColorHint="@color/input_outline_color"
        app:boxStrokeColor="@color/input_outline_color"
        app:hintTextColor="@color/input_outline_color"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/title">

        <com.google.android.material.textfield.TextInputEditText
            android:id="@+id/editTextName"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:singleLine="true"
            android:textColor="@color/material_on_background_emphasis_high_type" />

    </com.google.android.material.textfield.TextInputLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

Fragment:

class HomeBooksFragment : Fragment(), OnActionItemClickListener {

    private val TAG = HomeBooksFragment::class.java.simpleName

    private lateinit var realm: Realm
    private val adapter = ItemAdapterCookingBooks()

    private lateinit var result: RealmResults<Kochbuch>

    private lateinit var tracker: SelectionTracker<Long>

    private var actionmode: SelectionActionModeCallback? = null

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_home_books, container, false)
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        realm = Realm.getDefaultInstance()

        result = realm.where<Kochbuch>().sort("name").findAll()
        result.addChangeListener { t, _ ->
            updateUI(t)
        }
        adapter.data = ArrayList()
        rv.layoutManager = GridLayoutManager(context, 2)
        rv.adapter = adapter

        tracker = SelectionTracker.Builder<Long>(
            "mySelection",
            rv,
            StableIdKeyProvider(rv),
            ItemDetailLookup(rv),
            StorageStrategy.createLongStorage()
        ).withSelectionPredicate(
            SelectionPredicates.createSelectAnything()
        ).build()

        tracker.addObserver(
            object : SelectionTracker.SelectionObserver<Long>() {
                override fun onSelectionChanged() {
                    super.onSelectionChanged()
                    tracker.selection.size().let { i ->
                        when {
                            i >= 2 -> actionmode?.setEditEnabled(false)
                            i == 1 -> actionmode?.setEditEnabled(true)
                            i == 0 -> Log.d(TAG, "onSelectionChanged: zero items selected")
                            else -> null
                        }

                        if (tracker.hasSelection() && actionmode == null) {
                            actionmode = SelectionActionModeCallback()
                            view?.let { actionmode?.startActionMode(it, R.menu.selection_action_mode_menu, tracker, "$i Ausgwählt") }
                            actionmode?.setListener(this@HomeBooksFragment)
                        } else if (!tracker.hasSelection() && actionmode != null) {
                            actionmode?.finishActionMode()
                            actionmode = null
                        } else {
                            actionmode?.setTitle("$i Ausgewählt")
                        }
                    }
                }
            }
        )

        adapter.tracker = tracker

        updateUI(result)
    }
}

It is no problem with the ItemDetailLookup as I put everything in there in try-catch blocks and the error still showed up.

Further debugging showed, that it only happens when and item is displayed behind the dialog (On my screensize when seven items are diaplayed). However it also happens when the dialog is opened while another fragment is displayed in my viewpager. From this I suspect that the click event is somehow passed through the dialog to the underlaying fragment.

Any help would be appreciated.

Edit:

Also happens when showing other dialogs


Solution

  • For some reason the selection tracker tried to get the itemdetails of a viewholder that either did not exist or wasn't in the recyclerview and that produced the null pointer exeption. In my ViewHolder I have this method to return all necessary information about the viewholder to make the selection tracker work and here it is with the correct implementation that is null safe:

    fun getItemDetails(): ItemDetailsLookup.ItemDetails<Long> =
                object : ItemDetailsLookup.ItemDetails<Long>() {
                    override fun getPosition(): Int {
                        try {
                            return adapterPosition
                        } catch (e: Exception) {
                            return -1
                        }
                    }
                    override fun getSelectionKey(): Long? = itemId
                }
    

    For comparison look at my question where the previous implementation is posted.