androidkotlinkotlin-coroutinesthread-exceptions

Updating Views asynchronously


I am trying to populate a recyclerview with data from the web which I want to fetch asynchronously.

I have a function loadData() which is called onCreateView() which first makes a loading Indicator visible, then calls the suspend function loading the data and then tries to notify the view adapter to update.

But at this point I get the following exception:

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

which surprised me as my understanding was that only my get_top_books() function was called on a different thread and previously when I was showing the loading indicator I was apparently on the right thread.

So why is this run-time exception raised?

My code:

class DiscoverFragment: Fragment() {

    lateinit var loadingIndicator: TextView
    lateinit var viewAdapter: ViewAdapter
    var books = Books(arrayOf<String>("no books"), arrayOf<String>("no books"), arrayOf<String>("no books"))

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        val viewFrame = layoutInflater?.inflate(R.layout.fragment_discover, container, false)

        val viewManager = GridLayoutManager(viewFrame!!.context, 2)
        viewAdapter = ViewAdapter(books)
        loadingIndicator = viewFrame.findViewById<TextView>(R.id.loading_indicator)

        val pxSpacing = (viewFrame.context.resources.displayMetrics.density * 8f + .5f).toInt()

        val recyclerView = viewFrame.findViewById<RecyclerView>(R.id.recycler).apply {
            setHasFixedSize(true)
            layoutManager = viewManager
            adapter = viewAdapter
            addItemDecoration(RecyclerViewDecorationSpacer(pxSpacing, 2))
        }

        loadData()

        return viewFrame
    }

    fun loadData() = CoroutineScope(Dispatchers.Default).launch {
        loadingIndicator.visibility = View.VISIBLE
        val task = async(Dispatchers.IO) {
            get_top_books()
        }
        books = task.await()
        viewAdapter.notifyDataSetChanged()
        loadingIndicator.visibility = View.INVISIBLE
    }
}

Solution

  • After calling books = task.await() you are outside UI thread. It is because you use CoroutineScope(Dispatchers.Default). Change it to Dispatchers.Main:

    fun loadData() = CoroutineScope(Dispatchers.Main).launch {
            loadingIndicator.visibility = View.VISIBLE
            val task = async(Dispatchers.IO) {
                get_top_books()
            }
            books = task.await()
            viewAdapter.notifyDataSetChanged()
            loadingIndicator.visibility = View.INVISIBLE
        }