Updating Views asynchronously
Asked Answered
E

4

7

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
    }
}
Erstwhile answered 5/4, 2019 at 13:30 Comment(4)
Try using Dispatchers.Main for loadData() functionTirza
withContext(Dispatchers.Main)?Unhook
Consider using GlobalScope.launch(Dispatchers.Default) instead of CoroutineScope(Dispatchers.Default).launch.Durra
Also consider books = withContext(Dispatchers.IO) { get_top_books() }, since you don't need the concurrency.Durra
B
13

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
    }
Berny answered 5/4, 2019 at 14:57 Comment(3)
Thanks for the Answer. So the await() call removed me from the UI thread? If I change the Dispatcher.Default to Dispatchers.Main I get the following runtime exception: java.lang.IllegalStateException: Module with the Main dispatcher had failed to initialize Caused by: java.lang.AbstractMethodError: abstract method "java.lang.String kotlinx.coroutines.internal.MainDispatcherFactory.hintOnError()"Erstwhile
@JoschkaGoes Have you added org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.0 to your dependencies?Durra
@DominicFischer I just checked and I was on 1.0.1 and upgrading to 1.1.0 solved my issue! Thanks.Erstwhile
B
3

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
}
Berny answered 5/4, 2019 at 13:46 Comment(1)
Thanks the Handler method works, but I get an runtime exception if I use Dispatchers.MainErstwhile
E
2

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.

Erstwhile answered 5/4, 2019 at 20:57 Comment(1)
Remember that suspend function withContext() should be called only from a coroutine or another suspend functionHistrionics
F
0

UI related statements should be performed in the UI thread, I can't tell from the linked code only, but maybe you are changing UI in thsi function get_top_books().

Just put the related UI code in the UI thread like this

runOnUiThread(
    object : Runnable {
        override fun run() {
            Log.i(TAG, "runOnUiThread")
        }
    }
)
Fruiterer answered 5/4, 2019 at 13:39 Comment(1)
get_top_books() is currently just a function wich creates an empty data class of type Books and then sleeps for some time so that should not be the issue. And is there a way to call runOnUiThread() from my fragment? Quickly looking that up gave me the impression that this is an Activity thing.Erstwhile

© 2022 - 2024 — McMap. All rights reserved.