DEV Community

Peter Chege
Peter Chege

Posted on

Generating UI state in android for jetpack compose using sealedX library

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
Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

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>
)

Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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")
            }
        }
    }

}
Enter fullscreen mode Exit fullscreen mode

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

            }

        }
    }
}


Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)