如何从协程的后台线程更新UI线程?

11

我有一个函数 displayDirectoryContents2(file: File),它扫描所有文件并检查文件和目录。我想要的是在 UI 线程的 textview 中显示当前文件路径

lateinit var textView: TextView


GlobalScope.launch(Dispatchers.IO) {
 displayDirectoryContents2(file)
}

该函数的代码

private fun displayDirectoryContents2(dir: File?){
    try {
        val files = dir?.listFiles()!!

        files.forEach {

            if (it.isDirectory) {
                displayDirectoryContents2(it)
            } else { 
                   if (it.isFile) {
                    textView.text = it.name // to Update the file name in UI thread
              }
        }


    } catch (e: IOException) {
        e.printStackTrace()
    }
}

我对Kotlin协程还很陌生。实际上,我想在后台线程中运行displayDirectoryContents2(file: File)函数,并在UI线程中更新函数正在读取的文件名,就像AsyncTask一样。


你可以切换上下文到主线程(withContext(Dispatchers.Main){})。 - Animesh Sahu
4个回答

6

您可以切换调度程序上下文(Dispatchers.IO用于逻辑,然后转到Dispatchers.Main以更新用户界面),或将代码移到ViewModel中,并在其中使用相同的上下文切换技术或使用LiveData的postvalue()方法。 下面是执行后者的示例。 您可以在此处阅读ViewModel:https://developer.android.com/topic/libraries/architecture/viewmodel

import androidx.lifecycle.LiveData 
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class MyViewModel() : ViewModel() {
    private val files: MutableLiveData<List<String>> by lazy {
        MutableLiveData<List<String>>()
    }
    fun loadFiles(path: String) {
        viewModelScope.launch(){
            doLoadFiles()
        }
    }
    private suspend fun doLoadFiles() {
        withContext(Dispatchers.IO) {
            val results = listOf("patha", "pathb")//replace with your actual code
            files.postValue(results)
        }
    }
    fun getFiles(): LiveData<List<String>> = files
}

然后从您的活动中这样调用它。
import androidx.appcompat.app.AppCompatActivity
import android.view.Menu
import android.view.MenuItem
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders

import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val model = ViewModelProviders.of(this)[MyViewModel::class.java]
        model.getFiles().observe(this, Observer<List<String>>{ paths ->
            // update UI
            println (paths)
        })
        model.loadFiles("S")

    }

在你的 build.gradle 文件中,确保导入相关依赖。

 def lifecycle_ver = "2.2.0-rc02"
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_ver"
    implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_ver"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_ver"

3
您可以将displayDirectoryContents2定义为挂起函数,然后使用withContext切换上下文。
suspend fun displayDirectoryContents2() {
    ...
    withContext(Dispatchers.Main) {
        textView.text = it.name
    }
    ...
}

3

最好的方法是先创建一个函数,该函数返回在IO调度程序上执行的文件名的Flow<String>

fun File.filenameFlow() = flow<String> { traverseAndEmit(this@filenameFlow) }
        .flowOn(Dispatchers.IO)

private suspend fun FlowCollector<String>.traverseAndEmit(dir: File) {
    dir.listFiles()?.forEach {
        when {
            it.isDirectory -> traverseAndEmit(it)
            it.isFile -> emit(it.name)
        }
    }
}

现在您可以简单地在GUI线程中的任何点上进行收集,而无需阻塞:
File("target").filenameFlow().collect { textView.text = it }

2

使用 suspend 函数在主线程之外扫描文件。

suspend fun loadFiles(): List<String> = withContext(Dispatchers.IO) {
// code for getting the files 
return@withContext list } 

完成后,在活动的onCreate()函数中使用lifecycleScope.launch{ }更新UI线程。如下所示:

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    lifecycleScope.launch { // Dispatchers.Main
        textView.text = loadFiles()[i]
    }
}

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