Skip to main content

Command Palette

Search for a command to run...

Taming Jetpack

A Deep Dive into Compose Previews with ViewModels

Published
2 min read
Taming Jetpack
E

Hello there, I'm Eric Wafula, an Android Developer passionate about pushing mobile technology's boundaries. Over the past 2 years, I have honed my skills in Java, Kotlin, and the Android SDK, using them to build apps that redefine how users interact with digital platforms.

My fascination with technology, coupled with an inherent problem-solving knack, led me on a self-guided journey into Android development. In this path, I've created a range of projects, from dynamic e-commerce apps to innovative productivity tools. Each application I've built has taught me more about the complexities of coding and the delicate intricacies of user-friendly design.

I started this blog to document my experiences and share my insights from my continuous exploration of Android development. Whether you're a self-taught developer, a tech enthusiast, or just curious about how apps are made, there's something here for you.

When I'm not knee-deep in code, you'll find me contributing to open-source projects, experimenting with the latest tech trends, or connecting with other developers in various online communities. I'm a firm believer in lifelong learning and the power of sharing knowledge, which is the main drive behind this blog.

Welcome to my corner of the internet. Let's embark on this exciting journey in the realm of Android development together!

Jetpack Compose being a modern UI Toolkit brought a lot of improvements with its declarative approach to UI development. If you are familiar with Jetpack compose, then you have definitely encountered a few problems when it comes to working with previews.

Have you ever asked yourself why the preview window suddenly breaks when passing viewModels to your composable functions?

Well... Simply put this way, compose previews are not run in the context of an activity or a fragment and don't have access to the ViewModelProvider or lifecycle owners.

So how do we go about making our previews work with viewModels?

We can do that by having a separate composable that holds our screen's content.

I have prepared the following app to illustrate this.

It's a simple counter app where you can either increase, decrease or reset the digit in the card.

Here are the code snippets for the event class and the viewModel

sealed class CounterEvent {
    object Increase : CounterEvent()
    object Decrease : CounterEvent()
    object Reset : CounterEvent()
}
class CounterViewModel : ViewModel() {

    var counter by mutableStateOf(0)
        private set

    fun onEvent(event: CounterEvent) {
        when (event) {
            CounterEvent.Increase -> {
                counter++
            }
            CounterEvent.Decrease -> {
                counter--
            }
            CounterEvent.Reset -> {
                counter = 0
            }
        }
    }

}

The example below shows how we can separate our screen's content with a separate composable that only receives the state value instead of the ViewModel.

@Composable
fun CounterScreen(
    viewModel: CounterViewModel = viewModel()
) {
    CounterScreenContent(state = viewModel.counter, onEvent = viewModel::onEvent)
}

@Composable
fun CounterScreenContent(
    state: Int,
    onEvent: (CounterEvent) -> Unit
) {
    Box(modifier = Modifier.padding(24.dp)) {
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(horizontal = 24.dp, vertical = 12.dp),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Card(
                modifier = Modifier
                    .fillMaxWidth()
            ) {
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(24.dp),
                    contentAlignment = Alignment.Center
                ) {
                    Text(text = state.toString(), fontSize = 24.sp, fontWeight = FontWeight.Bold)
                }
            }
            Spacer(modifier = Modifier.height(23.dp))
            FilledTonalButton(
                modifier = Modifier.align(Alignment.End),
                onClick = { onEvent(CounterEvent.Reset) }
            ) {
                Text(text = "Reset")
            }
        }
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .align(Alignment.BottomCenter),
            horizontalArrangement = Arrangement.SpaceBetween
        ) {
            Button(onClick = { onEvent(CounterEvent.Increase) }) {
                Text(text = "Increase")
            }
            Button(onClick = { onEvent(CounterEvent.Decrease) }) {
                Text(text = "Decrease")
            }
        }
    }
}

@Preview(showBackground = true)
@Composable
fun CounterScreenPreview() {
    ComposePreviewsTheme {
        CounterScreenContent(state = 0, onEvent = { })
    }
}

There are so many ways to go around this, however, I found this approach to be straightforward and easy to work with.

That's all folks.