Showing posts with label Jetpack Compose. Show all posts
Showing posts with label Jetpack Compose. Show all posts

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! 💻


Building a Scalable Architecture for an eCommerce App with Jetpack Compose

Design a scalable architecture for an eCommerce app built with Jetpack Compose. This architecture will support key features like offline product caching, real-time inventory updates, paginated product listings, and modular UI with feature separation. We’ll focus on best practices for scalability, maintainability, and modularity, ensuring the app can handle future growth efficiently.

Overview of the App Architecture

The architecture for this app will be based on Clean Architecture, separating concerns into Presentation, Domain, and Data layers. We will also modularize the app to ensure flexibility, and each feature (e.g., Product, Cart, Inventory) will be handled in a separate module.

We'll incorporate Jetpack Compose for UI, Room for offline caching, Paging 3 for efficient product listing, and Firebase/Realtime Database or WebSocket for real-time inventory updates.

Layered Architecture Breakdown

1. Presentation Layer (UI)

The Presentation Layer is responsible for the user interface and user interactions. With Jetpack Compose, we can easily build reactive and dynamic UIs. The UI will be composed of Composables, while ViewModels will handle the UI state and interact with the Domain layer.

Key Components:

  • Jetpack Compose: For building the user interface in a declarative way.

  • ViewModel: Handles state management and communicates with the Domain layer.

  • StateFlow/LiveData: For managing UI state like loading, success, and error states.

  • Navigation: Jetpack Navigation Compose to manage the app's navigation.

Example Composables:

@Composable
fun ProductListScreen(viewModel: ProductListViewModel) {
    val products by viewModel.products.collectAsState()
    val isLoading by viewModel.isLoading.collectAsState()
    val isError by viewModel.isError.collectAsState()

    if (isLoading) {
        CircularProgressIndicator()
    } else if (isError) {
        Text("Error fetching products")
    } else {
        LazyColumn {
            items(products) { product ->
                ProductItem(product = product)
            }
        }
    }
}

2. Domain Layer

The Domain Layer holds the business logic and use cases. This layer abstracts the data layer and provides clean interfaces for the Presentation layer to interact with. The domain layer consists of Use Cases and Repository interfaces.

Key Components:

  • Use Cases: Define business logic, such as fetching products, pagination, and handling inventory.

  • Repositories: Interface that defines data-fetching operations like fetching products, updating inventory, and more.

Example Use Case:

class GetProductListUseCase(private val productRepository: ProductRepository) {
    suspend operator fun invoke(page: Int): Result<List<Product>> {
        return productRepository.getPaginatedProducts(page)
    }
}

3. Data Layer

The Data Layer handles data fetching, caching, and the communication with external services (like APIs and Firebase). This layer includes repositories for both remote data (API calls) and local data (Room Database). We’ll use Room for offline caching and Paging 3 for efficient data loading.

Key Components:

  • Room: Used for offline caching of products and inventory data.

  • API Services: Retrofit or Ktor for interacting with remote APIs for products and real-time updates.

  • Firebase/Realtime Database: Used for real-time inventory updates.

  • Paging 3: Efficiently handles pagination for product lists.

Offline Caching Example with Room:

@Entity(tableName = "product")
data class ProductEntity(
    @PrimaryKey val id: Int,
    val name: String,
    val price: Double,
    val stockQuantity: Int
)

@Dao
interface ProductDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertProducts(products: List<ProductEntity>)

    @Query("SELECT * FROM product")
    suspend fun getAllProducts(): List<ProductEntity>
}

Repository Example:

class ProductRepositoryImpl(
    private val apiService: ApiService,
    private val productDao: ProductDao
) : ProductRepository {

    override suspend fun getPaginatedProducts(page: Int): Result<List<Product>> {
        val productsFromCache = productDao.getAllProducts()
        if (productsFromCache.isNotEmpty()) {
            return Result.success(productsFromCache.map { it.toDomain() })
        }

        try {
            val response = apiService.getProducts(page)
            productDao.insertProducts(response.products.map { it.toEntity() })
            return Result.success(response.products.map { it.toDomain() })
        } catch (e: Exception) {
            return Result.failure(e)
        }
    }
}

4. Real-Time Inventory Updates

For real-time inventory updates, we can use Firebase Realtime Database or WebSocket. When the stock quantity of a product changes, the app will update the product's data in real time, and the UI will reflect the updated information.

Firebase Example:

class FirebaseInventoryRepository {
    private val database = FirebaseDatabase.getInstance().getReference("inventory")

    fun observeInventoryUpdates(productId: Int, callback: (Int) -> Unit) {
        database.child("products").child(productId.toString()).child("stockQuantity")
            .addValueEventListener(object : ValueEventListener {
                override fun onDataChange(snapshot: DataSnapshot) {
                    val stockQuantity = snapshot.getValue(Int::class.java) ?: 0
                    callback(stockQuantity)
                }

                override fun onCancelled(error: DatabaseError) {
                    // Handle error
                }
            })
    }
}

5. Modularization

To ensure that the app remains maintainable as it grows, we will modularize the codebase. Each feature, such as the Product module, Cart module, and Inventory module, will be developed in separate modules.

This separation ensures that each module is responsible for one feature and can be developed and tested independently. It also improves build times and allows for easier team collaboration.

Modularization Example:

// In build.gradle for 'product' module
dependencies {
    implementation project(":core")
    implementation "androidx.compose.ui:ui:$compose_version"
}

6. Offline Handling and Connectivity

The app should handle offline scenarios gracefully, providing users with cached data when they are not connected to the internet. We can use the ConnectivityManager to check the network status and display cached products when offline. When the network is available, the app should fetch real-time data.

Offline Strategy:

  • Room Database: Cache products and inventory locally.

  • Network Status: Use ConnectivityManager to determine if the app is online or offline.

7. Real-Time Sync with Firebase

Firebase can be used for real-time syncing of inventory data. Using Firebase Realtime Database, the app can listen for changes to inventory quantities and update the UI instantly. Alternatively, WebSocket can be used to get real-time updates from the backend.

My thoughts

This architecture leverages modern Android tools like Jetpack Compose, Room, Paging 3, Firebase, and Clean Architecture to build a scalable and maintainable eCommerce app. The use of modularization ensures that each feature is self-contained, while the domain-driven design keeps the business logic separated from the UI.

By incorporating offline caching, real-time updates, and pagination, this architecture provides a robust foundation for building a seamless, scalable eCommerce experience that performs well even in scenarios with slow or no network connectivity.

📢 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! 💻

Jetpack Compose Memory Issues: Causes, Impact, and Best Solutions

Memory performance is crucial to Android app development, especially when using Jetpack Compose. Inefficient memory management can lead to high memory usage, performance bottlenecks, and even app crashes due to OutOfMemoryError. In this article, we’ll explore the key moments when memory performance issues occur in Jetpack Compose, their root causes, and practical solutions to optimize memory usage.


When Do Memory Performance Issues Occur?

1. Unnecessary Recompositions

Jetpack Compose follows a declarative UI paradigm, where the UI updates when the state changes. However, inefficient recompositions can increase memory usage.

  • Occurs When:

    • misusing mutable states.
    • Not specifying keys in lists.
    • Using remember and rememberSaveable improperly.
  • Example of Bad Practice:

    @Composable
    fun Counter() {
        var count by remember { mutableStateOf(0) }
        Text(text = "Count: $count")
        Button(onClick = { count++ }) {
            Text("Increase")
        }
    }
    

    Here, every button click triggers a recomposition of the entire function.

  • Solution: Use remember Correctly

    @Composable
    fun Counter() {
        var count by remember { mutableStateOf(0) }
        Column {
            Text(text = "Count: $count")
            Button(onClick = { count++ }) {
                Text("Increase")
            }
        }
    }
    

    Now, only Text inside the Column is recommended when the count changes.


2. Large Image and Resource Loading

Mishandling images in Jetpack Compose can lead to excessive memory consumption.

  • Occurs When:

    • Loading high-resolution images without downscaling.
    • Keeping unnecessary image references in memory.
  • Example of Inefficient Image Handling:

    Image(
        painter = painterResource(id = R.drawable.large_image),
        contentDescription = "Large Image",
        modifier = Modifier.fillMaxSize()
    )
    
  • Solution: Use coil for Efficient Image Loading

    AsyncImage(
        model = ImageRequest.Builder(LocalContext.current)
            .data("https://example.com/large_image.jpg")
            .memoryCacheKey("large_image")
            .crossfade(true)
            .build(),
        contentDescription = "Large Image",
        modifier = Modifier.fillMaxSize()
    )
    

    Why? Coil automatically caches and optimizes image loading, reducing memory footprint.


3. Holding References to Large Objects

If an object is stored persistently in memory without proper cleanup, it can lead to memory leaks.

  • Occurs When:

    • Using remember without DisposableEffect or LaunchedEffect.
    • Keeping references to Activity or Context in composables.
  • Example of Memory Leak:

    val context = LocalContext.current
    val activity = context as Activity // Leaking the activity reference
    
  • Solution: Use Weak References

    @Composable
    fun SafeContextUsage() {
        val context = LocalContext.current.applicationContext // Avoid holding activity reference
    }
    

4. Misusing Coroutines in Jetpack Compose

Misusing coroutines can cause unnecessary memory consumption.

  • Occurs When:

    • Launching long-running coroutines in recomposing composables.
    • Forgetting to cancel coroutines.
  • Bad Practice (Coroutine Leak):

    @Composable
    fun FetchData() {
        val scope = CoroutineScope(Dispatchers.IO)
        scope.launch {
            // API call
        }
    }
    

    Here, a new coroutine scope is created every time the function recomposes.

  • Solution: Use LaunchedEffect

    @Composable
    fun FetchData() {
        LaunchedEffect(Unit) {
            // API call runs only once
        }
    }
    

    This ensures the coroutine starts only once per composition.


5. Using Large Lists Without Optimization

Rendering large lists without optimizations can cause high memory usage and laggy performance.

  • Occurs When:

    • Not using LazyColumn or LazyRow.
    • Keeping a large dataset in memory.
  • Bad Practice (Non-Optimized List):

    Column {
        items.forEach { item ->
            Text(text = item.name)
        }
    }
    

    This loads all items at once, increasing memory usage.

  • Solution: Use LazyColumn with Keys

    LazyColumn {
        items(items, key = { it.id }) { item ->
            Text(text = item.name)
        }
    }
    

    Why? LazyColumn only renders visible items, reducing memory usage.


Summary

Memory performance in Jetpack Compose can be impacted by improper state management, excessive recompositions, large object references, inefficient coroutine usage, and unoptimized lists. You can ensure a smooth and memory-efficient Android app by following best practices like using remember correctly, optimizing image loading, avoiding memory leaks, managing coroutines properly, and leveraging LazyColumn.

By proactively handling these issues, your app will perform better and offer a seamless user experience with optimal resource utilization.


Thanks for reading! I'd love to know what you think about the article. Did it resonate with you?  Any suggestions for improvement? I’m always open to hearing your feedback so that I can improve my posts! 👇. Happy coding! 💻

Difference Between observeAsState and collectAsState in Android Kotlin

Jetpack Compose, Google's modern UI toolkit for Android, simplifies state management by leveraging declarative programming. When dealing with state changes in Compose, developers often encounter two commonly used functions: observeAsState() and collectAsState(). Understanding their differences is crucial to building efficient and reactive UI components.

In this article, we will explore these functions, their use cases, and a practical example demonstrating their behavior. We will also discuss which one is better suited for different scenarios in an Android app.

What is observeAsState()?

observeAsState() is used to observe LiveData inside a composable function. It converts a LiveData object into a Compose State<T>, making integrating LiveData-based state management into a Compose UI easier.

Syntax:

@Composable
fun MyScreen(viewModel: MyViewModel) {
    val uiState by viewModel.uiState.observeAsState()
    
    Text(text = uiState ?: "Loading...")
}

When to Use?

  • When your ViewModel exposes a LiveData object.
  • If your app follows the traditional MVVM architecture with LiveData.
  • When you need automatic lifecycle awareness without additional coroutine handling.

What is collectAsState()?

collectAsState() is used to collect Flow inside a composable function and represent it as State<T>. Since Flow is more modern and supports reactive stream processing, it is a preferred choice for state management.

Syntax:

@Composable
fun MyScreen(viewModel: MyViewModel) {
    val uiState by viewModel.uiStateFlow.collectAsState()
    
    Text(text = uiState)
}

When to Use?

  • When your ViewModel exposes a Flow instead of LiveData.
  • If you prefer a modern, coroutine-based approach for state management.
  • When you need fine-grained control over data streams, such as handling backpressure or retry mechanisms.

Practical Example: Comparing observeAsState() and collectAsState()

Let’s compare these functions with a simple ViewModel that exposes both LiveData and Flow:

class MyViewModel : ViewModel() {
    private val _uiStateLiveData = MutableLiveData("Hello from LiveData")
    val uiStateLiveData: LiveData<String> = _uiStateLiveData

    private val _uiStateFlow = MutableStateFlow("Hello from Flow")
    val uiStateFlow: StateFlow<String> = _uiStateFlow
}

Composable Function Using observeAsState()

@Composable
fun LiveDataExample(viewModel: MyViewModel) {
    val uiState by viewModel.uiStateLiveData.observeAsState()
    
    Text(text = uiState ?: "Loading...")
}

Composable Function Using collectAsState()

@Composable
fun FlowExample(viewModel: MyViewModel) {
    val uiState by viewModel.uiStateFlow.collectAsState()
    
    Text(text = uiState)
}

Key Differences

Feature observeAsState() collectAsState()
Backed by LiveData Flow
Threading Runs on the Main thread Requires CoroutineContext
Lifecycle-aware Yes Yes
Performance Slightly less efficient More efficient for reactivity
Best for Legacy MVVM with LiveData Modern apps with Kotlin Flow

Which One is Better for Your App?

It depends on your app’s architecture and use case:

  • If your app is already using LiveData extensively, stick with observeAsState() to maintain consistency.
  • If your app is using Kotlin Flow, prefer collectAsState() since it is more performant and offers better stream handling capabilities.
  • For new projects, consider using Flow and collectAsState() as it aligns better with modern Android development best practices.

Summary

Both observeAsState() and collectAsState() serve similar purposes—updating the UI reactively in Jetpack Compose. However, observeAsState() is best for legacy projects that use LiveData, while collectAsState() is ideal for modern, coroutine-based architectures. By choosing the right approach, you can ensure a smooth and efficient Compose-based UI experience.

Would you like to explore deeper performance benchmarks or specific edge cases? Let me know in the comments!

Thanks for reading! ðŸŽ‰ I'd love to know what you think about the article. Did it resonate with you? ðŸ’­ Any suggestions for improvement? I’m always open to hearing your feedback to improve my posts! ðŸ‘‡ðŸš€. Happy coding! ðŸ’»✨


MVVM vs MVI vs MVP: Which Architecture Fits Your Android Kotlin Compose Project?

When developing Android apps using Kotlin and Jetpack Compose, the architecture you choose should align with your application's needs, scalability, and maintainability. Let's explore the best architecture and discuss other alternatives with examples to help you make the best decision.

1. MVVM (Model-View-ViewModel) Architecture

Overview:

MVVM is the most commonly recommended architecture for Android apps using Jetpack Compose. It works seamlessly with Compose’s declarative UI structure and supports unidirectional data flow.

  • Model: Represents the data and business logic (e.g., network requests, database calls, etc.).
  • View: Composed of composable functions in Jetpack Compose. It displays the UI and reacts to state changes.
  • ViewModel: Holds UI-related state and business logic. It is lifecycle-aware and acts as a bridge between the View and Model.

How MVVM Works:

  • The View is responsible for presenting data using Compose. It observes the state exposed by the ViewModel via StateFlow or LiveData.
  • The ViewModel holds and processes the data and updates the state in response to user actions or external data changes.
  • The Model handles data fetching and business logic and communicates with repositories or data sources.

Benefits:

  • Separation of concerns: The View and Model are decoupled, making the app easier to maintain.
  • Reactivity: With Compose's state-driven UI, the View updates automatically when data changes in the ViewModel.
  • Scalability: MVVM works well for larger, complex apps.

Example:

// ViewModel
class MyViewModel : ViewModel() {
    private val _state = MutableStateFlow(MyState())
    val state: StateFlow<MyState> get() = _state

    fun fetchData() {
        // Simulate network request
        _state.value = _state.value.copy(data = "Fetched Data")
    }
}

// Composable View
@Composable
fun MyScreen(viewModel: MyViewModel = viewModel()) {
    val state by viewModel.state.collectAsState()

    Column {
        Text(text = state.data)
        Button(onClick = { viewModel.fetchData() }) {
            Text("Fetch Data")
        }
    }
}

Best For:

  • Real-time applications (e.g., chat apps, social media, etc.)
  • Apps with dynamic and complex UI requiring frequent backend updates.
  • Enterprise-level applications where clear separation of concerns and scalability are required.

2. MVI (Model-View-Intent) Architecture

Overview:

MVI focuses on unidirectional data flow and immutable state. It's more reactive than MVVM and ensures that the View always displays the latest state.

  • Model: Represents the application’s state, typically immutable.
  • View: Displays the UI and reacts to state changes.
  • Intent: Represents the actions that the View triggers (e.g., button clicks, user input).

How MVI Works:

  • The View sends Intents (user actions) to the Presenter (or ViewModel).
  • The Presenter updates the Model (state) based on these actions and then triggers a state change.
  • The View observes the state and re-renders itself accordingly.

Benefits:

  • Unidirectional data flow: The state is always predictable as changes propagate in one direction.
  • Immutable state: Reduces bugs associated with mutable state and ensures UI consistency.
  • Reactive: Well-suited for applications with frequent UI updates based on state changes.

Example:

// MVI - State, ViewModel
data class MyState(val data: String = "")

class MyViewModel : ViewModel() {
    private val _state = MutableStateFlow(MyState())
    val state: StateFlow<MyState> get() = _state

    fun processIntent(intent: MyIntent) {
        when (intent) {
            is MyIntent.FetchData -> {
                _state.value = MyState("Fetched Data")
            }
        }
    }
}

// Composable View
@Composable
fun MyScreen(viewModel: MyViewModel = viewModel()) {
    val state by viewModel.state.collectAsState()

    Column {
        Text(text = state.data)
        Button(onClick = { viewModel.processIntent(MyIntent.FetchData) }) {
            Text("Fetch Data")
        }
    }
}

Best For:

  • Complex UI interactions: Apps with multiple states and actions that must be tightly controlled.
  • Real-time data-driven apps where state changes must be captured and handled immutably.
  • Apps that require a highly reactive UI, such as games or media streaming apps.

3. MVP (Model-View-Presenter) Architecture

Overview:

MVP is a simpler architecture often used in legacy apps. In MVP, the Presenter controls the logic and updates the View, which is passive and only responsible for displaying data.

  • Model: Represents the data and business logic.
  • View: Displays UI and delegates user interactions to the Presenter.
  • Presenter: Acts as a middleman, processing user input and updating the View.

How MVP Works:

  • The View delegates all user actions (clicks, input, etc.) to the Presenter.
  • The Presenter fetches data from the Model and updates the View accordingly.

Benefits:

  • Simple and easy to implement for small applications.
  • Decouples UI logic from the data layer.

Example:

// MVP - Presenter
interface MyView {
    fun showData(data: String)
}

class MyPresenter(private val view: MyView) {
    fun fetchData() {
        // Simulate fetching data
        view.showData("Fetched Data")
    }
}

// Composable View
@Composable
fun MyScreen(view: MyView) {
    val presenter = remember { MyPresenter(view) }

    Column {
        Button(onClick = { presenter.fetchData() }) {
            Text("Fetch Data")
        }
    }
}

class MyViewImpl : MyView {
    override fun showData(data: String) {
        println("Data: $data")
    }
}

Best For:

  • Simple apps with minimal business logic.
  • Legacy projects that already follow the MVP pattern.
  • Applications with simple user interactions that don’t require complex state management.

Conclusion: Which Architecture to Choose?

Architecture Strengths Best For Example Use Cases
MVVM Seamless integration with Jetpack ComposeClear separation of concernsScalable and testable Large, complex appsReal-time appsTeam-based projects E-commerce apps, banking apps, social apps
MVI Immutable stateUnidirectional data flowReactive UI Highly interactive appsReal-time dataComplex state management Messaging apps, live score apps, media apps
MVP Simple to implementGood for small appsEasy to test Small appsLegacy appsSimple UI interactions Note-taking apps, simple tools, legacy apps

Best Recommendation:

  • MVVM is generally the best architecture for most Android Kotlin Compose apps due to its scalability, maintainability, and seamless integration with Compose.
  • MVI is ideal for apps that require complex state management and reactive UI updates.
  • MVP is still useful for simple apps or projects that already follow MVP.

Thanks for reading! 🎉 I'd love to know what you think about the article. Did it resonate with you? 💭 Any suggestions for improvement? I’m always open to hearing your feedback to improve my posts! 👇🚀. Happy coding! 💻✨

Cheat sheet for using Kotlin Coroutines with Flow in Jetpack Compose Android

 Here’s a cheat sheet for using Kotlin Coroutines with Flow in Android Jetpack Compose:

1. Basic Setup

To use Flow, ensure you have the following dependencies in your build.gradle:

dependencies {
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0"
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1"
}

2. Creating a Flow

You can create a Flow using the flow builder:

fun getData(): Flow<String> = flow {
    emit("Loading data...") // Emit a value
    delay(1000)
    emit("Data fetched successfully") // Emit another value
}

3. Collecting Data in Compose

In Jetpack Compose, use LaunchedEffect or collectAsState to collect the Flow and update the UI reactively.

With LaunchedEffect (Ideal for side-effects):

@Composable
fun DataDisplay() {
    val dataFlow = getData()
    
    LaunchedEffect(dataFlow) {
        dataFlow.collect { data ->
            // Handle the data and update UI accordingly
            Log.d("FlowData", data)
        }
    }
}

With collectAsState (Ideal for UI updates):

@Composable
fun DataDisplay() {
    val dataFlow = getData().collectAsState(initial = "Loading...")

    Text(text = dataFlow.value) // Display the collected data
}

4. State and Flow

If you need to expose a Flow inside a ViewModel:

class MyViewModel : ViewModel() {
    private val _dataFlow = MutableStateFlow("Loading...")
    val dataFlow: StateFlow<String> = _dataFlow

    init {
        viewModelScope.launch {
            delay(1000)  // Simulate data loading
            _dataFlow.value = "Data loaded!"
        }
    }
}

5. Flow Operators

Flow provides a set of operators to transform, filter, or combine flows.

map:

fun getUpperCaseData(): Flow<String> {
    return getData().map { it.toUpperCase() }
}

filter:

fun getFilteredData(): Flow<String> {
    return getData().filter { it.contains("Data") }
}

catch:

Handles errors in the flow.

fun safeGetData(): Flow<String> = flow {
    emit("Start fetching data...")
    throw Exception("Error while fetching data")
}.catch { exception ->
    emit("Error: ${exception.message}")
}

collectLatest:

Collect the latest value, cancelling the previous collection if a new value arrives.

LaunchedEffect(Unit) {
    getData().collectLatest { value ->
        // Handle the latest value
    }
}

6. Flow vs LiveData

  • Flow is more powerful for reactive programming, allowing better control and advanced operators.
  • LiveData is a lifecycle-aware data holder, and StateFlow can be used similarly in Compose.

7. Flow for Paging

Paging data can be fetched using a Flow. You can use the Paging library in combination with Flow to stream paginated data.

val pager = Pager(PagingConfig(pageSize = 20)) {
    MyPagingSource()
}.flow.cachedIn(viewModelScope)

8. Using stateIn to Convert Flow to StateFlow

If you need to convert a Flow into a StateFlow, you can use stateIn to collect it in a StateFlow.

val stateFlow = getData().stateIn(viewModelScope, SharingStarted.Lazily, "Initial value")

9. Handling Multiple Flows

You can combine multiple flows using operators like combine or zip.

val flow1 = flowOf("Data 1")
val flow2 = flowOf("Data 2")
val combinedFlow = combine(flow1, flow2) { data1, data2 ->
    "$data1 - $data2"
}

10. Error Handling

Flows provide a way to handle errors using catch and onEach.

fun getDataWithErrorHandling(): Flow<String> = flow {
    emit("Fetching data")
    throw Exception("Data fetch failed")
}.catch { exception ->
    emit("Error: ${exception.message}")
}

11. Timeouts

You can also apply timeouts to a flow, canceling it if it takes too long:

val result = withTimeoutOrNull(2000) {
    flowOf("Data fetched").collect()
}

12. Flow in ViewModel

Example of using Flow in a ViewModel for UI data:

class MyViewModel : ViewModel() {
    private val _myFlow = MutableStateFlow("Initial value")
    val myFlow: StateFlow<String> = _myFlow

    init {
        viewModelScope.launch {
            delay(2000)  // Simulate a delay
            _myFlow.value = "Updated value"
        }
    }
}

This is a basic guide to help you get started with Coroutines and Flow in Jetpack Compose. You can extend these patterns as needed based on the complexity of your application.

Best Practices for Handling Errors in Kotlin Compose

When building Android apps with Kotlin and Jetpack Compose, error handling is critical in ensuring a smooth and robust user experience. Something will inevitably go wrong in any application—whether it's a network failure, API error, or unexpected runtime exception—and how you handle these errors can make or break your app.

In this blog post, we'll explore best practices for handling errors in Kotlin Compose. We’ll break down various approaches for dealing with errors and provide examples that can be easily implemented into your Compose-based Android apps.

Why Error Handling Matters

Error handling is about more than just preventing crashes. It's about gracefully managing unexpected situations and providing users with meaningful feedback. Effective error handling leads to:

  • Improved user experience: Users aren't left in the dark when something goes wrong.
  • Increased app stability: By handling errors, you prevent crashes and ensure your app remains functional even in failure scenarios.
  • Better debugging: When you can catch and log errors, you can quickly identify issues and fix them.

In Kotlin Compose, handling errors properly involves managing UI states (such as loading, success, and error) and informing users about the issue with appropriate messages.

Best Practices for Error Handling in Kotlin Compose

  1. Use Sealed Classes to Represent UI States Using sealed classes is a great way to represent different states in your application, such as loading, success, and error. This pattern keeps your code clean and predictable by clearly defining each state's meaning.

  2. Handle Network and API Errors Gracefully Always check the response from an API call. Handle HTTP errors like 404, 500, etc., and ensure you provide meaningful error messages to the user.

  3. Catch Exceptions for Unexpected Scenarios Unexpected exceptions such as network timeouts or parsing issues can occur during runtime. Using try-catch blocks ensures that these errors don’t crash the app, and you can show a user-friendly error message instead.

  4. Show Loading States Displaying a loading indicator while data is being fetched or processed helps to manage user expectations. It signals that the app is working on an operation and is responsive even when the user has to wait.

  5. Provide a Retry Mechanism for Recoverable Errors Some errors, like network failures, might be temporary and can be fixed by retrying the operation. Offering a retry button or a similar mechanism helps users recover from these errors without leaving the app.

Example of Handling Errors in Kotlin Compose

Let’s take a practical example of fetching user data from a REST API and handling various types of errors, such as network issues, API errors, and null responses.

Step 1: Set up Retrofit for API Calls

interface ApiService {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") id: Int): Response<User>
}

Step 2: Create a ViewModel to Manage UI States

We’ll use sealed classes to represent different states: loading, success, and error.

class UserViewModel : ViewModel() {
    private val _state = mutableStateOf<UserState>(UserState.Loading)
    val state: State<UserState> = _state

    fun getUser(id: Int) {
        viewModelScope.launch {
            _state.value = UserState.Loading
            try {
                // Make network request
                val response = ApiClient.apiService.getUser(id)

                // Handle API response
                if (response.isSuccessful) {
                    val user = response.body()
                    if (user != null) {
                        _state.value = UserState.Success(user)
                    } else {
                        _state.value = UserState.Error("No user data found")
                    }
                } else {
                    // Handle API error codes like 404, 500
                    _state.value = UserState.Error("API Error: ${response.code()}")
                }
            } catch (e: Exception) {
                // Handle network errors or unexpected exceptions
                _state.value = UserState.Error("Network Error: ${e.localizedMessage}")
            }
        }
    }
}

sealed class UserState {
    object Loading : UserState()
    data class Success(val user: User) : UserState()
    data class Error(val message: String) : UserState()
}

Step 3: Displaying the UI Based on State

In the Compose UI, we will observe the state and update the UI based on whether it's in the loading, success, or error state.

@Composable
fun UserScreen(userViewModel: UserViewModel) {
    val state by userViewModel.state.observeAsState(UserState.Loading)

    when (state) {
        is UserState.Loading -> {
            // Show loading indicator
            CircularProgressIndicator()
        }
        is UserState.Success -> {
            // Show user data
            val user = (state as UserState.Success).user
            Text("User Name: ${user.name}")
            Text("User Email: ${user.email}")
        }
        is UserState.Error -> {
            // Show error message
            val errorMessage = (state as UserState.Error).message
            Text("Error: $errorMessage", color = Color.Red)
            // Optionally, add a retry button here
            Button(onClick = { userViewModel.getUser(1) }) {
                Text("Retry")
            }
        }
    }
}

@Composable
fun UserScreenWithButton(userViewModel: UserViewModel) {
    Column {
        Button(onClick = { userViewModel.getUser(1) }) {
            Text("Get User")
        }
        UserScreen(userViewModel)
    }
}

Error Scenarios and How to Handle Them

1. Network Errors

Network issues are common in mobile applications. This can happen due to no internet connection, slow network, or server unavailability. In such cases, we catch the exception and display an error message.

catch (e: Exception) {
    _state.value = UserState.Error("Network Error: ${e.localizedMessage}")
}

For example, if the device is offline or the request times out, the error message could look like:

Network Error: java.net.UnknownHostException: Unable to resolve host "api.example.com"

2. API Errors (HTTP Status Codes)

The server might return different HTTP status codes such as 404 (Not Found), 500 (Internal Server Error), or others. We need to handle these cases gracefully by checking the response code.

if (!response.isSuccessful) {
    _state.value = UserState.Error("API Error: ${response.code()}")
}

For example, a 404 error could result in the message:

API Error: 404

3. Null Responses

Sometimes, the server might return a 200 OK response, but the response body could be null. It’s essential to handle these cases by checking for null data and updating the state accordingly.

if (user == null) {
    _state.value = UserState.Error("No user data found")
}

In this case, the message could be:

No user data found

4. Unexpected Exceptions

Unexpected issues, such as JSON parsing errors or null pointer exceptions, can occur. We should always catch such exceptions to prevent crashes.

catch (e: Exception) {
    _state.value = UserState.Error("Unexpected Error: ${e.localizedMessage}")
}

This could result in messages like:

Unexpected Error: java.lang.NullPointerException

Summary

Error handling is essential to building stable and reliable Android applications. Best practices, such as using sealed classes to represent different UI states, handling API errors, catching exceptions, and providing meaningful feedback to users, can help you build a more robust and user-friendly app.

Remember to always:

  • Represent UI states clearly using sealed classes.
  • Gracefully handle network and API errors with proper messages.
  • Display loading states to manage user expectations.
  • Provide a retry mechanism for recoverable errors.

Implementing these best practices in your Kotlin Compose apps will create a more stable, resilient, and user-friendly user experience.

📢 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! 💻✨


How Does ViewModel Work Internally in Android Kotlin Compose

In modern Android development, using Jetpack Compose for building UIs and the ViewModel for managing UI-related data has become an essential practice. The combination of ViewModel and Jetpack Compose ensures your app is robust and scalable. But how exactly does the ViewModel work internally in Android Kotlin Compose, and what are its key benefits? In this article, we'll dive into the internals of ViewModel, provide an example of how it works in Compose, and highlight the benefits of using this architecture in your Android apps.

Understanding ViewModel in Jetpack Compose

At a high level, a ViewModel is a lifecycle-conscious component designed to store and manage UI-related data. It survives configuration changes such as screen rotations, ensuring that the UI data is retained without needing to be reloaded or recomputed.

The Internal Working of ViewModel:

The ViewModel is part of Android's Jetpack libraries and is designed to separate the UI-related data and logic from the UI components (such as Activities, Fragments, or Composables). Here's a breakdown of how it works:

  1. Lifecycle Awareness:

    • The ViewModel is scoped to the lifecycle of an Activity or Fragment. It is created when the associated lifecycle owner (activity/fragment) is initialized and is automatically cleared when the lifecycle owner is permanently destroyed (such as when an Activity is finishing).
    • Unlike UI components, ViewModel survives configuration changes (like screen rotations) because it's not tied directly to the UI lifecycle. This makes it an ideal choice for managing UI state.
  2. Data Storage:

    • Inside the ViewModel, data is typically stored in immutable properties (such as StateFlow or LiveData). These properties are observed by the UI (composables) to trigger recompositions whenever the data changes.
    • Mutable data within the ViewModel can be updated, but the exposed properties should always remain immutable to prevent modification outside of the ViewModel. This helps maintain consistency and simplifies state management.
  3. State Flow / LiveData:

    • The data that the ViewModel manages is often exposed via StateFlow, LiveData, or other observable data types. This allows the UI to observe data changes and react to those changes by recomposing the relevant parts of the screen.
    • StateFlow is especially powerful in Jetpack Compose since it integrates seamlessly with Compose's reactive nature, triggering recompositions automatically when the state updates.

How ViewModel Integrates with Jetpack Compose

Jetpack Compose simplifies working with ViewModel by providing a viewModel() function, which retrieves the ViewModel associated with the current composable. You can then use StateFlow or LiveData from the ViewModel to manage UI state and trigger recompositions when needed.

Example: Using ViewModel in Jetpack Compose

Let’s take a look at a simple example where we manage user data in a ViewModel and display it in a Composable:

1. ViewModel Class:

class UserViewModel : ViewModel() {
    // StateFlow is used to represent immutable state data.
    private val _userState = MutableStateFlow(User("John Doe", "john.doe@example.com"))
    val userState: StateFlow<User> = _userState

    // Function to update the user name
    fun updateUserName(newName: String) {
        _userState.value = _userState.value.copy(name = newName)
    }
}

data class User(val name: String, val email: String)

2. Composable Function:

@Composable
fun UserProfileScreen(viewModel: UserViewModel = viewModel()) {
    // Collect the current state from the ViewModel
    val user by viewModel.userState.collectAsState()

    Column(modifier = Modifier.padding(16.dp)) {
        Text(text = "Name: ${user.name}")
        Text(text = "Email: ${user.email}")
        
        // Button to update the name
        Button(onClick = { viewModel.updateUserName("Jane Doe") }) {
            Text(text = "Change Name")
        }
    }
}

In the above example:

  • UserViewModel holds the user data and exposes it as a StateFlow.
  • The UserProfileScreen composable observes the userState from the ViewModel and automatically recomposes whenever the state changes (e.g., when the user clicks the "Change Name" button).
  • The updateUserName() function updates the state inside the ViewModel, and the composable reacts to this change by recomposing the UI.

How Does This Work Internally?

  • When the UserProfileScreen composable is first displayed, it calls viewModel() to retrieve the instance of UserViewModel.
  • The userState is observed using collectAsState() in the composable, which makes it reactively bind to the ViewModel.
  • When the button is clicked, the updateUserName() function is called in the ViewModel, which updates the userState. This triggers a recomposition of the composable, causing it to reflect the updated data (e.g., showing "Jane Doe" instead of "John Doe").
  • If the Activity or Fragment containing this screen is rotated, the ViewModel remains intact, and the user data does not get lost.

Benefits of Using ViewModel in Kotlin + Jetpack Compose

  • Separation of Concerns
  • Lifecycle Awareness
  • Centralized State Management
  • Testability
  • Smooth UI Updates
  • Reduced Boilerplate

Summary

The ViewModel in Android Kotlin Compose is crucial for managing UI-related data in a lifecycle-conscious manner. Internally, it helps separate business logic from the UI layer, ensures state persistence during configuration changes, and facilitates the writing of modular, testable code.

With Jetpack Compose, you can leverage the power of ViewModel and reactive state management to build more maintainable, scalable, and efficient Android applications. Its integration with StateFlow makes handling dynamic UI updates simple, resulting in smoother user experiences.