Sean Blahovici
Sean Blahovici

Reputation: 5750

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!

Upvotes: 6

Views: 3395

Answers (3)

Sean Blahovici
Sean Blahovici

Reputation: 5750

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.

Upvotes: 10

Marko Topolnik
Marko Topolnik

Reputation: 200168

This is overengineered, there are several layers trying to do the same thing. I suggest you go back a few steps, undo the abstractions and get into the mood of using coroutines directly. Implement a suspend fun according to this template. You don't need the crutches of Either, handle exceptions in the most natural way: a try-catch around a suspend fun call.

You should end up with a signature as follows:

suspend fun movieList(): List<MovieEntity>

Call site:

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

Upvotes: 1

fangzhzh
fangzhzh

Reputation: 2252

That's is not the way how firebase works. Firebase is based on callback.

I recommend architecture component's livedata.

Please check the following example.

here is a link: https://android.jlelse.eu/android-architecture-components-with-firebase-907b7699f6a0

Upvotes: -1

Related Questions