Android development has evolved significantly over the years. With the introduction of Jetpack Compose, Google transformed the way developers build user interfaces by replacing XML-based layouts with a modern, declarative UI framework. For developers transitioning from traditional Android development, understanding the structure of a Jetpack Compose project is essential for building scalable and maintainable applications.
In this article, we’ll explore the architecture, folder structure, and key components of a typical Jetpack Compose project.
What is Jetpack Compose?
Jetpack Compose is Android’s modern toolkit for building native user interfaces. Instead of defining UI layouts in XML files, developers describe the UI using Kotlin functions called Composable Functions.
Key advantages include:
- Less boilerplate code
- Improved readability
- Faster UI development
- Better state management
- Seamless Kotlin integration
Creating a New Jetpack Compose Project
When creating a new Android project in Android Studio and selecting Empty Activity with Compose, Android Studio generates a project structure similar to the following:
MyComposeApp/
│
├── app/
│ ├── src/
│ │ ├── main/
│ │ │ ├── java/com/example/mycomposeapp/
│ │ │ │ ├── MainActivity.kt
│ │ │ │ ├── ui/
│ │ │ │ │ ├── theme/
│ │ │ │ │ │ ├── Color.kt
│ │ │ │ │ │ ├── Theme.kt
│ │ │ │ │ │ └── Type.kt
│ │ │ ├── AndroidManifest.xml
│
├── build.gradle
├── settings.gradle
└── gradle.properties
Let’s examine each component in detail.
MainActivity.kt
The entry point of most Android applications is the MainActivity.
Example:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApp()
}
}
}
Responsibilities
- Launches the application.
- Hosts Compose UI content.
- Connects Android lifecycle with Compose.
- Initializes navigation and dependencies.
The setContent {} block replaces the traditional setContentView() method used with XML layouts.
Composable Functions
Composable functions are designed to describe how the user interface should look based on the current state of the application. Unlike the traditional Android View system, where developers manually update UI components when data changes, Jetpack Compose automatically handles UI updates through a process called recomposition. When the state of a composable changes, Compose intelligently redraws only the affected parts of the UI, resulting in cleaner code and improved performance.
Another important advantage of composable functions is their reusability and modularity. Developers can create small, self-contained UI components and combine them to build complex screens. For example, a custom button, card, or toolbar can be implemented once as a composable function and reused throughout the entire application. This approach not only reduces code duplication but also makes applications easier to maintain, test, and scale as the project grows. By breaking the UI into reusable composables, developers can create a more organized and maintainable codebase while ensuring a consistent user experience across the app.
Example:
@Composable
fun Greeting(name: String) {
Text(
text = "Hello $name"
)
}
Important Characteristics
- Annotated with
@Composable - Describe UI declaratively
- Can be nested
- Automatically update when state changes
Usage:
Greeting("John")
The UI Package
As a Jetpack Compose application grows, placing all composable functions in a single file quickly becomes difficult to manage. To maintain a clean and scalable codebase, developers typically group related UI elements into a dedicated ui package. This package serves as the central location for everything related to the presentation layer, including screens, reusable components, navigation logic, themes, and UI state management. Such organization makes it easier for team members to locate files, understand the project structure, and collaborate effectively.
Example structure:
ui/
├── screens/
├── components/
├── navigation/
├── theme/
└── state/
Screens Folder
The screens package is responsible for containing the main user interface screens of the application. Each screen typically represents a specific feature or user journey, such as a login page, home dashboard, profile page, or settings screen. By keeping each screen in its own file, developers can manage complex interfaces more efficiently and prevent UI code from becoming overly large and difficult to maintain. This organization also makes it easier to locate and update specific parts of the application when new requirements arise.
Example:
screens/
├── HomeScreen.kt
├── ProfileScreen.kt
├── LoginScreen.kt
└── SettingsScreen.kt
Example screen:
@Composable
fun HomeScreen() {
Column {
Text("Welcome Home")
}
}
Benefits
- Better organization
- Easier navigation management
- Clear separation between features
Components Folder
Reusable UI elements are typically stored in a components package.
Examples:
components/
├── AppButton.kt
├── AppTextField.kt
├── LoadingIndicator.kt
└── AppToolbar.kt
Example:
@Composable
fun AppButton(
text: String,
onClick: () -> Unit
) {
Button(
onClick = onClick
) {
Text(text)
}
}
Advantages:
- Code reusability
- Consistent design
- Easier maintenance
Theme Folder
Compose automatically generates a theme package.
Example:
theme/
├── Color.kt
├── Theme.kt
└── Type.kt
Color.kt
The Color.kt file is used to define the color palette for the entire application. Instead of hardcoding color values throughout the UI, developers store them as reusable constants in a centralized location. This approach helps maintain consistency across different screens and components while making it easier to update the application’s visual appearance. For example, primary, secondary, background, surface, and error colors are typically declared in this file and then referenced throughout the project.
val PrimaryColor = Color(0xFF6200EE)
val SecondaryColor = Color(0xFF03DAC5)
Type.kt
The Type.kt file is responsible for defining the typography styles used throughout the application. Typography includes text properties such as font size, font weight, font family, letter spacing, and line height. By centralizing these styles in one location, developers can ensure a consistent visual hierarchy and reading experience across all screens. Common text styles such as headings, titles, body text, captions, and labels are typically configured in this file and then applied through the application’s theme.
val Typography = Typography(
bodyLarge = TextStyle(
fontSize = 16.sp
)
)
Theme.kt
The Theme.kt file serves as the central location for applying the application’s visual design system. It combines the color scheme, typography, and other styling configurations into a single theme that can be used throughout the app. By wrapping the application’s content with a custom theme composable, developers ensure that all screens and components automatically inherit the same design settings. This creates a consistent look and feel without requiring individual styling for each UI element.
@Composable
fun MyComposeTheme(
content: @Composable () -> Unit
) {
MaterialTheme(
colorScheme = lightColorScheme(),
typography = Typography,
content = content
)
}
Usage:
setContent {
MyComposeTheme {
HomeScreen()
}
}
Navigation Package
Most modern Jetpack Compose applications use Navigation Compose to manage movement between screens in a simple and declarative way. Navigation Compose is part of the Android Jetpack libraries and provides a framework for defining navigation routes, handling screen transitions, and passing data between destinations. Instead of relying on traditional Fragment-based navigation, Compose applications can navigate directly between composable screens, resulting in cleaner code and a more streamlined architecture.
Structure:
navigation/
├── NavGraph.kt
├── Routes.kt
└── BottomNav.kt
Example route definition:
sealed class Screen(val route: String) {
object Home : Screen("home")
object Profile : Screen("profile")
}
Navigation graph:
NavHost(
navController = navController,
startDestination = "home"
) {
composable("home") {
HomeScreen()
}
composable("profile") {
ProfileScreen()
}
}
Benefits:
- Type-safe navigation
- Centralized route management
- Easier screen transitions
State Management
One of the key reasons developers appreciate Jetpack Compose is its built-in reactive state management system. In traditional Android development, updating the user interface often required manually finding views and changing their properties whenever data changed. Jetpack Compose takes a different approach by automatically observing state changes and updating the UI accordingly. This declarative model allows developers to focus on describing what the UI should look like for a given state rather than managing the process of updating individual UI elements.
Reactive state management makes applications more predictable, maintainable, and easier to debug. When a state value changes, Compose triggers a process called recomposition, which redraws only the parts of the interface affected by that change. This results in efficient UI updates and reduces the complexity of managing dynamic content. Whether handling user input, loading data from an API, or responding to application events, Compose’s state management system helps ensure that the UI always remains synchronized with the underlying data, creating a smoother and more responsive user experience.
Example:
@Composable
fun CounterScreen() {
var count by remember {
mutableStateOf(0)
}
Column {
Text("Count: $count")
Button(
onClick = {
count++
}
) {
Text("Increment")
}
}
}
Whenever count changes, the UI automatically recomposes.
ViewModel Layer
In production applications, state is typically managed using ViewModels.
Example:
class HomeViewModel : ViewModel() {
private val _counter =
MutableStateFlow(0)
val counter =
_counter.asStateFlow()
fun increment() {
_counter.value++
}
}
Composable:
@Composable
fun HomeScreen(
viewModel: HomeViewModel = viewModel()
) {
val count by viewModel.counter
.collectAsState()
Text("Count: $count")
}
Benefits:
- Survives configuration changes
- Separates UI from business logic
- Easier testing
Data Layer Structure
A well-structured Compose project often includes:
data/
├── remote/
├── local/
├── repository/
└── model/
Model
Data classes representing application data.
data class User(
val id: Int,
val name: String
)
Remote
Handles API calls.
interface ApiService {
@GET("users")
suspend fun getUsers(): List<User>
}
Local
Handles Room database operations.
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getUsers(): List<User>
}
Repository
Acts as a bridge between UI and data sources.
class UserRepository(
private val api: ApiService
) {
suspend fun getUsers() =
api.getUsers()
}
Recommended Project Structure
For medium to large applications, a feature-based structure is often preferred:
com.example.app
│
├── core
│ ├── network
│ ├── database
│ ├── utils
│
├── features
│ ├── auth
│ │ ├── data
│ │ ├── domain
│ │ ├── presentation
│
│ ├── home
│ │ ├── data
│ │ ├── domain
│ │ ├── presentation
│
├── navigation
├── ui
└── MainActivity.kt
This architecture scales much better than organizing files solely by type.
Compose Lifecycle and Recomposition
A key concept in Compose is Recomposition.
When state changes:
- Compose detects the change.
- Affected composables are re-executed.
- UI updates automatically.
- Unchanged UI remains untouched.
Example:
var text by remember {
mutableStateOf("")
}
Changing text triggers recomposition only where necessary.
Best Practices for Organizing a Compose Project
1. Keep Composables Small
Instead of:
HomeScreen()
containing hundreds of lines, split it into:
HomeHeader()
HomeContent()
HomeFooter()
2. Reuse Components
Create reusable buttons, cards, dialogs, and text fields.
3. Separate UI and Business Logic
Avoid API calls directly inside composables.
Use:
Composable
↓
ViewModel
↓
Repository
↓
API/Database
4. Use State Hoisting
Pass state and events from parent composables.
Example:
@Composable
fun SearchField(
text: String,
onTextChange: (String) -> Unit
)
This improves testability and reusability.
5. Adopt MVVM Architecture
A typical Compose architecture looks like:
UI (Compose)
↓
ViewModel
↓
Repository
↓
Data Sources
This pattern is widely used in modern Android development.
Conclusion
Understanding the structure of a Jetpack Compose project is fundamental to building maintainable Android applications. While a simple Compose project may only contain a few files, real-world applications benefit from a clear separation of concerns through dedicated layers such as UI, ViewModel, Repository, and Data.
By organizing your project into reusable components, feature-based modules, and properly managed state layers, you can create scalable Android applications that are easier to develop, test, and maintain. Jetpack Compose not only simplifies UI development but also encourages cleaner architecture and modern Android development practices.
Android Framework To be a Senior Android Developer
Comment Test
I care. So, what do you think of her, Han? Don’t underestimate the Force. I don’t know what you’re talking about. I am a member of the Imperial Senate on a diplomatic mission.
Comment Test 2
I care. So, what do you think of her, Han? Don’t underestimate the Force. I don’t know what you’re talking about. I am a member of the Imperial Senate on a diplomatic mission.