Jetpack Compose

Design Tokens

Design Tokens in Material Design 3 with Jetpack Compose

If you’ve been diving into modern Android development, you’ve probably heard the buzz about Material Design 3 (also known as Material You) and Jetpack Compose. Today, we’re going to explore one of the most powerful yet underappreciated features that ties them together: Design Tokens. Understanding Design Tokens in Material 3 and Jetpack Compose will transform...

Membership Required

You must be a member to access this content.

View Membership Levels

Already a member? Log in here
Custom App Theme in Jetpack Compose

How to Build a Consistent Custom App Theme in Jetpack Compose (Material 3)

Creating a polished Android app starts with one crucial decision: your app’s visual identity. If you’ve been wondering how to make your Jetpack Compose app look consistent and professional across every screen, you’re in the right place. In this guide, I’ll walk you through building a Custom App Theme in Jetpack Compose using Material 3....

Membership Required

You must be a member to access this content.

View Membership Levels

Already a member? Log in here
CompositionLocal in Jetpack Compose

CompositionLocal in Jetpack Compose: How to Avoid Prop Drilling

When building apps with Jetpack Compose, you’ll often pass data down through multiple layers of composables. At first, this feels fine. But as your UI grows, you may find yourself passing the same parameter through five or six functions just to reach a deeply nested child. That pattern is called prop drilling. It works, but...

Membership Required

You must be a member to access this content.

View Membership Levels

Already a member? Log in here
Material 3 colorScheme

Material 3 colorScheme Explained: How Dynamic Color Really Works

Have you ever wondered how Android apps magically match your wallpaper colors? Or how Material Design creates those perfectly harmonious color palettes that just work? 

That’s the magic of Material 3’s dynamic color system, and today, we’re diving deep into how it all comes together using Kotlin and Jetpack Compose.

By the end of this guide, you’ll understand exactly how the Material 3 colorScheme works, how to implement it in your Android apps with Kotlin, and how to harness the power of dynamic theming to create stunning user interfaces that feel personal and cohesive.

Let’s get started..!

What Is Material 3 ColorScheme?

The Material 3 colorScheme is Google’s revolutionary approach to app theming that goes way beyond simple primary and secondary colors. Think of it as a complete color system that automatically generates a harmonious palette of colors designed to work together beautifully.

Here’s what makes it special:

Dynamic Color Generation: Instead of manually picking dozens of color shades, the Material 3 colorScheme generates an entire palette from a single seed color. This means you get consistent, accessible, and visually appealing colors without the guesswork.

Adaptive Theming: The system automatically adjusts for light and dark modes, ensuring your app looks great in any setting.

Wallpaper Integration: On Android 12 and above, your app can automatically extract colors from the user’s wallpaper, creating a truly personalized experience.

Accessibility Built-In: Every color in the Material 3 colorScheme meets WCAG (Web Content Accessibility Guidelines) accessibility standards when used correctly, so you don’t have to worry about contrast ratios.

Understanding Dynamic Color: The Foundation

Dynamic color is the heart of Material 3 on Android. But what exactly is it?

Imagine your phone’s wallpaper is a beautiful sunset with warm orange and purple tones. With dynamic color, your apps can extract those colors and build their entire theme around them. It’s personalization taken to the next level.

How Dynamic Color Works on Android

The process is actually quite elegant:

  1. Color Extraction: The system analyzes your wallpaper using the Monet color system
  2. Palette Generation: Using color science algorithms, it creates a full tonal palette
  3. Role Assignment: Colors are assigned to specific UI roles (more on this shortly)
  4. Adaptation: The scheme automatically adapts for light and dark themes

The beauty of the Material 3 colorScheme is that all this complexity is handled for you in Jetpack Compose. You just need to understand how to use it.

The Color Roles: Your New Best Friends

Here’s where Material 3 gets really interesting. Instead of thinking in terms of “primary,” “secondary,” and “tertiary” colors alone, the Material 3 colorScheme introduces color roles.

Think of color roles as jobs that colors perform in your UI. Let’s break down the main players:

Primary Colors

Primary: This is your brand color, the star of the show. It appears on prominent buttons and active states.

OnPrimary: Text and icons that sit on top of the primary color. The Material 3 colorScheme ensures this has enough contrast to be readable.

PrimaryContainer: A lighter (or darker in dark mode) version used for less prominent components.

OnPrimaryContainer: Text that appears on primary containers.

Secondary Colors

Secondary: Provides visual variety and highlights less prominent components.

OnSecondary: You guessed it — text on secondary colors.

SecondaryContainer: For chips, cards, and other contained elements.

OnSecondaryContainer: Text on those containers.

Tertiary Colors

Tertiary: Adds even more variety, often used for accents and special highlights.

OnTertiary, TertiaryContainer, OnTertiaryContainer: Following the same pattern.

Surface and Background Colors

Surface: The background color for cards, sheets, and menus.

OnSurface: Text and icons on surfaces.

SurfaceVariant: Alternative surface with subtle differences.

OnSurfaceVariant: Text with lower emphasis.

Background: The main background of your app.

OnBackground: Primary text on the background.

Error Colors

Error: For error states and destructive actions.

OnError: Text on error colors.

ErrorContainer: Background for error messages.

OnErrorContainer: Text in error containers.

Special Roles

Outline: Borders and dividers.

OutlineVariant: Subtle borders.

Scrim: Semi-transparent overlays.

InverseSurface, InverseOnSurface, InversePrimary: For high-contrast elements like tooltips.

SurfaceTint: Used for elevation overlays in Material 3.

Phew! That’s a lot of colors, right..? 

But here’s the magic: the Material 3 colorScheme generates all of these automatically, ensuring they work harmoniously together.

In short, a typical Material 3 colorScheme includes:

  • primary – main brand color
  • onPrimary – content placed on primary
  • secondary – supporting color
  • tertiary – optional accent color
  • background – app background
  • surface – cards and sheets
  • error – error states
  • onSurface – text/icons on surfaces

Each color has a paired onColor to guarantee readability.

This pairing is key to accessibility.

Setting Up Your Kotlin Project

Before we dive into code, let’s make sure your Android project is ready for Material 3.

Adding Dependencies

First, add the necessary dependencies to your build.gradle.kts (Module level):

Kotlin
dependencies {
    // Jetpack Compose BOM (Bill of Materials)
    implementation(platform("androidx.compose:compose-bom:2024.02.00"))
    
    // Material 3
    implementation("androidx.compose.material3:material3")
    
    // Other Compose dependencies
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.ui:ui-tooling-preview")
    implementation("androidx.activity:activity-compose:1.8.2")
    
    // Core KTX
    implementation("androidx.core:core-ktx:1.12.0")
    
    // Lifecycle
    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
}

What each dependency does:

  • compose-bom: Manages Compose versions automatically
  • material3: The Material 3 components and colorScheme system
  • ui and ui-tooling-preview: Core Compose UI and preview support
  • activity-compose: Integration with Android activities

Update Your Theme File

Create a new file called Theme.kt in your ui.theme package:

Kotlin
package com.softaai.myapp.ui.theme

import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat

@Composable
fun MyAppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    // We'll fill this in shortly!
}

This is your foundation. Now let’s build on it!

Implementing Material 3 ColorScheme in Kotlin

Let’s start with the simplest implementation and work our way up to more advanced features.

Basic Static ColorScheme

Here’s how to create a basic Material 3 colorScheme with custom colors:

Kotlin
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color

// Light theme colors
private val LightColorScheme = lightColorScheme(
    primary = Color(0xFF6750A4),
    onPrimary = Color(0xFFFFFFFF),
    primaryContainer = Color(0xFFEADDFF),
    onPrimaryContainer = Color(0xFF21005D),
    secondary = Color(0xFF625B71),
    onSecondary = Color(0xFFFFFFFF),
    secondaryContainer = Color(0xFFE8DEF8),
    onSecondaryContainer = Color(0xFF1D192B),
    tertiary = Color(0xFF7D5260),
    onTertiary = Color(0xFFFFFFFF),
    tertiaryContainer = Color(0xFFFFD8E4),
    onTertiaryContainer = Color(0xFF31111D),
    error = Color(0xFFB3261E),
    onError = Color(0xFFFFFFFF),
    errorContainer = Color(0xFFF9DEDC),
    onErrorContainer = Color(0xFF410E0B),
    background = Color(0xFFFFFBFE),
    onBackground = Color(0xFF1C1B1F),
    surface = Color(0xFFFFFBFE),
    onSurface = Color(0xFF1C1B1F),
    surfaceVariant = Color(0xFFE7E0EC),
    onSurfaceVariant = Color(0xFF49454F),
    outline = Color(0xFF79747E),
    outlineVariant = Color(0xFFCAC4D0),
    scrim = Color(0xFF000000),
    inverseSurface = Color(0xFF313033),
    inverseOnSurface = Color(0xFFF4EFF4),
    inversePrimary = Color(0xFFD0BCFF),
)

// Dark theme colors
private val DarkColorScheme = darkColorScheme(
    primary = Color(0xFFD0BCFF),
    onPrimary = Color(0xFF381E72),
    primaryContainer = Color(0xFF4F378B),
    onPrimaryContainer = Color(0xFFEADDFF),
    secondary = Color(0xFFCCC2DC),
    onSecondary = Color(0xFF332D41),
    secondaryContainer = Color(0xFF4A4458),
    onSecondaryContainer = Color(0xFFE8DEF8),
    tertiary = Color(0xFFEFB8C8),
    onTertiary = Color(0xFF492532),
    tertiaryContainer = Color(0xFF633B48),
    onTertiaryContainer = Color(0xFFFFD8E4),
    error = Color(0xFFF2B8B5),
    onError = Color(0xFF601410),
    errorContainer = Color(0xFF8C1D18),
    onErrorContainer = Color(0xFFF9DEDC),
    background = Color(0xFF1C1B1F),
    onBackground = Color(0xFFE6E1E5),
    surface = Color(0xFF1C1B1F),
    onSurface = Color(0xFFE6E1E5),
    surfaceVariant = Color(0xFF49454F),
    onSurfaceVariant = Color(0xFFCAC4D0),
    outline = Color(0xFF938F99),
    outlineVariant = Color(0xFF49454F),
    scrim = Color(0xFF000000),
    inverseSurface = Color(0xFFE6E1E5),
    inverseOnSurface = Color(0xFF313033),
    inversePrimary = Color(0xFF6750A4),
)

Now let’s use these in your theme:

Kotlin
@Composable
fun MyAppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
    
    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}

Here,

  • We defined two complete Material 3 colorScheme objects (light and dark)
  • The theme composable selects the appropriate scheme based on system settings
  • MaterialTheme applies the colorScheme to your entire app
  • Every color role is explicitly defined for maximum control

This is the manual approach. But there’s a much easier way..!

Dynamic Color: The Android 12+ Magic

Here’s where things get really exciting. On Android 12 (API 31) and above, you can use dynamic color to automatically match the user’s wallpaper.

Implementing Dynamic Color

Kotlin
@Composable
fun MyAppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = true, // Enable/disable dynamic color
    content: @Composable () -> Unit
) {
    val context = LocalContext.current
    
    // Determine which colorScheme to use
    val colorScheme = when {
        // Use dynamic color if available (Android 12+)
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            if (darkTheme) dynamicDarkColorScheme(context)
            else dynamicLightColorScheme(context)
        }
        // Fall back to custom dark colors
        darkTheme -> DarkColorScheme
        // Fall back to custom light colors
        else -> LightColorScheme
    }
    
    // Update the system bars to match theme
    val view = LocalView.current
    if (!view.isInEditMode) {
        SideEffect {
            val window = (view.context as Activity).window
            window.statusBarColor = colorScheme.surface.toArgb()
            WindowCompat.getInsetsController(window, view)
                .isAppearanceLightStatusBars = !darkTheme
        }
    }
    
    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}

Let’s break down what’s happening:

  1. Version Check: We check if the device runs Android 12+ (API 31+)
  2. Dynamic ColorScheme: If supported, we use dynamicDarkColorScheme() or dynamicLightColorScheme()
  3. Fallback: On older devices, we fall back to our custom color schemes
  4. System Bars: We update the status bar color to match our theme
  5. Edge-to-Edge: The window insets controller adjusts the status bar appearance

That’s it..! 

Your app now supports dynamic theming with the Material 3 colorScheme.

Using the Theme in Your MainActivity

Don’t forget to wrap your content in the theme:

Kotlin
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyAppTheme {
                // Your app content here
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    ProfileScreen()
                }
            }
        }
    }
}

Important points:

  • MyAppTheme wraps all your composables
  • Surface uses colorScheme.background for the base color
  • Everything inside automatically has access to the Material 3 colorScheme

Accessing Colors in Your Composables

Now that you have your Material 3 colorScheme set up, how do you actually use these colors in your UI?

The Easy Way: Built-in Components

Material 3 components automatically use the appropriate colors from your colorScheme:

Kotlin
@Composable
fun AutomaticColorExample() {

    Column(
        modifier = Modifier.padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        // Primary button - automatically uses primary color
        Button(onClick = { /* Do something */ }) {
            Text("Primary Button")
        }
        
        // Tonal button - uses primaryContainer
        FilledTonalButton(onClick = { /* Do something */ }) {
            Text("Tonal Button")
        }
        
        // Outlined button - uses outline color
        OutlinedButton(onClick = { /* Do something */ }) {
            Text("Outlined Button")
        }
        
        // Card - automatically uses surface color
        Card {
            Text(
                text = "This card uses surface colors",
                modifier = Modifier.padding(16.dp)
            )
        }
    }
}

No manual color assignment needed..! The Material 3 colorScheme handles it automatically.

The Manual Way: Direct Access

Sometimes you need direct access to specific colors:

Kotlin
@Composable
fun ManualColorExample() {
    // Access the current Material 3 colorScheme
    val colorScheme = MaterialTheme.colorScheme
    
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .background(colorScheme.primaryContainer)
            .padding(16.dp)
    ) {
        Text(
            text = "Custom colored container",
            color = colorScheme.onPrimaryContainer,
            style = MaterialTheme.typography.headlineSmall
        )
    }
}

Key points:

  • Use MaterialTheme.colorScheme to access all color roles
  • Always pair surfaces with their corresponding “on” colors
  • The Material 3 colorScheme ensures all combinations are accessible

Building Real UI with Material 3 ColorScheme

Let’s create some practical examples that showcase the Material 3 colorScheme in action.

Custom Button with ColorScheme

Kotlin
@Composable
fun ThemedButton(
    text: String,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    isPrimary: Boolean = true
) {
    val colorScheme = MaterialTheme.colorScheme
    
    // Choose colors based on button type
    val backgroundColor = if (isPrimary) {
        colorScheme.primary
    } else {
        colorScheme.secondary
    }
    
    val contentColor = if (isPrimary) {
        colorScheme.onPrimary
    } else {
        colorScheme.onSecondary
    }
    
    Button(
        onClick = onClick,
        modifier = modifier,
        colors = ButtonDefaults.buttonColors(
            containerColor = backgroundColor,
            contentColor = contentColor
        )
    ) {
        Text(text)
    }
}

// Usage
@Composable
fun ButtonExample() {
    Column(
        modifier = Modifier.padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        ThemedButton(
            text = "Primary Action",
            onClick = { /* Handle click */ },
            isPrimary = true
        )
        
        ThemedButton(
            text = "Secondary Action",
            onClick = { /* Handle click */ },
            isPrimary = false
        )
    }
}

Here,

  • We’re accessing the Material 3 colorScheme directly
  • Colors automatically adjust for light/dark mode and dynamic color
  • Accessibility is maintained through proper color role usage

Status Card with Conditional Colors

Kotlin
@Composable
fun StatusCard(
    title: String,
    message: String,
    isError: Boolean = false,
    modifier: Modifier = Modifier
) {
    val colorScheme = MaterialTheme.colorScheme
    
    // Choose colors based on status
    val containerColor = if (isError) {
        colorScheme.errorContainer
    } else {
        colorScheme.primaryContainer
    }
    
    val contentColor = if (isError) {
        colorScheme.onErrorContainer
    } else {
        colorScheme.onPrimaryContainer
    }
    
    val iconColor = if (isError) {
        colorScheme.error
    } else {
        colorScheme.primary
    }
    
    Card(
        modifier = modifier.fillMaxWidth(),
        colors = CardDefaults.cardColors(
            containerColor = containerColor
        )
    ) {
        Row(
            modifier = Modifier.padding(16.dp),
            horizontalArrangement = Arrangement.spacedBy(12.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Icon(
                imageVector = if (isError) {
                    Icons.Default.Error
                } else {
                    Icons.Default.CheckCircle
                },
                contentDescription = null,
                tint = iconColor,
                modifier = Modifier.size(32.dp)
            )
            
            Column {
                Text(
                    text = title,
                    style = MaterialTheme.typography.titleMedium,
                    color = contentColor,
                    fontWeight = FontWeight.Bold
                )
                Spacer(modifier = Modifier.height(4.dp))
                Text(
                    text = message,
                    style = MaterialTheme.typography.bodyMedium,
                    color = contentColor
                )
            }
        }
    }
}

// Usage
@Composable
fun StatusCardExample() {
    Column(
        modifier = Modifier.padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(12.dp)
    ) {
        StatusCard(
            title = "Success",
            message = "Your profile has been updated successfully",
            isError = false
        )
        
        StatusCard(
            title = "Error",
            message = "Failed to save changes. Please try again.",
            isError = true
        )
    }
}

This example shows how the Material 3 colorScheme adapts to different UI states seamlessly.

Complete Profile Screen

Let’s build a realistic profile screen using the Material 3 colorScheme:

Kotlin
@Composable
fun ProfileScreen() {
    val colorScheme = MaterialTheme.colorScheme
    
    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("Profile") },
                colors = TopAppBarDefaults.topAppBarColors(
                    containerColor = colorScheme.surface,
                    titleContentColor = colorScheme.onSurface
                )
            )
        }
    ) { paddingValues ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .background(colorScheme.background)
                .padding(paddingValues)
                .padding(16.dp),
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            // Profile Header Card
            Card(
                modifier = Modifier.fillMaxWidth(),
                colors = CardDefaults.cardColors(
                    containerColor = colorScheme.primaryContainer
                )
            ) {
                Column(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(20.dp),
                    horizontalAlignment = Alignment.CenterHorizontally
                ) {
                    // Profile Image
                    Surface(
                        modifier = Modifier.size(100.dp),
                        shape = CircleShape,
                        color = colorScheme.primary
                    ) {
                        Icon(
                            imageVector = Icons.Default.Person,
                            contentDescription = "Profile Picture",
                            modifier = Modifier
                                .fillMaxSize()
                                .padding(20.dp),
                            tint = colorScheme.onPrimary
                        )
                    }
                    
                    Spacer(modifier = Modifier.height(16.dp))
                    
                    Text(
                        text = "Amol Pawar",
                        style = MaterialTheme.typography.headlineMedium,
                        color = colorScheme.onPrimaryContainer,
                        fontWeight = FontWeight.Bold
                    )
                    
                    Text(
                        text = "[email protected]",
                        style = MaterialTheme.typography.bodyMedium,
                        color = colorScheme.onPrimaryContainer.copy(alpha = 0.8f)
                    )
                }
            }
            
            // Settings Section
            Card(
                modifier = Modifier.fillMaxWidth(),
                colors = CardDefaults.cardColors(
                    containerColor = colorScheme.surface
                )
            ) {
                Column {
                    SettingsItem(
                        icon = Icons.Default.Notifications,
                        title = "Notifications",
                        onClick = { /* Handle click */ }
                    )
                    
                    HorizontalDivider(color = colorScheme.outlineVariant)
                    
                    SettingsItem(
                        icon = Icons.Default.Security,
                        title = "Privacy & Security",
                        onClick = { /* Handle click */ }
                    )
                    
                    HorizontalDivider(color = colorScheme.outlineVariant)
                    
                    SettingsItem(
                        icon = Icons.Default.Help,
                        title = "Help & Support",
                        onClick = { /* Handle click */ }
                    )
                }
            }
            
            // Action Buttons
            Button(
                onClick = { /* Edit profile */ },
                modifier = Modifier.fillMaxWidth(),
                colors = ButtonDefaults.buttonColors(
                    containerColor = colorScheme.primary,
                    contentColor = colorScheme.onPrimary
                )
            ) {
                Text("Edit Profile")
            }
            
            OutlinedButton(
                onClick = { /* Sign out */ },
                modifier = Modifier.fillMaxWidth(),
                colors = ButtonDefaults.outlinedButtonColors(
                    contentColor = colorScheme.error
                ),
                border = BorderStroke(1.dp, colorScheme.error)
            ) {
                Text("Sign Out")
            }
        }
    }
}

@Composable
fun SettingsItem(
    icon: ImageVector,
    title: String,
    onClick: () -> Unit
) {
    val colorScheme = MaterialTheme.colorScheme
    
    Surface(
        onClick = onClick,
        color = Color.Transparent
    ) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp),
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically
        ) {
            Row(
                horizontalArrangement = Arrangement.spacedBy(16.dp),
                verticalAlignment = Alignment.CenterVertically
            ) {
                Icon(
                    imageVector = icon,
                    contentDescription = null,
                    tint = colorScheme.primary
                )
                Text(
                    text = title,
                    style = MaterialTheme.typography.bodyLarge,
                    color = colorScheme.onSurface
                )
            }
            
            Icon(
                imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
                contentDescription = "Navigate",
                tint = colorScheme.onSurfaceVariant
            )
        }
    }
}
  • Every color comes from the Material 3 colorScheme
  • Semantic color names ensure accessibility
  • Works perfectly in light and dark modes
  • Adapts to dynamic colors automatically
  • Zero hard-coded color values

Advanced ColorScheme Techniques

Creating Color Variants

Sometimes you need variations of your Material 3 colorScheme colors:

Kotlin
@Composable
fun ColorVariantExample() {
    val colorScheme = MaterialTheme.colorScheme
    
    // Create lighter or darker variants using alpha
    val primaryLight = colorScheme.primary.copy(alpha = 0.1f)
    val primaryDark = colorScheme.primary.copy(alpha = 0.9f)
    
    Column(
        modifier = Modifier.padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        // Light variant
        Surface(
            modifier = Modifier
                .fillMaxWidth()
                .height(50.dp),
            color = primaryLight
        ) {
            Box(contentAlignment = Alignment.Center) {
                Text(
                    "Light Variant",
                    color = colorScheme.onSurface
                )
            }
        }
        
        // Original color
        Surface(
            modifier = Modifier
                .fillMaxWidth()
                .height(50.dp),
            color = colorScheme.primary
        ) {
            Box(contentAlignment = Alignment.Center) {
                Text(
                    "Original Primary",
                    color = colorScheme.onPrimary
                )
            }
        }
        
        // Dark variant
        Surface(
            modifier = Modifier
                .fillMaxWidth()
                .height(50.dp),
            color = primaryDark
        ) {
            Box(contentAlignment = Alignment.Center) {
                Text(
                    "Dark Variant",
                    color = colorScheme.onPrimary
                )
            }
        }
    }
}

Composing with Surface Tint

Material 3 introduces surface tint for elevation. Here’s how to use it:

Kotlin
@Composable
fun ElevatedCardExample() {
    val colorScheme = MaterialTheme.colorScheme
    
    Column(
        modifier = Modifier.padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(12.dp)
    ) {
        // Card with different elevation levels
        listOf(0.dp, 2.dp, 4.dp, 6.dp).forEach { elevation ->
            Card(
                modifier = Modifier.fillMaxWidth(),
                elevation = CardDefaults.cardElevation(
                    defaultElevation = elevation
                ),
                colors = CardDefaults.cardColors(
                    containerColor = colorScheme.surface
                )
            ) {
                Text(
                    text = "Elevation: $elevation",
                    modifier = Modifier.padding(16.dp),
                    color = colorScheme.onSurface
                )
            }
        }
    }
}

The Material 3 colorScheme automatically applies surfaceTint (usually the primary color) to create subtle elevation effects.

Creating Theme Toggle

Let users switch between light and dark themes:

Kotlin
@Composable
fun ThemeToggleExample() {
    var isDarkTheme by remember { mutableStateOf(false) }
    
    MyAppTheme(darkTheme = isDarkTheme) {
        val colorScheme = MaterialTheme.colorScheme
        
        Scaffold { paddingValues ->
            Column(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(paddingValues)
                    .background(colorScheme.background)
                    .padding(16.dp)
            ) {
                Card(
                    modifier = Modifier.fillMaxWidth(),
                    colors = CardDefaults.cardColors(
                        containerColor = colorScheme.surfaceVariant
                    )
                ) {
                    Row(
                        modifier = Modifier
                            .fillMaxWidth()
                            .padding(16.dp),
                        horizontalArrangement = Arrangement.SpaceBetween,
                        verticalAlignment = Alignment.CenterVertically
                    ) {
                        Row(
                            horizontalArrangement = Arrangement.spacedBy(12.dp),
                            verticalAlignment = Alignment.CenterVertically
                        ) {
                            Icon(
                                imageVector = if (isDarkTheme) {
                                    Icons.Default.DarkMode
                                } else {
                                    Icons.Default.LightMode
                                },
                                contentDescription = null,
                                tint = colorScheme.onSurfaceVariant
                            )
                            Text(
                                text = "Dark Mode",
                                style = MaterialTheme.typography.bodyLarge,
                                color = colorScheme.onSurfaceVariant
                            )
                        }
                        
                        Switch(
                            checked = isDarkTheme,
                            onCheckedChange = { isDarkTheme = it },
                            colors = SwitchDefaults.colors(
                                checkedThumbColor = colorScheme.primary,
                                checkedTrackColor = colorScheme.primaryContainer
                            )
                        )
                    }
                }
            }
        }
    }
}

Material Theme Builder Integration

Want to generate a complete Material 3 colorScheme visually..? Google provides an amazing tool called Material Theme Builder.

Using Material Theme Builder

  1. Visit Material Theme Builder
  2. Choose your primary color or upload an image
  3. Customize secondary and tertiary colors if desired
  4. Click “Export” and select “Jetpack Compose (Theme.kt)”

The tool generates complete Kotlin code.

Just copy this code into your Theme.kt file and you’re done..!

Pro tip: The Material Theme Builder ensures all colors are harmonious and accessible, saving you hours of manual color picking.

Best Practices for Material 3 ColorScheme

1. Always Use Semantic Names

Use the semantic color roles instead of hard-coded values:

Kotlin
// Good: Uses Material 3 colorScheme
Box(
    modifier = Modifier.background(MaterialTheme.colorScheme.surface)
)

// Bad: Hard-coded color
Box(
    modifier = Modifier.background(Color.White)
)

Why this matters: Semantic names adapt automatically to light/dark mode and dynamic color.

2. Pair Colors Correctly

Always use the “on” variant for text on colored surfaces:

Kotlin
// Good: Proper pairing ensures readability
Card(
    colors = CardDefaults.cardColors(
        containerColor = colorScheme.primaryContainer
    )
) {
    Text(
        text = "Accessible text",
        color = colorScheme.onPrimaryContainer
    )
}

// Bad: Might have contrast issues
Card(
    colors = CardDefaults.cardColors(
        containerColor = colorScheme.primaryContainer
    )
) {
    Text(
        text = "Poor contrast",
        color = colorScheme.secondary // Wrong pairing!
    )
}

3. Provide Dynamic Color Fallbacks

Always have a backup for devices that don’t support dynamic color:

Kotlin
val colorScheme = when {
    dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
        if (darkTheme) dynamicDarkColorScheme(context)
        else dynamicLightColorScheme(context)
    }
    darkTheme -> DarkColorScheme // Fallback
    else -> LightColorScheme     // Fallback
}

4. Test Both Themes

Always test your UI in both light and dark modes:

Kotlin
@Preview(
    name = "Light Mode",
    showBackground = true
)
@Preview(
    name = "Dark Mode",
    showBackground = true,
    uiMode = Configuration.UI_MODE_NIGHT_YES
)
@Composable
fun PreviewProfileScreen() {
    MyAppTheme {
        ProfileScreen()
    }
}

5. Leverage Preview Parameters

Use preview parameters to test different scenarios:

Kotlin
@Preview(name = "Light - No Dynamic Color", showBackground = true)
@Composable
fun LightStaticPreview() {
    MyAppTheme(darkTheme = false, dynamicColor = false) {
        MyComposable()
    }
}

@Preview(name = "Dark - No Dynamic Color", showBackground = true)
@Composable
fun DarkStaticPreview() {
    MyAppTheme(darkTheme = true, dynamicColor = false) {
        MyComposable()
    }
}

Understanding Color Harmonization

The Material 3 colorScheme uses advanced color science to ensure harmony. Here’s what happens under the hood:

HCT Color Space

Material 3 uses HCT (Hue, Chroma, Tone) instead of RGB or HSL:

  • Hue: The color type (0–360 degrees)
  • Chroma: The colorfulness or saturation (0–120+)
  • Tone: The perceived lightness (0–100)

Tonal Palettes

When you provide a seed color, the Material 3 colorScheme:

  1. Converts it to HCT color space
  2. Generates tonal palettes at specific chroma levels
  3. Maps tones to color roles based on their purpose
  4. Ensures all combinations meet WCAG contrast requirements

Example of Tone Mapping

Kotlin
Primary Palette (High Chroma):
- primary: tone 40 (light) / tone 80 (dark)
- onPrimary: tone 100 (light) / tone 20 (dark)
- primaryContainer: tone 90 (light) / tone 30 (dark)

Neutral Palette (Low Chroma):
- surface: tone 99 (light) / tone 10 (dark)
- onSurface: tone 10 (light) / tone 90 (dark)
Tonal Palettes (Light Theme)

You don’t need to understand all the math — just know that it works beautifully..!

Troubleshooting Common Issues

Issue 1: Colors Not Updating

Problem: Changes to colorScheme don’t appear in the UI.

Solution: Make sure you’re wrapping your content in the theme:

Kotlin
setContent {
    MyAppTheme {  // Essential wrapper!
        Surface(
            modifier = Modifier.fillMaxSize(),
            color = MaterialTheme.colorScheme.background
        ) {
            MyApp()
        }
    }
}

Issue 2: Dynamic Colors Not Working

Problem: App doesn’t match wallpaper colors.

Solution: Check these points:

  1. Device Version: Dynamic color requires Android 12+ (API 31+)
  2. Feature Flag: Ensure dynamicColor = true in your theme
  3. Wallpaper: Try changing your wallpaper to trigger update
  4. Build Version: Verify Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
Kotlin
// Debug logging
val colorScheme = when {
    dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
        Log.d("Theme", "Using dynamic colors from wallpaper")
        if (darkTheme) dynamicDarkColorScheme(context)
        else dynamicLightColorScheme(context)
    }
    else -> {
        Log.d("Theme", "Using static fallback colors")
        if (darkTheme) DarkColorScheme else LightColorScheme
    }
}

Issue 3: Wrong Colors in Previews

Problem: Compose previews show incorrect or default colors.

Solution: Always wrap preview content in your theme:

Kotlin
@Preview
@Composable
fun MyComposablePreview() {
    MyAppTheme {  // Never forget the theme wrapper!
        MyComposable()
    }
}

Issue 4: System Bars Not Matching Theme

Problem: Status bar and navigation bar don’t match app theme.

Solution: Update system bars in your theme composable:

Kotlin
val view = LocalView.current
if (!view.isInEditMode) {
    SideEffect {
        val window = (view.context as Activity).window
        // Set status bar color
        window.statusBarColor = colorScheme.surface.toArgb()
        // Set navigation bar color
        window.navigationBarColor = colorScheme.surface.toArgb()
        // Adjust icon colors
        WindowCompat.getInsetsController(window, view).apply {
            isAppearanceLightStatusBars = !darkTheme
            isAppearanceLightNavigationBars = !darkTheme
        }
    }
}

Issue 5: Gradle Build Errors

Problem: Cannot resolve Material 3 symbols.

Solution: Ensure you have the correct dependencies:

Kotlin
dependencies {
    // Use BOM for version management
    implementation(platform("androidx.compose:compose-bom:2024.02.00"))
    implementation("androidx.compose.material3:material3")
    
    // Or specify version explicitly
    implementation("androidx.compose.material3:material3:1.2.0")
}

Complete Working Example

Here’s a complete, copy-paste-ready example:

build.gradle.kts (Module)

Kotlin
plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
}

android {
    namespace = "com.softaai.myapp"
    compileSdk = 34
    
    defaultConfig {
        applicationId = "com.softaai.myapp"
        minSdk = 21
        targetSdk = 34
        versionCode = 1
        versionName = "1.0"
    }
    
    buildFeatures {
        compose = true
    }
    
    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.8"
    }
    
    kotlinOptions {
        jvmTarget = "1.8"
    }
}

dependencies {
    implementation(platform("androidx.compose:compose-bom:2024.02.00"))
    implementation("androidx.compose.material3:material3")
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.ui:ui-tooling-preview")
    implementation("androidx.activity:activity-compose:1.8.2")
    implementation("androidx.core:core-ktx:1.12.0")
    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
    
    debugImplementation("androidx.compose.ui:ui-tooling")
}

Theme.kt

Kotlin
package com.softaai.myapp.ui.theme

import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat

private val LightColorScheme = lightColorScheme(
    primary = Color(0xFF6750A4),
    onPrimary = Color(0xFFFFFFFF),
    primaryContainer = Color(0xFFEADDFF),
    onPrimaryContainer = Color(0xFF21005D),
    secondary = Color(0xFF625B71),
    onSecondary = Color(0xFFFFFFFF),
    secondaryContainer = Color(0xFFE8DEF8),
    onSecondaryContainer = Color(0xFF1D192B),
    tertiary = Color(0xFF7D5260),
    onTertiary = Color(0xFFFFFFFF),
    tertiaryContainer = Color(0xFFFFD8E4),
    onTertiaryContainer = Color(0xFF31111D),
    error = Color(0xFFB3261E),
    onError = Color(0xFFFFFFFF),
    errorContainer = Color(0xFFF9DEDC),
    onErrorContainer = Color(0xFF410E0B),
    background = Color(0xFFFFFBFE),
    onBackground = Color(0xFF1C1B1F),
    surface = Color(0xFFFFFBFE),
    onSurface = Color(0xFF1C1B1F),
    surfaceVariant = Color(0xFFE7E0EC),
    onSurfaceVariant = Color(0xFF49454F),
    outline = Color(0xFF79747E),
    outlineVariant = Color(0xFFCAC4D0),
    scrim = Color(0xFF000000),
    inverseSurface = Color(0xFF313033),
    inverseOnSurface = Color(0xFFF4EFF4),
    inversePrimary = Color(0xFFD0BCFF),
)

private val DarkColorScheme = darkColorScheme(
    primary = Color(0xFFD0BCFF),
    onPrimary = Color(0xFF381E72),
    primaryContainer = Color(0xFF4F378B),
    onPrimaryContainer = Color(0xFFEADDFF),
    secondary = Color(0xFFCCC2DC),
    onSecondary = Color(0xFF332D41),
    secondaryContainer = Color(0xFF4A4458),
    onSecondaryContainer = Color(0xFFE8DEF8),
    tertiary = Color(0xFFEFB8C8),
    onTertiary = Color(0xFF492532),
    tertiaryContainer = Color(0xFF633B48),
    onTertiaryContainer = Color(0xFFFFD8E4),
    error = Color(0xFFF2B8B5),
    onError = Color(0xFF601410),
    errorContainer = Color(0xFF8C1D18),
    onErrorContainer = Color(0xFFF9DEDC),
    background = Color(0xFF1C1B1F),
    onBackground = Color(0xFFE6E1E5),
    surface = Color(0xFF1C1B1F),
    onSurface = Color(0xFFE6E1E5),
    surfaceVariant = Color(0xFF49454F),
    onSurfaceVariant = Color(0xFFCAC4D0),
    outline = Color(0xFF938F99),
    outlineVariant = Color(0xFF49454F),
    scrim = Color(0xFF000000),
    inverseSurface = Color(0xFFE6E1E5),
    inverseOnSurface = Color(0xFF313033),
    inversePrimary = Color(0xFF6750A4),
)

@Composable
fun MyAppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    val context = LocalContext.current
    
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            if (darkTheme) dynamicDarkColorScheme(context)
            else dynamicLightColorScheme(context)
        }
        darkTheme -> DarkColorScheme
        else -> LightColorScheme
    }
    
    val view = LocalView.current
    if (!view.isInEditMode) {
        SideEffect {
            val window = (view.context as Activity).window
            window.statusBarColor = colorScheme.surface.toArgb()
            window.navigationBarColor = colorScheme.surface.toArgb()
            WindowCompat.getInsetsController(window, view).apply {
                isAppearanceLightStatusBars = !darkTheme
                isAppearanceLightNavigationBars = !darkTheme
            }
        }
    }
    
    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}

MainActivity.kt

Kotlin
package com.softaai.myapp

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import com.softaai.myapp.ui.theme.MyAppTheme
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyAppTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    // Your app content here
                    ProfileScreen()
                }
            }
        }
    }
}

Quick Reference Guide

Essential ColorScheme Properties

Kotlin
val colorScheme = MaterialTheme.colorScheme

// Primary colors
colorScheme.primary                // Main brand color
colorScheme.onPrimary              // Text on primary
colorScheme.primaryContainer       // Lighter primary variant
colorScheme.onPrimaryContainer     // Text on primary container

// Secondary colors
colorScheme.secondary              // Secondary accent
colorScheme.onSecondary            // Text on secondary
colorScheme.secondaryContainer     // Lighter secondary
colorScheme.onSecondaryContainer   // Text on secondary container

// Tertiary colors
colorScheme.tertiary               // Third accent color
colorScheme.onTertiary             // Text on tertiary
colorScheme.tertiaryContainer      // Lighter tertiary
colorScheme.onTertiaryContainer    // Text on tertiary container

// Surface and background
colorScheme.surface                // Card/sheet background
colorScheme.onSurface              // Text on surfaces
colorScheme.surfaceVariant         // Alternative surface
colorScheme.onSurfaceVariant       // Text on surface variant
colorScheme.background             // Main app background
colorScheme.onBackground           // Text on background

// Error colors
colorScheme.error                  // Error state color
colorScheme.onError                // Text on error
colorScheme.errorContainer         // Error message background
colorScheme.onErrorContainer       // Text in error messages

// Utility colors
colorScheme.outline                // Borders and dividers
colorScheme.outlineVariant         // Subtle borders
colorScheme.surfaceTint            // Elevation tint (usually primary)
colorScheme.scrim                  // Semi-transparent overlays

// Inverse colors (for tooltips, snackbars)
colorScheme.inverseSurface         // High-contrast surface
colorScheme.inverseOnSurface       // Text on inverse surface
colorScheme.inversePrimary         // Primary color on inverse surface

Common Usage Patterns

Kotlin
// Primary button
Button(
    onClick = { },
    colors = ButtonDefaults.buttonColors(
        containerColor = MaterialTheme.colorScheme.primary,
        contentColor = MaterialTheme.colorScheme.onPrimary
    )
) {
    Text("Click Me")
}

// Surface card
Card(
    colors = CardDefaults.cardColors(
        containerColor = MaterialTheme.colorScheme.surface
    )
) {
    Text(
        text = "Card Content",
        color = MaterialTheme.colorScheme.onSurface,
        modifier = Modifier.padding(16.dp)
    )
}

// Custom background
Box(
    modifier = Modifier
        .background(MaterialTheme.colorScheme.primaryContainer)
        .padding(16.dp)
) {
    Text(
        text = "Custom Container",
        color = MaterialTheme.colorScheme.onPrimaryContainer
    )
}

// Error state
Surface(
    color = MaterialTheme.colorScheme.errorContainer,
    modifier = Modifier.padding(8.dp)
) {
    Text(
        text = "Error message",
        color = MaterialTheme.colorScheme.onErrorContainer,
        modifier = Modifier.padding(16.dp)
    )
}

Conclusion

The Material 3 colorScheme is a game-changer for Android app theming. It takes the complexity out of color design and gives you a robust, accessible, and beautiful color system right out of the box.

Here’s what we’ve covered:

Understanding: The Material 3 colorScheme generates complete, harmonious palettes from seed colors or wallpaper

Color Roles: Semantic color names ensure accessibility and visual consistency

Implementation: Simple Kotlin setup with Jetpack Compose and dynamic color support

Dynamic Color: Apps automatically match user wallpapers on Android 12+ for personalized experiences

Best Practices: Use semantic colors, test both themes, provide fallbacks, and optimize performance

Real Examples: Production-ready Kotlin code you can use in your projects immediately

Troubleshooting: Solutions to common issues developers face

The beauty of the Material 3 colorScheme is that it makes professional color design accessible to everyone. You don’t need to be a color theory expert to create stunning, accessible Android apps.

Getting Started Today

Start your next Android project by:

  1. Adding Dependencies: Include Material 3 in your build.gradle.kts
  2. Creating Your Theme: Set up lightColorScheme() and darkColorScheme() in Theme.kt
  3. Enabling Dynamic Color: Support Android 12+ wallpaper theming
  4. Using Semantic Colors: Reference MaterialTheme.colorScheme throughout your composables
  5. Testing Thoroughly: Preview in both light and dark modes

The Material 3 colorScheme handles the complexity of color science, accessibility, and harmonization so you can focus on building amazing user experiences. And that’s what makes it so powerful.

Additional Resources

Official Documentation:

Tools:

Sample Projects:

Now go build something beautiful with the Material 3 colorScheme in Kotlin..!

Compose Preview

Compose Preview Explained: How It Works, Why It Matters, and Where It Falls Short

If you’re building Android apps with Jetpack Compose, chances are you’ve already used Compose Preview. Or at least clicked the little Preview tab in Android Studio and hoped it would magically show your UI.

Sometimes it does.
Sometimes it doesn’t.

In this blog, we’ll break down Compose Preview, covering everything from core mechanics to practical tips. You’ll learn:

  • What Compose Preview actually is
  • How it works under the hood
  • Why it matters for real-world development
  • Where it struggles and why
  • When to trust it and when not to

Let’s start with the basics.

What Is Compose Preview?

Compose Preview is a design-time tool in Android Studio that lets you see your Jetpack Compose UI without running the app on a device or emulator.

It renders composable functions directly inside the IDE.

That means:

  • Faster feedback
  • No APK install
  • No waiting for Gradle every time you tweak padding or text size

In short, Compose Preview helps you design UI faster.

A Simple Compose Preview Example

Let’s start with a basic example.

Kotlin
@Composable
fun Greeting(name: String) {
    Text(text = "Hello, $name!")
}

This composable works, but Android Studio can’t preview it yet. Why?

Because Greeting needs a parameter.

That’s where Compose Preview comes in.

Adding a Preview Function

Kotlin
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    Greeting(name = "Android")
}
  • @Preview tells Android Studio: Render this composable
  • showBackground = true adds a white background so text is readable
  • GreetingPreview() supplies sample data ("Android")

This preview function is not used in production.
It exists only for design-time visualization.

That’s an important detail many beginners miss.

How Compose Preview Works Behind the Scenes

Compose Preview does not run your full app.

Instead, Android Studio:

  1. Compiles the composable function
  2. Runs it in a special design-time environment
  3. Skips most Android framework components
  4. Renders the UI using sample data

That’s why previews are fast.

And that’s also why they’re limited.

Why Compose Preview Matters So Much

1. Faster UI Iteration

With Compose Preview, you can:

  • Adjust spacing
  • Change colors
  • Try different text styles
  • Experiment with layouts

All without touching an emulator.

For UI-heavy screens, this saves hours over time.

2. Encourages Smaller, Cleaner Composables

Compose Preview works best with small, focused composables.

That naturally pushes you toward:

  • Better separation of concerns
  • Reusable UI components
  • Clearer code structure

This directly improves long-term maintainability.

3. Better Design Collaboration

Designers and developers can:

  • Review UI changes quickly
  • Compare states side by side
  • Validate layouts early

Compose Preview becomes a shared visual language.

Advanced Compose Preview Features You Should Know

Beyond basic previews, several advanced features make Compose Preview even more powerful.

Preview with Different Device Configurations

The @Preview annotation accepts parameters that let you simulate different devices, screen sizes, and system settings.

Kotlin
@Preview(
    name = "Small phone",
    device = Devices.PIXEL_3A,
    showSystemUi = true
)
@Preview(
    name = "Large phone",
    device = Devices.PIXEL_7_PRO,
    showSystemUi = true
)
@Preview(
    name = "Tablet",
    device = Devices.PIXEL_TABLET,
    showSystemUi = true
)
@Preview(
    name = "Foldable",
    device = Devices.FOLDABLE,
    showSystemUi = true
)
@Preview(
    name = "Landscape",
    device = Devices.PIXEL_7_PRO,
    widthDp = 891,
    heightDp = 411
)
@Preview(
    name = "Dark Theme",
    uiMode = Configuration.UI_MODE_NIGHT_YES,
    showBackground = true
)
@Preview(showBackground = true)
@Composable
fun ResponsiveLayoutPreview() {
    MaterialTheme {
        Surface(
            modifier = Modifier.fillMaxSize(),
            tonalElevation = 4.dp
        ) {
            Column(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(24.dp),
                verticalArrangement = Arrangement.spacedBy(20.dp)
            ) {

                // Header
                Text(
                    text = "Responsive UI",
                    style = MaterialTheme.typography.headlineMedium,
                    fontWeight = FontWeight.Bold
                )

                Text(
                    text = "Adaptive layouts across form factors",
                    style = MaterialTheme.typography.bodyMedium,
                    color = MaterialTheme.colorScheme.onSurfaceVariant
                )

                Divider()

                Column(
                    verticalArrangement = Arrangement.spacedBy(12.dp)
                ) {
                    FeatureRow("Phones", "Compact & large screens")
                    FeatureRow("Tablets", "Expanded content layouts")
                    FeatureRow("Foldables", "Posture-aware UI")
                    FeatureRow("Themes", "Light & Dark mode ready")
                }
            }
        }
    }
}

@Composable
private fun FeatureRow(
    title: String,
    subtitle: String
) {
    Column {
        Text(
            text = title,
            style = MaterialTheme.typography.titleMedium,
            fontWeight = FontWeight.SemiBold
        )
        Text(
            text = subtitle,
            style = MaterialTheme.typography.bodySmall,
            color = MaterialTheme.colorScheme.onSurfaceVariant
        )
    }
}

Let me break down what’s happening here:

  • device = Devices.PIXEL_7_PRO: This tells Compose Preview to render your composable as if it’s running on a Pixel 7 Pro device, matching that specific screen size and dimensions.
  • showSystemUi = true: This parameter displays the system UI elements like the status bar and navigation bar, giving you a more realistic preview of how your app will look.
  • uiMode = Configuration.UI_MODE_NIGHT_YES: This simulates dark mode, letting you verify that your colors and themes work properly in both light and dark settings.

You can stack multiple @Preview annotations on the same function to see all these variations simultaneously.

Preview Parameters for Dynamic Content

Sometimes you want to test your composables with different data sets. The @PreviewParameter annotation helps with this.

Kotlin
class UserStateProvider : PreviewParameterProvider<Boolean> {
    override val values = sequenceOf(true, false)
}

@Preview(showBackground = true)
@Composable
fun StatusBadgePreview(
    @PreviewParameter(UserStateProvider::class) isActive: Boolean
) {
    Box(
        modifier = Modifier
            .size(100.dp)
            .background(
                color = if (isActive) Color.Green else Color.Red,
                shape = CircleShape
            ),
        contentAlignment = Alignment.Center
    ) {
        Text(
            text = if (isActive) "Active" else "Inactive",
            color = Color.White,
            fontWeight = FontWeight.Bold
        )
    }
}

Here,

The UserStateProvider class implements PreviewParameterProvider<Boolean>, which means it provides a sequence of Boolean values for previewing. The values property returns both true and false.

When you use @PreviewParameter(UserStateProvider::class) on the isActive parameter, Compose Preview automatically generates two separate previews—one for each value in the sequence. You get both the active and inactive states without writing separate preview functions.

This approach is incredibly useful when testing with lists of data, different user types, or various configuration options.

Interactive Preview Mode

Recent versions of Android Studio introduced interactive preview mode, which lets you click buttons, scroll lists, and interact with your UI directly in the preview pane. This feature brings you even closer to the actual app experience without leaving the IDE.

To enable it, look for the interactive mode toggle in the preview pane toolbar. Keep in mind that interactions are limited to the composable being previewed — you can’t navigate to other screens or trigger real network calls.

Where Compose Preview Falls Short

Compose Preview is helpful, but it’s not perfect.

Let’s talk honestly about its limitations.

1. No Real Runtime Logic

Compose Preview does not handle:

  • Network calls
  • Database access
  • ViewModel state from real sources
  • Dependency injection (Hilt, Koin)

If your composable depends on runtime data, preview will break.

That’s why preview-friendly composables should take simple, deterministic parameters that can be easily mocked in previews, rather than ViewModels.

2. Limited Interaction Support

You can’t:

  • Click buttons meaningfully
  • Trigger navigation
  • Test animations properly
  • Simulate gestures accurately

Compose Preview shows how things look, not how they behave.

For behavior, you still need:

  • Emulators
  • Physical devices
  • UI tests

3. Can Be Slow in Large Projects

As your project grows:

  • Previews may take longer to render
  • IDE memory usage increases
  • Sometimes previews just refuse to refresh

This isn’t your fault. It’s a known trade-off.

4. Not a Replacement for Testing

Compose Preview is not a test.

It won’t catch:

  • Crashes
  • Logic bugs
  • Edge-case states
  • Performance issues

Think of it as a design aid, not a quality gate.

Best Practices for Using Compose Preview

To get the most out of Compose Preview:

Keep Preview Functions Simple and Focused

Your preview functions should be straightforward and serve a single purpose. Don’t overcomplicate them with business logic or complex data transformations.

Kotlin
// Good: Simple and clear
@Preview(showBackground = true)
@Composable
fun LoadingButtonPreview() {
    LoadingButton(
        text = "Loading",
        isLoading = true,
        onClick = { }
    )
}

// Avoid: Too much logic in preview
@Preview(showBackground = true)
@Composable
fun ComplicatedPreview() {
    val viewModel = remember { MyViewModel() }
    val state by viewModel.uiState.collectAsState()
    // This won't work well in preview..!
}

The first preview is clean and predictable. The second tries to instantiate a ViewModel, which likely depends on dependency injection, context, or other resources that aren’t available in preview mode.

Use Preview Groups for Organization

When you have many related previews, organize them into preview groups for better navigation.

Kotlin
annotation class ComponentPreviews

@ComponentPreviews
@Preview(name = "Small Button", widthDp = 100)
@Preview(name = "Medium Button", widthDp = 200)
@Preview(name = "Large Button", widthDp = 300)
@Composable
fun ButtonSizePreview() {
    Button(onClick = { }) {
        Text("Click Me")
    }
}

By creating a custom annotation like @ComponentPreviews and applying it alongside your @Preview annotations, you can filter and group previews in Android Studio. This becomes invaluable when working on large projects with hundreds of composables.

Create Preview Fixtures for Common Data

Maintain a separate file with preview fixtures — sample data objects you can reuse across multiple previews.

Kotlin
// PreviewFixtures.kt
object PreviewFixtures {
    val sampleUser = UserData(
        name = "Amol Pawar",
        email = "[email protected]",
        joinDate = "March 2022"
    )
    
    val sampleMessages = listOf(
        MessageData("Hello there!", "Amol", "9:00 AM"),
        MessageData("How are you?", "Rutuja", "9:05 AM"),
        MessageData("Doing great!", "Amol", "9:10 AM")
    )
    
    val longText = """
        This is a longer text sample that helps us test how our UI
        handles content that spans multiple lines. It's useful for
        checking text wrapping, overflow behavior, and spacing.
    """.trimIndent()
}

Then use these fixtures in your previews:

Kotlin
@Preview(showBackground = true)
@Composable
fun UserProfileWithFixturePreview() {
    UserProfile(
        userId = "sample",
        getUserData = { PreviewFixtures.sampleUser }
    )
}

This approach keeps your preview code DRY (Don’t Repeat Yourself) and makes it easier to maintain consistency across previews.

Test Edge Cases in Previews

Don’t just preview your happy path. Create previews for edge cases like empty states, error states, and extreme data conditions.

Kotlin
@Preview(name = "Empty List", showBackground = true)
@Composable
fun EmptyListPreview() {
    MessageList(messages = emptyList())
}

@Preview(name = "Very Long Name", showBackground = true)
@Composable
fun LongNamePreview() {
    ProfileCard(
        name = "Soundarya Bhagayalaxmi Venkateshwari Basapa Rao",
        isOnline = true,
        profileImageUrl = null
    )
}

@Preview(name = "Single Character", showBackground = true)
@Composable
fun SingleCharPreview() {
    ProfileCard(
        name = "X",
        isOnline = false,
        profileImageUrl = null
    )
}

These edge case previews help you catch layout issues before they reach production. Does your text truncate properly? Do your empty states look intentional rather than broken? 

The Future of Compose Preview

The Compose Preview tool continues to evolve with each Android Studio release. Recent improvements include better performance, enhanced animation support, and more sophisticated interactive capabilities.

Looking ahead, we can expect:

  • Deeper integration with design tools: Better collaboration between designers and developers through improved Figma integration and design token support.
  • AI-assisted previews: Automated generation of preview functions based on your composable parameters and common usage patterns.
  • Enhanced debugging: More powerful inspection tools for understanding why your UI renders the way it does.
  • Cloud-based previews: The ability to share interactive previews with team members without requiring them to open Android Studio.

The Android development community actively shapes these improvements through feedback, so don’t hesitate to file feature requests or bug reports.

Conclusion

Despite its limitations, Compose Preview is an essential part of modern Android development. The speed and convenience it offers make it ideal for rapid UI iteration and component-level design work.

The key is knowing when to use it. Compose Preview works best for visual validation and layout refinement, while emulators or real devices are still necessary for testing interactions, animations, and real data flows.

When used with preview-friendly composables and best practices, Compose Preview significantly improves development speed and feedback. It turns UI work into a more iterative, design-driven process rather than a cycle of long builds and guesswork.

Happy previewing..!

Responsive and Adaptive UI in Jetpack Compose

How to Build Responsive and Adaptive UI in Jetpack Compose for Every Screen Size

If you’ve ever opened your beautifully designed app on a tablet only to see it looking like a stretched-out phone screen, you know the pain. Or maybe you’ve watched your carefully crafted layout break apart on a foldable device. Trust me, I’ve been there. The good news..? Responsive and Adaptive UI in Jetpack Compose makes...

Membership Required

You must be a member to access this content.

View Membership Levels

Already a member? Log in here
onValueChange = { value = it }

Understanding onValueChange = { value = it } in Jetpack Compose

Jetpack Compose introduces a very different mental model compared to XML-based Android UI. One line that often confuses beginners (and even experienced Android devs at first) is:

Kotlin
onValueChange = { value = it }

Especially when value is defined like this:

Kotlin
var value by remember { mutableStateOf(0) }

At first glance, this line looks almost too simple — and that’s exactly why it’s confusing. 

Let’s break down what’s really happening, why it’s written this way, and how it fits into Compose’s state-driven architecture.

The Big Picture: Compose Is State-Driven

Before diving into syntax, it’s important to understand how Compose thinks.

In classic Android:

  • You updated UI elements directly
  • UI held its own state
  • You manually synced UI ↔ data

In Jetpack Compose:

  • State owns the UI
  • UI is a function of state
  • When state changes → UI recomposes automatically

This single line:

Kotlin
onValueChange = { value = it }

is the bridge between user interaction and state updates.

What remember { mutableStateOf(...) } Really Does

Consider this state declaration:

Kotlin
var value by remember { mutableStateOf(0) }

This does three important things:

1. mutableStateOf

Creates an observable state holder.
Compose watches this value and tracks where it’s used.

2. remember

Ensures the state survives re-composition.
Without remember, the value would reset every time Compose redraws the UI.

3. by keyword

This is Kotlin property delegation. It allows you to write:

Kotlin
value = 5

instead of:

Kotlin
value.value = 5

So value behaves like a normal variable, but Compose is quietly observing it.

What onValueChange Is (Conceptually)

Most interactive Compose components (such as TextField, Slider, Checkbox) follow the same pattern:

Kotlin
Component(
    value = currentState,
    onValueChange = { /* update state */ }
)

This is intentional and consistent.

onValueChange is:

  • A callback function
  • Triggered every time the user interacts
  • Passed the new value as a parameter

Compose itself does not store the value internally.
You are responsible for updating the state.

Breaking Down { value = it }

Let’s rewrite the lambda in a more explicit way:

Kotlin
onValueChange = { newValue ->
    value = newValue
}

Now it’s clearer.

  • it (or newValue) is the latest value from the UI
  • value = it updates your state
  • Updating state triggers recomposition

This is not assigning a random variable — it’s updating the single source of truth.

How the Data Flow Actually Works

Here’s the real flow behind the scenes:

  1. User interacts with the UI (types text, drags slider, etc.)
  2. Compose calls onValueChange(newValue)
  3. You update state (value = newValue)
  4. Compose detects the state change
  5. Any composables reading value recompose automatically

This is called unidirectional data flow, and it’s a core Compose principle.

Kotlin
State → UI<br>UI interaction → Callback → State update → Recomposition

Simple Example with TextField

Kotlin
@Composable
fun NameInput() {
    var name by remember { mutableStateOf("") }

    TextField(
        value = name,
        onValueChange = { name = it },
        label = { Text("Enter your name") }
    )
}

Here,

  • name controls what the TextField displays
  • Typing triggers onValueChange
  • The new text is assigned to name
  • The TextField redraws with updated text

If you remove onValueChange, the field becomes read-only.

Why Compose Doesn’t Update the Value Automatically

This design is intentional.

Compose avoids hidden internal state because:

  • It prevents bugs
  • It makes UI predictable
  • It improves testability
  • It aligns with modern architecture (MVI, Redux-style patterns)

You always know where your data lives.

Common Beginner Mistakes

Forgetting to update state

Kotlin
onValueChange = { }

Result: UI never changes.

Not using remember

Kotlin
var value by mutableStateOf(0)

Result: Value resets on every recomposition.

Expecting Compose to “save” the value

Compose renders, it doesn’t store business state. That’s your job (or ViewModel’s).

Why This Pattern Is So Powerful

Once you understand this line, you understand 50% of Compose.

It enables:

  • Clean separation of UI and state
  • Easy state hoisting
  • Predictable recomposition
  • Seamless ViewModel integration

Example with state hoisting:

Kotlin
@Composable
fun Counter(value: Int, onValueChange: (Int) -> Unit) {
    Slider(
        value = value.toFloat(),
        onValueChange = { onValueChange(it.toInt()) }
    )
}

Now the parent owns the state — not the UI.

Final Mental Model (Remember This)

Compose does not mutate UI.
Compose reacts to state changes.

And this line:

JavaScript
onValueChange = { value = it }

is simply saying:

“When the user changes something, update my state — and let Compose handle the rest.”

Once this clicks, Jetpack Compose stops feeling confusing and starts feeling refreshingly simple..!

State Management in Jetpack Compose

Modern State Management in Jetpack Compose: Flows, Side Effects, and UI State

State management is the backbone of any modern Android app. If state is messy, your UI becomes unpredictable, buggy, and hard to maintain. Jetpack Compose was designed to solve many of these problems, but only if you understand how State Management in Jetpack Compose actually works. In this post, we will break down modern state...

Membership Required

You must be a member to access this content.

View Membership Levels

Already a member? Log in here
Composition Over Inheritance

What Is Composition Over Inheritance? The Built-In Compose Way Explained

If you’ve been writing object-oriented code for a while, you’ve probably used inheritance a lot. It feels natural. You create a base class, extend it, override a few methods, and move on.

But as projects grow, inheritance often becomes hard to manage. Classes get tightly coupled. Changes ripple through the codebase. Small tweaks break unexpected things.

This is where Composition Over Inheritance comes in.

In this post, we’ll break down what Composition Over Inheritance really means, why it matters, and how it’s used naturally in modern Kotlin development, especially with Jetpack Compose. 

What Does “Composition Over Inheritance” Mean?

Composition Over Inheritance is a design principle that says:

Prefer building classes by combining smaller, reusable components instead of extending base classes.

In simpler terms:

  • Inheritance says “is a”
  • Composition says “has a”

Instead of forcing behavior through class hierarchies, you compose behavior by using other objects.

A Simple Real-World Example

Think of a smartphone.

A smartphone has a camera, battery, speaker, and screen.

It does not inherit from Camera, Battery, or Speaker.

That’s composition.

If you used inheritance here, the design would fall apart fast.

The Problem With Inheritance

Inheritance looks clean at first, but it comes with hidden costs.

Example Using Inheritance (Problematic)

Kotlin
open class Vehicle {
    open fun move() {
        println("Vehicle is moving")
    }
}

open class Car : Vehicle() {
    override fun move() {
        println("Car is driving")
    }
}

class ElectricCar : Car() {
    override fun move() {
        println("Electric car is driving silently")
    }
}

At first glance, this seems fine.

But now imagine:

  • You want a flying car
  • You want a boat-car
  • You want a self-driving electric truck

Your inheritance tree explodes.

Changes to Vehicle affect every subclass. You’re locked into decisions you made early, often before requirements were clear.

This is exactly what Composition Over Inheritance helps you avoid.

Composition Over Inheritance Explained With Kotlin

Let’s rewrite the same idea using composition.

Create Small, Focused Behaviors

Kotlin
interface Engine {
    fun move()
}
Kotlin
class GasEngine : Engine {
    override fun move() {
        println("Driving using gas engine")
    }
}
Kotlin
class ElectricEngine : Engine {
    override fun move() {
        println("Driving silently using electric engine")
    }
}

Each class has one clear responsibility.

Compose the Behavior

Kotlin
class Car(private val engine: Engine) {

    fun drive() {
        engine.move()
    }
}

Now the Car has an engine, instead of being forced into a rigid hierarchy.

Use It

Kotlin
fun main() {
    val electricCar = Car(ElectricEngine())
    electricCar.drive()

    val gasCar = Car(GasEngine())
    gasCar.drive()
}

Output:

Driving silently using electric engine
Driving using gas engine

This is Composition Over Inheritance in action.

Why Composition Over Inheritance Is Better

Here’s why modern Kotlin developers strongly prefer this approach.

1. Less Coupling

Your classes depend on interfaces, not concrete implementations.

2. Easier Changes

You can swap behaviors without rewriting class hierarchies.

3. Better Testability

You can inject fake or mock implementations easily.

4. Cleaner Code

Smaller classes. Clear responsibilities. Fewer surprises.

Composition Over Inheritance in Jetpack Compose

Jetpack Compose is built almost entirely on Composition Over Inheritance.

That’s not an accident.

Traditional UI (Inheritance-Heavy)

Kotlin
class CustomButton : Button {
    // override styles, behavior, states
}

This leads to rigid UI components that are hard to reuse.

Compose Way (Composition First)

Kotlin
@Composable
fun MyButton(
    text: String,
    onClick: () -> Unit
) {
    Button(onClick = onClick) {
        Text(text)
    }
}

Here’s what’s happening:

  • MyButton is not extending Button
  • It uses Button
  • Behavior is passed in, not inherited

This is Composition Over Inheritance at the UI level.

Why Compose Feels Easier to Work With

Compose avoids deep inheritance trees entirely.

Instead:

  • UI is built from small composable functions
  • Each function does one thing
  • You combine them like building blocks

That’s composition by design.

Delegation: Kotlin’s Built-In Support for Composition

Kotlin makes Composition Over Inheritance even easier with delegation.

Example Using Delegation

Kotlin
interface Logger {
    fun log(message: String)
}

class ConsoleLogger : Logger {
    override fun log(message: String) {
        println(message)
    }
}

class UserService(private val logger: Logger) : Logger by logger

Now UserService automatically uses ConsoleLogger’s implementation without inheritance.

This keeps your code flexible and clean.

When Should You Still Use Inheritance?

Inheritance is not evil. It’s just often overused.

Inheritance works best when:

  • There is a true “is-a” relationship
  • The base class is stable
  • You control both parent and child classes

If those conditions are missing, Composition Over Inheritance is usually the safer choice.

Conclusion

Let’s wrap it up.

  • Composition Over Inheritance means building behavior using objects, not class hierarchies
  • Kotlin makes composition easy with interfaces and delegation
  • Jetpack Compose is a real-world example of this principle done right
  • Composition leads to flexible, testable, and maintainable code

If you’re writing Kotlin today, especially with Compose, you’re already using Composition Over Inheritance whether you realized it or not.

And once you start designing with it intentionally, your code gets simpler, not harder.

ViewModel and rememberSaveable

The Truth About ViewModel and rememberSavable: Configuration Changes vs Process Death

If you’ve built Android apps with Jetpack Compose, you’ve probably run into the question: Should I use ViewModel or rememberSaveable? Both help you keep state alive, but they work very differently depending on what’s happening to your app — like when the screen rotates or when the system kills your process.

This post will break down ViewModel and rememberSaveable, explain when to use each, and show real code examples so it finally clicks.

The Basics: Why State Preservation Matters

On Android, your app doesn’t always stay alive. Two big events affect your app’s state:

  1. Configuration changes — like screen rotations, language changes, or switching dark mode. The activity is destroyed and recreated, but the process usually stays alive.
  2. Process death — when Android kills your app’s process (e.g., to reclaim memory) and later restores it when the user comes back.

If you don’t handle these correctly, your users lose whatever they were doing. That’s where ViewModel and rememberSaveable come in.

remember: The Starting Point in Compose

At the simplest level, you use remember in Jetpack Compose to keep state alive across recompositions.

Kotlin
@Composable
fun CounterScreen() {
    var count by remember { mutableStateOf(0) }

    Button(onClick = { count++ }) {
        Text("Count: $count")
    }
}
  • Here, count won’t reset when Compose redraws the UI.
  • But if the device rotates (configuration change), the state is lost because remember only survives recompositions, not activity recreation.

That’s why we need more powerful tools.

rememberSaveable: Survives Configuration Changes and Process Death

rememberSaveable goes one step further. It automatically saves your state into a Bundle using Android’s saved instance state mechanism.

Kotlin
@Composable
fun CounterScreen() {
    var count by rememberSaveable { mutableStateOf(0) }

    Button(onClick = { count++ }) {
        Text("Count: $count")
    }
}

What happens here:

  • Rotate the screen? count survives.
  • App is killed and restored (process death)? count also survives, because it was written to the saved instance state.

Limitations:

  • Only works with data types that can be written to a Bundle (primitives, Strings, parcelables, etc.).
  • Not ideal for large objects or data fetched from a repository.

ViewModel: Survives Configuration Changes, Not Process Death

A ViewModel is a lifecycle-aware container designed to hold UI data. It’s tied to a LifecycleOwner like an activity or a navigation back stack entry.

Kotlin
class CounterViewModel : ViewModel() {
    var count by mutableStateOf(0)
}

@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
    Button(onClick = { viewModel.count++ }) {
        Text("Count: ${viewModel.count}")
    }
}

What happens here:

  • Rotate the screen? count survives. The same ViewModel instance is reused.
  • App is killed (process death)? count is lost. The ViewModel does not persist beyond process death.

Configuration Changes vs Process Death: Who Wins?

Here’s the clear breakdown:

When to Use rememberSaveable

Use rememberSaveable for small, lightweight UI state that:

  • Must survive both rotation and process death.
  • Can easily be serialized into a Bundle.

Examples:

  • Current tab index.
  • Form text fields.
  • Simple filter/sort options.

When to Use ViewModel

Use ViewModel for more complex or long-lived state that:

  • Doesn’t need to survive process death.
  • Might involve business logic, repositories, or data streams.
  • Should be scoped to the screen or navigation graph.

Examples:

  • Data loaded from a database or network.
  • Complex business logic.
  • State shared across multiple composables in the same screen.

Can You Combine Them? Yes.

Often, the best solution is to use ViewModel and rememberSaveable together.
 For example, a ViewModel manages your main UI state, but a few critical fields use rememberSaveable so they’re restored even after process death.

Kotlin
@Composable
fun FormScreen(viewModel: FormViewModel = viewModel()) {
    var userInput by rememberSaveable { mutableStateOf("") }

    Column {
        TextField(
            value = userInput,
            onValueChange = { userInput = it }
        )

        Button(onClick = { viewModel.submit(userInput) }) {
            Text("Submit")
        }
    }
}

Here:

  • userInput is lightweight and saved with rememberSaveable.
  • The ViewModel takes care of processing and persisting the submitted data.

Conclusion

The truth about ViewModel and rememberSaveable is simple once you think in terms of configuration changes vs process death:

  • remember → Only survives recomposition.
  • rememberSaveable → Survives both rotation and process death (small, serializable state).
  • ViewModel → Survives rotation, great for business logic, but not process death.

Use them in combination, not competition. Each tool has its place, and knowing when to reach for which makes your Compose apps more resilient, smoother, and user-friendly.

error: Content is protected !!