在主线程外运行时对 Cloud Firestore 进行同步调用

Posted

技术标签:

【中文标题】在主线程外运行时对 Cloud Firestore 进行同步调用【英文标题】:Making synchronous calls to Cloud Firestore when running off the main thread 【发布时间】:2018-11-22 08:39:58 【问题描述】:

我正在基于 android Clean Architecture Kotlin 版本 (https://github.com/android10/Android-CleanArchitecture-Kotlin) 构建应用。

使用这种架构,每次你想调用一个用例时,都会启动一个 Kotlin 协程,并将结果发布到主线程中。这是通过以下代码实现的:

abstract class UseCase<out Type, in Params> where Type : Any 

abstract suspend fun run(params: Params): Either<Failure, Type>

fun execute(onResult: (Either<Failure, Type>) -> Unit, params: Params) 
    val job = async(CommonPool)  run(params) 
    launch(UI)  onResult.invoke(job.await()) 

在他的示例架构中,Android10 先生使用 Retrofit 在 kotlin couroutine 内进行同步 api 调用。例如:

override fun movies(): Either<Failure, List<Movie>> 
            return when (networkHandler.isConnected) 
                true -> request(service.movies(),  it.map  it.toMovie()  , emptyList())
                false, null -> Left(NetworkConnection())
            
        

private fun <T, R> request(call: Call<T>, transform: (T) -> R, default: T): Either<Failure, R> 
            return try 
                val response = call.execute()
                when (response.isSuccessful) 
                    true -> Right(transform((response.body() ?: default)))
                    false -> Left(ServerError())
                
             catch (exception: Throwable) 
                Left(ServerError())
            
        

'Either' 表示不相交的类型,意味着结果要么是失败,要么是你想要的 T 类型的对象。

他的 service.movi​​es() 方法是这样实现的(使用改造)

@GET(MOVIES) fun movies(): Call<List<MovieEntity>>

现在这是我的问题。我正在用 Google Cloud Firestore 替换改造。我知道目前,Firebase/Firestore 是一个全异步库。我想知道是否有人知道对 Firebase 进行同步 API 调用的更优雅的方法。

我实现了自己的 Call 版本:

interface Call<T: Any> 
    fun execute(): Response<T>

    data class Response<T>(var isSuccessful: Boolean, var body: T?, var failure: Failure?)

我的API调用在这里实现

override fun movieList(): Call<List<MovieEntity>> = object : Call<List<MovieEntity>> 
        override fun execute(): Call.Response<List<MovieEntity>> 
            return movieListResponse()
        
    

    private fun movieListResponse(): Call.Response<List<MovieEntity>> 
        var response: Call.Response<List<MovieEntity>>? = null
        FirebaseFirestore.getInstance().collection(DataConfig.databasePath + MOVIES_PATH).get().addOnCompleteListener  task ->
            response = when 
                !task.isSuccessful -> Call.Response(false, null, Failure.ServerError())
                task.result.isEmpty -> Call.Response(false, null, MovieFailure.ListNotAvailable())
                else -> Call.Response(true, task.result.mapTo(ArrayList())  MovieEntity.fromSnapshot(it) , null)
            
        
        while (response == null)
            Thread.sleep(50)

        return response as Call.Response<List<MovieEntity>>
    

当然,最后的while循环让我很困扰。有没有其他更优雅的方法来等待响应被分配,然后再从movieListResponse方法返回? p>

我尝试在从 Firebase get() 方法返回的 Task 上调用 await(),但 movieListResponse 方法无论如何都会立即返回。感谢您的帮助!

【问题讨论】:

所有用于移动和网络客户端的 Firebase API 都是异步的。协程实际上也是异步的,但编译器只是通过内部重组代码使它们看起来是同步的。 【参考方案1】:

所以我在 Google Tasks API 中找到了我想要的内容:“如果您的程序已经在后台线程中执行,您可以阻止任务以同步获取结果并避免回调”https://developers.google.com/android/guides/tasks#blocking

所以我之前有问题的代码变成了:

private fun movieListResponse(): Call.Response<List<MovieEntity>> 
        return try 
            val taskResult = Tasks.await(FirebaseFirestore.getInstance().
                    collection(DataConfig.databasePath + MOVIES_PATH).get(), 2, TimeUnit.SECONDS)
            Call.Response(true, taskResult.mapTo(ArrayList())  MovieEntity.fromSnapshot(it) , null)
         catch (e: ExecutionException) 
            Call.Response(false, null, Failure.ServerError())
         catch (e: InterruptedException) 
            Call.Response(false, null, Failure.InterruptedError())
         catch (e: TimeoutException) 
            Call.Response(false, null, Failure.TimeoutError())
        
    

注意我不再需要我的 Thread.sleep while 循环。 此代码只能在后台线程/kotlin 协程中运行。

【讨论】:

漂亮,感谢您的链接。很高兴找到一个答案,它实际上提供了一种以阻塞方式使用 Tasks API 的方法!每当有人问这样的问题时,绝大多数人都会回答 “这不是你使用异步东西的方式!!!!!!!!!” 而不是“这是阻止它的方法,但请不要在主线程上做”... @Sean Blahovici 我试图在我的测试中使用Tasks.await 来完成,但我一直遇到错误:Must not be called on the main application thread,尽管将代码包装在launch 块中987654326@协程范围。你能提供一个完整的例子来说明如何在测试中使用 Tasks.await 吗?我也在使用 Robolectric,如果这有什么不同的话。 @Peter 在这种情况下,我只需将 Tasks.await() 之类的“难以测试”的代码放在接口后面并模拟其行为。如果你真的想测试它,那么我认为你会想要使用 Instrumented 测试,将你的目标模块直接注入到集成测试中并以这种方式测试行为。【参考方案2】:

这是过度设计的,有好几层试图做同样的事情。我建议你退后几步,撤消抽象,进入直接使用协程的心情。根据this template实现suspend fun。您不需要Either 的拐杖,以最自然的方式处理异常:try-catch 围绕suspend fun 调用。

您应该得到如下签名:

suspend fun movieList(): List<MovieEntity>

调用站点:

launch(UI) 
    try 
        val list = movieList()
        ...
     catch (e: FireException) 
        // handle
    

【讨论】:

我同意这有点过度设计,但我的目标是让我的域和演示文稿独立于 Firebase。【参考方案3】:

这不是 firebase 的工作方式。 Firebase 是基于回调的。

我推荐架构组件的livedata。

请检查以下示例。

这是一个链接:https://android.jlelse.eu/android-architecture-components-with-firebase-907b7699f6a0

【讨论】:

我在表示层使用 LiveData。我正在尝试将我的表示层和域层与 Firebase 分离。

以上是关于在主线程外运行时对 Cloud Firestore 进行同步调用的主要内容,如果未能解决你的问题,请参考以下文章

在调试文件夹外运行时找不到 DLL

Spring Application在同一集群内运行时无法连接到Kafka,但在从集群外运行时可以工作[重复]

如何通过 Cloud Functions 将文件上传到 Cloud Storage 并使用 Firestore 控制对 Cloud Storage 的访问?

如何使用 ReactJs 对 Cloud Firestore 数据进行分页

如何使用 Firebase Cloud Firestore 对方法进行单元测试?

如何通过 Desc SwiftUI 对 Cloud Firestore 数据进行排序