Introduction to Kotlin DSL
Kotlin DSL (Domain-Specific Language) is a powerful feature of Kotlin that allows you to create declarative and intuitive APIs. It is widely used in the Kotlin ecosystem, with the Gradle Kotlin DSL being a classic example. With Kotlin DSL, you can transform complex configurations into simple and organized blocks.
But what if we could bring the same concept to Jetpack Compose? Imagine defining your UI components using a declarative and flexible approach. This article explores how to build a custom DSL for a Compose component, like CustomButton.
CustomButton
This article was originally published on Medium: Creating a Kotlin DSL for Jetpack Compose Components
Why Create a DSL for Compose Components?
The declarative approach of Jetpack Compose already makes UI definitions more intuitive. However, when dealing with reusable and highly configurable components, a DSL provides additional advantages:
- Clear Readability: Configurations are organized into logical blocks, making them easier to understand.
- Reusability: You can encapsulate repetitive logic and create simple APIs for reuse.
- Extensibility: Adding new configurations is straightforward and intuitive.
Let’s build a practical example to better understand how this works.
Building a DSL from Scratch
Defining the Configuration Class
First, we create a class to hold the configurations for our component:
class CustomButtonConfig {
var text: String = ""
var icon: ImageVector? = null
var onClick: (() -> Unit)? = null
var backgroundColor: Color = MaterialTheme.colors.primary
var textColor: Color = Color.White
val shapes = mutableListOf<ComposedShape>()
}
This class defines the properties that users can configure in the component, including composed shapes.
Creating the @DslMarker
Annotation
To isolate the scope of the DSL and avoid conflicts between different blocks, we create a custom annotation:
@DslMarker
annotation class CustomButtonDSL
We update the relevant classes to apply this annotation:
@CustomButtonDSL
class ComposedShape {
val shapes = mutableListOf<ShapeConfig>()
}
@CustomButtonDSL
sealed class ShapeConfig {
data class TriangleConfig(var base: Float, var height: Float, var color: Color) : ShapeConfig()
data class RhombusConfig(var sideLength: Float, var angle: Float, var color: Color) : ShapeConfig()
}
Creating DSL Functions for Shapes in the Button Context
We add functions to configure shapes within the button DSL:
fun CustomButtonConfig.composedShape(block: ComposedShape.() -> Unit) {
shapes.add(ComposedShape().apply(block))
}
fun ComposedShape.triangle(base: Float, height: Float, color: Color) {
shapes.add(ShapeConfig.TriangleConfig(base, height, color))
}
fun ComposedShape.rhombus(sideLength: Float, angle: Float, color: Color) {
shapes.add(ShapeConfig.RhombusConfig(sideLength, angle, color))
}
Updating the CustomButton
Component
We modify the customButton
function to render composed shapes:
@Composable
fun customButton(configure: CustomButtonConfig.() -> Unit) {
val config = CustomButtonConfig().apply(configure)
Button(
onClick = config.onClick ?: {},
colors = ButtonDefaults.buttonColors(backgroundColor = config.backgroundColor),
modifier = Modifier.padding(8.dp)
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Render Shapes
config.shapes.forEach { composedShape ->
composedShape.shapes.forEach { shape ->
when (shape) {
is ShapeConfig.TriangleConfig -> {
Text(
text = "\u25B2", // Triangle representation
color = shape.color,
modifier = Modifier.size(shape.base.dp, shape.height.dp)
)
}
is ShapeConfig.RhombusConfig -> {
Text(
text = "\u25C6", // Rhombus representation
color = shape.color,
modifier = Modifier.size(shape.sideLength.dp)
)
}
}
}
}
// Render Text and Icon
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
config.icon?.let {
Icon(imageVector = it, contentDescription = null, tint = config.textColor)
}
if (config.icon != null) Spacer(modifier = Modifier.width(8.dp))
Text(text = config.text, color = config.textColor)
}
}
}
}
Practical Examples
Example 1: Button with Triangle and Rhombus
customButton {
text = "With Shapes"
textColor = Color.Black
backgroundColor = Color.LightGray
onClick = { println("Button with shapes clicked!") }
composedShape {
triangle(base = 50f, height = 30f, color = Color.Red)
rhombus(sideLength = 40f, angle = 45f, color = Color.Blue)
}
}
Example 2: Simple Button
customButton {
text = "Click Here"
textColor = Color.Black
onClick = { println("Button clicked!") }
}
Example 3: Button with Icon
customButton {
text = "Favorite"
icon = Icons.Default.Favorite
textColor = Color.Black
onClick = { println("Favorite clicked!") }
}
Example 4: Button with Custom Style
customButton {
text = "Send"
icon = Icons.Default.Send
onClick = { println("Send clicked!") }
backgroundColor = Color.Green
textColor = Color.White
}
Benefits of Kotlin DSL
- Improved Readability: Configurations are organized into clear, logical blocks.
- Reduced Boilerplate: Less repetitive code, more focus on what matters.
- Extensibility: New features can be added to the DSL without altering existing code.
- Productivity: The declarative API speeds up development and enhances the developer experience.
Examples
Conclusion
Creating a Kotlin DSL for Compose components allows you to leverage the power of Kotlin to simplify and organize your UI in a declarative way. Besides making the code more readable, this approach promotes reusability and scalability of components.
Why not give it a try and create your own DSL? With Kotlin, the possibilities are endless. Let’s get to work!
Top comments (0)