Sealed Classes - Handle API Responses in Android

In this article, we are going to talk about the usages of Sealed Class in Android Development.

A sealed class allows you to represent constrained hierarchies in which an object can only be of one of the given types.

Let us Handle API Responses (Success/Error) in Android using Sealed Classes.

Just create a generic sealed class named Resource in a separate file in your data package or utils/others package. 

We have named it Resource & created it generic because we’ll use this class to wrap our different types of API responses

Basically, we need to update our UI on these three events i.e. Success, Error, Loading

So, we have created these as child classes of Resources to represent different states of UI. 

Now, in case of success, we’ll get data so we’ll wrap it with Resource. Success and in case of error, we’ll wrap the error message with Resource. Error & in case of loading we’ll just return Resource.Loading object (you can also alter it to wrap your loading data according to your needs).

sealed class DataHandler<T>(
    val data: T? = null,
    val message: String? = null
) {

    // We'll wrap our data in this 'Success'
    // class in case of success response from api
    class SUCCESS<T>(data: T) : DataHandler<T>(data)

    // We'll pass error message wrapped in this 'Error'
    // class to the UI in case of failure response
    class ERROR<T>(data: T? = null, message: String) : DataHandler<T>(data, message)

    // We'll just pass object of this Loading
    // class, just before making an api call
    class LOADING<T> : DataHandler<T>()
}

..

How to Handle the API Result (Sealed Class)

We can handle the result using `when` expression in Kotlin. 

Check for the what kind of result returned from the Repository/Data Layer from application. Execute your logic based on the result state passed from the repository layer.

     //...............
        lifecycleScope.launchWhenStarted {
            viewModel.userLoginResponse.collect { dataHandlerResponse ->

                // to Handle the API Result (Sealed Class)
                when (dataHandlerResponse) {
                    is DataHandler.SUCCESS -> {
                    }
                    is DataHandler.ERROR -> {
                    }
                    is DataHandler.LOADING -> {

                    }
                }
            }
        }

..

In our view model, we are going to call API and set data to sealed class:

 private val _userLoginResponse: MutableStateFlow<DataHandler<LoginData>> = MutableStateFlow(DataHandler.LOADING())
    val userLoginResponse: StateFlow<DataHandler<LoginData>> = _userLoginResponse
    
    fun userLoginAuth(formData: RequestBody) = viewModelScope.launch {
        // loading
        _userLoginResponse.value = DataHandler.LOADING()
        try {
            
            val response = networkRepository.getLogin(formData) //api
            
            if (response.isSuccessful) {
                response.body()?.let {
                    // success response
                    _userLoginResponse.value = DataHandler.SUCCESS(it)
                }
            }
            else{
                // failure response
                val jObjError = JSONObject(response.errorBody()!!.string())
                _userLoginResponse.value = DataHandler.ERROR(message = jObjError.getString("message").toString())
            }

        }
        catch (e: Exception) {
             // exception 
            _userLoginResponse.value = DataHandler.ERROR(null, e.message.toString())
        }
    }

..

Another example:

Better Way to Manage UI State in Android

To use Sealed Class to manage app failures or exceptions

sealed class AppFailure {
    /**
     * Global Failure classes
     * These failures will be used across all over the app including Data Layer, Domain Layer, Framework Layer
     */
    class GeneralFailure(var message: String, var errorCode: Int? = null) : AppFailure()
    class UnauthorizedFailure(var message: String = "Unauthorized", errorCode: Int? = null) : AppFailure()
    class LoginFailure(var message: String = "Unable to login", errorCode: Int? = null) : AppFailure()
    class ServerFailure(var message: String = "Unable to connect to the server side", errorCode: Int? = null) : AppFailure()
    class NoInternetFailure(var message: String = "Device is not connected to the internet", errorCode: Int? = null) : AppFailure()

    /**
     * Feature based failures
     */
    abstract class FeatureFailure : AppFailure() {
        class AFailure(var message: String, var errorCode: Int? = null) : FeatureFailure()
        class BFailure(var message: String, var errorCode: Int? = null) : FeatureFailure()
        class AnotherFailure(var message: String, var errorCode: Int? = null) : FeatureFailure()
    }
}

Let's take a look into this class. We have a Sealed Class called "AppFailure" which represent all failure's of the application. I hope that the class names make sence about what kind of failure we are going to deal with.

We have an abstract class named "FeatureFailure" which stands for feature based failure. We might have several feature with several kind of failure. We can inlcude all of our feature based failure inside the "FeatureFailure" class.

..

To use sealed class for API Result

/**
 * This sealed class represent the any kind of API result across the application
 */
sealed class APIResult {
    object Loading : APIResult()
    class Success(var data: Any) : APIResult()
    class Error(var failure: AppFailure) : APIResult()
}

Let's talk about how we can use the Sealed class to handle API Result. "APIResult" class has two classes called "Success" and "Error" with one object called "Loading". Hope classes name make sense regarding what kind of result they are carrying.

..

To Handle the API Result (Sealed Class)

/**
 * Handle your API calling result here.
 * Place this code inside your view model
 *
 * [Make sure to execute your API calls either using Kotlin's Coroutine or RxJava]
 */
fun handleAPIResult(result: APIResult) {
    when (result) {
        is APIResult.Loading -> {
            // Show loading
        }
        is APIResult.Success -> {
            // Hide Loading
            // Write your logic here
        }
        is APIResult.Error -> {
            // Hide loading
            /**
             * Hand over the [APIResult.Error] value which is [AppFailure] to the [handleFailure]
             * function to handle the failure state
             */
            handleFailure(result.failure)
        }
    }
}

We have written a function called "handleAPIResult" which take "APIResult" as a param for this function. We can handle the result using `when` expression in Kotlin. Check for the what kind of result returned from the Repository/Data Layer from application. Execute your logic based on the result state passed from the repository layer.

..

To handle failures in Framework Layer (Sealed Class)

/**
 * Handle all failures here. Place this code into your Base Activity class
 *
 * @param failure, [AppFailure]
 */
fun handleFailure(failure: AppFailure) {
    when (failure) {
        is AppFailure.GeneralFailure -> {
            Log.d("TAG", "handleFailure: ${failure.message}")
        }
        is AppFailure.LoginFailure -> {
            Log.d("TAG", "handleFailure: ${failure.message}")
        }
        is AppFailure.UnauthorizedFailure -> {
            Log.d("TAG", "handleFailure: ${failure.message}")
        }
        is AppFailure.NoInternetFailure -> {
            Log.d("TAG", "handleFailure: ${failure.message}")
        }
        is AppFailure.ServerFailure -> {
            Log.d("TAG", "handleFailure: ${failure.message}")
        }
        is AppFailure.FeatureFailure.AFailure -> {
            Log.d("TAG", "handleFailure: ${failure.message}")
        }
        is AppFailure.FeatureFailure.BFailure -> {
            Log.d("TAG", "handleFailure: ${failure.message}")
        }
        is AppFailure.FeatureFailure.AnotherFailure -> {
            Log.d("TAG", "handleFailure: ${failure.message}")
        }
        else -> {
            Log.d("TAG", "handleFailure: Something went wrong}")
        }
    }
}

It's super easy to handle all of the failures happen in the application. Based on the state of the Failure, execute your logic to deal the failures.


Here is full source code:

sealed class AppFailure {
    /**
     * Global Failure classes
     * These failures will be used across all over the app including Data Layer, Domain Layer, Framework Layer
     */
    class GeneralFailure(var message: String, var errorCode: Int? = null) : AppFailure()
    class UnauthorizedFailure(var message: String = "Unauthorized", errorCode: Int? = null) : AppFailure()
    class LoginFailure(var message: String = "Unable to login", errorCode: Int? = null) : AppFailure()
    class ServerFailure(var message: String = "Unable to connect to the server side", errorCode: Int? = null) : AppFailure()
    class NoInternetFailure(var message: String = "Device is not connected to the internet", errorCode: Int? = null) : AppFailure()

    /**
     * Feature based failures
     */
    abstract class FeatureFailure : AppFailure() {
        class AFailure(var message: String, var errorCode: Int? = null) : FeatureFailure()
        class BFailure(var message: String, var errorCode: Int? = null) : FeatureFailure()
        class AnotherFailure(var message: String, var errorCode: Int? = null) : FeatureFailure()
    }
}


/**
 * This sealed class represent the any kind of API result across the application
 */
sealed class APIResult {
    object Loading : APIResult()
    class Success(var data: Any) : APIResult()
    class Error(var failure: AppFailure) : APIResult()
}

/**
 * Handle all failures here. Place this code into your Base Activity class
 *
 * @param failure, [AppFailure]
 */
fun handleFailure(failure: AppFailure) {
    when (failure) {
        is AppFailure.GeneralFailure -> {
            Log.d("TAG", "handleFailure: ${failure.message}")
        }
        is AppFailure.LoginFailure -> {
            Log.d("TAG", "handleFailure: ${failure.message}")
        }
        is AppFailure.UnauthorizedFailure -> {
            Log.d("TAG", "handleFailure: ${failure.message}")
        }
        is AppFailure.NoInternetFailure -> {
            Log.d("TAG", "handleFailure: ${failure.message}")
        }
        is AppFailure.ServerFailure -> {
            Log.d("TAG", "handleFailure: ${failure.message}")
        }
        is AppFailure.FeatureFailure.AFailure -> {
            Log.d("TAG", "handleFailure: ${failure.message}")
        }
        is AppFailure.FeatureFailure.BFailure -> {
            Log.d("TAG", "handleFailure: ${failure.message}")
        }
        is AppFailure.FeatureFailure.AnotherFailure -> {
            Log.d("TAG", "handleFailure: ${failure.message}")
        }
        else -> {
            Log.d("TAG", "handleFailure: Something went wrong}")
        }
    }
}

/**
 * Handle your API calling result here.
 * Place this code inside your view model
 *
 * [Make sure to execute your API calls either using Kotlin's Coroutine or RxJava]
 */
fun handleAPIResult(result: APIResult) {
    when (result) {
        is APIResult.Loading -> {
            // Show loading
        }
        is APIResult.Success -> {
            // Hide Loading
            // Write your logic here
        }
        is APIResult.Error -> {
            // Hide loading
            /**
             * Hand over the [APIResult.Error] value which is [AppFailure] to the [handleFailure]
             * function to handle the failure state
             */
            handleFailure(result.failure)
        }
    }
}

..

..

Comments