Using Jetpack Compose in Your Android Applications
·5 min read
This post is based on a talk I gave at DevFest Bangladesh 2022, organised by GDG Sonargaon.
Jetpack Compose has changed how I build Android UIs. After building apps with the old View system for years, switching to Compose felt like removing scaffolding I'd always assumed was load-bearing. Here's what I covered at the event, and what I think every Android developer should know about Compose.
Why Compose Changes Things
The View system was built for Java in 2008. XML layouts, View binding, ViewHolders, adapters — none of these exist in Compose.
Compose is declarative: you describe what the UI should look like given the current state, and Compose handles the rendering. When state changes, the affected parts of the UI recompose automatically.
// Old way — imperativebinding.userName.text = user.namebinding.userAvatar.load(user.avatarUrl)// Compose way — declarative@Composablefun UserCard(user: User) { Row { AsyncImage(model = user.avatarUrl, contentDescription =
The UserCard Composable will automatically re-render whenever user changes. No manual binding, no notifyDataSetChanged().
Understanding Recomposition
Compose redraws parts of the UI when their inputs change. This is called recomposition. Understanding it is the most important thing for writing performant Compose code.
@Composablefun Counter() { var count by remember { mutableStateOf(0) } Column { // Only this Text recomposes when count changes Text(text = "Count: $count") // This button never recomposes (its inputs don't change) Button(onClick = { count++ }) { Text("Increment") } }}
remember keeps state alive across recompositions. Without it, state resets every time the Composable redraws.
State Management in Compose
State flows down, events flow up. This is called unidirectional data flow and it's central to how Compose works.
Composables should be side-effect free — their only job is to produce UI. For side effects (launching a coroutine, registering a callback), use effect handlers:
@Composablefun TrackingScreen(userId: String) { val scope = rememberCoroutineScope() // Runs once when userId changes LaunchedEffect(userId) { analytics.trackScreen("tracking", userId) } // Runs on composition and on cleanup DisposableEffect(Unit) { val listener = locationManager.addListener { ... } onDispose { locationManager.removeListener(listener) } }}
LaunchedEffect — for one-off coroutine work tied to a key.
DisposableEffect — for anything that needs cleanup (listeners, subscriptions).
SideEffect — for synchronising Compose state to non-Compose code on every successful recomposition.
Migrating from the View System
You don't need to rewrite everything at once. Compose and the View system interoperate:
// Use a Composable inside an existing XML layoutclass MyFragment : Fragment() { override fun onCreateView(...) = ComposeView(requireContext()).apply { setContent { MyTheme { MyComposable() } } }}// Use an existing View inside a ComposableAndroidView(factory = { context -> MapView(context).apply { /* setup */ }})
Start with new screens in Compose. Migrate existing screens incrementally when they need significant changes.
Common Mistakes to Avoid
Reading state outside of Compose. State reads inside a Composable trigger recomposition. Reads outside (in callbacks, coroutines) don't.
Creating state in the wrong place. State that needs to survive recomposition goes in remember. State that needs to survive navigation goes in ViewModel.
Not using key in lazy lists. Without keys, Compose can't track list item identity across changes — leading to incorrect animations and unnecessary recompositions.
Skipping theming. Wrap your app in a MaterialTheme. Composables like Surface, Card, and Button read colour and typography from the theme automatically.
The Learning Curve
Compose is different enough from the View system that it takes real time to click. The first week I used it I was slower than with XML. By the second month I was faster. By the third month I didn't want to go back.
The investment is worth it. The boilerplate is gone, the testing is cleaner, and the UI code finally reads like what it does.