The Modern Android Way
“Reusable UI is not just a coding habit — it’s a design philosophy that scales apps, teams, and ideas.”
As Android engineers, we often find ourselves solving the same UI problem over and over — a stylized button, a progress loader, a card with actions. In traditional XML days, we would duplicate layouts or inflate custom views.
But with Jetpack Compose, UI development is declarative, modular, and reactive — a perfect environment for reusable, scalable, and testable components.
In this article, I’ll walk through how to design custom reusable UI components using modern Compose patterns aligned with Clean Architecture and feature modularization — the same structure that powers enterprise-grade fintech apps.
Why Reusable UI Components Matter
Reusable UI is more than DRY (Don’t Repeat Yourself). It enables:
-
Consistency across the app’s look and feel.
-
Scalability, so new features integrate faster.
-
Testability, since each UI piece is independent.
-
Theming flexibility, to support brand-level customization.
-
Faster CI/CD, since UI updates are isolated to one module.
Modular Architecture Overview
Compose works beautifully with modularization.
Here’s a recommended structure for large apps:
app/
├── MainActivity.kt
├── navigation/
core/
├── designsystem/
│ ├── components/
│ │ ├── CustomButton.kt
│ │ ├── CustomProgress.kt
│ │ ├── CustomCard.kt
│ ├── theme/
│ ├── typography/
├── utils/
feature/
├── dashboard/
│ ├── ui/
│ ├── viewmodel/
│ ├── domain/
Rule of thumb:
-
core/designsystem
→ Your reusable UI kit (like Material 3 but customized). -
feature/*
→ Screens that consume those components. -
app
→ Wires navigation and dependency injection.
Step 1: Creating a Reusable Custom Button
Let’s start simple — a custom button that can be reused across modules.
@Composable
fun AppButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
backgroundColor: Color = MaterialTheme.colorScheme.primary,
textColor: Color = Color.White,
enabled: Boolean = true
) {
Button(
onClick = onClick,
modifier = modifier
.fillMaxWidth()
.height(56.dp),
enabled = enabled,
colors = ButtonDefaults.buttonColors(
containerColor = backgroundColor,
contentColor = textColor
),
shape = RoundedCornerShape(12.dp)
) {
Text(
text = text,
style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.Bold)
)
}
}
What makes this reusable
-
Parameterized:
text
,onClick
,color
,enabled
. -
Styled centrally using your theme.
-
No hardcoded logic — just a pure, stateless composable.
Step 2: Making It Themed & Adaptive
Your core/designsystem/theme
defines your app’s brand look:
@Composable
fun AppTheme(content: @Composable () -> Unit) {
MaterialTheme(
colorScheme = lightColorScheme(
primary = Color(0xFF1565C0),
secondary = Color(0xFF64B5F6)
),
typography = Typography,
content = content
)
}
Now, every AppButton
automatically aligns with the theme colors and typography across your app.
Step 3: Building a Custom Progress Indicator
Let’s add a reusable loading animation with Compose’s Canvas
:
@Composable
fun AppProgressBar(
progress: Float,
modifier: Modifier = Modifier,
color: Color = MaterialTheme.colorScheme.primary,
strokeWidth: Dp = 6.dp
) {
Canvas(modifier = modifier.size(100.dp)) {
val sweepAngle = 360 * progress
drawArc(
color = color,
startAngle = -90f,
sweepAngle = sweepAngle,
useCenter = false,
style = Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Round)
)
}
}
Reusable Advantage:
This progress bar can be dropped into any screen — dashboard loading, biometric scanning, or file uploads — without extra setup.
Step 4: Adding Motion and State
Composable components are state-driven. Let’s animate progress reactively:
@Composable
fun AnimatedProgress(targetValue: Float) {
val progress by animateFloatAsState(
targetValue = targetValue,
animationSpec = tween(1500)
)
AppProgressBar(progress = progress)
}
Every change to targetValue
triggers an animation — clean, declarative, and side-effect-safe.
Step 5: Composable Composition
Compose encourages composition over inheritance. You can easily compose smaller reusable elements:
@Composable
fun AppCard(
title: String,
subtitle: String,
icon: ImageVector,
onClick: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.surfaceVariant)
.clickable { onClick() }
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(icon, contentDescription = null, tint = MaterialTheme.colorScheme.primary)
Spacer(Modifier.width(12.dp))
Column {
Text(title, style = MaterialTheme.typography.titleMedium)
Text(subtitle, style = MaterialTheme.typography.bodySmall)
}
}
}
Now your feature modules can combine AppCard
, AppButton
, and AppProgressBar
to build complex UIs effortlessly.
Step 6: Integrating Reusable Components in Features
In your feature module (e.g., Dashboard):
@Composable
fun DashboardScreen(viewModel: DashboardViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsState()
when (uiState) {
is UiState.Loading -> AnimatedProgress(0.7f)
is UiState.Success -> AppCard(
title = "Balance",
subtitle = "$12,450.00",
icon = Icons.Default.AttachMoney,
onClick = { /* Navigate */ }
)
is UiState.Error -> AppButton("Retry", onClick = viewModel::fetchData)
}
}
The result: clean, modular, theme-aware UIs that require no repetitive logic.
Step 7: Accessibility and Testing
Accessibility should never be an afterthought. Compose makes it simple:
Modifier.semantics {
contentDescription = "Loading ${progress * 100}%"
}
For testing:
@get:Rule val composeTestRule = createComposeRule()
@Test
fun testButtonDisplaysText() {
composeTestRule.setContent { AppButton("Submit", onClick = {}) }
composeTestRule.onNodeWithText("Submit").assertExists()
}
Design System Philosophy
Think of your core/designsystem
as a mini Material library for your brand.
When a designer updates the theme or typography, every screen reflects it automatically.
That’s how large teams scale UI with confidence — Compose turns UI consistency into an architectural feature.
Key Takeaways
Principle | Description |
---|---|
Stateless Composables | Avoid internal logic; take data via parameters. |
Theming | Define global color, shape, and typography systems. |
Composition > Inheritance | Build larger components from smaller ones. |
Accessibility | Always use semantics for TalkBack support. |
Testing | Use ComposeTestRule for UI validation. |
Modularity | Keep your design system separate from features. |
My Thoughts
As Android engineers, our UI should evolve as fast as user expectations.
Jetpack Compose gives us the freedom to innovate — while modular architecture keeps our codebase structured and scalable.
Reusable UI isn’t just a best practice; it’s a strategy for sustainable growth in modern Android apps.
“Compose taught us that great UIs aren’t built — they’re composed.”
📢 Feedback: Did you find this article helpful? Let me know your thoughts or suggestions for improvements! Please leave a comment below. I’d love to hear from you! 👇
Happy coding! 💻
0 comments:
Post a Comment