Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Making synchronous calls to Cloud Firestore when running off the main thread

I am building an app based off of the Android Clean Architecture Kotlin version (https://github.com/android10/Android-CleanArchitecture-Kotlin).

Using this architecture, each time you want to invoke a use case, a Kotlin coroutine is launched and the result is posted in the main thread. This is achieved by this code:

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()) }
}

In his example architecture, Mr. Android10 uses Retrofit to make a synchronous api call inside the kotlin couroutine. For example:

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' represents a disjoint type, meaning the result will either be a Failure or the object of type T you want.

His service.movies() method is implemented like so (using retrofit)

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

Now here is my question. I am replacing retrofit with Google Cloud Firestore. I know that currently, Firebase/Firestore is an all async library. I want to know if anyone knows of a method more elegant way of making a synchronous API call to Firebase.

I implemented my own version of Call:

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

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

and my API call is implemented here

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>>
    }

Of course, the while loop at the end bothers me. Is there any other, more elegant ways, to wait for the response to be assigned before returning from the movieListResponse method?

I tried calling await() on the Task that is returned from the Firebase get() method, but the movieListResponse method would return immediately anyway. Thanks for the help!

like image 658
Sean Blahovici Avatar asked Jun 12 '18 21:06

Sean Blahovici


1 Answers

So I found what I was looking for in the Google Tasks API: "If your program is already executing in a background thread you can block a task to get the result synchronously and avoid callbacks" https://developers.google.com/android/guides/tasks#blocking

So my previous problematic code becomes:

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())
        }
    }

Note I no longer need my Thread.sleep while loop. This code should only be run in a background thread/kotlin coroutine.

like image 76
Sean Blahovici Avatar answered Nov 05 '22 19:11

Sean Blahovici