This repository showcases various design patterns implemented in Jetpack Compose, highlighting their use cases and integration with modern Android development.
Grounded in available product and source data
Project maintained by @Cavin
Note
This site outlines how to apply these design patterns to the creation and population of composables in a Jetpack Compose or Compose Multiplatform project.
Creational Patterns
Structural Patterns
Behavioral Patterns
A factory design pattern is a generative design pattern that helps to abstract how an object is created. This makes your code more flexible and extensible.
The basic idea of the factory design pattern is to delegate object creation to a factory class. This factory class determines which object is created.
Sample Scenario
Here's a real-world example of the Factory Design Pattern in Jetpack Compose, focusing on a scenario where you want to implement different card layouts for displaying various types of content in a news application:
Scenario: You have a news app where each news item can be displayed in several formats like ' simple' (only text), 'rich' (with image), or 'interactive' (includes interactive elements like a poll).
interface NewsCardFactory { @Composable fun CreateCard(newsItem: NewsItem)
} data class NewsItem(val title: String, val content: String, val imageUrl: String?)
class SimpleCardFactory : NewsCardFactory { @Composable override fun CreateCard(newsItem: NewsItem) { Card( modifier = Modifier .fillMaxWidth() .padding(8.dp), shape = RoundedCornerShape(8.dp) ) { Column( modifier = Modifier.padding(16.dp) ) { Text(newsItem.title, style = MaterialTheme.typography.titleSmall) Text(newsItem.content, style = MaterialTheme.typography.bodyMedium) } } }
}
class RichCardFactory : NewsCardFactory { @Composable override fun CreateCard(newsItem: NewsItem) { Card( modifier = Modifier .fillMaxWidth() .padding(8.dp), shape = RoundedCornerShape(8.dp) ) { Column(modifier = Modifier.padding(16.dp)) { newsItem.imageUrl?.let { url -> AsyncImage( model = url, contentDescription = null, contentScale = ContentScale.FillBounds, modifier = Modifier .fillMaxWidth() .height(200.dp) .clip(RoundedCornerShape(3.dp)) ) } Text(newsItem.title, style = MaterialTheme.typography.titleSmall) Text(newsItem.content, style = MaterialTheme.typography.bodyMedium) } } }
}
enum class CardType { SIMPLE, RICH } @Composable
fun CardFactoryProvider( cardType: CardType = CardType.SIMPLE, content: @Composable () -> Unit
) { val factory = when (cardType) { CardType.SIMPLE -> SimpleCardFactory() CardType.RICH -> RichCardFactory() } CompositionLocalProvider(LocalCardFactory provides factory) { content() }
} private val LocalCardFactory = staticCompositionLocalOf<NewsCardFactory> { SimpleCardFactory() // Default
}
@Composable
fun NewsFeed( newsItems: List<NewsItem>, modifier: Modifier,
) { var selectedCardType by remember { mutableStateOf(CardType.SIMPLE) } CardFactoryProvider(cardType = selectedCardType) { // Or dynamically choose based on item type LazyColumn(modifier = modifier) { item { Row(modifier = Modifier.padding(8.dp)) { Button(onClick = { selectedCardType = CardType.SIMPLE }) { Text("Simple Card") } Spacer(modifier = Modifier.width(8.dp)) Button(onClick = { selectedCardType = CardType.RICH }) { Text("Rich Card") } } } items(newsItems) { item -> val cardFactory = LocalCardFactory.current cardFactory.CreateCard(item) } } }
}
This approach allows for a flexible and extensible design where new types of cards can be added by creating new factories without altering existing code that uses these cards. It uses the Factory Pattern to manage the creation of different UI components based on the type of news item, enhancing modularity and maintainability of the UI.
Back to the beginning of the documentation
The abstract factory design pattern uses a factory class to create objects from multiple families. This pattern abstracts the object creation process, making your code more readable and flexible.
Sample Scenario
ThemeComponentsFactory (Abstract Factory)
├── LightThemeFactory
└── DarkThemeFactory ├── ThemeButton │ ├── LightThemeButton │ └── DarkThemeButton └── ThemeCard ├── LightThemeCard └── DarkThemeCard
Define interfaces for themed components:
interface ThemeButton { @Composable fun Create(onClick: () -> Unit, content: @Composable () -> Unit)
} interface ThemeCard { @Composable fun Create(content: @Composable () -> Unit)
}
Define the factory interface that creates themed components:
interface ThemeComponentsFactory { fun createButton(): ThemeButton fun createCard(): ThemeCard
}
Implement theme-specific factories:
class LightThemeFactory : ThemeComponentsFactory { override fun createButton(): ThemeButton = LightThemeButton() override fun createCard(): ThemeCard = LightThemeCard()
} class DarkThemeFactory : ThemeComponentsFactory { override fun createButton(): ThemeButton = DarkThemeButton() override fun createCard(): ThemeCard = DarkThemeCard()
}
@Composable
fun ThemeSwitchingApp() { var isDarkTheme by remember { mutableStateOf(false) } val themeFactory: ThemeComponentsFactory = if (isDarkTheme) { DarkThemeFactory() } else { LightThemeFactory() } Column( modifier = Modifier.fillMaxSize().padding(16.dp) ) { themeFactory.createButton().Create( onClick = { isDarkTheme = !isDarkTheme } ) { Text("Switch Theme") } themeFactory.createCard().Create { Text("This is a themed card") } }
}
Return to the beginning of the documentation
Sample Scenario Imagine we want to manage a global theme configuration for an app, allowing access to the theme state from multiple places without passing it explicitly.
object ThemeConfig { private var darkModeEnabled: Boolean = false fun isDarkModeEnabled(): Boolean = darkModeEnabled fun toggleDarkMode() { darkModeEnabled = !darkModeEnabled }
}
@Composable
fun ThemeToggleButton() { val isDarkMode = remember { mutableStateOf(ThemeConfig.isDarkModeEnabled()) } Button(onClick = { ThemeConfig.toggleDarkMode() isDarkMode.value = ThemeConfig.isDarkModeEnabled() }) { Text(if (isDarkMode.value) "Switch to Light Mode" else "Switch to Dark Mode") }
} @Composable
fun AppContent() { val isDarkMode = ThemeConfig.isDarkModeEnabled() MaterialTheme(colorScheme = if (isDarkMode) darkColors() else lightColors()) { ThemeToggleButton() }
}
object keyword inherently implements the singleton pattern.object MySingleton { fun doSomething() { println("Singleton Instance") }
}
lazy delegate to create a singleton only when accessed for the first time.class MySingleton private constructor() { companion object { val instance: MySingleton by lazy { MySingleton() } }
}
class MySingleton private constructor() { companion object { @Volatile private var instance: MySingleton? = null fun getInstance(): MySingleton { return instance ?: synchronized(this) { instance ?: MySingleton().also { instance = it } } } }
}
Return to the beginning of the documentation
Sample Scenario
In Kotlin, data class provides a built-in copy() method that simplifies the implementation of
the Prototype Pattern. This is particularly useful when creating multiple variations of an object
with similar properties.
data class Document( val title: String, val content: String, val author: String )
@Composable
fun DocumentCard(document: AppDocument, modifier: Modifier = Modifier) { Card( modifier = modifier.padding(8.dp), shape = MaterialTheme.shapes.medium ) { Column(modifier = Modifier.padding(16.dp)) { Text(text = "Title: ${document.title}", style = MaterialTheme.typography.bodyLarge) Text(text = "Content: ${document.content}", style = MaterialTheme.typography.bodyMedium) Text(text = "Author: ${document.author}", style = MaterialTheme.typography.bodySmall) } }
}
@Composable
fun ProtoTypeView() { // Original prototype val originalDocument = AppDocument( title = "Prototype Pattern", content = "This is the original document content.", author = "John Doe" ) // Clone the prototype and modify properties val clonedDocument = originalDocument.copy( title = "Cloned Prototype", content = "This is the cloned document content." ) // UI Layout Column( modifier = Modifier .fillMaxSize() .padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { Text("Documents", style = MaterialTheme.typography.titleLarge) // Display Original Document DocumentCard(document = originalDocument, modifier = Modifier.fillMaxWidth()) // Display Cloned Document DocumentCard(document = clonedDocument, modifier = Modifier.fillMaxWidth()) }
}
Return to the beginning of the documentation
The Adapter pattern makes the interfaces of two different classes or interfaces similar to each other, allowing these classes or interfaces to be used together. In this way, it is possible to use an existing class or interface class in a new system or project without having to change or rewrite it.