【问题标题】:Function cannot return item with Coroutine block函数无法使用协程块返回项目
【发布时间】:2020-05-09 13:29:57
【问题描述】:

我正在使用 Coroutines 从 Room 数据库中检索记录,因为它必须在后台线程中运行。我想通过函数返回结果。

class LessonRepository(val app: Application) {

    private val courseDao = MyDatabase.getDatabase(app).courseDao()
}

    fun getCourseData(): Course {

        var course: Course

        CoroutineScope(Dispatchers.IO).launch {
            course = courseDao.getCourse(globalSelectedCourse)
        }
        return course
    }

视图模型

class LessonViewModel(app: Application): AndroidViewModel(app) {

    private val lessonDataRepository = LessonRepository(app)
    val lessonData = lessonDataRepository.lessonData
    val selectedLesson = MutableLiveData<Lesson>()

    fun getCourseData() : Course {
        return lessonDataRepository.getCourseData()
    }
}

我想在我的片段中使用返回值:

class DetailFragment : Fragment(), LessonRecyclerAdapter.LessonItemListener {
.
.
.
        viewModel = ViewModelProvider(this).get(LessonViewModel::class.java)

        val course = viewModel.getCourseData()
.
.
.
    }

但是,Android Studio 在返回语句 return course 中给了我一个错误指示符,表明必须初始化 course。我怎样才能成功返回course的值?

-- 更新:--

我正在尝试获取该记录的值并在我的片段中使用它,如下所示:

val course = viewModel.viewModelScope.launch { viewModel.getCourseData() }

textViewName.text = course.Name
textViewInstructor.text = course.instructor

【问题讨论】:

    标签: android kotlin android-room kotlin-coroutines


    【解决方案1】:

    您应该利用 Room 自 2.1.0 版以来提供的协程支持。官方文档指出:

    您可以将暂停 Kotlin 关键字添加到您的 DAO 方法中以使 它们使用 Kotlin 协程功能进行异步处理。这确保 它们不能在主线程上执行。

    您可以查看here

    所以你应该在getCourse DAO 方法中添加suspend 关键字:

    DAO 接口

    @Query("MY SQL QUERY")
    suspend fun getCourse(selectedCourse: SelectedCourseType): Course
    

    如果你这样做了,那么无论你是否在主线程上发出请求,你都可以返回结果:

    存储库

    suspend fun getCourseData() = courseDao.getCourse(globalSelectedCourse)
    

    视图模型

    suspend fun getCourseData() = lessonDataRepository.getCourseData()
    

    您还应该使用lifecycleScope,它可以在 Fragments 上找到,如您所见here

    片段

    fun doSomething() {
        ...    
        viewLifecycleOwner.lifecycleScope.launch {
            val course = viewModel.getCourseData()
            textViewName.text = course.Name
            textViewInstructor.text = course.instructor
        }
        ...    
    }
    

    PS:考虑将所选课程作为参数传递给整个调用链,而不是在您的存储库中使用全局变量。

    【讨论】:

    • 感谢您的意见。我实际上在除 DAO 之外的提到的函数上使用了暂停。所以我确实添加了它并按照您的建议使用 ... viewLifecycleOwner.lifecycleScope 因为应用程序有时会崩溃。
    • 虽然@Animesh Sahu 的回答是正确的并且确实有效,但是在某些情况下应用程序崩溃了,可能是因为某些DAO 方法在主线程上运行。所以我会接受这个作为完整的工作答案。
    【解决方案2】:

    你这样做是错误的。可能你对并发或并发运行的任务有一些误解。

    让我消除你的疑虑。

    1. launch 不阻塞,它是launch and forget。你永远不知道值是什么时候设置的。您唯一能做的就是致电join() 以确保已完成。
    2. 但是对于希望从协程返回结果的任务,仍然使用启动块并没有得到很好的优化。我们在这里使用 async/withContext。
    3. async 在 CoroutineScope 上被调用,而 withContext 是一个顶级函数,它需要 CoroutineContext 作为其参数。
    4. withContext 挂起调用者协程直到它完成。虽然async 没有,但async 的返回值是Deferred&lt;T&gt;,当您调用.await() 时,调用者协程将暂停,直到任务完成,类似于withContext。

    所以你可以通过以下方式完成你的任务。

    选项 1:最优化的版本

    让你的函数挂起并使用 withContext。它将暂停调用协程,直到获取课程。

    suspend fun getCourseData(): Course {
        return withContext(Dispatchers.IO) {
            courseDao.getCourse(globalSelectedCourse)
        }
    }
    
    // or simpler
    suspend fun getCourseData(): Course =
        withContext(Dispatchers.IO) {
            courseDao.getCourse(globalSelectedCourse)
        }
    

    选项 2:使用异步,并返回 Deferred。

    // declare scope elsewhere. It is not intended to create scope everytime you want to launch a task
    val scope = CoroutineScope(Dispatchers.IO)
    
    // using async at the end of function is a naming scheme by Kotlin recommendation.
    fun getCourseDataAsync(): Deferred<Course> =
        scope.async {
            courseDao.getCourse(globalSelectedCourse)
        }
    
    //Now when you call the function, call await(), it is suspending, it will suspend the calling coroutine till the course is fetched.
    val course: Course = getCourseDataAsync().await()
    

    OP 的更新更新

    正如我在 cmets 中所建议的,您不能在协程块之外使用暂停代码块。因为你不能挂起一个非挂起的函数。

    如下操作:

    // in fragment
    suspend fun getCourseData() : Course {
        return lessonDataRepository.getCourseData()
    }
    
    viewModel.viewModelScope.launch {
        val course = viewModel.getCourseData()
    
        textViewName.text = course.Name
        textViewInstructor.text = course.instructor
    }
    

    【讨论】:

    • 我不能使用挂起函数,因为我是从我的片段中调用它
    • @Ajeeli 您可以在协程中调用任何挂起函数。例如在片段中你可以调用viewModelOwner.viewModelScope.launch{/* call here */}
    • @Ajeeli 您无法在协程之外获取数据。 viewModel.viewModelScope.launch { val cource = viewModel.getCourseData() } 由于 callsite 函数(从你调用它的地方)没有挂起,所以那里没有 Continuation
    • @Ajeeli 您能否提供您如何使用该课程/希望在协程之外使用它以便进一步帮助您,请编辑您的问题。
    • @Ajeeli 更新了适合您用例的答案!
    猜你喜欢
    • 1970-01-01
    • 2014-11-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-10-27
    • 1970-01-01
    • 2019-10-10
    相关资源
    最近更新 更多