Categories
android firebase google-cloud-firestore java kotlin

Why does my function that calls an API or launches a coroutine return an empty or null value?

22

(Disclaimer: There are a ton of questions which arise from people asking about data being null/incorrect when using asynchronous operations through requests such as facebook,firebase, etc. My intention for this question was to provide a simple answer for that problem to everyone starting out with asynchronous operations in android)

I’m trying to get data from one of my operations, when I debug it using breakpoints or logs, the values are there, but when I run it they are always null, how can I solve this ?

Firebase

firebaseFirestore.collection("some collection").get()
    .addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
        @Override
        public void onSuccess(QuerySnapshot documentSnapshots) {
            //I want to return these values I receive here... 
        });
//...and use the returned value here.

Facebook

GraphRequest request = GraphRequest.newGraphPathRequest(
    accessToken,
    "some path",
    new GraphRequest.Callback() {
        @Override
        public void onCompleted(GraphResponse response) {
            //I want to return these values I receive here...
        }
    });
request.executeAsync();
//...and use the returned value here.

Kotlin coroutine

var result: SomeResultType? = null
someScope.launch {
    result = someSuspendFunctionToRetrieveSomething()
    //I want to return the value I received here... 
}
Log.d("result", result.toString()) //...but it is still null here.

Etc.

0

    4

    There’s a particular pattern of this nature I’ve seen repeatedly, and I think an explanation of what’s happening would help. The pattern is a function/method that calls an API, assigning the result to a variable in the callback, and returns that variable.

    The following function/method always returns null, even if the result from the API is not null.

    Kotlin

    fun foo(): String? {
       var myReturnValue: String? = null
       someApi.addOnSuccessListener { result ->
           myReturnValue = result.value
       }.execute()
       return myReturnValue
    }
    

    Kotlin coroutine

    fun foo(): String? {
       var myReturnValue: String? = null
       lifecycleScope.launch { 
           myReturnValue = someApiSuspendFunction()
       }
       return myReturnValue
    }
    

    Java 8

    private String fooValue = null;
    
    private String foo() {
        someApi.addOnSuccessListener(result -> fooValue = result.getValue())
            .execute();
        return fooValue;
    }
    

    Java 7

    private String fooValue = null;
    
    private String foo() {
        someApi.addOnSuccessListener(new OnSuccessListener<String>() {
            public void onSuccess(Result<String> result) {
                fooValue = result.getValue();
            }
        }).execute();
        return fooValue;
    }
    

    The reason is that when you pass a callback or listener to an API function, that callback code will only be run some time in the future, when the API is done with its work. By passing the callback to the API function, you are queuing up work, but the current function (foo() in this case) returns immediately before that work begins and before that callback code is run.

    Or in the case of the coroutine example above, the launched coroutine is very unlikely to complete before the function that started it.

    Your function that calls the API cannot return the result that is returned in the callback (unless it’s a Kotlin coroutine suspend function). The solution, explained in the other answer, is to make your own function take a callback parameter and not return anything.

    Alternatively, if you’re working with coroutines, you can make your function suspend instead of launching a separate coroutine. When you have suspend functions, somewhere in your code you must launch a coroutine and handle the results within the coroutine. Typically, you would launch a coroutine in a lifecycle function like onCreate(), or in a UI callback like in an OnClickListener.

    0

      3

      Other answer explains how to consume APIs based on callbacks by exposing a similar callbacks-based API in the outer function. However, recently Kotlin coroutines become more and more popular, especially on Android and while using them, callbacks are generally discouraged for such purposes. Kotlin approach is to use suspend functions instead. Therefore, if our application uses coroutines already, I suggest not propagating callbacks APIs from 3rd party libraries to the rest of our code, but converting them to suspend functions.

      Converting callbacks to suspend

      Let’s assume we have this callback API:

      interface Service {
          fun getData(callback: Callback<String>)
      }
      
      interface Callback<in T> {
          fun onSuccess(value: T)
          fun onFailure(throwable: Throwable)
      }
      

      We can convert it to suspend function using suspendCoroutine():

      private val service: Service
      
      suspend fun getData(): String {
          return suspendCoroutine { cont ->
              service.getData(object : Callback<String> {
                  override fun onSuccess(value: String) {
                      cont.resume(value)
                  }
      
                  override fun onFailure(throwable: Throwable) {
                      cont.resumeWithException(throwable)
                  }
              })
          }
      }
      

      This way getData() can return the data directly and synchronously, so other suspend functions can use it very easily:

      suspend fun otherFunction() {
          val data = getData()
          println(data)
      }
      

      Note that we don’t have to use withContext(Dispatchers.IO) { ... } here. We can even invoke getData() from the main thread as long as we are inside the coroutine context (e.g. inside Dispatchers.Main) – main thread won’t be blocked.

      Cancellations

      If the callback service supports cancelling of background tasks then it is best to cancel when the calling coroutine is itself cancelled. Let’s add a cancelling feature to our callback API:

      interface Service {
          fun getData(callback: Callback<String>): Task
      }
      
      interface Task {
          fun cancel();
      }
      

      Now, Service.getData() returns Task that we can use to cancel the operation. We can consume it almost the same as previously, but with small changes:

      suspend fun getData(): String {
          return suspendCancellableCoroutine { cont ->
              val task = service.getData(object : Callback<String> {
                  ...
              })
      
              cont.invokeOnCancellation {
                  task.cancel()
              }
          }
      }
      

      We only need to switch from suspendCoroutine() to suspendCancellableCoroutine() and add invokeOnCancellation() block.

      Example using Retrofit

      interface GitHubService {
          @GET("users/{user}/repos")
          fun listRepos(@Path("user") user: String): Call<List<Repo>>
      }
      
      suspend fun listRepos(user: String): List<Repo> {
          val retrofit = Retrofit.Builder()
              .baseUrl("https://api.github.com/")
              .build()
      
          val service = retrofit.create<GitHubService>()
      
          return suspendCancellableCoroutine { cont ->
              val call = service.listRepos(user)
      
              call.enqueue(object : Callback<List<Repo>> {
                  override fun onResponse(call: Call<List<Repo>>, response: Response<List<Repo>>) {
                      if (response.isSuccessful) {
                          cont.resume(response.body()!!)
                      } else {
                          // just an example
                          cont.resumeWithException(Exception("Received error response: ${response.message()}"))
                      }
                  }
      
                  override fun onFailure(call: Call<List<Repo>>, t: Throwable) {
                      cont.resumeWithException(t)
                  }
              })
      
              cont.invokeOnCancellation {
                  call.cancel()
              }
          }
      }
      

      Native support

      Before we start converting callbacks to suspend functions, it is worth checking whether the library that we use does support suspend functions already: natively or with some extension. Many popular libraries like Retrofit or Firebase support coroutines and suspend functions. Usually, they either provide/handle suspend functions directly or they provide suspendable waiting on top of their asynchronous task/call/etc. object. Such waiting is very often named await().

      For example, Retrofit supports suspend functions directly since 2.6.0:

      interface GitHubService {
          @GET("users/{user}/repos")
          suspend fun listRepos(@Path("user") user: String): List<Repo>
      }
      

      Note that we not only added suspend, but also we no longer return Call, but the result directly. Now, we can use it without all this enqueue() boilerplate:

      val repos = service.listRepos(user)