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 ProductsUiState.Loading -> {
// show something when loading
}
is ProductsUiState.Error -> {
// show the error message
}
is ProductsUiState.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 theExtensiveSealed
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
Top comments (0)