As the rate of data communication increases, the complexity of the application architecture also increases. How an application handles API responses will determine its overall architectural design and code complexity.
In this post, you will cover how to model Retrofit responses with Coroutines and Sealed classes to reduce code complexity and make your application architecture consistent.
Before you dive in, make sure your project includes Coroutines and Retrofit dependencies.
First things first, let’s see an example of Retrofit API calls. The
fetchPoster function requests a list of posters to the network, and the
PosterRemoteDataSource returns the result of the
This snippet is a basic example of calling the Retrofit API and handling the response. It works well. But suppose you need to handle the response and exceptions in a multi-layer architecture as in the API data flow below:
In this architecture, you will face the following problem: results are ambiguous on call sites.
fetchPoster function may return an empty list if the body of the API response is empty. So if you return an empty list or null when the network request fails, other layers have no idea how to figure out whether the request was successful or not.
You also need to handle exceptions somewhere in this multi-layer architecture, because API calls may throw an exception and it can be propagated to the call site. This means you should write lots of try-catch boilerplate code for each API request.
So how do you solve this problem? It’s simple: Wrap every possible scenario of an API response with a sealed class as in the figure below:
By passing a wrapper class to the call site, the presentation layer can handle results depending on the response type. For example, configuring UI elements and displaying a different placeholder/toast depending on error types.
Let’s see how to construct the wrapper class with a sealed class.
Sealed classes represent quite more restricted class hierarchies than normal classes in Kotlin. All subclasses of a sealed class are known at compile-time, which allows you to use them exhaustively in the when expression.
As you’ve seen in the figure above, there are typically three scenarios where you’d want to construct a sealed class:
Each scenario represents different API results from a Retrofit API call:
- NetworkResult.Success: Represents a network result that successfully received a response containing body data.
- NetworkResult.Error: Represents a network result that successfully received a response containing an error message.
- NetworkResult.Exception: Represents a network result that faced an unexpected exception before getting a response from the network such as IOException and UnKnownHostException.
If you use Kotlin version 1.5 or higher, you can also design the wrapper class with a Sealed Interface:
With a sealed interface, subclasses don’t need to be placed in the same package, which means you can use the class name as it is. However, sealed interfaces must have public visibility for all properties and they can expose unintended API surfaces.
This article covers modeling Retrofit responses with a sealed class, but you can use a sealed interface instead depending on your architectural design.
Let’s see how to get the NetworkResult Sealed class from a Retrofit response with the
handleApi function receives an executable lambda function, which returns a Retrofit response. After executing the lambda function, the
handleApi function returns NetworkResult.Success if the response is successful and the body data is a non-null value.
If the response includes an error, it returns NetworkResult.Error, which contains a status code and error message. You also need to handle any exceptional cases a Retrofit call may throw, like HttpException and IOException.
This is an example of the
handleApi function in a data layer:
PosterRemoteDataSource returns a NetworkResult by executing the
handleApi function, which executes
fetchPosters network requests. After getting a NetworkResultyou can handle the response exhaustively in the
when expression as seen in the ViewModel example below:
We improved the process of handling responses and exceptions with the
handleApi function as seen in the data flow below:
Everything works fine, but you still need to write the
handleApi function repeatedly for each network request. It means not only the data layer has a dependency on the
handleApibut also the responsibility of handling the API responses.
So, how do we improve this process? One of the best ways is by building a custom Retrofit CallAdapter, which allows you to delegate call responses; you can also return a preferred type in the Retrofit side as seen in the figure below:
Let’s see how to implement and adopt a custom Retrofit CallAdapter step by step.
To delegate Retrofit responses, you need to write a custom Retrofit Call class, which implements a Call interface as seen below:
Let’s take a look at the
enqueue first function:
enqueue function sends a request asynchronously and notifies the callback of its response. As you can see in the code above, it delegates API responses to the callback of the
For getting API responses, you should override
onFailure functions for the
- OnResponse: Will be invoked if an API call receives a
Failureresponse from the network. After receiving the response, you can use the
handleApifunction for getting a
NetworkResultand pass it to the callback.
- OnFailure: Will be invoked if an error occurred when talking to the server, creating the request, or processing the response. It will create a
NetworkResult.Exceptionwith a given throwable, and pass it to the callback.
For other functions, you can just delegate API behaviors to the
NetworkResultCall class. Now, let’s see how to implement a Custom Retrofit CallAdapter.
Retrofit CallAdapter adapts a Call with response type, which delegates to call. You can implement the CallAdapter as seen below:
adapt function, you can just return an instance of the
NetworkResultCall class that you’ve implemented in the previous step.
Now let’s see how to implement the Retrofit CallAdapterFactory.
Retrofit CallAdapterFactory creates CallAdapter instances based on the return type of the service interface methods to the Retrofit builder.
You can implement a custom CallAdapterFactory as seen below:
get function, you should check the return type of the service interface methods, and return a proper CallAdapter. The NetworkResultCallAdapterFactory class above creates an instance of NetworkResultCallAdapter if the return type of the service interface method is
That’s all. Now let’s see how to apply the Retrofit CallAdapterFactory to the Retrofit builder.
You can apply the NetworkResultCallAdapterFactory to your Retrofit builder:
Now, you can use the NetworkResult as a return type of the service interface methods with the
As a result, the data flow will look like the figure below. The Retrofit CallAdapter handles Retrofit responses and exceptions, so the responsibility of the data layer has been significantly reduced.
Each layer can expect the result type of the Retrofit API call to be NetworkResultso you can write useful extensions for the NetworkResult class.
For example, you can perform a given action on the encapsulated value or exception if an instance of the NetworkResult represents its dedicated response type as seen in the example below:
Those extensions return themselves by using the apply scope function, so you can use them sequentially:
If you want to use a reliable solution that has been used by lots of real world products and significantly save your time for modeling Retrofit responses, you can also check out the open-source library, Sandwich.
Sandwich is a sealed API library that provides a Retrofit CallAdapter by default and lots of useful functionalities such as operators, global response handling, and great compatibility with LiveData and Flow. For further information, check out the README.
If you want to handle more comprehensive responses from different resources, Kotlin’s Result class is also a great candidate.
The Results class is included in Kotlin’s standard library by default, so you can use the Results in your project without further steps. For more details, check out the Encapsulate successful or failed function execution README.
You can watch a live talk about “Modeling Retrofit Responses with Sealed class and Coroutines” at Android Worldwide soon!
In this article, you saw how to model Retrofit responses with sealed classes and coroutines.
If you want to see the code covered in the article, check out the open-source repository Sandwich on GitHub, which has been used in lots of real-world products.
You can find the author of this article on Twitter @github_skydoves if you have any questions or feedback. If you’d like to stay up to date with Stream, follow us on Twitter @getstream_io for more great technical content.
As always, happy coding!