Building Custom Reusable UI Components in Jetpack Compose

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! ๐Ÿ’ป

Functional vs Non-Functional Requirements in Modern Android App Development

A Senior Android Engineer’s Guide to Building Reliable, Scalable, and Delightful Apps

“Great apps don’t just work — they feel right.”



In modern Android app development, success isn’t measured only by what your app does (features), but also by how it performs, scales, and delights users.

That’s where Functional and Non-Functional Requirements (NFRs) come into play.

Functional requirements define what the system should do — the visible behaviors and actions.
Non-functional requirements define how the system should behave — the invisible qualities that separate a mediocre app from a world-class product.

Let’s explore both through real-world Android use cases, best practices, and architecture principles.


Functional Requirements — The “What”

These describe the core features and interactions users directly experience.
They define the app’s functional capabilities — tasks, data processing, business rules, and UI behaviors.

Examples in Android

  1. User Authentication

    • Sign-in with biometric, PIN, or OAuth (e.g., Google, Schwab Secure Login).

    • Use BiometricPrompt API and Jetpack Security for encryption.

  2. Data Fetching & Display

    • Fetch real-time stock prices using Retrofit + Coroutines + Flow.

    • Display data using Jetpack Compose with sealed UI states (Loading, Success, Error).

  3. Offline Mode

    • Cache the latest currency or weather data using Room or DataStore.

    • Sync changes when the device is online using WorkManager.

  4. Push Notifications

    • Implement FCM for trade alerts or balance updates.

    • Handle foreground and background states gracefully.

  5. Accessibility & Localization

    • Support TalkBack, font scaling, and dynamic color theming.

    • Provide localized strings (en, es, ne) with Android’s resources/values structure.

Best Practices

  • Follow MVVM Clean Architecture for modular, testable design.

  • Use sealed classes for predictable UI state management.

  • Use Kotlin Coroutines + Flow for structured concurrency and reactive data flow.

  • Validate business logic in the domain layer, not UI.


Non-Functional Requirements — The “How”

These define the quality attributes that make your app performant, secure, scalable, and user-friendly.

They often determine whether the app succeeds in real-world conditions.

Common Non-Functional Aspects

Category Description Android Example
Performance App speed, memory use, responsiveness Optimize recompositions in Jetpack Compose using remember and stable keys
Security Data protection, secure communication Use EncryptedSharedPreferences, TLS 1.3, certificate pinning
Scalability Ability to handle growing users/data Modular architecture + Repository pattern
Reliability Stability under various network or device conditions Use Retry strategies with Flow.retryWhen()
Usability UX clarity and accessibility Material 3 components, motion transitions, accessible color contrast
Maintainability Ease of code updates and testing Follow SOLID, Clean Architecture, Dependency Injection via Hilt
Compatibility Support for different Android API levels & devices Leverage backward compatibility libraries (AppCompat, Core KTX)
Observability Logs, crash monitoring, metrics Integrate Firebase Crashlytics, Macrobenchmark, or Perfetto

Use Case 1 — Mobile Banking App

Functional:

  • Biometric login

  • Fund transfer

  • Real-time transaction updates

Non-Functional:

  • End-to-end encryption (TLS + Keystore)

  • PCI-DSS compliance for card data

  • Low latency (under 200ms API response time)

  • Smooth UI transitions (Compose animation APIs)

Best Practice:

  • Use WorkManager for secure background synchronization.

  • Employ Hilt for dependency management across features.

  • Implement ProGuard & R8 for obfuscation.


Use Case 2 — Trading App (Thinkorswim-like Example)

Functional:

  • Real-time stock charts

  • Trade placement

  • Watchlist synchronization

Non-Functional:

  • High throughput WebSocket connections

  • Optimized rendering with custom Canvas charts

  • Security: Token encryption via Jetpack Security

  • Accessibility for visually impaired traders (TalkBack + custom semantics)

Best Practice:

  • Use Coroutines + Channels for WebSocket streams.

  • Employ Macrobenchmark to test frame drops during chart updates.

  • Build with modular architecture: core-network, core-ui, feature-trade, feature-watchlist.


Modern Best Practices for Balancing Both

Principle Description
Shift-Left Quality Consider NFRs early in the development cycle.
CI/CD Automation Integrate Lint, Detekt, Unit/UI tests in Jenkins or Bitrise.
Structured Concurrency Use viewModelScope and SupervisorJob for predictable task cancellation.
Jetpack Compose Performance Tuning Use derivedStateOf, LaunchedEffect, and smart recomposition strategies.
Monitoring & Observability Integrate Firebase Performance, ANR Watchdog, and custom metrics.

Final Thoughts

Functional requirements define your product’s purpose, while non-functional requirements define its quality and soul.

As a Senior Android Engineer, your mission is to deliver both:

  • Features that users love, and

  • Experiences that feel fast, safe, and delightful.

“An app may function well, but it only thrives when it’s performant, secure, and inclusive.”
— Dharma Kshetri


๐Ÿ“ข 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! ๐Ÿ’ป