Stateful and stateless components in Jetpack compose

Keep 2 versions of the same composable:Stateless and Stateful

What are stateful and stateless composables in Jetpack compose ?

A stateless component is a component that doesn't maintain any internal state. It's purely a function of its inputs, meaning that the output of the component is solely determined by the props that are passed to it. These components are also known as "dumb components" because they don't have any logic beyond rendering what they're given.

On the other hand, a stateful component is a component that does maintain an internal state. It's a function of its inputs as well as its own internal state. Stateful components are also known as "smart components" because they have logic beyond rendering what they're given. They can respond to user events, perform complex calculations, and modify their own state, which can trigger a re-render of the component.

An example of a stateful Composable
@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }

    Column {
        Text("Count: $count")
        Button(onClick = { count++ }) {
            Text("Increment")
        }
    }
}

@Composable
fun HomeScreen(
    viewModel:HomeScreenViewModel = hiltViewModel(),
) {
    val posts = viewModel.posts.collectAsStateWithLifecycle()
    LazyColumn(
        modifier = Modifier.fillMaxSize()
    ){
        items(items =posts){ post -> 
            Post(post = post)
        }
    }
}

In the 1st composable the state is stored in the composable. This is said to be stateful since the composable stores its current state

In the 2nd composable the state is stored in a viewModel. Normally this is what you usually do in project

While this approach works fine there are a few issues that arise They include

  • Testing: Testing stateful composables can be more difficult because they require you to manage and manipulate their internal state. This can make it harder to write tests that are reliable and cover all possible scenarios.

  • They break previews: Stateful composables cannot be easily previewed in development mode since they for them to be initialized the need a customview model instance which will in turn lead to a custom repository for the view model and custom mock data for the data sources (local or remote etc)

What is the solution then ?

In comes stateless composables. As discussed earlier stateless composables do not manage state they simply get state passed on to them. When the UI needs to make changes we can pass them to the necessary functions as parameters

But we will need to eventually instantiate the view model right ?

Yes. We will need 2 different versions of the same composable, A stateless and stateful one then we use the stateless one for testing and previews.

Here's an example

An example of a stateful Composable
//Stateful version of the composable
@Composable
fun HomeScreen(
    viewModel:HomeScreenViewModel = hiltViewModel(),
) {
    val posts = viewModel.posts.collectAsStateWithLifeCycle()
    HomeScreenContent(posts = posts.value)
}

//Stateless version of the composable
@Composable
fun HomeScreenContent(
    posts:List<Post>
) {
    LazyColumn(
        modifier = Modifier.fillMaxSize()
    ){
        items(items = posts){ post -> 
            Post(post = post)
        }
    }
}

Above is the stateful and stateless version of the composable. It a good practice to prefix the stateless composable with "Content" then pass it to the stateful version and pass the state and the state modifiers functions for example in this case you could add a like post function and pass its implementation to the HomeScreenContent composable.

NB: This should only to apply to screen level composables since the other composables in the screen composable are already statelss

I recently learnt about this new concept and decided to share it For more examples check how it was implemented in the following repos

Philip Lackner also did a video addressing the same issue Video Link

I hope this article helped you in a way :)