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

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.

Commonly Used Modifiers in Jetpack Compose

Jetpack Compose, Android’s modern UI toolkit, brings a declarative approach to building user interfaces. One of the powerful features of Compose is the use of Modifier, which allows developers to customize the behavior, appearance, and layout of UI components. Modifiers are chainable, meaning you can combine multiple modifiers to create highly customizable UI components.

1. padding()

The padding() modifier adds space around a component. You can specify uniform padding for all sides or individual padding for each side (top, bottom, left, right).

Text(
    text = "Hello, World!",
    modifier = Modifier.padding(16.dp) // Adds 16dp padding around the text
)

2. background()

The background() modifier allows you to set a background color or shape for a component.

Text(
    text = "Background Example",
    modifier = Modifier.background(Color.Blue) // Sets a blue background
)

3. fillMaxWidth()

The fillMaxWidth() modifier makes a component take up the entire width of its parent container.

Box(
    modifier = Modifier.fillMaxWidth() // Fills the entire width of the parent container
) {
    Text(text = "This text takes up the full width")
}

4. fillMaxHeight()

Similarly, the fillMaxHeight() modifier ensures that a component fills the entire height of the parent container.

Box(
    modifier = Modifier.fillMaxHeight() // Fills the entire height of the parent container
) {
    Text(text = "This text takes up the full height")
}

5. size()

The size() modifier allows you to define the exact width and height of a component.

Box(
    modifier = Modifier.size(200.dp) // Sets the width and height to 200dp
) {
    Text(text = "Fixed Size Box")
}

6. align()

The align() modifier aligns a component within its parent container. You can align elements to the top, bottom, left, right, center, and so on.

Box(
    modifier = Modifier.fillMaxSize() // Makes the Box fill the available space
) {
    Text(
        text = "Aligned to Center",
        modifier = Modifier.align(Alignment.Center) // Aligns text to the center of the Box
    )
}

7. clickable()

The clickable() modifier makes a component respond to click events. You can define a lambda function to handle the click behavior.

Text(
    text = "Click Me",
    modifier = Modifier.clickable { 
        // Define click action
        println("Text clicked!")
    }
)

8. border()

The border() modifier adds a border around a component. You can specify the border’s width and color.

Box(
    modifier = Modifier.border(2.dp, Color.Red) // Adds a 2dp red border
) {
    Text(text = "Box with Border")
}

9. offset()

The offset() modifier shifts the position of a component by a specified amount. This can be useful for creating custom layouts or animations.

Text(
    text = "Offset Text",
    modifier = Modifier.offset(x = 20.dp, y = 10.dp) // Moves the text 20dp to the right and 10dp down
)

10. graphicsLayer()

The graphicsLayer() modifier allows you to apply transformations like rotation, scaling, and translation to a component. This is often used for animations or visual effects.

Text(
    text = "Rotated Text",
    modifier = Modifier.graphicsLayer(
        rotationZ = 45f // Rotates the text 45 degrees
    )
)

Example: Combining Modifiers

Now, let’s combine these modifiers in an example to demonstrate how they work together.

@Composable
fun CustomBoxExample() {
    Box(
        modifier = Modifier
            .size(300.dp) // Set size to 300x300dp
            .background(Color.Gray) // Set background color to gray
            .border(4.dp, Color.Black) // Add a black border around the box
            .padding(16.dp) // Add padding inside the box
            .clickable {
                // Handle click event
                println("Box clicked!")
            }
    ) {
        Text(
            text = "Click Me!",
            modifier = Modifier
                .align(Alignment.Center) // Align the text in the center
                .graphicsLayer(rotationZ = 15f) // Rotate the text 15 degrees
        )
    }
}

Explanation of the Example:

  1. Box Modifier:

    • size(300.dp): Sets the size of the box to 300x300 dp.
    • background(Color.Gray): Applies a gray background color.
    • border(4.dp, Color.Black): Adds a 4dp black border around the box.
    • padding(16.dp): Adds 16dp padding inside the box, affecting all the content inside it.
    • clickable: Makes the box clickable, with an action printed in the log.
  2. Text Modifier:

    • align(Alignment.Center): Centers the text within the box.
    • graphicsLayer(rotationZ = 15f): Rotates the text 15 degrees to give a slanted appearance.

This combination of modifiers creates a clickable box with centered, rotated text inside, offering flexibility in creating complex UI layouts.

Summary

Jetpack Compose offers a robust set of built-in modifiers that simplify building and customizing UIs. By combining modifiers such as padding(), background(), size(), clickable(), and others, you can create rich, responsive layouts without the need for boilerplate code. Compose's declarative nature enables modifiers to be applied clearly and intuitively, enhancing the readability and maintainability of your UI code. 


References:  https://developer.android.com/composehttps://github.com/android/compose-samples



Designing a Vertical Bottom Navigation Bar in Compose Android Kotlin

 In mobile app design, navigation is crucial to providing a smooth and intuitive user experience. One common navigation pattern is the bottom navigation bar, where users can easily switch between different sections of the app. In this article, we'll explore how to design a bottom navigation bar with vertical icons and text using Jetpack Compose for Android.

This design style places the icon at the top with the text below it, stacked vertically. We'll ensure the navigation bar is responsive and the icons and text are spaced evenly across the screen, no matter the device size.

Why Use a Vertical Bottom Navigation Bar?

While traditional bottom navigation bars usually place icons and text horizontally, a vertical layout offers a unique aesthetic and user experience. It can also help make the design look cleaner and more compact, especially for apps with fewer navigation options.

A vertical stack of icons and text ensures clarity, readability, and accessibility. When icons and labels are aligned in a column, they can easily scale and adapt across different screen sizes.

Key Features of Our Design:

  1. Vertical Alignment: The icons are placed above the text, giving a clear, stacked look.
  2. Equal Space Distribution: Each item in the bottom navigation bar will take up equal space, ensuring a balanced layout.
  3. Customizable Icons: We'll use the built-in Material icons provided by Jetpack Compose for a consistent and professional look.

Let's Dive Into the Code

Below is the code to create a vertical bottom navigation bar with equal space distribution for each tab.

Full Code Implementation:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            ComposeBottomBarTheme {
                BottomNavBarExample()
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BottomNavBarExample() {
    var selectedTab by remember { mutableStateOf(0) }
    var title by remember { mutableStateOf("Home") }

    Scaffold(
        topBar = {
            // Using the TopAppBar from Material3
            TopAppBar(
                title = { Text(text = title) },
                colors = TopAppBarDefaults.smallTopAppBarColors(
                    containerColor = Color.White, // Background color of the app bar
                    titleContentColor = Color.Black // Color of the title
                )
            )
        },
        bottomBar = {
            BottomAppBar(
                modifier = Modifier.padding(16.dp),
                containerColor = Color.White
            ) {
                Row(
                    modifier = Modifier.fillMaxWidth(),
                    horizontalArrangement = Arrangement.SpaceAround
                ) {
                    // Home Tab
                    IconButton(
                        onClick = { selectedTab = 0 },
                        modifier = Modifier.weight(1f) // Ensures equal space distribution
                    ) {
                        Column(horizontalAlignment = Alignment.CenterHorizontally) {
                            Icon(
                                imageVector = Icons.Filled.Home,
                                contentDescription = "Home",
                                tint = if (selectedTab == 0) Color.Blue else Color.Gray
                            )
                            Text(
                                text = "Home",
                                color = if (selectedTab == 0) Color.Blue else Color.Gray,
                                style = MaterialTheme.typography.bodySmall
                            )
                        }
                    }

                    // Search Tab
                    IconButton(
                        onClick = { selectedTab = 1 },
                        modifier = Modifier.weight(1f) // Ensures equal space distribution
                    ) {
                        Column(horizontalAlignment = Alignment.CenterHorizontally) {
                            Icon(
                                imageVector = Icons.Filled.Search,
                                contentDescription = "Search",
                                tint = if (selectedTab == 1) Color.Blue else Color.Gray
                            )
                            Text(
                                text = "Search",
                                color = if (selectedTab == 1) Color.Blue else Color.Gray,
                                style = MaterialTheme.typography.bodySmall
                            )
                        }
                    }

                    // Notifications Tab
                    IconButton(
                        onClick = { selectedTab = 2 },
                        modifier = Modifier.weight(1f) // Ensures equal space distribution
                    ) {
                        Column(horizontalAlignment = Alignment.CenterHorizontally) {
                            Icon(
                                imageVector = Icons.Filled.Notifications,
                                contentDescription = "Notifications",
                                tint = if (selectedTab == 2) Color.Blue else Color.Gray
                            )
                            Text(
                                text = "Notifications",
                                color = if (selectedTab == 2) Color.Blue else Color.Gray,
                                style = MaterialTheme.typography.bodySmall
                            )
                        }
                    }

                    // Profile Tab
                    IconButton(
                        onClick = { selectedTab = 3 },
                        modifier = Modifier.weight(1f) // Ensures equal space distribution
                    ) {
                        Column(horizontalAlignment = Alignment.CenterHorizontally) {
                            Icon(
                                imageVector = Icons.Filled.AccountCircle,
                                contentDescription = "Profile",
                                tint = if (selectedTab == 3) Color.Blue else Color.Gray
                            )
                            Text(
                                text = "Profile",
                                color = if (selectedTab == 3) Color.Blue else Color.Gray,
                                style = MaterialTheme.typography.bodySmall
                            )
                        }
                    }
                }
            }
        }
    ) { paddingValues ->
        // Content based on selected tab with the provided padding values
        Column(modifier = Modifier.padding(paddingValues)) {
            when (selectedTab) {
                0 -> {
                    title = "Home"
                    Text("Home Content", modifier = Modifier.padding(16.dp))
                }
                1 -> {
                    title = "Search"
                    Text("Search Content", modifier = Modifier.padding(16.dp))
                }
                2 -> {
                    title = "Notifications"
                    Text("Notifications Content", modifier = Modifier.padding(16.dp))
                }
                3 -> {
                    title = "Profile"
                    Text("Profile Content", modifier = Modifier.padding(16.dp))
                }
            }
        }
    }
}

Benefits of This Approach:

  1. Responsiveness: The layout adjusts to different screen sizes and ensures equal space distribution across the available screen width.
  2. Clear Navigation: The vertical alignment of the icon and text improves readability and makes it easier for users to understand the app's navigation structure.
  3. Customizable: You can replace the icons and texts to match the requirements of your app. The layout will adapt accordingly.

Summary and Output:



By using Jetpack Compose's Column, IconButton, and Row, we can easily create a bottom navigation bar with vertical icons and text, ensuring equal space distribution across the screen. This approach provides flexibility, clear navigation, and responsiveness for your Android apps.