Android Development: Building a Custom Design System with Jetpack Compose

Alexandros Damianakis
4 min readMay 30, 2024

--

Introduction

Jetpack Compose revolutionizes Android UI development with its declarative approach, enabling developers to build engaging and dynamic user interfaces effortlessly. In this article, we’ll explore the process of creating a custom design system using Jetpack Compose, empowering developers to craft visually stunning and consistent UIs tailored to their app’s needs.

Embracing Custom Implementations over Material Design

While Material Design offers a comprehensive set of components and styles, opting for custom implementations provides several advantages. Custom solutions are tailored to your project’s requirements, reduce dependency footprints, enhance maintainability, and offer greater flexibility and extensibility.

Step-by-Step Guide

1. Setting Up Your Project

To begin, create a new Android project in Android Studio and select the “Empty Compose Activity” template. Configure your project’s `build.gradle` files with the necessary dependencies for Jetpack Compose. This foundational step establishes the groundwork for building your custom design system.

2. Defining Colors

Define your color palette in a `Color.kt` file, specifying both light and dark theme colors. This ensures consistency and coherence across your app’s UI elements.

// Color.kt
package com.example.yourapp.ui.theme

import androidx.compose.ui.graphics.Color

// Light theme Colors
val purple500 = Color(0xFF9F3DE6)
val white = Color(0xFFFFFFFF)
val red500 = Color(0xFFCC3746)
val gray200 = Color(0XFFEAEEFA)
val gray800 = Color(0XFF414042)

// Dark Theme Colors
val purple700 = Color(0xFFBB86FC)
val gray800_2 = Color(0XFF414042)
val teal200 = Color(0xFF03DAC6)
val gray200_2 = Color(0XFFEAEEFA)

3. Defining Color Schemes

Create a `ColorScheme.kt` file to define light and dark color schemes. These schemes encapsulate your app’s primary and secondary colors, ensuring uniformity in design.

// ColorScheme.kt
package com.example.yourapp.ui.theme

import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color

data class AppColorScheme(
val background: Color,
val onBackground: Color,
val primary: Color,
val onPrimary: Color,
val secondary: Color,
val onSecondary: Color
)

val darkColorScheme = AppColorScheme(
background = gray800,
onBackground = white,
primary = purple700,
onPrimary = gray800_2,
secondary = teal200,
onSecondary = gray800_2
)

val lightColorScheme = AppColorScheme(
background = gray200,
onBackground = gray800,
primary = purple500,
onPrimary = white,
secondary = red500,
onSecondary = white
)

val localAppColorScheme = staticCompositionLocalOf {
lightColorScheme // Default to light color scheme
}

4. Typography

Craft your typography styles in a `Typography.kt` file, specifying font families, weights, and sizes. Custom typography adds personality and readability to your app’s text elements.

Enhance your app’s typography by integrating custom fonts. Explore font repositories like Google Fonts, Adobe Fonts, and Font Squirrel to find the perfect typefaces. Download font files and implement them into your project’s resources. Ensure consistency and legibility in your typography for a cohesive user experience. The code snippet below uses the Handlee and Inter fonts to demonstrate this approach.

// Typography.kt
package com.example.yourapp.ui.theme

import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import com.example.yourapp.R

val Handlee = FontFamily(
Font(R.font.handleeregular, FontWeight.Normal)
)

val Inter = FontFamily(
Font(R.font.intermedium, FontWeight.Normal)
)

data class AppTypography(
val titleLarge: TextStyle,
val titleNormal: TextStyle,
val body: TextStyle,
val labelLarge: TextStyle,
val labelNormal: TextStyle,
val labelSmall: TextStyle
)

val localAppTypography = staticCompositionLocalOf {
AppTypography(
titleLarge = TextStyle(
fontFamily = Handlee,
fontWeight = FontWeight.Bold,
fontSize = 24.sp
),
titleNormal = TextStyle(
fontFamily = Handlee,
fontWeight = FontWeight.Normal,
fontSize = 24.sp
),
body = TextStyle(
fontFamily = Inter,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
),
labelLarge = TextStyle(
fontFamily = Inter,
fontWeight = FontWeight.Normal,
fontSize = 14.sp
),
labelNormal = TextStyle(
fontFamily = Inter,
fontWeight = FontWeight.Normal,
fontSize = 12.sp
),
labelSmall = TextStyle(
fontFamily = Inter,
fontWeight = FontWeight.Normal,
fontSize = 10.sp
)
)
}

5. Shapes

Define custom shapes for containers and buttons in a Shape.kt file. These shapes enhance visual appeal and provide a cohesive look and feel to your app's components.

// Shape.kt
package com.example.yourapp.ui.theme

import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.dp

data class AppShape(
val container: Shape = RoundedCornerShape(12.dp),
val button: Shape = RoundedCornerShape(50.dp)
)

val localAppShape = staticCompositionLocalOf {
AppShape(
container = RectangleShape,
button = RectangleShape
)
}

6. Sizes

Specify size dimensions for various UI elements in a Size.kt file. Consistent sizing ensures harmonious proportions and layout across different screen sizes.

// Size.kt
package com.example.yourapp.ui.theme

import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

data class AppSize(
val large: Dp = 24.dp,
val medium: Dp = 16.dp,
val small: Dp = 14.dp
)

val localAppSize = staticCompositionLocalOf {
AppSize(
large = Dp.Unspecified,
medium = Dp.Unspecified,
small = Dp.Unspecified
)
}

7. Theme

Create a custom theme that integrates your defined colors, typography, shapes, and sizes. Use a CompositionLocalProvider to propagate these theme values throughout your app.

// MyCustomAppTheme.kt
package com.example.yourapp.ui.theme

import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.tooling.preview.Preview

@Composable
fun MyCustomAppTheme(
isDarkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = if (isDarkTheme) darkColorScheme else lightColorScheme
CompositionLocalProvider(
localAppColorScheme provides colorScheme,
localAppTypography provides typography,
localAppShape provides shape,
localAppSize provides size,
content = content
)
}

object MyCustomAppTheme {
val colorScheme: AppColorScheme
@Composable get() = localAppColorScheme.current

val typography: AppTypography
@Composable get() = localAppTypography.current

val shape: AppShape
@Composable get() = localAppShape.current

val size: AppSize
@Composable get() = localAppSize.current
}

8. Applying the Theme

Wrap your application content in the custom theme.

// MainActivity
package com.example.yourapp

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.example.yourapp.ui.theme.MyCustomAppTheme

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyCustomAppTheme {
// Your app content
}
}
}
}

9. Creating Custom Components

Define reusable UI components that follow your design system.

// CustomText.kt
package com.example.yourapp.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.example.yourapp.ui.theme.MyCustomAppTheme

@Composable
fun CustomText(text: String) {
BasicText(
text = text,
style = MyCustomAppTheme.typography.body,
modifier = Modifier
.background(MyCustomAppTheme.colorScheme.background)
.padding(MyCustomAppTheme.size.medium)
)
}

Conclusion

Adopting a Custom Design System with Jetpack Compose streamlines the development process by providing a unified framework for creating visually consistent user interfaces. Defining colors, typography, shapes, and sizes early on ensures efficiency and flexibility throughout development. With custom implementations and unique fonts, developers can tailor the user experience precisely to their app’s requirements, accelerating development cycles and enhancing user satisfaction on Android platforms.

--

--