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
}
}
After calling books = task.await()
you are outside UI thread. You should run all UI related code in the main thread. To do this you can use Dispatchers.Main
.
CoroutineScope(Dispatchers.Main).launch {
viewAdapter.notifyDataSetChanged()
loadingIndicator.visibility = View.INVISIBLE
}
Or using Handler
Handler(Looper.getMainLooper()).post {
viewAdapter.notifyDataSetChanged()
loadingIndicator.visibility = View.INVISIBLE
}
Or you can use Activty
instance to call runOnUiThread
method.
activity!!.runOnUiThread {
viewAdapter.notifyDataSetChanged()
loadingIndicator.visibility = View.INVISIBLE
}
Changing the Dispatchers.Default
to Dispatchers.Main
and upgrading my version of kotlinx-coroutines-android
to 1.1.1
did the trick.
Changing
val task = async(Dispatchers.IO) {
get_top_books()
}
books = task.await()
to
books = withContext(Dispatchers.IO) {
get_top_books()
}
is also a bit more elegant. Thanks to everyone who responded especially @DominicFischer who had the idea to check my dependencies.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With