Thursday 30 September 2021

RecyclerView swipe functionality breaks after orientation change

I created a RecyclerView that refreshes its list based on a database call. Each row has an options menu that is revealed when the user swipes. My original issue was that after an orientation change, the swipe gestures no longer revealed the menu. I hit all my expected breakpoints with onCreateViewHolder() and the onSwipe(). However, the row remained as the HIDE_MENU view type after swiping.

So I tried to introduce LiveData to persist the state of the list after orientation changes. The RecyclerView was still created and populated with items but now the swipe gesture crashes the application with an error:

java.lang.IndexOutOfBoundsException: Index: 0, Size: 0

Do I need to use LiveData to fix the original issue of preserving my swipe functionality after orientation changes? If not, please can someone explain why the item view types are no longer updated after orientation changes.

If I do need to use a ViewModel, what am I doing that is causing the list adapter not to receive the updated list?

HistoryFragment

class HistoryFragment : Fragment() {
    private val historyViewModel by activityViewModels<HistoryViewModel>()

    override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
    ): View? {
        val root = inflater.inflate(R.layout.fragment_history, container, false)
        historyViewModel.getHistoryList().observe(viewLifecycleOwner, {
            refreshRecyclerView(it)
        })
        return root
    }
    
    private fun updateHistoryList() {
        val dbHandler = MySQLLiteDBHandler(requireContext(), null)
        val historyList = dbHandler.getHistoryList() as MutableList<HistoryObject>
        historyViewModel.setHistoryList(historyList)
    }

    private fun refreshRecyclerView(historyList: MutableList<HistoryObject>) {
        val historyListAdapter = HistoryListAdapter(historyList)
        val callback = HistorySwipeHelper(historyListAdapter)
        val helper = ItemTouchHelper(callback)
        history_list.adapter = historyListAdapter
        helper.attachToRecyclerView(history_list)
    }
    
    private fun setupSort() {
        val sortSpinner: Spinner = history_list_controls_sort
        sortSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
            override fun onNothingSelected(parent: AdapterView<*>?) {}
            override fun onItemSelected(
                parent: AdapterView<*>?,
                view: View?,
                position: Int,
                id: Long
            ) {
                updateHistoryList()
            }
        }
    }

    override fun onViewCreated(
        view: View,
        savedInstanceState: Bundle?
    ) {
        setupSort()
    }
}

HistoryListAdapter

const val SHOW_MENU = 1
const val HIDE_MENU = 2

class HistoryListAdapter(private var historyData: MutableList<HistoryObject>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return if (viewType == SHOW_MENU) {
            val inflatedView = LayoutInflater.from(parent.context).inflate(R.layout.history_list_view_row_items_menu, parent, false)
            MenuViewHolder(inflatedView)
        } else {
            val inflatedView = LayoutInflater.from(parent.context).inflate(R.layout.history_list_view_row_items_description, parent, false)
            HistoryItemViewHolder(inflatedView)
        }
    }

    override fun getItemViewType(position: Int): Int {
        return if (historyData[position].showMenu) {
            SHOW_MENU
        } else {
            HIDE_MENU
        }
    }

    override fun getItemCount(): Int {
        return historyData.count()
    }

    fun showMenu(position: Int) {
        historyData.forEachIndexed { idx, it ->
            if (it.showMenu) {
                it.showMenu = false
                notifyItemChanged(idx)
            }
        }
        historyData[position].showMenu = true
        notifyItemChanged(position)
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val item: HistoryObject = historyData[position]
        if (holder is HistoryItemViewHolder) {
            holder.bindItem(item)
            ...
        }

        if (holder is MenuViewHolder) {
            holder.bindItem(item)
            ...
        }
    }

    class HistoryItemViewHolder(v: View, private val clickHandler: (item: HistoryObject) -> Unit) : RecyclerView.ViewHolder(v) {
        private var view: View = v
        private var item: HistoryObject? = null

        fun bindItem(item: HistoryObject) {
            this.item = item
            ...
        }
    }

    class MenuViewHolder(v: View, private val deleteHandler: (item: HistoryObject) -> Unit) : RecyclerView.ViewHolder(v) {
        private var view: View = v
        private var item: HistoryObject? = null

        fun bindItem(item: HistoryObject) {
            this.item = item
            ...
        }
    }
}

HistorySwipeHelper

class HistorySwipeHelper(private val adapter: HistoryListAdapter) : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {
    override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { return false }

    override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
        adapter.showMenu(viewHolder.adapterPosition)
    }

    override fun getSwipeThreshold(viewHolder: RecyclerView.ViewHolder): Float {
        return 0.1f
    }
}

HistoryViewModel

class HistoryViewModel(private var historyListHandle: SavedStateHandle) : ViewModel() {
    fun getHistoryList(): LiveData<MutableList<HistoryObject>> {
        return historyListHandle.getLiveData(HISTORY_LIST_KEY)
    }

    fun setHistoryList(newHistoryList: MutableList<HistoryObject>) {
        historyListHandle.set(HISTORY_LIST_KEY, newHistoryList)
    }

    companion object {
        const val HISTORY_LIST_KEY = "MY_HISTORY_LIST"
    }
}

Activity

class MainActivity : AppCompatActivity() {
    private val historyViewModel: HistoryViewModel by lazy {
        ViewModelProvider(this).get(HistoryViewModel::class.java)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        historyViewModel.setHistoryList(mutableListOf())
    }
}

Thanks in advance. If this question is too broad I can try again and decompose it.



from RecyclerView swipe functionality breaks after orientation change

No comments:

Post a Comment