Atomic Design System in Jetpack Compose
Introduction
Welcome to Think-it's comprehensive guide on leveraging Jetpack Compose for creating an atomic design system in Android development. In this post, you'll learn how to build a scalable, maintainable design system leveraging the atomic design methodology. We'll cover everything from the basics of Jetpack Compose to implementing design tokens and assembling complete pages.
Overview of What You'll Learn
- Understand the key features and benefits of Jetpack Compose.
- Learn the principles of atomic design and how they apply to UI development.
- Gain hands-on experience in creating an atomic design system with Jetpack Compose.
- Discover best practices for maintaining consistency and scalability in your design system.
- Explore theming and customization techniques to match brand guidelines.
Introduction to Jetpack Compose
Jetpack Compose is a modern toolkit for building native Android UIs. It simplifies and accelerates UI development with less code, powerful tools, and intuitive Kotlin APIs. Jetpack Compose is ideal for creating dynamic and responsive user interfaces. As a modern UI toolkit, Jetpack Compose aligns with Think-it's commitment to innovative and sustainable technology solutions, enabling more efficient and maintainable code for Android applications.
Think-it's Approach to Jetpack Compose
At Think-it, we view Jetpack Compose as more than just a UI toolkit. It's a powerful tool for creating sustainable, efficient, and scalable Android applications. Our approach integrates ethical engineering practices with Jetpack Compose's capabilities, ensuring that the applications we develop not only meet technical requirements but also contribute to our clients' missions of solving global challenges.
Declarative UI:
Jetpack Compose follows a declarative paradigm, which means you describe what your UI should look like based on the current state of your app. Instead of imperatively specifying how to create UI elements (as in traditional imperative UI frameworks), you declare the desired UI structure and let Compose handle the rest.
For example, consider a simple Compose function that creates a button:
In this function, you declare a button with the specified text and an onClick
action. Compose takes care of rendering the button when needed.
@Composable
fun MyButton(text: String, onClick: () -> Unit) {
Button(onClick = onClick) {
Text(text)
}
}
Function-Based UI:
Compose encourages you to create UI components as functions. Each function represents a part of your UI hierarchy. These functions are composable—they can be combined and nested to build complex UIs. Compose automatically recomposes only the affected parts when the state changes. This approach simplifies UI development, as you don’t need to manage view hierarchies or worry about view recycling.
No XML Layouts:
Unlike traditional Android development, where you define UI layouts using XML files (e.g., activity_main.xml
), Compose allows you to create UI elements directly in Kotlin code. This eliminates the need for separate layout files, reduces boilerplate, and makes UI development more concise.
Reactive UI:
Compose reacts to changes in your app’s state. When the state changes (e.g., data updates, user interactions), Compose recomposes the relevant parts of the UI. You express UI components as functions of your app’s state, making it easy to keep the UI in sync with the data.
What is an Atomic Design System?
Atomic design is a methodology for creating design systems that are both flexible and robust. It breaks down interfaces into smaller components, which are then combined to form more complex structures. The five stages of atomic design are:
- Atoms: Basic building blocks (e.g., buttons, text fields).
- Molecules: Combinations of atoms (e.g., input field with label).
- Organisms: Groups of molecules and atoms forming distinct sections (e.g., navigation bar).
- Templates: Page-level components that define layout but not content.
- Pages: Templates with real content, representing the final UI.
Importance of Design Systems and Atomic Design Principles
Design systems play a crucial role in modern UI development by providing a cohesive and reusable set of design elements and guidelines. Atomic design principles further enhance this approach by breaking down the UI into smaller, manageable components. This not only promotes consistency but also makes it easier to scale and maintain your design system over time.
Benefits of Using Atomic Design in Modern UI Development
Consistency: Ensures a unified look and feel across the entire application. Reusability: Promotes the creation of reusable components, reducing duplication and development time. Scalability: Facilitates the growth of the design system as the application evolves. Maintainability: Simplifies the process of updating and maintaining UI components.
Atomic Design in Mobile Applications
Creating the Atomic Design System
Design Tokens
Design tokens are the single source of truth for UI styles, ensuring consistency across the design system. Common tokens include colors, typography, spacing, and icons.
Common Design Tokens
- Colors: Primary, secondary, background, text colors.
- Typography: Font families, sizes, weights.
- Spacing: Margins, padding, grid layout dimensions.
Example:
object DesignTokens {
val primaryColor = Color(0xFF6200EE)
val secondaryColor = Color(0xFF03DAC5)
val typography = Typography(
h1 = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 24.sp,
letterSpacing = 0.15.sp
),
body1 = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
letterSpacing = 0.5.sp
)
)
object Spacings {
val small = 8.dp
val tiny = 4.dp
val medium = 16.dp
val large = 24.dp
val huge = 32.dp
}
object Icons {
val home = R.drawable.ic_home
val search = R.drawable.ic_search
val login = R.drawable.ic_login
}
}
Atoms
Atoms are the smallest components in a design system. They are the building blocks for more complex elements.
Example:
@Composable
fun PrimaryButton(
@StringRes textResId: Int,
onClick: () -> Unit,
@DrawableRes tralingIconResId: Int? = null
) {
Button(
onClick = onClick,
modifier = Modifier.padding(
horizontal = DesignTokens.Spacings.large,
vertical = DesignTokens.Spacings.small
),
colors = ButtonDefaults.buttonColors(backgroundColor = DesignTokens.primaryColor),
) {
if(tralingIconResId !=null) {
Icon(
painter = painterResource(id = tralingIconResId),
contentDescription = null,
modifier = Modifier.padding(end = DesignTokens.Spacings.small)
)
}
Text(stringResource(id = textResId), style = DesignTokens.typography.button)
}
}
@Composable
fun PrimaryText(
@StringRes textResId: Int,
style: TextStyle = DesignTokens.typography.body1,
modifier: Modifier = Modifier
) {
Text(
text = stringResource(id = textResId),
style = style,
color = DesignTokens.primaryColor
modifier = modifier
)
}
@Composable
fun IconButton(@DrawableRes iconResId: Int, onClick: () -> Unit) {
IconButton(onClick = onClick) {
Icon(painterResource(id = iconResId), contentDescription = null)
}
}
Molecules
Molecules are combinations of atoms that form more complex UI elements. For example, an input field with a label can be considered a molecule.
Example:
@Composable
fun LabeledInputField(
@StringRes labelResId: Int,
value: String,
onValueChange: (String) -> Unit
) {
Column(modifier = Modifier.padding(DesignTokens.Spacings.medium)) {
CustomText(
textResId = labelResId,
style = DesignTokens.typography.body1,
modifier = Modifier.padding(bottom = DesignTokens.Spacings.tiny)
)
TextField(value = value, onValueChange = onValueChange)
}
}
Organisms
Organisms are more complex UI components composed of molecules and atoms. These components form significant parts of the UI, such as a login form or a navigation bar.
Example:
@Composable
fun LoginForm(onLoginClick: (String, String) -> Unit) {
var usernameValue by remember { mutableStateOf("") }
var passwordValue by remember { mutableStateOf("") }
Column(
modifier = Modifier
.padding(DesignTokens.Spacings.medium)
.fillMaxWidth()
) {
LabeledInputField(
labelResId = R.string.username,
value = usernameValue,
onValueChange = { usernameValue = it }
)
LabeledInputField(
labelResId = R.string.password,
value = passwordValue,
onValueChange = { passwordValue = it }
)
PrimaryButton(
textResId = R.string.login,
tralingIconResId = DesignTokens.Icons.login
onClick = {
onLoginClick(usernameValue, passwordValue)
}
)
}
}
Templates
Templates define the overall layout structure by combining various organisms.
Example:
@Composable
fun MainLayout(content: @Composable () -> Unit) {
Scaffold(
topBar = { TopAppBar(title = { Text("App Title") }) },
content = { content() }
)
}
Pages
Pages are the final UI screens that users interact with, created by combining templates and filling them with content.
Example:
@Composable
fun LoginPage() {
MainLayout {
LoginForm(onLoginClick = { /* Handle login */ })
}
}
Best Practices and Tips
Consistency and Scalability
Maintaining consistency is crucial for a scalable design system. Use design tokens to ensure uniform styles, and document your components for easier maintenance and onboarding.
Theming and Customization
Jetpack Compose makes it easy to implement themes and customize components to match brand guidelines.
Jetpack Compose with Material UI
-
Material Design Integration: Material Design is a design system developed by Google that provides guidelines for creating consistent, visually appealing, and user-friendly interfaces across platforms. Jetpack Compose seamlessly integrates with Material Design components. You can use Material components directly in your Compose UI code, ensuring a consistent look and feel. Material components provide pre-styled widgets like buttons, cards, text fields, and more. At Think-it, we leverage Material Design principles in Jetpack Compose to create sustainable and scalable UI solutions that align with our clients' brand identities while maintaining consistency and accessibility. By using them, you automatically follow Material Design principles without writing custom styles.
-
Custom Theming: Material Theming allows you to customize Material Design components to match your app’s brand identity. You can define your own color schemes, typography, and shapes. For example, you can create a custom theme that aligns with your brand colors.
Example:
@Composable
fun AppTheme(
content: @Composable () -> Unit
) {
MaterialTheme(
colorScheme = lightColorScheme(
primary = DesignTokens.primaryColor,
secondary = DesignTokens.secondaryColor
),
typography = DesignTokens.typography,
content = content
)
}
-
Dynamic Theming: Jetpack Compose makes it straightforward to support both light and dark themes. You can switch between themes dynamically based on user preferences or system settings. Material components automatically adapt to the selected theme, ensuring a consistent experience across different modes.
Example:
private val LightColorScheme = lightColorScheme(
primary = DesignTokens.primaryColor,
secondary = DesignTokens.secondaryColor,
// ..
)
private val DarkColorScheme = darkColorScheme(
primary = DesignTokens.primaryDarkColor,
secondary = DesignTokens.secondaryDarkColor,
// ..
)
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = if (!darkTheme) {
LightColorScheme
} else {
DarkColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = DesignTokens.typography,
content = content
)
}
Customizing Components to Match Brand Guidelines
Ensure your components adhere to brand guidelines by consistently applying design tokens and custom themes. Regularly update the design system to reflect any changes in the brand's visual identity.
Conclusion
In this guide, we've explored how to create an atomic design system using Jetpack Compose, from defining design tokens to assembling complete pages. Partner with Think-it to leverage our expertise in Jetpack Compose and atomic design systems, and see how we can enhance your development workflow while aligning with your sustainability goals.