Compose Fundamentals: Core Concepts of Declarative UI
Table of Contents
Jetpack Compose (v1.9 stable, Aug 2025) revolutionizes Android UI with declarative paradigms. No XML, pure Kotlin functions. Focus: State drives UI.
Declarative vs Imperative UI
Direct: Use declarative (Compose) – auto-updates UI on state change. Imperative (Views) manual, error-prone.
- Imperative: Commands like "set text" – forgets state on rotation.
- Declarative: "Show text if state=X" – framework handles all.
| Aspect | Imperative (Views) | Declarative (Compose) |
|---|---|---|
| Code | Manual findViewById + setText | StateFlow triggers recompose |
| State | Bundle saves needed | remember { } auto-saves |
| Perf | Full redraws | Smart diffs |
- Imperative Pros : Legacy support
- Imperative Cons : Boilerplate, leaks
- Declarative Pros : Reactive, testable
- Declarative Cons : Learn Kotlin DSL
Full Toggle Example: See Practice section.
Compose Setup and First Composable
Direct: Add to build.gradle, enable compiler. Run first "Hello" composable.
build.gradle (Module: app) – Full
plugins {
id 'com.android.application' // Standard Android plugin
id 'org.jetbrains.kotlin.android' // Kotlin support
id 'kotlin-kapt' // Annotation processing if needed
}
android {
namespace 'com.example.myapp' // App namespace
compileSdk 35 // Target Android 15
defaultConfig {
applicationId "com.example.myapp" // Unique app ID
minSdk 24 // Minimum API level (Android 7.0+)
targetSdk 35 // Target API level
versionCode 1
versionName "1.0"
}
buildFeatures {
compose true // Enable Jetpack Compose
}
composeOptions {
kotlinCompilerExtensionVersion '1.5.15' // Compose compiler version for 1.9
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.13.1' // Core KTX
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.4' // Lifecycle runtime
implementation 'androidx.activity:activity-compose:1.9.1' // Activity integration for Compose
implementation platform('androidx.compose:compose-bom:2024.10.00') // Compose BOM for versions
implementation 'androidx.compose.ui:ui' // Compose UI core
implementation 'androidx.compose.ui:ui-graphics' // Graphics utilities
implementation 'androidx.compose.ui:ui-tooling-preview' // Preview support
implementation 'androidx.compose.material3:material3' // Material Design 3 components
debugImplementation 'androidx.compose.ui:ui-tooling' // UI tooling for debug
debugImplementation 'androidx.compose.ui:ui-test-manifest' // Test manifest
}
First Composable – Full MainActivity.kt
package com.example.myapp
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent // Sets Compose content in Activity
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme // Material 3 theme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable // Marks composable functions
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview // For previewing in Android Studio
import com.example.myapp.ui.theme.MyAppTheme // Custom theme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyAppTheme {
Surface(
modifier = Modifier.fillMaxSize(), // Fill entire screen
color = MaterialTheme.colorScheme.background // Background color from theme
) {
Greeting("Android") // Call the Greeting composable
}
}
}
}
}
// Simple Greeting composable function
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!", // Interpolated text
modifier = modifier // Apply modifier if provided
)
}
// Preview function for Android Studio
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
MyAppTheme {
Greeting("Android")
}
}
- Sync Gradle → Run → See "Hello Android!"
Composable Functions and @Composable
Direct: @Composable marks UI builders. Parameters flow down, emit UI up.
- Rules: No side-effects outside; use remember for state.
- Best: Pure functions = reusable, testable.
Full Nested Example
@Composable
fun App() {
Column { // Vertical layout container
Greeting("World") // Calls child composable
Button(onClick = { /* Action */ }) { // Button with click handler
Text("Click") // Button label
}
}
}
// Greeting composable
@Composable
fun Greeting(name: String) {
Text("Hello $name!") // Emits Text UI node
}
Compose Compiler and Runtime
Direct: Compiler (Kotlin 1.9+) slots params to stable IDs. Runtime composes tree.
- Compiler: Transforms @Composable to set-stable calls.
- Runtime: Builds/rebuilds UI tree via CompositionLocal.
Version: kotlinCompilerExtensionVersion '1.5.15' (Compose 1.9). No manual IDs needed.
Preview and Tooling Support
Direct: @Preview for live Android Studio views. No run needed.
- Tooling: Layout Inspector, Theme Adapter. Debug with ui-tooling.
- Best: @Preview(params) for variants.
Full Preview Example
import android.content.res.Configuration // For UI mode
@Preview(name = "Light", showBackground = true) // Light theme preview
@Preview(name = "Dark", uiMode = Configuration.UI_MODE_NIGHT_YES) // Dark theme preview
@Composable
fun GreetingPreview() {
MyAppTheme {
Greeting("Preview")
}
}
Composition and Recomposition
Direct: Composition builds initial tree. Recomposition updates changed parts only.
- Skip: Unchanged composables (stability checks).
- Best: Use derivedStateOf for derived values.
Full Recompose Example
import androidx.compose.runtime.mutableIntStateOf // For mutable state
import androidx.compose.runtime.remember // Remember state across recompositions
@Composable
fun Counter() {
var count by remember { mutableIntStateOf(0) } // Initial state: 0, triggers recompose on change
Text("Count: $count") // Displays current count
Button(onClick = { count++ }) { // Increment on click
Text("Increment") // Button label
}
// Only Text + Button recompose on count change; efficient!
}
Practice: Hello Compose App
Direct: Build toggle app. Full code below. Run & tweak.
Full MainActivity.kt
package com.example.helloapp
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.* // Layout modifiers
import androidx.compose.material3.* // Material 3 components
import androidx.compose.runtime.* // State and remember
import androidx.compose.ui.Alignment // Alignment utilities
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp // Density-independent pixels
import androidx.compose.ui.unit.sp // Scalable pixels
import com.example.helloapp.ui.theme.HelloAppTheme // Custom theme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
HelloAppTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
HelloScreen() // Main screen composable
}
}
}
}
}
// Main screen with toggle functionality
@Composable
fun HelloScreen() {
var isToggled by rememberSaveState { false } // Persist state across config changes
Column( // Vertical stack
modifier = Modifier
.fillMaxSize()
.padding(16.dp), // Padding around edges
verticalArrangement = Arrangement.Center, // Center vertically
horizontalAlignment = Alignment.CenterHorizontally // Center horizontally
) {
Text( // Dynamic text based on state
text = if (isToggled) "Toggled!" else "Hello Compose!",
fontSize = 24.sp, // Text size
modifier = Modifier.padding(bottom = 16.dp) // Bottom padding
)
Button(onClick = { isToggled = !isToggled }) { // Toggle state on click
Text("Toggle") // Button label
}
}
}
- Steps: Copy to project → Sync → Run → Rotate screen (state saves).