Jetpack Compose Project Structure Guide
Jetpack Compose Project Structure Guide (2025)
Table of Contents
1. Introduction
2. Core Architectural Principles
3. Recommended Architectures
4. Detailed File Structures
5. Best Practices
6. Common Patterns
7. Real-World Examples
8. References
Introduction
Jetpack Compose is Android's modern declarative UI toolkit. Unlike the traditional View-based
system, Compose encourages a different approach to project organization. This guide provides
comprehensive information on structuring Jetpack Compose projects in 2025.
Key Differences from Traditional Android
Single Activity Architecture: Most Compose apps use one Activity with multiple Composable
screens
Declarative UI: UI is described as functions, not XML layouts
State-driven: UI automatically updates when state changes
Composables as Building Blocks: Small, reusable functions replace Fragments/Views
Core Architectural Principles
1. Separation of Concerns
Don't put all your code in Activities or Composables. Each component should have a single, well-
defined responsibility.
Key Points:
Activities/Composables should only handle UI
ViewModels manage UI state and business logic
Repositories handle data operations
Use Cases (optional) contain business logic
2. Single Source of Truth (SSOT)
Each piece of data should have one owner that modifies it and exposes it as immutable.
3. Unidirectional Data Flow (UDF)
State flows down: From ViewModel to UI
Events flow up: From UI to ViewModel
1 ┌─────────────┐
2 │ ViewModel │ ──► State flows down
3 └─────────────┘
4 ▲
5 │ Events flow up
6 ┌─────────────┐
7 │ UI │
8 └─────────────┘
4. Layered Architecture
Modern Android apps typically have 2-3 layers:
1. UI Layer (Presentation): Displays data (Composables, ViewModels)
2. Domain Layer (Optional): Business logic (Use Cases)
3. Data Layer: Manages app data (Repositories, Data Sources)
Recommended Architectures
Architecture 1: Clean Architecture with MVVM (Most Popular)
Best for: Medium to large projects, teams, scalable apps
1 [Link]/
2 ├── data/
3 │ ├── local/
4 │ │ ├── dao/
5 │ │ │ └── [Link]
6 │ │ ├── entity/
7 │ │ │ └── [Link]
8 │ │ └── database/
9 │ │ └── [Link]
10 │ ├── remote/
11 │ │ ├── api/
12 │ │ │ └── [Link]
13 │ │ ├── dto/
14 │ │ │ └── [Link]
15 │ │ └── interceptor/
16 │ │ └── [Link]
17 │ └── repository/
18 │ └── [Link]
19 │
20 ├── domain/
21 │ ├── model/
22 │ │ └── [Link]
23 │ ├── repository/
24 │ │ └── [Link] (interface)
25 │ └── usecase/
26 │ ├── [Link]
27 │ └── [Link]
28 │
29 ├── presentation/
30 │ ├── navigation/
31 │ │ ├── [Link]
32 │ │ └── [Link]
33 │ ├── theme/
34 │ │ ├── [Link]
35 │ │ ├── [Link]
36 │ │ └── [Link]
37 │ ├── components/
38 │ │ ├── [Link]
39 │ │ └── [Link]
40 │ ├── screens/
41 │ │ ├── home/
42 │ │ │ ├── [Link]
43 │ │ │ ├── [Link]
44 │ │ │ ├── [Link]
45 │ │ │ └── [Link]
46 │ │ ├── detail/
47 │ │ │ ├── [Link]
48 │ │ │ └── [Link]
49 │ │ └── auth/
50 │ │ ├── login/
51 │ │ │ ├── [Link]
52 │ │ │ └── [Link]
53 │ │ └── register/
54 │ │ ├── [Link]
55 │ │ └── [Link]
56 │ └── common/
57 │ └── utils/
58 │ └── [Link]
59 │
60 ├── di/
61 │ ├── [Link]
62 │ ├── [Link]
63 │ └── [Link]
64 │
65 └── [Link]
Characteristics:
Three clear layers: Data, Domain, Presentation
Domain layer contains business logic (Use Cases)
Each screen has its own ViewModel, State, and Events
Dependency Injection organized in separate modules
Architecture 2: Feature-First Modularization (2025 Trend)
Best for: Large apps, multiple teams, clear feature boundaries
1 [Link]/
2 ├── core/
3 │ ├── data/
4 │ │ ├── local/
5 │ │ └── remote/
6 │ ├── domain/
7 │ │ └── model/
8 │ ├── ui/
9 │ │ ├── components/
10 │ │ │ ├── [Link]
11 │ │ │ └── [Link]
12 │ │ └── theme/
13 │ │ ├── [Link]
14 │ │ ├── [Link]
15 │ │ └── [Link]
16 │ ├── navigation/
17 │ │ └── [Link]
18 │ ├── di/
19 │ │ └── [Link]
20 │ └── utils/
21 │ └── [Link]
22 │
23 ├── feature/
24 │ ├── auth/
25 │ │ ├── data/
26 │ │ │ ├── repository/
27 │ │ │ │ └── [Link]
28 │ │ │ └── remote/
29 │ │ │ └── [Link]
30 │ │ ├── domain/
31 │ │ │ ├── model/
32 │ │ │ │ └── [Link]
33 │ │ │ ├── repository/
34 │ │ │ │ └── [Link]
35 │ │ │ └── usecase/
36 │ │ │ ├── [Link]
37 │ │ │ └── [Link]
38 │ │ ├── presentation/
39 │ │ │ ├── login/
40 │ │ │ │ ├── [Link]
41 │ │ │ │ ├── [Link]
42 │ │ │ │ └── [Link]
43 │ │ │ └── register/
44 │ │ │ ├── [Link]
45 │ │ │ └── [Link]
46 │ │ └── di/
47 │ │ └── [Link]
48 │ │
49 │ ├── book_list/
50 │ │ ├── data/
51 │ │ ├── domain/
52 │ │ ├── presentation/
53 │ │ └── di/
54 │ │
55 │ └── book_detail/
56 │ ├── data/
57 │ ├── domain/
58 │ ├── presentation/
59 │ └── di/
60 │
61 └── app/
62 └── [Link]
Characteristics:
Each feature is self-contained with its own layers
Core module contains shared code
Better for parallel development
Easier to test and maintain feature boundaries
Architecture 3: Simple MVVM (Small Projects)
Best for: Small apps, prototypes, learning projects
1 [Link]/
2 ├── data/
3 │ ├── model/
4 │ │ └── [Link]
5 │ ├── repository/
6 │ │ └── [Link]
7 │ └── api/
8 │ └── [Link]
9 │
10 ├── ui/
11 │ ├── screens/
12 │ │ ├── home/
13 │ │ │ ├── [Link]
14 │ │ │ └── [Link]
15 │ │ ├── detail/
16 │ │ │ ├── [Link]
17 │ │ │ └── [Link]
18 │ │ └── auth/
19 │ │ ├── [Link]
20 │ │ └── [Link]
21 │ ├── components/
22 │ │ ├── [Link]
23 │ │ └── [Link]
24 │ ├── theme/
25 │ │ ├── [Link]
26 │ │ ├── [Link]
27 │ │ └── [Link]
28 │ └── navigation/
29 │ └── [Link]
30 │
31 ├── di/
32 │ └── [Link]
33 │
34 └── [Link]
Characteristics:
Two layers: Data and UI
No domain layer (simpler)
ViewModel directly uses Repository
Good for learning and small projects
Detailed File Structures
UI Layer (Presentation)
Screen Organization
Each screen typically contains:
[Link] (Composable)
1 @Composable
2 fun HomeScreen(
3 viewModel: HomeViewModel = hiltViewModel(),
4 onNavigateToDetail: (String) -> Unit
5 ) {
6 val state by [Link]()
7
8 HomeContent(
9 state = state,
10 onEvent = viewModel::onEvent,
11 onNavigateToDetail = onNavigateToDetail
12 )
13 }
14
15 @Composable
16 private fun HomeContent(
17 state: HomeState,
18 onEvent: (HomeEvent) -> Unit,
19 onNavigateToDetail: (String) -> Unit
20 ) {
21 // UI implementation
22 }
[Link] (State Management)
1 @HiltViewModel
2 class HomeViewModel @Inject constructor(
3 private val getBooksUseCase: GetBooksUseCase
4 ) : ViewModel() {
5
6 private val _state = MutableStateFlow(HomeState())
7 val state: StateFlow<HomeState> = _state.asStateFlow()
8
9 fun onEvent(event: HomeEvent) {
10 when(event) {
11 is [Link] -> loadBooks()
12 is [Link] -> searchBooks([Link])
13 }
14 }
15 }
[Link] (UI State)
1 data class HomeState(
2 val books: List<Book> = emptyList(),
3 val isLoading: Boolean = false,
4 val error: String? = null,
5 val searchQuery: String = ""
6 )
[Link] (User Actions)
1 sealed class HomeEvent {
2 object LoadBooks : HomeEvent()
3 data class SearchBooks(val query: String) : HomeEvent()
4 data class BookClicked(val bookId: String) : HomeEvent()
5 }
Navigation
[Link]
1 @Composable
2 fun AppNavGraph(
3 navController: NavHostController = rememberNavController()
4 ) {
5 NavHost(
6 navController = navController,
7 startDestination = [Link]
8 ) {
9 composable([Link]) {
10 HomeScreen(
11 onNavigateToDetail = { bookId ->
12 [Link]([Link](bookId))
13 }
14 )
15 }
16 composable(
17 route = [Link],
18 arguments = listOf(
19 navArgument("bookId") { type = [Link] }
20 )
21 ) {
22 DetailScreen()
23 }
24 }
25 }
[Link] (Navigation Routes)
1 sealed class Screen(val route: String) {
2 object Home : Screen("home")
3 object Detail : Screen("detail/{bookId}") {
4 fun createRoute(bookId: String) = "detail/$bookId"
5 }
6 object Login : Screen("login")
7 }
Components
Reusable Composables
1 presentation/components/
2 ├── [Link]
3 ├── [Link]
4 ├── [Link]
5 ├── [Link]
6 └── [Link]
Theme
1 presentation/theme/
2 ├── [Link] // Color definitions
3 ├── [Link] // Theme configuration
4 ├── [Link] // Typography
5 └── [Link] // Shape styles
Domain Layer
Purpose: Business logic, independent of Android framework
Models
[Link] (Domain Model)
1 data class Book(
2 val id: String,
3 val title: String,
4 val author: String,
5 val price: Double,
6 val coverUrl: String
7 )
Use Cases
[Link]
1 class GetBooksUseCase @Inject constructor(
2 private val repository: BookRepository
3 ) {
4 suspend operator fun invoke(): Result<List<Book>> {
5 return [Link]()
6 }
7 }
[Link]
1 class AddBookUseCase @Inject constructor(
2 private val repository: BookRepository
3 ) {
4 suspend operator fun invoke(book: Book): Result<Unit> {
5 // Business logic validation
6 if ([Link]()) {
7 return [Link](Exception("Title cannot be empty"))
8 }
9 return [Link](book)
10 }
11 }
Repository Interfaces
[Link] (Interface)
1 interface BookRepository {
2 suspend fun getBooks(): Result<List<Book>>
3 suspend fun getBookById(id: String): Result<Book>
4 suspend fun addBook(book: Book): Result<Unit>
5 }
Data Layer
Repository Implementation
[Link]
1 class BookRepositoryImpl @Inject constructor(
2 private val remoteDataSource: BookRemoteDataSource,
3 private val localDataSource: BookLocalDataSource
4 ) : BookRepository {
5
6 override suspend fun getBooks(): Result<List<Book>> {
7 return try {
8 // Try remote first
9 val books = [Link]()
10 // Cache locally
11 [Link](books)
12 [Link]([Link] { [Link]() })
13 } catch (e: Exception) {
14 // Fallback to local
15 val cachedBooks = [Link]()
16 [Link]([Link] { [Link]() })
17 }
18 }
19 }
Remote Data Source
1 data/remote/
2 ├── api/
3 │ └── [Link] // Retrofit interface
4 ├── dto/
5 │ └── [Link] // Network models
6 └── interceptor/
7 └── [Link] // Auth handling
[Link] (Retrofit)
1 interface BookApiService {
2 @GET("books")
3 suspend fun getBooks(): List<BookDto>
4
5 @GET("books/{id}")
6 suspend fun getBookById(@Path("id") id: String): BookDto
7
8 @POST("books")
9 suspend fun addBook(@Body book: BookDto): BookDto
10 }
Local Data Source
1 data/local/
2 ├── dao/
3 │ └── [Link] // Room DAO
4 ├── entity/
5 │ └── [Link] // Room entity
6 └── database/
7 └── [Link] // Room database
[Link] (Room)
1 @Dao
2 interface BookDao {
3 @Query("SELECT * FROM books")
4 suspend fun getAllBooks(): List<BookEntity>
5
6 @Query("SELECT * FROM books WHERE id = :id")
7 suspend fun getBookById(id: String): BookEntity?
8
9 @Insert(onConflict = [Link])
10 suspend fun insertBooks(books: List<BookEntity>)
11 }
Dependency Injection
Using Hilt (Recommended 2025)
1 di/
2 ├── [Link] // General app dependencies
3 ├── [Link] // Network dependencies
4 ├── [Link] // Database dependencies
5 └── [Link] // Repository bindings
[Link]
1 @Module
2 @InstallIn(SingletonComponent::class)
3 object AppModule {
4
5 @Provides
6 @Singleton
7 fun provideContext(@ApplicationContext context: Context): Context {
8 return context
9 }
10 }
[Link]
1 @Module
2 @InstallIn(SingletonComponent::class)
3 object NetworkModule {
4
5 @Provides
6 @Singleton
7 fun provideRetrofit(): Retrofit {
8 return [Link]()
9 .baseUrl("[Link]
10 .addConverterFactory([Link]())
11 .build()
12 }
13
14 @Provides
15 @Singleton
16 fun provideBookApiService(retrofit: Retrofit): BookApiService {
17 return [Link](BookApiService::[Link])
18 }
19 }
[Link]
1 @Module
2 @InstallIn(SingletonComponent::class)
3 abstract class RepositoryModule {
4
5 @Binds
6 @Singleton
7 abstract fun bindBookRepository(
8 impl: BookRepositoryImpl
9 ): BookRepository
10 }
Best Practices
1. State Management
✅ Do:
1 // Use StateFlow for UI state
2 private val _state = MutableStateFlow(HomeState())
3 val state: StateFlow<HomeState> = _state.asStateFlow()
4
5 // Collect in Composable
6 val state by [Link]()
❌ Don't:
1 // Don't use LiveData in new Compose projects
2 val state: LiveData<HomeState> = _state
3
4 // Don't expose mutable state
5 val state: MutableStateFlow<HomeState> = _state
2. Screen Organization
✅ Do:
One screen = one file (unless very large)
Separate stateful and stateless composables
Use preview functions for UI development
1 @Composable
2 fun HomeScreen(viewModel: HomeViewModel) { /* Stateful */ }
3
4 @Composable
5 private fun HomeContent(state: HomeState) { /* Stateless */ }
6
7 @Preview
8 @Composable
9 private fun HomeContentPreview() {
10 HomeContent(state = HomeState())
11 }
3. Component Organization
✅ Do:
Create reusable components in components/ folder
Keep components small and focused
Use meaningful names
components/
✅ Specific
1
├── [Link]
├── [Link] ✅ Clear purpose
2
✅ Reusable
3
4 └── [Link]
❌ Don't:
components/
❌ Too generic
1
├── [Link]
❌ Unclear
2
├── [Link]
❌ Non-descriptive
3
4 └── [Link]
4. Navigation
✅ Do:
Use type-safe navigation
Define routes in sealed class
Pass minimal data through navigation
❌ Don't:
Hardcode routes as strings
Pass entire objects through navigation
Create circular navigation dependencies
5. Dependency Injection
✅ Do:
Use Hilt (recommended for 2025)
Inject into ViewModels, not Composables
Keep DI modules organized
1 @HiltViewModel
2 class HomeViewModel @Inject constructor(
3 private val useCase: GetBooksUseCase
4 ) : ViewModel()
❌ Don't:
1 @Composable
2 fun HomeScreen(useCase: GetBooksUseCase) { // Don't inject here
3 // ...
4 }
6. Error Handling
✅ Do:
1 sealed class Result<T> {
2 data class Success<T>(val data: T) : Result<T>()
3 data class Error<T>(val message: String) : Result<T>()
4 data class Loading<T> : Result<T>()
5 }
7. Testing Structure
1 test/
2 ├── data/
3 │ └── repository/
4 │ └── [Link]
5 ├── domain/
6 │ └── usecase/
7 │ └── [Link]
8 └── presentation/
9 └── viewmodel/
10 └── [Link]
11
12 androidTest/
13 └── ui/
14 └── [Link]
Common Patterns
Pattern 1: State and Events
State: What the UI looks like Events: What the user does
1 // State
2 data class LoginState(
3 val email: String = "",
4 val password: String = "",
5 val isLoading: Boolean = false,
6 val error: String? = null
7 )
8
9 // Events
10 sealed class LoginEvent {
11 data class EmailChanged(val email: String) : LoginEvent()
12 data class PasswordChanged(val password: String) : LoginEvent()
13 object LoginClicked : LoginEvent()
14 }
15
16 // ViewModel
17 class LoginViewModel : ViewModel() {
18 private val _state = MutableStateFlow(LoginState())
19 val state = _state.asStateFlow()
20
21 fun onEvent(event: LoginEvent) {
22 when(event) {
23 is [Link] -> {
24 _state.update { [Link](email = [Link]) }
25 }
26 is [Link] -> {
27 _state.update { [Link](password = [Link]) }
28 }
29 is [Link] -> login()
30 }
31 }
32 }
Pattern 2: Side Effects
For one-time events (navigation, show snackbar):
1 sealed class UiEvent {
2 data class ShowSnackbar(val message: String) : UiEvent()
3 data class Navigate(val route: String) : UiEvent()
4 }
5
6 class HomeViewModel : ViewModel() {
7 private val _eventFlow = Channel<UiEvent>()
8 val eventFlow = _eventFlow.receiveAsFlow()
9
10 fun showError(message: String) {
11 [Link] {
12 _eventFlow.send([Link](message))
13 }
14 }
15 }
16
17 @Composable
18 fun HomeScreen(viewModel: HomeViewModel) {
19 LaunchedEffect(key1 = true) {
20 [Link] { event ->
21 when(event) {
22 is [Link] -> {
23 // Show snackbar
24 }
25 is [Link] -> {
26 // Navigate
27 }
28 }
29 }
30 }
31 }
Pattern 3: Repository Pattern
1 // Interface in domain layer
2 interface BookRepository {
3 suspend fun getBooks(): Flow<List<Book>>
4 }
5
6 // Implementation in data layer
7 class BookRepositoryImpl(
8 private val api: BookApiService,
9 private val dao: BookDao
10 ) : BookRepository {
11
12 override suspend fun getBooks(): Flow<List<Book>> = flow {
13 // Emit cached data first
14 emit([Link]().map { [Link]() })
15
16 // Fetch fresh data
17 val freshBooks = [Link]()
18 [Link]([Link] { [Link]() })
19
20 // Emit fresh data
21 emit([Link] { [Link]() })
22 }
23 }
Real-World Examples
Example 1: E-commerce App Structure
1 [Link]/
2 ├── core/
3 │ ├── data/
4 │ ├── ui/
5 │ ├── navigation/
6 │ └── utils/
7 ├── feature/
8 │ ├── auth/
9 │ ├── home/
10 │ ├── product_list/
11 │ ├── product_detail/
12 │ ├── cart/
13 │ ├── checkout/
14 │ └── profile/
15 └── app/
Example 2: Social Media App Structure
1 [Link]/
2 ├── core/
3 │ ├── data/
4 │ ├── ui/
5 │ └── navigation/
6 ├── feature/
7 │ ├── auth/
8 │ ├── feed/
9 │ ├── post_create/
10 │ ├── profile/
11 │ ├── messages/
12 │ └── notifications/
13 └── app/
Example 3: News App Structure
1 [Link]/
2 ├── data/
3 │ ├── local/
4 │ ├── remote/
5 │ └── repository/
6 ├── domain/
7 │ ├── model/
8 │ └── usecase/
9 ├── presentation/
10 │ ├── screens/
11 │ │ ├── home/
12 │ │ ├── article/
13 │ │ ├── category/
14 │ │ └── search/
15 │ ├── components/
16 │ ├── theme/
17 │ └── navigation/
18 └── di/
References
Official Documentation
1. Android Developers - Guide to App Architecture [Link]
Official guide from Google on modern Android app architecture
2. Jetpack Compose - UI
Architecture [Link]
Understanding UDF and state management in Compose
3. Android Architecture Components [Link]
ViewModel, LiveData, Room, and other components
4. Modern Android App Architecture [Link]
architecture
Comprehensive learning path from Google
Community Resources
5. Now in Android (Google Sample) [Link]
Production-quality app by Google showing best practices
6. Jonas Rodehorst - How to Structure Jetpack Compose Project [Link]
[Link]/blog/how-to-structure-your-jetpack-compose-project
Detailed blog post on different structure approaches
7. Medium - Clean Architecture Guide Multiple articles on implementing Clean Architecture with
Compose
Key Principles Summary
1. Separation of Concerns: Each component has one responsibility
2. Single Source of Truth: One owner per data type
3. Unidirectional Data Flow: State down, events up
4. Layered Architecture: UI → Domain → Data
5. Testability: Design for easy testing
6. Scalability: Structure that grows with your app
Quick Decision Guide
When to use which architecture?
Project Size Recommended Architecture Complexity
Small/Prototype Simple MVVM Low
Medium Clean Architecture Medium
Large/Team Feature-First Modular High
Key Questions to Ask:
1. How many developers?
Solo: Simple MVVM
Small team: Clean Architecture
Large team: Feature-First
2. Expected app size?
<10 screens: Simple MVVM
10-30 screens: Clean Architecture
30+ screens: Feature-First
3. Long-term maintenance?
Short-term: Simple MVVM
Long-term: Clean Architecture or Feature-First
4. Need multi-module support?
No: Clean Architecture
Yes: Feature-First
Conclusion
There's no "one-size-fits-all" architecture. The best structure depends on:
Your app's complexity
Team size
Long-term goals
Performance requirements
For 2025, the trends are:
✅ Feature-first modularization for large apps
✅ Clean Architecture with MVVM for most projects
✅ Hilt for dependency injection
✅ StateFlow over LiveData
✅ Use Cases for complex business logic
✅ Type-safe navigation
Start simple, evolve as needed!
Last Updated: November 2025 Based on official Android documentation and current industry
practices