Compose Multiplatform: Design Systems with Jetpack Compose

Creating a robust and consistent user interface across multiple platforms is a common challenge in modern software development. Kotlin Multiplatform (KMP) offers a way to share code across different platforms, and when combined with Jetpack Compose, it becomes a powerful tool for building cross-platform applications with shared UI components. This article explores how to create a multiplatform design system in Jetpack Compose.

What is a Multiplatform Design System?

A multiplatform design system is a set of UI components, guidelines, and tools that can be shared across various platforms, such as Android, iOS, Web, and Desktop. It ensures consistency in branding, user experience, and visual appearance, while also reducing development time and maintenance efforts.

Why Use Compose Multiplatform for a Design System?

  • Code Sharing: Kotlin Multiplatform allows you to share UI logic and components across platforms.
  • Consistency: Ensures a consistent look and feel across all platforms.
  • Efficiency: Reduces development time and effort by reusing code.
  • Maintainability: Simplifies maintenance and updates with a single source of truth for UI components.

Setting Up a Compose Multiplatform Project

Before diving into the design system, you need to set up a Kotlin Multiplatform project with Jetpack Compose support.

Step 1: Create a New Kotlin Multiplatform Project

You can create a new KMP project using the Kotlin Multiplatform wizard in IntelliJ IDEA or Android Studio.

Step 2: Configure Gradle Files

Ensure that your build.gradle.kts files are properly configured for each target platform (Android, iOS, Desktop, Web).

Step 3: Add Dependencies

Add necessary dependencies for Jetpack Compose in the commonMain source set to share the UI code.


dependencies {
    implementation(compose.ui)
    implementation(compose.material)
    implementation(compose.runtime)
    @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
    implementation(compose.components.resources)
    
    // Add platform-specific dependencies in respective source sets
    androidMainImplementation(compose.uiToolingPreview)
    desktopMainImplementation(compose.uiToolingPreview)
}

Building the Design System Components

The core of a multiplatform design system consists of reusable UI components, styles, and themes. Here’s how to create these components.

1. Defining Colors

Define a set of colors in a shared module (commonMain) using Kotlin:


import androidx.compose.ui.graphics.Color

object AppColors {
    val primary = Color(0xFF6200EE)
    val secondary = Color(0xFF3700B3)
    val background = Color(0xFFFFFFFF)
    val text = Color(0xFF000000)
}

2. Defining Typography

Similarly, define typography using Compose’s TextStyle:


import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp

object AppTypography {
    val h1 = TextStyle(
        fontWeight = FontWeight.Bold,
        fontSize = 32.sp,
        color = AppColors.text
    )
    val body1 = TextStyle(
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp,
        color = AppColors.text
    )
}

3. Creating a Theme

Use Compose’s MaterialTheme to create a consistent theme across platforms:


import androidx.compose.material.MaterialTheme
import androidx.compose.material.Typography
import androidx.compose.runtime.Composable

@Composable
fun AppTheme(content: @Composable () -> Unit) {
    MaterialTheme(
        colors = androidx.compose.material.lightColors(
            primary = AppColors.primary,
            secondary = AppColors.secondary,
            background = AppColors.background
        ),
        typography = Typography(
            h1 = AppTypography.h1,
            body1 = AppTypography.body1
        ),
        content = content
    )
}

4. Creating Reusable Components

Create reusable UI components that adhere to the design system’s guidelines. For example, a custom button:


import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun AppButton(text: String, onClick: () -> Unit) {
    Button(
        onClick = onClick,
        contentPadding = PaddingValues(horizontal = 20.dp, vertical = 12.dp)
    ) {
        Text(text = text)
    }
}

Platform-Specific Adaptations

While the goal is to share as much code as possible, some platform-specific adaptations might be necessary.

1. Platform-Specific Resources

Use resource files to handle platform-specific assets like images and icons. Kotlin Multiplatform supports resource management through platform-specific source sets.

2. Custom Renderers

For complex components, you might need to use custom renderers or platform-specific implementations to achieve the desired look and feel on each platform.

Example: Using the Design System in Different Platforms

Android

import androidx.compose.ui.platform.setContent
import androidx.compose.ui.window.application
import androidx.activity.ComponentActivity
import android.os.Bundle
import androidx.compose.material.Scaffold
import androidx.compose.material.TopAppBar

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            AppTheme {
                Scaffold(
                    topBar = { TopAppBar(title = { Text("My App") }) }
                ) {
                    AppButton(text = "Click Me", onClick = { /* Handle click */ })
                }
            }
        }
    }
}
Desktop

import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application

fun main() = application {
    Window(onCloseRequest = ::exitApplication, title = "Desktop App") {
        AppTheme {
            Scaffold(
                topBar = { TopAppBar(title = { Text("My App") }) }
            ) {
                AppButton(text = "Click Me", onClick = { /* Handle click */ })
            }
        }
    }
}

Best Practices for Compose Multiplatform Design Systems

  • Modularity: Break down the design system into smaller, reusable modules.
  • Versioning: Use semantic versioning to manage updates and changes to the design system.
  • Documentation: Provide clear documentation and examples for each component and guideline.
  • Testing: Implement comprehensive testing strategies to ensure the design system works correctly on all supported platforms.
  • Continuous Integration: Integrate the design system into your CI/CD pipeline for automated builds and tests.

Conclusion

Building a multiplatform design system with Compose Multiplatform is an effective way to create consistent and maintainable user interfaces across different platforms. By sharing UI components, styles, and themes, you can reduce development time, improve code quality, and ensure a unified user experience. While some platform-specific adaptations may be necessary, the benefits of code sharing and consistency make it a valuable approach for modern software development.