These are some of the common Jetpack Compose mistakes that you need to avoid that lay performance, break UI and annoy users. Find out the things the most experienced Android developers wished they knew.
As someone who has reviewed hundreds of Jetpack Compose codebases in the last two years, nay, the same mistakes come up time and time, which motes me to write this post. Even seasoned Android developers who are used to traditional View systems get caught by these when moving to Compose.
The worst part? Top of the list is: a lot of these problems you do not notice until your app gets into production with real users and an array of different devices.

1. Calling Functions Inside Composables
This is the single biggest performance killer I see in Compose apps:
@Composable
fun UserProfile(userId: String) {
val userData = fetchUserData(userId) // DON'T DO THIS
Text(text = userData.name)
}Notice that function call triggers every recomposition. Always use LaunchedEffect or remember with appropriate keys.
@Composable
fun UserProfile(userId: String) {
var userData by remember { mutableStateOf<User?>(null) }
LaunchedEffect(userId) {
userData = fetchUserData(userId)
}
userData?.let { user ->
Text(text = user.name)
}
}2. Ignoring Composition Locals
Developers end up creating prop drilling nightmare rather than use CompositionLocal for theme data, user preferences or dependency injection.
// Instead of passing theme through 5 levels of composables
val LocalAppTheme = compositionLocalOf { AppTheme() }
@Composable
fun MyApp() {
CompositionLocalProvider(LocalAppTheme provides currentTheme) {
MainScreen()
}
}3. Overusing State Hoisting
Not all state should exist at the top level. If something is a local UI state, e.g. expanded/collapsed states, focus, or animations, keep it in the component it is used.
@Composable
fun ExpandableCard() {
// This state doesn't need to be hoisted
var isExpanded by remember { mutableStateOf(false) }
Card(
modifier = Modifier.clickable { isExpanded = !isExpanded }
) {
// Card content
}
}4. Misunderstanding remember() Keys
Remembering without keys causes stale data and confounding behavior:
@Composable
fun ProductList(products: List<Product>) {
// Wrong: remembers first products list forever
val processedProducts = remember { processProducts(products) }
// Right: recomputes when products change
val processedProducts = remember(products) { processProducts(products) }
}5. Creating Objects in Composable Body
This leads to a waste of work, as each recomposition creates new objects like:
@Composable
fun AnimatedIcon() {
// Creates new AnimationSpec every recomposition
val animationSpec = tween<Float>(durationMillis = 300)
// Better: remember expensive objects
val animationSpec = remember { tween<Float>(durationMillis = 300) }
}6. Ignoring Modifier Order
The order of modifers is very important as it could layout and speed. First background, then padding, then clickable, then size constraints
// Wrong order - padding affects clickable area
Modifier
.padding(16.dp)
.clickable { }
.background(Color.Blue)
// Correct order
Modifier
.clickable { }
.background(Color.Blue)
.padding(16.dp)7. Not Using derivedStateOf
Extract derived state using derivedStateOf when state depends on other state to avoid needless recomposition:
@Composable
fun FilteredList(items: List<Item>, query: String) {
// Recomputes on every recomposition
val filteredItems = items.filter { it.name.contains(query) }
// Better: only recomputes when dependencies change
val filteredItems by remember {
derivedStateOf { items.filter { it.name.contains(query) } }
}
}8. Blocking the Composition Thread
And Compose is being executed on the main thread. Heavy computations when composing freezes your UI:
@Composable
fun ExpensiveCalculation(data: List<Int>) {
// Blocks composition
val result = data.map { heavyComputation(it) }
// Better: move to background
var result by remember { mutableStateOf<List<Int>>(emptyList()) }
LaunchedEffect(data) {
result = withContext(Dispatchers.Default) {
data.map { heavyComputation(it) }
}
}
}9. Improper Side Effect Management
Side effect APIs used incorrectly → Memory leaks and crashes
- LaunchedEffect: For suspend functions
- DisposableEffect: For cleanup (listeners, observers)
- SideEffect: For non-Compose APIs that require the latest values
@Composable
fun LocationTracker() {
DisposableEffect(Unit) {
val locationListener = createLocationListener()
locationManager.requestLocationUpdates(locationListener)
onDispose {
locationManager.removeUpdates(locationListener)
}
}
}10. Not Testing Recomposition Behavior
Compose UIs are tested like plain old Views, causing bugs related to recomposition to go undetected by many developers. Use composition testing tools:
@Test
fun testRecompositionBehavior() {
var recompositionCount = 0
composeTestRule.setContent {
RecompositionCounter { recompositionCount++ }
MyComposable()
}
// Assert recomposition count expectations
}The Road to Compose Mastery
The Android team at google has stated those apps that follow these practices experience 40 percent fewer performance issues and much higher user experience scores.
The key insight? Jetpack Compose is less of a new UI toolkit and more of a new paradigm, and with that comes the requirement of unlearning some of our old Android development habits.
Use that as a starting point to audit your current Compose code for these types of problems. Take the time to rectify each error singularly and leverage the composition debugging tools in Android Studio to confirm that you are addressing the issues.
Developers who nail concepts such as this early on will build together the fluid performant Android apps users love and competitors cannot begin to create.