You have a new app idea, a tight deadline, and a strong desire to use Jetpack Compose. The framework promises faster UI development and less boilerplate, but the moment you open Android Studio, the questions pile up: Which navigation library should I use? How do I handle state without falling into recomposition traps? Should I go all-in on Material 3 or stick with Material 2? This checklist is designed for busy Android developers who want to launch a Compose-based app in under a week. We'll walk through seven practical steps, from choosing a minimum SDK to setting up a theming system, so you can move fast without second-guessing. Each step includes concrete criteria, trade-offs, and a decision framework to help you avoid the most common pitfalls.
We assume you're comfortable with Kotlin basics and have at least a passing familiarity with Compose. If you're brand new to declarative UI, we recommend spending an hour with the official Compose tutorial before diving into this checklist. Ready? Let's set a timer for ten minutes and get your project off the ground.
1. Define Your Minimum SDK and Compose BOM Version
Before writing a single Composable function, you need to decide which devices your app will support. This decision ripples through every other choice: navigation libraries, third-party dependencies, and even which Compose APIs you can use.
Start by setting a minimum SDK (minSdk) of 21 (Android 5.0) if you can. This covers about 95% of active devices according to recent distribution data. Going lower (e.g., 19 or 16) means you'll need to enable multidex and may lose access to newer Compose features like rememberSaveable with custom saver classes. Going higher (e.g., 26 or 28) reduces your user base but simplifies testing and lets you use APIs like Flow without compatibility libraries.
Next, pin a specific Compose BOM (Bill of Materials) version. The BOM aligns all Compose library versions so you don't accidentally mix incompatible releases. As of early 2025, the stable BOM is 2024.12.01 (or later). Use a fixed version, not a range like +, to ensure reproducible builds. If you need the latest features, consider using a beta or RC BOM, but be prepared for API changes.
How to set the BOM in your build.gradle
dependencies {
implementation(platform("androidx.compose:compose-bom:2024.12.01"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.ui:ui-tooling-preview")
debugImplementation("androidx.compose.ui:ui-tooling")
}
One common mistake is forgetting to add the ui-tooling-preview dependency, which is required for previews to work in Android Studio. Another is adding BOM dependencies without the version tag — the BOM handles versions, so you can omit them for Compose libraries, but you still need explicit versions for non-Compose dependencies like androidx.navigation.
Trade-off: Using a BOM means you trust the curated set of versions. If you need a newer version of a specific library (e.g., material3 1.3.0), you can override it by specifying a version explicitly, but this may cause conflicts. Test thoroughly after any override.
2. Choose Your Navigation Strategy: Compose Navigation, Voyager, or Decompose
Navigation is one of the first architectural decisions you'll make. The wrong choice can lead to messy back stack handling or deep link issues later. We'll compare three popular approaches for Compose apps.
Option A: Jetpack Navigation Compose (Official)
This is the Google-recommended solution. It integrates tightly with the Navigation component, supports type-safe arguments via Kotlin serialization, and works well with bottom navigation and deep links. The learning curve is moderate: you define a NavHost with routes and use NavController to navigate. It's the safest choice for most apps because of its long-term support and large community.
Option B: Voyager
Voyager is a multiplatform navigation library that offers a more Compose-idiomatic API with features like tab navigation, bottom sheet navigation, and built-in lifecycle handling. It supports iOS and desktop via Kotlin Multiplatform, making it a strong choice if you plan to share navigation logic across platforms. However, it's a third-party library with a smaller community, and you'll need to manage updates yourself.
Option C: Decompose
Decompose takes a different approach: it separates navigation logic from the UI using a component tree. It's powerful for complex flows (e.g., wizards, nested navigation) and works well with MVI-like architectures. The trade-off is a steeper learning curve and more boilerplate. It's best suited for large apps with intricate navigation requirements.
Decision criteria
- If you want zero third-party risk and deep link support out of the box → Jetpack Navigation Compose.
- If you plan to go multiplatform (Android + iOS) and prefer a simpler API → Voyager.
- If your app has complex navigation flows (e.g., multi-step forms, nested graphs) → Decompose.
For most quickstart projects, we recommend starting with Jetpack Navigation Compose. It's the default for a reason: it's well-documented, tested at scale, and you can always migrate to another library later if needed.
3. Set Up State Management: ViewModel + StateFlow or Compose State?
State management is where many Compose projects hit their first snag. The core question is: should you use ViewModel with StateFlow, or keep state entirely within Compose using mutableStateOf and remember? The answer depends on the scope of the state.
Local UI state vs. screen-level state
Local UI state — like whether a dropdown is expanded or the current text in a TextField — should stay in Compose using remember { mutableStateOf() }. This keeps the state close to the UI and avoids unnecessary ViewModel overhead. Screen-level state, such as a list of items fetched from a network, belongs in a ViewModel exposed as a StateFlow. This separation prevents recomposition issues and makes state easier to test.
Common pitfalls
- Putting everything in ViewModel: You end up with a ViewModel that has dozens of state fields, making it hard to maintain. Keep local state in Compose.
- Using mutableStateOf without remember: This creates a new state object on every recomposition, causing infinite loops. Always wrap in
remember. - Collecting flows in the wrong scope: Use
collectAsStateWithLifecycle()from the lifecycle-runtime-compose library to automatically stop collection when the lifecycle drops below STARTED.
Quick setup for screen-level state
class MyViewModel : ViewModel() {
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
fun loadData() {
viewModelScope.launch {
_uiState.value = UiState(loading = true)
// fetch data
_uiState.value = UiState(data = result)
}
}
}
@Composable
fun MyScreen(viewModel: MyViewModel = viewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
// render UI based on uiState
}
This pattern gives you a single source of truth, lifecycle-aware collection, and testability. Avoid using LiveData in new Compose projects — StateFlow is more idiomatic and works better with Compose's snapshot system.
4. Theming and Material 3: When to Customize and When to Stick with Defaults
Material 3 (Material You) is the default theme for new Compose projects. It brings dynamic color support, updated typography, and new components like Navigation Bar and Carousel. But customizing a theme can quickly become a rabbit hole. Here's how to approach theming for a quickstart.
Start with the default Material 3 theme
When you create a new Compose project, Android Studio generates a Theme.kt file with a basic Material 3 theme. This uses dynamicLightColorScheme and dynamicDarkColorScheme on Android 12+ devices, falling back to a default palette. For a quick launch, this is often enough. You can override specific colors later without rebuilding the entire theme.
When to customize
If your app has a strong brand identity (e.g., specific primary, secondary, and tertiary colors), you should define a custom ColorScheme. Use the lightColorScheme() and darkColorScheme() functions with your brand colors. Be careful with contrast: Material 3's tonal palette uses a primary color and generates surface, on-surface, and container colors. Test your palette on real devices to ensure readability.
Typography and shapes
Material 3 defines a type scale with 15 text styles. For most apps, overriding displayLarge, headlineMedium, titleLarge, bodyLarge, and labelSmall is sufficient. Use Typography() to set custom fonts. Similarly, shapes can be customized via Shapes() with small, medium, and large corner radii. Keep shapes consistent: using rounded corners for cards and buttons creates a cohesive look.
Common mistake: Over-theming early
Many teams spend days perfecting a theme before writing any UI. Instead, start with the default theme, build a few screens, and adjust colors and typography as you go. This iterative approach prevents wasted effort on styles that may change when you see the app on a real device.
5. Dependency Injection: Hilt vs. Koin vs. Manual DI
Dependency injection (DI) is essential for testability and separation of concerns, but it adds upfront complexity. For a quickstart, you need to choose a DI framework that balances simplicity with scalability.
Hilt (Recommended for most apps)
Hilt is the standard DI library for Android, built on Dagger. It integrates seamlessly with ViewModel, WorkManager, and Navigation. Setup involves adding the Hilt Gradle plugin, annotating your Application class with @HiltAndroidApp, and using @HiltViewModel for ViewModels. The learning curve is moderate, but the boilerplate is minimal once you understand the annotations. Hilt is the safest choice for production apps because of its compile-time safety and Google support.
Koin (Lightweight alternative)
Koin is a DSL-based DI library that doesn't require annotation processing or code generation. It's easier to learn and faster to set up for small projects. You define modules with single { } and factory { } and start the Koin context in your Application class. The trade-off is that Koin resolves dependencies at runtime, so errors appear only when you run the app. For a quickstart prototype, Koin can be a good choice, but for a production app with many dependencies, Hilt's compile-time checks are valuable.
Manual DI (When to skip a framework)
If your app has fewer than 10 classes that need injection, you can manage dependencies manually using a simple AppContainer object. This avoids any framework overhead and is easy to understand. However, as the app grows, manual DI becomes unwieldy. We recommend using Hilt from the start unless you're building a very small app (e.g., a single-screen utility).
Decision table
| Factor | Hilt | Koin | Manual DI |
|---|---|---|---|
| Setup time | 30-60 minutes | 15-30 minutes | 10 minutes |
| Compile-time safety | Yes | No | No |
| Best for | Medium to large apps | Small to medium apps | Tiny apps / prototypes |
6. Risks of Skipping or Rushing These Steps
Cutting corners on the setup phase can lead to costly rework later. Here are the most common risks we've seen in Compose projects.
Risk 1: Incompatible library versions
If you don't pin a BOM version, you may end up with mismatched Compose libraries that cause cryptic crashes at runtime. For example, using material3 1.2.0 with compose-ui 1.6.0 can lead to missing methods or unexpected behavior. Always use a BOM and test on a physical device before writing much code.
Risk 2: State management spaghetti
Without a clear state management strategy, you'll end up with state scattered across ViewModels, Composables, and global singletons. This makes debugging recomposition issues a nightmare. Stick to the ViewModel + StateFlow pattern for screen-level state and local Compose state for UI details. Avoid using mutableStateOf in ViewModels — use StateFlow instead.
Risk 3: Theming inconsistency
If you customize colors and typography without a central theme, you'll end up with hardcoded values scattered across Composables. Changing the brand color later becomes a hunt-and-replace nightmare. Use Material 3's MaterialTheme object to access colors and typography everywhere.
Risk 4: Navigation rework
Choosing a navigation library without considering deep links, back stack behavior, and testing can force a rewrite. For example, if you use Voyager and later need complex deep link handling, you may find the library lacking. Start with Jetpack Navigation Compose unless you have a strong reason to use something else.
Risk 5: Ignoring previews
Compose previews are a huge productivity boost, but they require the ui-tooling-preview dependency and proper theme setup. Skipping previews means you'll spend more time deploying the app to see changes. Add previews early and use them to iterate on UI quickly.
7. Mini-FAQ: Quick Answers to Common Questions
Here are answers to questions that often come up during the first week of a Compose project.
Should I use Compose for a production app in 2025?
Yes. Compose is stable, well-supported, and used in many production apps. The performance is comparable to the View system, and the developer experience is significantly better for dynamic UIs. However, if your app relies heavily on custom views or complex Canvas drawing, you may need to interoperate with View system components using AndroidView.
Do I need to learn the View system first?
No, but it helps. If you're new to Android development, you can start directly with Compose. However, understanding the View system's lifecycle and layout model will give you a deeper understanding of how Compose works under the hood. Many tutorials assume some View system knowledge, so be prepared to fill gaps.
How do I handle configuration changes (e.g., screen rotation)?
Compose handles configuration changes automatically when you use rememberSaveable for local state and ViewModel for screen-level state. The ViewModel survives configuration changes by default. For complex state, use SavedStateHandle in the ViewModel to persist data across process death.
What about testing Compose UI?
Compose has a testing framework called compose-ui-test that allows you to write UI tests using ComposeTestRule. You can find elements by text, content description, or test tags and perform actions like clicks and assertions. For unit tests, test ViewModels and business logic separately from the UI.
Can I use Compose with Kotlin Multiplatform?
Yes, Compose Multiplatform is now stable for Android and iOS (desktop and web are in alpha). If you plan to share UI code across platforms, use Compose Multiplatform with a shared module. Note that some Compose libraries (like Navigation Compose) are Android-only, so you may need platform-specific alternatives.
Next actions: 1) Set your minSdk and BOM version today. 2) Create a new project with Jetpack Navigation Compose and Hilt. 3) Build one screen with a ViewModel and a theme. 4) Add a UI test for that screen. 5) Deploy a beta to a small group of testers. These five steps will get you from zero to a launch-ready Compose app in less than a week.
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!