Showing posts with label Andorid. Show all posts
Showing posts with label Andorid. Show all posts

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! πŸ’»✨


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.

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.

Jetpack Compose Testing in Android with Kotlin

Jetpack Compose revolutionizes UI development in Android by offering a declarative and efficient approach. However, testing these UI components is equally essential to ensure your app's functionality, stability, and responsiveness. This article explores how to test Jetpack Compose UIs in Android using Kotlin, focusing on practical examples and best practices.


Why Test Jetpack Compose UIs?

  • Reliability: Ensures your composables behave as expected in various scenarios.

  • Regression Prevention: Helps detect bugs introduced by new changes.

  • Maintainability: Makes refactoring and adding features safer.

  • User Satisfaction: Validates that UI flows and user interactions work seamlessly.

Jetpack Compose provides robust tools and APIs to test UIs efficiently. Let’s dive into the specifics.


Setting Up Your Compose Testing Environment

To test Jetpack Compose components, you need to include relevant dependencies in your project.

Gradle Dependencies

Add these dependencies to your build.gradle file:

dependencies {
    // Jetpack Compose UI Testing
    androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.5.0'

    // Compose Tooling for debug builds
    debugImplementation 'androidx.compose.ui:ui-tooling:1.5.0'
    debugImplementation 'androidx.compose.ui:ui-test-manifest:1.5.0'

    // Hilt for Dependency Injection (optional)
    androidTestImplementation 'com.google.dagger:hilt-android-testing:2.44'
    kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.44'
}

Types of Tests in Jetpack Compose

  1. Unit Tests for ViewModel Logic

    • Test the business logic separately from UI.

  2. UI Tests for Composables

    • Validate the behavior and appearance of Compose components.

  3. Integration Tests

    • Test end-to-end workflows combining UI, ViewModel, and Repository layers.

In this article, we focus on UI and integration tests.


Creating a Composable Component

Here’s a simple composable to display a list of users:

@Composable
fun UserList(users: List<String>, onClick: (String) -> Unit) {
    LazyColumn {
        items(users) { user ->
            Text(
                text = user,
                modifier = Modifier
                    .fillMaxWidth()
                    .clickable { onClick(user) }
                    .padding(16.dp)
            )
        }
    }
}

Preview the Composable

Jetpack Compose tooling allows you to preview the component:

@Preview(showBackground = true)
@Composable
fun PreviewUserList() {
    UserList(users = listOf("Alice", "Bob", "Charlie")) {}
}

Writing UI Tests for Jetpack Compose

1. Testing Static Content

To validate static content in a composable:

Test Code

@RunWith(AndroidJUnit4::class)
class UserListTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun `displays user names correctly`() {
        val users = listOf("Alice", "Bob", "Charlie")

        composeTestRule.setContent {
            UserList(users = users, onClick = {})
        }

        // Assert that all user names are displayed
        users.forEach { user ->
            composeTestRule.onNodeWithText(user).assertExists()
        }
    }
}

2. Testing User Interactions

To test click events or other interactions:

Test Code

@Test
fun `clicking on a user triggers callback`() {
    val users = listOf("Alice", "Bob")
    var clickedUser = ""

    composeTestRule.setContent {
        UserList(users = users, onClick = { clickedUser = it })
    }

    // Simulate a click on "Alice"
    composeTestRule.onNodeWithText("Alice").performClick()

    // Verify the callback is triggered with the correct user
    assertEquals("Alice", clickedUser)
}

3. Testing Dynamic States

Jetpack Compose components often depend on state. To test dynamic behavior:

Composable with State

@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }

    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Text(text = "Count: $count", style = MaterialTheme.typography.h4)
        Button(onClick = { count++ }) {
            Text("Increment")
        }
    }
}

Test Code

@Test
fun `counter increments correctly`() {
    composeTestRule.setContent { Counter() }

    // Verify initial state
    composeTestRule.onNodeWithText("Count: 0").assertExists()

    // Perform click
    composeTestRule.onNodeWithText("Increment").performClick()

    // Verify updated state
    composeTestRule.onNodeWithText("Count: 1").assertExists()
}

Best Practices for Compose Testing

  1. Use Tags for Identifiers

    • Assign unique tags for complex or dynamic components using Modifier.testTag().

    • Example:

      Text(
          text = "Hello",
          modifier = Modifier.testTag("GreetingText")
      )

      In tests:

      composeTestRule.onNodeWithTag("GreetingText").assertExists()
  2. Mock External Dependencies

    • Use libraries like MockK or Mockito to simulate ViewModel or Repository behavior.

  3. Use Idling Resources

    • Ensure asynchronous operations are complete before assertions.

  4. Run Tests on CI/CD Pipelines

    • Automate test execution with Jenkins, GitHub Actions, or Bitrise.

  5. Test Edge Cases

    • Validate empty states, error messages, and extreme inputs.


Sumarry

Jetpack Compose simplifies UI development, and its robust testing tools ensure high-quality apps. By combining static content tests, interaction validations, and state management testing, you can build reliable and maintainable Compose applications. Start incorporating these testing techniques into your workflow to create apps that delight users and withstand changes.

More details: Testing in Jetpack Compose , Test your Compose layout

Thanks for checking out my article!  I’d love to hear your feedback. Was it helpful? Are there any areas I should expand on? Drop a comment below or DM me! Your opinion is important! πŸ‘‡πŸ’¬. Happy coding! πŸ’»✨

Displaying a Custom List Using MVVM Architecture with Jetpack Compose in Android Kotlin

 In Android development, organizing your code in a structured way is essential for scalability and maintainability. One of the most popular architectural patterns for Android apps is MVVM (Model-View-ViewModel). MVVM helps you separate concerns and enhances testability, making your code more manageable. In this post, we’ll learn how to display a custom list using MVVM architecture with Jetpack Compose, Kotlin, and Coroutines.

What We Will Build

We will create an Android app using Jetpack Compose to display a custom list of data (for example, a list of users or items). The app will use MVVM to separate the UI, data handling, and business logic. The app will also use Kotlin Coroutines for asynchronous operations like fetching data from a network or a local database.

Prerequisites

  • Basic knowledge of Jetpack Compose, MVVM architecture, and Kotlin.
  • Android Studio installed with Kotlin support.

Steps to Build the Custom List App with MVVM and Coroutines

Let’s break this down into the following steps:

  1. Create the Data Model: Define the data you want to display in the list.
  2. Create the Repository: Handle data fetching, either from a network or a local database.
  3. Create the ViewModel: Expose the data to the UI and manage UI-related data.
  4. Create the Composables: Use Jetpack Compose to create the UI that observes the ViewModel.

1. Create the Data Model

The data model represents the data structure that will be displayed in the list. In this case, we will define a simple User data model.

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

This model will be used to represent individual items in the list.


2. Create the Repository

In the MVVM architecture, the Repository is responsible for managing data and fetching it from different sources (e.g., network, local database). Here’s an example of a simple repository using a suspended function (asynchronous operation) to simulate fetching data from a remote API.

import com.example.test.data.User
import kotlinx.coroutines.delay

class UserRepository {
    // Simulate fetching data asynchronously
    suspend fun getUsers(): List<User> {
        // Simulating network delay using a delay function
        delay(2000) // Simulating a network call delay
        return listOf(
            User(1, "John Doe", "johndoe@example.com"),
            User(2, "Jane Smith", "janesmith@example.com"),
            User(3, "Alex Johnson", "alexjohnson@example.com"),
            User(4, "Mark john", "markjohn@example.com"),
            User(5, "Bill laste", "billlaste@example.com"),
            User(6, "Deep lucifer", "deeplucifer@example.com"),
            User(7, "Kora Frank", "korafrank@example.com"),
            User(8, "Atticus Austin", "atticusaustin@example.com"),
            User(9, "Eve Reese", "evereese@example.com"),
            User(10, "Scarlet Frost", "scarletfrost@example.com"),
            User(11, "Nyla Martin", "nylamartin@example.com"),
            User(12, "Tony Moran", "tonymoran@example.com"),
            User(13, "Rudy Escobar", "rudyescobar@example.com"),
            User(14, "Waverly Clay", "waverlyclay@example.com"),
            User(15, "Zev Velez", "zevvelez@example.com")
        )
    }
}

This repository will return a list of User objects after a simulated network delay.


3. Create the ViewModel

The ViewModel is responsible for handling the UI-related data and business logic. It acts as a bridge between the Repository and the UI. The ViewModel will call the UserRepository to fetch the list of users asynchronously using Kotlin Coroutines.

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.test.data.User
import com.example.test.respository.UserRepository
import kotlinx.coroutines.launch

class UserViewModel : ViewModel() {
    private val userRepository = UserRepository()

    private val _users = MutableLiveData<List<User>>()
    val users: LiveData<List<User>> get() = _users

    fun fetchUsers() {
        viewModelScope.launch {
            val userList = userRepository.getUsers()
            _users.postValue(userList)
        }
    }
}

In the UserViewModel:

  • We use viewModelScope.launch to launch a coroutine in the ViewModel’s scope, ensuring that the coroutine will be cancelled when the ViewModel is cleared.
  • We fetch the data asynchronously from the repository and post the data to the LiveData object (_users), which is then observed by the UI.

4. Create the Composables

Now, let’s use Jetpack Compose to create the UI. We will display the list of users in a LazyColumn, which is the Compose equivalent of a RecyclerView. The UI will observe the LiveData exposed by the ViewModel.

Main Screen Composable

import androidx.lifecycle.viewmodel.compose.viewModel

@Composable
fun UserListScreen(userViewModel: UserViewModel = viewModel()) {
    // Observe the users LiveData from ViewModel
    val users by userViewModel.users.observeAsState(emptyList())

    // Fetch users when the composable is first displayed
    LaunchedEffect(Unit) {
        userViewModel.fetchUsers()
    }

    // Display the list of users
    LazyColumn(modifier = Modifier.fillMaxSize()) {
        items(users) { user ->
            UserItem(user)
        }
    }
}

@Composable
fun UserItem(user: User) {
    val context = LocalContext.current
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
            .clickable {
                Toast.makeText(context, user.name, Toast.LENGTH_SHORT).show()
            },
        elevation = CardDefaults.cardElevation(4.dp),
        shape = RoundedCornerShape(8.dp)
    ) {
        Column(
            modifier = Modifier.padding(16.dp)
        ) {
            Text(text = user.name, style = MaterialTheme.typography.headlineMedium)
            Text(text = "Email: ${user.email}", style = MaterialTheme.typography.bodyMedium)
        }
    }
}

Key Points:

  • We use observeAsState to observe changes to the users LiveData.
  • The LazyColumn is used to display the list of users in a scrollable view. It’s efficient and only renders the items that are visible on screen, similar to RecyclerView.
  • Each UserItem is displayed in a Card with padding and elevation.

Putting It All Together

In your MainActivity, you can set the UserListScreen composable to display the data.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            TestTheme {
                // Provide UserViewModel using the viewModel() function
                UserListScreen()
            }
        }
    }
}

Now, when the app runs, it will fetch the list of users asynchronously using Kotlin Coroutines and display the list in Jetpack Compose.



Conclusion

By following the steps above, we successfully built an app that displays a custom list of users using MVVM architecture, Jetpack Compose, and Kotlin Coroutines.

This pattern is not only clean and testable but also provides a seamless separation of concerns between the UI, business logic, and data management. Using LiveData, viewModelScope, and Jetpack Compose ensures a reactive UI that updates automatically when data changes, all while maintaining good performance and keeping the codebase manageable.

With Jetpack Compose's declarative approach, building UIs becomes easier and more intuitive, and combining it with MVVM and Coroutines ensures that your app remains scalable and maintainable.

I appreciate you taking the time to read my latest post! πŸ™ I’m always looking to improve, so your feedback would be incredibly helpful. What did you think of the content? Was there anything that could be better explained? Let me know in the comments! πŸ‘‡πŸŒŸ”. Happy coding! πŸ’»✨