When to Create Custom Components

Create Custom Components When:

  • You find yourself repeating the same UI code multiple times
  • You want to ensure consistent styling across your app
  • You need to encapsulate complex UI logic
  • You want to make your code more readable and maintainable
  • You're building a design system for your app
  • You need to create reusable UI patterns

Benefits of Custom Components

  • Reduces code duplication and makes maintenance easier
  • Ensures consistent design across your app
  • Makes your code more readable and organized
  • Allows for easy updates and changes
  • Improves testing and debugging
  • Makes your app more modular and scalable

Common Component Patterns

Pattern What It Does When to Use It
@Composable Function Creates a reusable UI component For any custom UI element
Parameters Makes components configurable When you need different variations
Modifier Parameter Allows external styling customization When you want flexible styling
State Management Handles component's internal state For interactive components

Practical Examples

Simple Custom Button
@Composable
fun CustomButton(
    text: String,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true
) {
    Button(
        onClick = onClick,
        modifier = modifier,
        enabled = enabled,
        colors = ButtonDefaults.buttonColors(
            containerColor = MaterialTheme.colorScheme.primary,
            contentColor = MaterialTheme.colorScheme.onPrimary
        ),
        shape = RoundedCornerShape(8.dp)
    ) {
        Text(
            text = text,
            style = MaterialTheme.typography.labelLarge
        )
    }
}

// Using the custom button
CustomButton(
    text = "Click Me",
    onClick = { /* Handle click */ },
    modifier = Modifier.padding(16.dp)
)

This creates a custom button with consistent styling. You can use it throughout your app to ensure all buttons look the same. The parameters make it flexible for different use cases.

Custom Card Component
@Composable
fun InfoCard(
    title: String,
    description: String,
    icon: ImageVector,
    onClick: (() -> Unit)? = null,
    modifier: Modifier = Modifier
) {
    Card(
        modifier = modifier
            .fillMaxWidth()
            .then(
                if (onClick != null) {
                    Modifier.clickable { onClick() }
                } else {
                    Modifier
                }
            ),
        elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
    ) {
        Row(
            modifier = Modifier.padding(16.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Icon(
                imageVector = icon,
                contentDescription = null,
                tint = MaterialTheme.colorScheme.primary,
                modifier = Modifier.size(24.dp)
            )
            
            Spacer(modifier = Modifier.width(16.dp))
            
            Column {
                Text(
                    text = title,
                    style = MaterialTheme.typography.titleMedium
                )
                Text(
                    text = description,
                    style = MaterialTheme.typography.bodyMedium,
                    color = MaterialTheme.colorScheme.onSurfaceVariant
                )
            }
        }
    }
}

// Using the custom card
InfoCard(
    title = "Weather",
    description = "Partly cloudy, 72°F",
    icon = Icons.Default.Cloud,
    onClick = { /* Navigate to weather */ }
)

This creates a reusable card component with an icon, title, and description. The optional onClick parameter makes it flexible for both clickable and non-clickable cards.

Custom Loading Component
@Composable
fun LoadingSpinner(
    message: String = "Loading...",
    modifier: Modifier = Modifier
) {
    Column(
        modifier = modifier,
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        CircularProgressIndicator(
            modifier = Modifier.size(48.dp),
            color = MaterialTheme.colorScheme.primary
        )
        
        Spacer(modifier = Modifier.height(16.dp))
        
        Text(
            text = message,
            style = MaterialTheme.typography.bodyLarge,
            color = MaterialTheme.colorScheme.onSurfaceVariant
        )
    }
}

// Using the loading component
LoadingSpinner(
    message = "Fetching data...",
    modifier = Modifier.fillMaxSize()
)

This creates a reusable loading component that you can use anywhere in your app. It provides consistent loading feedback with customizable messages.

Custom Form Input Component
@Composable
fun CustomTextField(
    value: String,
    onValueChange: (String) -> Unit,
    label: String,
    modifier: Modifier = Modifier,
    isError: Boolean = false,
    errorMessage: String? = null,
    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
    singleLine: Boolean = true
) {
    Column(modifier = modifier) {
        OutlinedTextField(
            value = value,
            onValueChange = onValueChange,
            label = { Text(label) },
            isError = isError,
            keyboardOptions = keyboardOptions,
            singleLine = singleLine,
            modifier = Modifier.fillMaxWidth()
        )
        
        if (isError && errorMessage != null) {
            Text(
                text = errorMessage,
                color = MaterialTheme.colorScheme.error,
                style = MaterialTheme.typography.bodySmall,
                modifier = Modifier.padding(start = 16.dp, top = 4.dp)
            )
        }
    }
}

// Using the custom text field
var email by remember { mutableStateOf("") }
var emailError by remember { mutableStateOf(false) }

CustomTextField(
    value = email,
    onValueChange = { email = it },
    label = "Email",
    isError = emailError,
    errorMessage = if (emailError) "Please enter a valid email" else null,
    keyboardOptions = KeyboardOptions(
        keyboardType = KeyboardType.Email
    )
)

This creates a custom text field component with built-in error handling. It includes validation feedback and can be easily reused throughout your app.

Complex Custom Component with State
@Composable
fun ExpandableCard(
    title: String,
    content: String,
    modifier: Modifier = Modifier
) {
    var expanded by remember { mutableStateOf(false) }
    
    Card(
        modifier = modifier.fillMaxWidth(),
        elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Row(
                modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.SpaceBetween,
                verticalAlignment = Alignment.CenterVertically
            ) {
                Text(
                    text = title,
                    style = MaterialTheme.typography.titleMedium
                )
                
                IconButton(onClick = { expanded = !expanded }) {
                    Icon(
                        imageVector = if (expanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
                        contentDescription = if (expanded) "Collapse" else "Expand"
                    )
                }
            }
            
            AnimatedVisibility(
                visible = expanded,
                enter = expandVertically() + fadeIn(),
                exit = shrinkVertically() + fadeOut()
            ) {
                Text(
                    text = content,
                    style = MaterialTheme.typography.bodyMedium,
                    modifier = Modifier.padding(top = 8.dp)
                )
            }
        }
    }
}

// Using the expandable card
ExpandableCard(
    title = "How to use this app",
    content = "This app helps you organize your tasks and stay productive. Tap the + button to add new tasks, and swipe to delete completed ones."
)

This creates a complex component with internal state management. It handles its own expansion/collapse behavior and includes smooth animations.

Creating a Component Library
// Create a file called MyComponents.kt
object MyComponents {
    @Composable
    fun PrimaryButton(
        text: String,
        onClick: () -> Unit,
        modifier: Modifier = Modifier
    ) {
        Button(
            onClick = onClick,
            modifier = modifier,
            colors = ButtonDefaults.buttonColors(
                containerColor = MaterialTheme.colorScheme.primary
            )
        ) {
            Text(text)
        }
    }
    
    @Composable
    fun SecondaryButton(
        text: String,
        onClick: () -> Unit,
        modifier: Modifier = Modifier
    ) {
        OutlinedButton(
            onClick = onClick,
            modifier = modifier
        ) {
            Text(text)
        }
    }
    
    @Composable
    fun InfoBox(
        title: String,
        message: String,
        modifier: Modifier = Modifier
    ) {
        Card(
            modifier = modifier,
            colors = CardDefaults.cardColors(
                containerColor = MaterialTheme.colorScheme.surfaceVariant
            )
        ) {
            Column(modifier = Modifier.padding(16.dp)) {
                Text(
                    text = title,
                    style = MaterialTheme.typography.titleSmall
                )
                Text(
                    text = message,
                    style = MaterialTheme.typography.bodyMedium
                )
            }
        }
    }
}

// Using components from the library
MyComponents.PrimaryButton(
    text = "Save",
    onClick = { /* Save action */ }
)

MyComponents.InfoBox(
    title = "Note",
    message = "Your changes have been saved successfully."
)

This shows how to create a component library that organizes all your custom components in one place. This makes it easy to maintain consistency and reuse components across your app.