Generating UI state in android for jetpack compose using sealedX

Peter Chege
4 min readMay 7, 2023

Hello guys, welcome back .In the previous article, we learnt about stateful and stateless composables and how to use them. In this article, we are going to learn how to generate UI states to avoid repetition

In android, we normally wrap our UI state in data classes to have a single point of reference when is comes to modifying the state. A common type of state we use when building our app is as shown below

data class HomeScreenState(
val msg: String = "",
val posts: List<Post> = emptyList(),
val success: Boolean = false,
val errorMessage: String = "",
val isLoading: Boolean = false,
)
private val _state = mutableStateOf(HomeScreenState())
val state: State<HomeScreenState> = _state

The data class above wraps all our UI state in one class.The state here is for a screen that fetches data from a remote API and displays the result on the screen. If your app is mostly presentational and depends on data from remote APIs, Then most of your screen states look like this. These types of data classes become repetitive, and we don’t like repetitive code so what should we do ?

IN comes the sealedX library

SealedX is a kotlin library by github_skydoves that generates data classes based of a given input. This library helps us prevent those repetitive data classes by generating them for us at build time. The library uses KSP to generate the necessary code for us

How do we use it ?

First of all make sure to follow the installation steps defined in the project readme As you may have already read, there are 2 annotations ExtensiveSealed and ExtensiveModel

ExtensiveSealed is an annotation that is used to anotate the sealed interface for our general UI state The most common example something like this

sealed interface UiState {
data class Success(val data: Extensive) : UiState
object Loading : UiState
object Error : UiState
}

When fetching data for a screen these are usually the 3 states we work with

The ExtensiveSealed annotation usually takes an array of parameters for the classes that need UI state to be generated as shown below

@ExtensiveSealed(
models = [
ExtensiveModel(HomeScreen::class),
]
)
sealed interface UiState {
data class Success(val data: Extensive) : UiState
object Loading : UiState
object Error (val message:String): UiState

data class HomeScreen(
val posts:List<Post>
)

Above the HomeScreen data class contains the expected data which is passed to the Extensive generic type Note : The keyword Extensive is from the library and is used as a placeholder for the given data class provided in the models array.

The ExtensiveModel keyword takes a class definition as an argument and is passed as an element to the models arrays in the ExtensiveSealed annotation

So what this does it generates a sealed Interface for each ExtensiveModel declaration like below

public sealed interface HomeScreenUiState {
public data class Error(
public val message: String,
) : HomeScreenUiState

public object Loading : HomeScreenUiState
public data class Success(
public val `data`: HomeScreen,
) : HomeScreenUiState
}

The name of the sealed interface will be name of the class passed to the ExtensiveModel appended with the name of the general sealed interface e.g. HomeScreen + UiState = HomeScreenUiState

If you pass an array of 3 ExtensiveModel instances , 3 sealed interfaces will be generated and so on

Once you have declared you finshed declaring the ExtensiveSealed and ExtensiveModel, its now time to generate them To generate the classes , just rebuild your project and you will see the generated classes in your project

Let’s start using our generated classes

@HiltViewModel
class HomeScreenViewModel @Inject constructor(
private val postRepository: PostRepository,
) : ViewModel() {
private val _uiState = MutableStateFlow<HomeScreenUiState>(HomeScreenUiState.Loading)
val uiState = _uiState.asStateFlow()

init {
getPosts()
}

private fun getPosts() {
viewModelScope.launch {
_uiState.value = HomeScreenUiState.Loading
try {
val response = productRepository.getAllProducts()
_uiState.value = HomeScreenUiState.Success(data = HomeScreen(posts = response.posts))
} catch (e: HttpException) {
_uiState.value = HomeScreenUiState.Error(message = "Please check your internet connection")

} catch (e: IOException) {
_uiState.value = HomeScreenUiState.Error(message = "Server down please try again later")
}
}
}
}

Above we just create our view model and fetch the data from our repository and store it into state We then consume our state in our composable like this

fun HomeScreen(
navController: NavController,
navHostController: NavController,
viewModel: HomeScreenViewModel = hiltViewModel(),
) {
val uiState = viewModel.uiState.collectAsStateWithLifecycle()
HomeScreenContent(uiState = uiState.value)

}
fun HomeScreenContent(
uiState:HomeScreenUiState
){
Box(modifier = Modifier.fillMaxSize()) {
when (uiState) {
is HomeScreenUiState.Loading -> {
// show something when loading

}
is HomeScreenUiState.Error -> {
// show the error message

}
is HomeScreenUiState.Success -> {
// show the posts
}
}
}
}

Above we just consume the data and show something based on the current state I am using 2 composable. The reason is explained in this previous article here

Limitations of this library

  • Currently, the library has a bug where when you try to define more than one sealed interfaces with the library it will append combine all the states from all the sealed interface instances annotated with the ExtensiveSealed keyword into the generated files hence it will cause a compilation error if you declare the same state in more than one instance I opened an issue in the repo hoping it will be fixed but until then try to only declare one instance annotated with the ExtensiveSealed

N/B: Note this is only a solution for when it comes to data fetching for multiple screens. If your app has complex states then this might not be ideal for your project

Thank you for reading and I hope you learnt something new until next time …..Bye see you next time

Don’t forget to follow my github and twitter account

--

--