Sitemap

Don’t Let a Timer Ruin Your Jetpack Compose UI: Best Practices Explained

3 min readMay 6, 2025

Jetpack Compose makes building UI faster and more intuitive with a declarative approach. However, one challenge developers face is managing recompositions efficiently, especially when state updates frequently, such as with a timer. In this post, we’ll build a timer component and show how it can unintentionally cause recomposition of unrelated UI elements. We’ll also explore ways to avoid these pitfalls using state hoisting LaunchedEffectand composition-local best practices.

🧱 The Scenario

You want to show:

  • A countdown timer.
  • A static profile card.
  • A score counter that only updates on user interaction.

Let’s say you build it like this:

@Composable
fun TimerScreen() {
var timer by remember { mutableStateOf(60) }

LaunchedEffect(Unit) {
while (timer > 0) {
delay(1000)
timer--
}
}

Column {
TimerDisplay(timer = timer)
ProfileCard()
ScoreCounter()
}
}

@Composable
fun TimerDisplay(timer: Int) {
Text(text = "Time left: $timer seconds")
}

@Composable
fun ProfileCard() {
// Imagine complex UI
Text(text = "User: Sumeet Panchal")
}

@Composable
fun ScoreCounter() {
var score by remember { mutableStateOf(0) }

Button(onClick = { score++ }) {
Text("Score: $score")
}
}

🚨 The Problem: Unnecessary Recompositions

The issue here is subtle but important.

Every second, timer state changes, triggering recomposition of everything inside TimerScreen():

  • TimerDisplay() ✅ Expected
  • ProfileCard() ❌ Unnecessary
  • ScoreCounter() ❌ Unnecessary

This leads to performance overhead, especially if these components are complex or interact with the system.

✅ The Fix: Split State Ownership and Use rememberUpdatedState

To avoid unnecessary recompositions:

  1. Move the timer logic to a separate component.
  2. Hoist state only where needed.
  3. Use rememberUpdatedState() or derivedStateOf() for fine-grained control.

Let’s refactor it:

@Composable
fun TimerScreen() {
Column {
TimerWrapper()
ProfileCard()
ScoreCounter()
}
}

@Composable
fun TimerWrapper() {
var timer by remember { mutableStateOf(60) }

LaunchedEffect(Unit) {
while (timer > 0) {
delay(1000)
timer--
}
}

TimerDisplay(timer = timer)
}

Now, only TimerWrapper recomposes every second, leaving the rest of the UI untouched.

🧠 Best Practices to Avoid Unnecessary Recompositions

  • Split Composables by Responsibility: Smaller composables with isolated state avoid triggering unrelated recompositions.
  • Use derivedStateOf When State Depends on Another:
val isUrgent by remember { derivedStateOf { timer < 10 } }
  • Prefer LaunchedEffect For Side Effects: Keep ticking logic outside your root composables to prevent accidental recompositions.
  • Avoid Passing Changing State Deep into Composables Unless Needed.
  • Use rememberUpdatedState In Callbacks: Helps avoid stale lambda captures.

🔍 Advanced Tip: Using StateFlow or ViewModel

If your timer is used across multiple screens or survives configuration changes, consider using a ViewModel and StateFlow:

class TimerViewModel : ViewModel() {
private val _timer = MutableStateFlow(60)
val timer: StateFlow<Int> = _timer

init {
viewModelScope.launch {
while (_timer.value > 0) {
delay(1000)
_timer.value -= 1
}
}
}
}

Then collect it in your Composable:

@Composable
fun TimerWrapper(viewModel: TimerViewModel = viewModel()) {
val timer by viewModel.timer.collectAsState()
TimerDisplay(timer)
}

📌 Conclusion

Jetpack Compose is powerful, but with power comes responsibility. In reactive UIs, frequent state changes can quickly snowball into performance issues if not managed carefully. Isolate your state, minimize recompositions, and you’ll enjoy both performance and clean architecture.

https://sumeetpanchal-21.medium.com/%EF%B8%8F-tricky-things-to-consider-while-building-ui-in-jetpack-compose-recomposition-state-and-lambda-78774cf7adb4

--

--

Sumeet Panchal
Sumeet Panchal

Written by Sumeet Panchal

Programming enthusiast specializing in Android and React Native, passionate about crafting intuitive mobile experiences and exploring innovative solutions.

No responses yet