异步更新视图

7
我正在尝试使用异步方式从网络中获取数据,并将其填充到 recyclerview 中。我有一个名为 loadData() 的函数,它在 onCreateView() 中被调用,首先使加载指示器可见,然后调用挂起函数来加载数据,最后尝试通知视图适配器来更新。但是,在这一点上,我遇到了以下异常:

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

这让我感到惊讶,因为我的理解是只有我的 get_top_books() 函数是在不同的线程上被调用的,并且之前当我显示加载指示器时,我显然在正确的线程上。那么为什么会引发此运行时异常呢?
我的代码:
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
    }
}

尝试在loadData()函数中使用Dispatchers.Main - y.allam
withContext(Dispatchers.Main)? - EpicPandaForce
考虑使用 GlobalScope.launch(Dispatchers.Default) 代替 CoroutineScope(Dispatchers.Default).launch - Dominic Fischer
还要考虑 books = withContext(Dispatchers.IO) { get_top_books() },因为你不需要并发。 - Dominic Fischer
4个回答

13

调用books = task.await()后,您不在UI线程中。这是因为您使用了CoroutineScope(Dispatchers.Default)。将其更改为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
    }

感谢回答。那么 await() 调用将我从 UI 线程中移除了?如果我将 Dispatcher.Default 更改为 Dispatchers.Main,我会得到以下运行时异常: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()" - Joschka Goes
1
@JoschkaGoes 你是否已经将 org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.0 添加到你的依赖项中了? - Dominic Fischer
@DominicFischer,我刚刚检查了一下,发现我使用的是1.0.1版本,升级到1.1.0版本后问题得到解决!谢谢。 - Joschka Goes

3
在调用 books = task.await() 后,你已经在 UI 线程之外了。你应该在主线程中运行所有与 UI 相关的代码。为此,你可以使用 Dispatchers.Main
CoroutineScope(Dispatchers.Main).launch {
    viewAdapter.notifyDataSetChanged()
    loadingIndicator.visibility = View.INVISIBLE
}

或者使用 Handler

Handler(Looper.getMainLooper()).post { 
    viewAdapter.notifyDataSetChanged()
    loadingIndicator.visibility = View.INVISIBLE
}

或者你可以使用 Activty 实例来调用 runOnUiThread 方法。

activity!!.runOnUiThread {
    viewAdapter.notifyDataSetChanged()
    loadingIndicator.visibility = View.INVISIBLE
}

谢谢Handler方法的工作,但如果我使用Dispatchers.Main,我会得到一个运行时异常。 - Joschka Goes

2
Dispatchers.Default更改为Dispatchers.Main并升级我的kotlinx-coroutines-android版本到1.1.1就解决了问题。

更改

val task = async(Dispatchers.IO) {
    get_top_books()
}
books = task.await()

to

books = withContext(Dispatchers.IO) {
    get_top_books()
}

“最初的回答”也更加优雅。感谢所有回复我的人,特别是@DominicFischer,他提出检查我的依赖项的想法。


请记住,挂起函数withContext()只能从协程或另一个挂起函数中调用。 - Boken

0

与UI相关的语句应该在UI线程中执行,我无法仅从链接的代码中判断,但也许您正在此函数get_top_books()中更改UI。

只需像这样将相关的UI代码放入UI线程即可

runOnUiThread(
    object : Runnable {
        override fun run() {
            Log.i(TAG, "runOnUiThread")
        }
    }
)

get_top_books() 目前只是一个创建类型为 Books 的空数据类并休眠一段时间的函数,因此不应该是问题所在。那么有没有办法从我的片段中调用 runOnUiThread()?快速查找让我觉得这是一个 Activity 的事情。 - Joschka Goes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接