Android new Results API and how to use it to make your code cleaner

Guilherme Krzisch
May 28, 2020 · 4 min read

Introduction

After years of having to receive activity results directly in Activity and Fragment classes, Google finally released a new API in order to decouple this functionality from these classes.

To demonstrate how this new API works, and how we could take advantage of its new capabilities to make our code cleaner and more testable, we are going to use a sample application. It consists of two Acitivities: ActivityA and ActivityB, where the first starts the second and waits for a success or a failure result.

Image for post
Image for post

Traditional approach

The official API since the first version of Android consisted of two points:

startActivityForResult(ActivityB.newIntent(this), ACTIVITY_A_REQUEST_CODE)
  • Receiving result (in ActivityA) from the other Activity (ActivityB) via onActivityResult
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { ... }

The second bullet point was what make it impossible for developers to decouple this logic from the Activity. The official explanation of why this could not be a callback (for example) on the startActivityForResult method, is that it would not work consistently after process death or configuration changes (e.g. when sending an Intent to receive a photo from the Camera, as this is a resource-intensive operation, especially on low-end devices, our Activity could be killed). A callback on the triggering method would not be saved under these conditions, but receiving the result on the onActivityResult works.

You can find the simple sample for this here.

New Results API approach

With the new Results API, we don’t have to deal with these two methods anymore: we can use the new prepareCall method to handle the result as a callback in the originating call. No more separate methods!

I will not dive into how it works, you can see for yourself on the official documentation. Here is the same sample as before, but using this new API. Compare this with the previous approach and check if you can see its benefits.

val startForResult = prepareCall(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> ... }

One caveat that you must remember is that all prepareCall methods should be called sequentially and in the same order every time the Activity is created.

Decouple New Results API using official documentation

One drawback with the previous approach, is that it stills is coupled with the Activity, but this is easily decoupled following the recommendations in the official docs: you can have another class implementing LifecycleObserver interface and use the corresponding ActivityResultRegistry.registerActivityResultCallback method that receives a Lifecycle owner.

private lateinit var startForResult : ActivityResultLauncher<Intent>override fun onCreate(owner: LifecycleOwner) {                                         startForResult = registry.registerActivityResultCallback(START_FOR_RESULT_KEY, owner, ActivityResultContracts.StartActivityForResult(), ActivityResultCallback { result -> ... }      
}

Following the same sample, you can find the example here.

Remove explicit dependency on LifecycleOwner

Note that, while in the previous approach we are no longer starting/receiving results directly in the Activity, our “separate class” (we called it Controller here, but you can call it whatever you want in your favorite presentation layer architecture — MVC/MVP/MVVM/MVI/…) still has to implement LifeycleObserver, which could be considered to be a tight coupling to the Android framework classes.

How can we remove this dependency? There is a hint in the official docs: when there is no LifecycleOwner, use ActivityResultRegistry.registerActivityResultCallback method that does not receives a Lifecycle owner, and remember to dispose() when no longer using it.

In order to do this, we can use, for example, LiveData: by subclassing it, we can call registerActivityResultCallback in the onActive method, and dispose the results in the onInactive method. There is one important caveat though: as we must always register when creating the Activity, we cannot use the usual observe method of LiveData, which automatically remove the observer when the view is not active. Instead, we must use the observeForever and manually remove the observer in the onDestroy method.

Here is the final sample code.

While in this article, we choose to use LiveData, which internally stills uses LifecycleOwner, keep in mind that a similar approach would also work with other libraries, as, for example, RxJava (or manually if you prefer). And perhaps, using other approaches would be a better idea in this case, as currently we cannot use the usual observe method of LiveData, which can be a source of bugs in real applications.

Conclusion

As an exercise for the reader, the next steps for this POC would be:

  • Add an example with Fragments. The resulting sample should be similar to what was shown in the article.
  • Add UI and unit tests: UI tests should be straightforward for all the solutions, but for unit tests the last approach has a clear advantage
  • Add an example with RxJava for the final approach
  • Use a custom contract instead of using the equivalent of the current API: in this article we have not touched upon another advantage of the new API, which is the ability of defining custom contracts with expected input and output, providing type-safety for our app

You can find the complete sample repository here.

Further reading

The Startup

Medium's largest active publication, followed by +753K people. Follow to join our community.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store