1

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.

Daniel Bank
  • 3,581
  • 3
  • 39
  • 50
  • 1
    Did you tried this: https://stackoverflow.com/a/56815186/10954249 – DeePanShu Sep 28 '21 at 06:07
  • I saw this solution but had some doubts because I saw people saying it was a bandaid (see https://stackoverflow.com/a/7990543/2016353). It does fix my immediate issue. I don't know if it is appropriate for my use case but I'll give it a go. Thank you – Daniel Bank Sep 28 '21 at 06:15
  • Just an update that the swipe gesture functionality is still broken after adding this. – Daniel Bank Sep 28 '21 at 06:54
  • Do you preserve the activity on orientation change in the manifest? – Zain Sep 30 '21 at 20:37
  • If I understand correctly how LIveData works then you have to have something like val historyList: MutableLiveData inside your HistoryViewModel – Alex Rmcf Oct 01 '21 at 07:27
  • Change Your ViewModel getHistoryList from LiveData> to LiveData Here the doc: https://developer.android.com/topic/libraries/architecture/livedata#the_advantages_of_using_livedata – Shogun Nassar Oct 07 '21 at 06:13

1 Answers1

1

You shouldn't create new adapter every time you get an update of your history list. Keep using the same adapter, just update the items and call notifyDataSetChanged() to update the state (of course you can use different methods to notify about the insertion/deletion/etc, but make it work with notifyDataSetChanged() first).

I'm pretty sure this will fix the issue.