Search code examples
androidkotlinasynchronousclosuresaws-amplify

How to save data from a Kotlin asynchronous closure?


I am calling an API in an asynchronous function, and would like to store the response from the callback.

Specifically, I am using the API category of AWS Amplify in an Android app, and would like to assign the return value to a LiveData variable in a ViewModel.

fun getMuscleGroup(id: String): ExampleData {
    var exampleData = ExampleData.builder().name("").build()

    Amplify.API.query(
        ModelQuery.get(ExampleData::class.java, id),
        { response ->
            Log.d("AmplifyApi", response.data.name)
            exampleData = response.data
        },
        { error -> Log.e("AmplifyApi", "Query failure", error) }
    )

    return exampleData
}

I can receive the response and it gets logged correctly, but the response is not assigned to the exampleData variable, since the function returns early.

In Android Studio, the variable exampleData is highlighted with the text:

Wrapped into a reference object to be modified when captured in a closure

As I am not that familiar with the multithreading APIs in kotlin, I am not sure, how to block the function until the remote API returns its asynchronous response.


Solution

  • The most basic way of doing this would be with standard Java thread safety constructs.

    fun getMuscleGroup(id: String): ExampleData {
        var exampleData = ExampleData.builder().name("").build()
        val latch = CountDownLatch(1)
        Amplify.API.query(
            ModelQuery.get(ExampleData::class.java, id),
            { response ->
                Log.d("AmplifyApi", response.data.name)
                exampleData = response.data
                latch.countDown()
            },
            { error -> 
                Log.e("AmplifyApi", "Query failure", error)
                latch.countDown()
            }
        )
    
        latch.await()
        return exampleData
    }
    

    Since this is on Android, this is probably a bad solution. I'm going to guess that getMuscleGroup is being called on the UI thread, and you do not want this method to actually block. The UI would freeze until the network call completes.

    The more Kotlin way of doing this would be to make the method a suspend method.

    suspend fun getMuscleGroup(id: String): ExampleData {
        return suspendCoroutine { continuation ->
          Amplify.API.query(
            ModelQuery.get(ExampleData::class.java, id),
            { response ->
                Log.d("AmplifyApi", response.data.name)
                continuation.resume(response.data)
            },
            { error -> 
                Log.e("AmplifyApi", "Query failure", error)
                // return default data
                continuation.resume(ExampleData.builder().name("").build())
            }
        }
    }
    

    This use Kotlin coroutines to suspend the coroutine until an answer is ready and then return the results.

    Other options would be to use callbacks instead of return values or an observable pattern like LiveData or RxJava.