Creating Animations in Jetpack Compose

Introduction

Animations play a crucial role in enhancing the user experience in mobile applications. Jetpack Compose, Google’s modern UI toolkit for Android, provides a powerful and flexible way to implement animations with minimal effort. Unlike the traditional View-based system, Jetpack Compose offers declarative APIs that make it easier to create smooth and visually appealing animations.

In this guide, we’ll explore different animation techniques in Jetpack Compose and demonstrate how to use them effectively with examples.

Why Use Animations in Jetpack Compose?

  • Enhance user experience by making UI interactions more engaging.
  • Improve visual feedback to guide users through transitions and changes.
  • Make complex UI changes smoother without manually handling animations.
  • Leverage declarative UI to create concise and readable animation logic.

Types of Animations in Jetpack Compose

Jetpack Compose provides various animation APIs to achieve different effects:

1. Simple Animations with animate*AsState

The animate*AsState functions allow us to animate basic values like Color, Dp, Float, or Int.

Example: Animating Color Change

@Composable
fun ColorAnimationExample() {
    var isActive by remember { mutableStateOf(false) }
    val backgroundColor by animateColorAsState(if (isActive) Color.Green else Color.Red)

    Box(
        modifier = Modifier
            .size(100.dp)
            .background(backgroundColor)
            .clickable { isActive = !isActive }
    )
}

2. Animated Visibility

Use AnimatedVisibility to show or hide a composable with animation.

Example: Fading In and Out

@Composable
fun AnimatedVisibilityExample() {
    var isVisible by remember { mutableStateOf(false) }

    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Button(onClick = { isVisible = !isVisible }) {
            Text("Toggle Visibility")
        }

        AnimatedVisibility(visible = isVisible) {
            Box(
                modifier = Modifier
                    .size(100.dp)
                    .background(Color.Blue)
            )
        }
    }
}

3. Animating Content Size

animateContentSize() automatically animates size changes within a composable.

Example: Expanding and Collapsing Box

@Composable
fun ExpandableBox() {
    var expanded by remember { mutableStateOf(false) }
    Box(
        modifier = Modifier
            .background(Color.Cyan)
            .clickable { expanded = !expanded }
            .animateContentSize()
            .size(if (expanded) 200.dp else 100.dp)
    )
}

4. Custom Animations with updateTransition

For more control, use updateTransition to animate multiple properties simultaneously.

Example: Scaling and Changing Color

@Composable
fun TransitionExample() {
    var isActive by remember { mutableStateOf(false) }
    val transition = updateTransition(targetState = isActive, label = "Box Transition")

    val size by transition.animateDp(label = "Size Animation") { state ->
        if (state) 150.dp else 100.dp
    }
    val color by transition.animateColor(label = "Color Animation") { state ->
        if (state) Color.Magenta else Color.Gray
    }

    Box(
        modifier = Modifier
            .size(size)
            .background(color)
            .clickable { isActive = !isActive }
    )
}

5. Infinite Animations with rememberInfiniteTransition

For continuous animations, use rememberInfiniteTransition.

Example: Pulsating Effect

@Composable
fun PulsatingEffect() {
    val infiniteTransition = rememberInfiniteTransition()
    val size by infiniteTransition.animateFloat(
        initialValue = 80f,
        targetValue = 100f,
        animationSpec = infiniteRepeatable(
            animation = tween(durationMillis = 1000, easing = LinearEasing),
            repeatMode = RepeatMode.Reverse
        ), label = "Pulsating Animation"
    )

    Box(
        modifier = Modifier
            .size(size.dp)
            .background(Color.Yellow)
    )
}

Conclusion

Jetpack Compose simplifies animation implementation while offering powerful tools to create dynamic UI interactions. Whether it’s basic color transitions, expanding/collapsing views, or continuous animations, Compose provides flexible APIs to enhance app experiences.

Key Takeaways

  • Use animate*AsState for animating simple values.
  • Use AnimatedVisibility to handle show/hide transitions.
  • Use animateContentSize for smooth content size changes.
  • Use updateTransition for complex multi-property animations.
  • Use rememberInfiniteTransition for continuous animations.

Call to Action

Start implementing animations in your Jetpack Compose projects today and take your app’s UI to the next level! Follow our blog for more Jetpack Compose tutorials.


Exploring Jetpack Hilt for Dependency Injection in Android: A Comprehensive Guide

Introduction
Dependency Injection (DI) is a crucial design pattern in modern Android development, enabling cleaner, more maintainable, and testable code. While Dagger has been the go-to DI framework for years, Jetpack Hilt has emerged as a simpler and more intuitive alternative. In this blog post, we’ll explore Jetpack Hilt for Dependency Injection, focusing on its integration with both XML and Kotlin. Whether you’re a beginner or an experienced developer, this guide will walk you through the essentials, provide practical examples, and help you master Hilt in no time.


What is Jetpack Hilt?

Jetpack Hilt is a dependency injection library built on top of Dagger, specifically designed for Android. It simplifies the DI process by reducing boilerplate code and providing built-in support for Android components like Activities, Fragments, and ViewModels. Hilt’s seamless integration with Kotlin and XML makes it a powerful tool for modern Android development.


Why is Jetpack Hilt Important?

Using Hilt for dependency injection offers several benefits:

  • Reduced Boilerplate Code: Hilt automates much of the setup required by Dagger.
  • Android-Friendly: Built-in support for Android components simplifies DI implementation.
  • Improved Testability: Easily inject mock dependencies for unit and instrumentation tests.
  • Scalability: Hilt scales well with large projects, making it ideal for enterprise-level apps.

Real-World Use Case: Building a Weather App

To demonstrate Hilt in action, let’s build a simple Weather App that fetches weather data from an API and displays it in the UI. We’ll use Hilt to inject dependencies like the API service, repository, and ViewModel.


How to Set Up Jetpack Hilt in Your Android Project

Let’s dive into the step-by-step process of integrating Hilt into your Android project.

Step 1: Add Dependencies

First, add the necessary dependencies to your build.gradle file:

// Project-level build.gradle
plugins {
    id 'com.android.application' version '7.3.0'
    id 'org.jetbrains.kotlin.android' version '1.7.20'
    id 'com.google.dagger.hilt.android' version '2.44' // Add Hilt plugin
}

// App-level build.gradle
dependencies {
    implementation 'com.google.dagger:hilt-android:2.44'
    kapt 'com.google.dagger:hilt-compiler:2.44'
    implementation 'androidx.activity:activity-ktx:1.7.0' // For Android components
}

Step 2: Initialize Hilt in Your Application

Create a custom Application class and annotate it with @HiltAndroidApp to enable Hilt in your project:

@HiltAndroidApp
class WeatherApplication : Application()

Step 3: Define Dependencies Using Hilt Modules

Create a Hilt module to provide instances of dependencies like the API service and repository:

@Module
@InstallIn(SingletonComponent::class)
object AppModule {

    @Provides
    @Singleton
    fun provideWeatherApi(): WeatherApi {
        return Retrofit.Builder()
            .baseUrl("https://api.weatherapi.com/v1/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(WeatherApi::class.java)
    }

    @Provides
    @Singleton
    fun provideWeatherRepository(api: WeatherApi): WeatherRepository {
        return WeatherRepository(api)
    }
}

Step 4: Inject Dependencies into ViewModel

Use the @HiltViewModel annotation to inject dependencies into your ViewModel:

@HiltViewModel
class WeatherViewModel @Inject constructor(
    private val repository: WeatherRepository
) : ViewModel() {

    private val _weatherData = MutableLiveData<WeatherResponse>()
    val weatherData: LiveData<WeatherResponse> get() = _weatherData

    fun fetchWeather(city: String) {
        viewModelScope.launch {
            val response = repository.getWeather(city)
            _weatherData.value = response
        }
    }
}

Step 5: Use ViewModel in Your Activity

Inject the ViewModel into your Activity using @AndroidEntryPoint:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    private val viewModel: WeatherViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        viewModel.weatherData.observe(this) { weather ->
            // Update UI with weather data
            findViewById<TextView>(R.id.weatherTextView).text = weather.toString()
        }

        // Fetch weather for a specific city
        viewModel.fetchWeather("New York")
    }
}

Best Practices for Using Jetpack Hilt

  • Use Modules for Complex Dependencies: Create Hilt modules to provide instances of classes that cannot be directly injected.
  • Leverage Scoping: Use @ActivityScoped, @FragmentScoped, or @ViewModelScoped to control the lifecycle of your dependencies.
  • Avoid Over-Injection: Only inject dependencies that are necessary for the component to function.

Common Challenges and Solutions

  • Challenge: Hilt not recognizing dependencies.
    Solution: Ensure all dependencies are properly annotated and included in Hilt modules.
  • Challenge: Build errors due to conflicting Dagger versions.
    Solution: Use compatible versions of Hilt and Dagger in your build.gradle file.

Conclusion

Jetpack Hilt simplifies dependency injection in Android development, making it easier to write clean, maintainable, and testable code. By following this guide, you’ve learned how to set up Hilt, inject dependencies, and apply best practices. Whether you’re working with Kotlin or XML, Hilt is a powerful tool that can streamline your development process.

Call-to-Action: Ready to implement Jetpack Hilt in your project? Start by integrating it into a small module and explore its benefits firsthand. Share your experience in the comments below!

Building a Navigation Graph with Jetpack Navigation in Jetpack Compose: A Comprehensive Guide

Navigating between screens is a fundamental part of any Android app. With Jetpack Navigation and Jetpack Compose, Google has made it easier than ever to handle navigation in a declarative and type-safe way. In this blog post, we’ll explore how to build a navigation graph with Jetpack Navigation in Jetpack Compose, step by step. Whether you’re new to Jetpack Compose or looking to modernize your app’s navigation, this guide will walk you through everything you need to know, complete with code samples and best practices.


What is Jetpack Navigation in Jetpack Compose?

Jetpack Navigation is a framework that simplifies the implementation of navigation in Android apps. When combined with Jetpack Compose, it provides a seamless way to manage navigation in a declarative and reactive manner. Key features include:

  • Declarative Navigation: Define navigation paths directly in Kotlin code.
  • Type-Safe Arguments: Pass data between destinations safely.
  • Deep Linking: Handle deep links seamlessly.
  • Animation Support: Add animations to transitions between screens.

Why Use Jetpack Navigation with Jetpack Compose?

Here are some reasons why Jetpack Navigation is a game-changer for Android developers using Jetpack Compose:

  1. Simplified Navigation Logic: No more Intent boilerplate code.
  2. Single Source of Truth: The navigation graph acts as a central hub for all navigation paths.
  3. Improved Maintainability: Easier to update and debug navigation flows.
  4. Built-in Back Stack Management: Automatically handles the back stack for you.

Setting Up Jetpack Navigation with Jetpack Compose

Before diving into building a navigation graph, let’s set up Jetpack Navigation in your Compose project.

Step 1: Add Dependencies

Add the following dependencies to your build.gradle file:

dependencies {
    implementation "androidx.navigation:navigation-compose:2.7.0"
}

Step 2: Enable ViewBinding (Optional but Recommended)

Enable ViewBinding in your build.gradle file to simplify UI interactions:

android {
    viewBinding {
        enabled = true
    }
}

Building a Navigation Graph in Jetpack Compose

A navigation graph in Jetpack Compose is defined directly in Kotlin code. Let’s create a simple navigation graph with three screens: Home, Profile, and Settings.

Step 1: Define Destinations

Create a sealed class to represent your destinations:

sealed class Screen(val route: String) {
    object Home : Screen("home")
    object Profile : Screen("profile")
    object Settings : Screen("settings")
}

Step 2: Set Up NavController and NavHost

In your MainActivity, set up the NavController and NavHost:

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.remember
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val navController = rememberNavController()
            NavHost(
                navController = navController,
                startDestination = Screen.Home.route
            ) {
                composable(Screen.Home.route) { HomeScreen(navController) }
                composable(Screen.Profile.route) { ProfileScreen(navController) }
                composable(Screen.Settings.route) { SettingsScreen(navController) }
            }
        }
    }
}

Step 3: Create Composable Screens

Define your composable screens and use the NavController to navigate between them:

@Composable
fun HomeScreen(navController: NavController) {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = "Home Screen")
        Button(onClick = { navController.navigate(Screen.Profile.route) }) {
            Text(text = "Go to Profile")
        }
    }
}

@Composable
fun ProfileScreen(navController: NavController) {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = "Profile Screen")
        Button(onClick = { navController.navigate(Screen.Settings.route) }) {
            Text(text = "Go to Settings")
        }
    }
}

@Composable
fun SettingsScreen(navController: NavController) {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = "Settings Screen")
        Button(onClick = { navController.popBackStack() }) {
            Text(text = "Go Back")
        }
    }
}

Passing Data Between Destinations

Jetpack Navigation supports type-safe arguments for passing data between destinations. Here’s how to pass a user ID from HomeScreen to ProfileScreen:

Step 1: Define Arguments in the Navigation Graph

Update your NavHost to include an argument for the user ID:

NavHost(
    navController = navController,
    startDestination = Screen.Home.route
) {
    composable(Screen.Home.route) { HomeScreen(navController) }
    composable(
        route = Screen.Profile.route + "/{userId}",
        arguments = listOf(navArgument("userId") { type = NavType.StringType })
    ) { backStackEntry ->
        val userId = backStackEntry.arguments?.getString("userId")
        ProfileScreen(navController, userId)
    }
    composable(Screen.Settings.route) { SettingsScreen(navController) }
}

Step 2: Pass Data When Navigating

Pass the user ID when navigating to ProfileScreen:

@Composable
fun HomeScreen(navController: NavController) {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = "Home Screen")
        Button(onClick = { navController.navigate(Screen.Profile.route + "/12345") }) {
            Text(text = "Go to Profile")
        }
    }
}

Step 3: Retrieve Data in the Destination

Retrieve the user ID in ProfileScreen:

@Composable
fun ProfileScreen(navController: NavController, userId: String?) {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = "Profile Screen")
        Text(text = "User ID: $userId")
        Button(onClick = { navController.navigate(Screen.Settings.route) }) {
            Text(text = "Go to Settings")
        }
    }
}

Conclusion: Mastering Jetpack Navigation in Jetpack Compose

Building a navigation graph with Jetpack Navigation in Jetpack Compose is a powerful way to simplify and streamline navigation in your Android app. By following this guide, you’ve learned how to:

  • Set up Jetpack Navigation in your Compose project.
  • Define destinations and actions in a navigation graph.
  • Pass data between destinations using type-safe arguments.

Key Takeaways

  • Use Jetpack Navigation to manage navigation in a declarative and type-safe way.
  • Define all navigation paths directly in Kotlin code.
  • Leverage type-safe arguments for passing data between destinations.

Call-to-Action

Ready to take your Android app’s navigation to the next level? Start implementing Jetpack Navigation with Jetpack Compose today! Share your experiences and questions in the comments below, and don’t forget to subscribe for more in-depth tutorials on Android development.

StateFlow in Jetpack: A Comprehensive Guide for Android Developers

In the ever-evolving world of Android development, managing UI state efficiently is critical for building responsive and maintainable applications. StateFlow, a part of Kotlin’s Coroutines library, has emerged as a powerful tool for handling state in a reactive and modern way. In this blog post, we’ll explore StateFlow in Jetpack, its benefits, and how to use it effectively in your Android projects. We’ll also include plenty of code samples to help you understand its implementation.


What is StateFlow?

StateFlow is a state holder observable flow that emits the current and new state updates to its collectors. It is designed to handle state management in a reactive way, making it an excellent choice for modern Android development, especially when working with Jetpack Compose or MVVM architecture.

Key Features of StateFlow

  • State Management: Holds and emits the current state to its collectors.
  • Reactive Programming: Integrates seamlessly with Kotlin Coroutines.
  • Thread Safety: Can be used on any thread, making it highly flexible.
  • Backpressure Handling: Built-in support for handling backpressure.

Why Use StateFlow in Jetpack?

StateFlow is particularly useful in modern Android development for the following reasons:

  1. Reactive State Management: StateFlow allows you to manage UI state reactively, ensuring your app remains responsive.
  2. Seamless Integration with Coroutines: StateFlow works natively with Kotlin Coroutines, making it a natural fit for modern Android apps.
  3. Jetpack Compose Compatibility: StateFlow integrates effortlessly with Jetpack Compose, enabling declarative UI updates.
  4. Thread Flexibility: Unlike LiveData, StateFlow is not restricted to the main thread, offering more flexibility in handling data streams.

Implementing StateFlow in Jetpack

Let’s walk through the steps to implement StateFlow in your Android app. We’ll start with a simple example and gradually build on it.

Step 1: Add Dependencies

To use StateFlow, add the necessary dependencies to your build.gradle file:

dependencies {
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1"
}

Step 2: Create a StateFlow Object

Create a ViewModel with a StateFlow object to hold a counter value.

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

class CounterViewModel : ViewModel() {
    private val _counter = MutableStateFlow(0)
    val counter: StateFlow<Int> get() = _counter

    fun incrementCounter() {
        viewModelScope.launch {
            _counter.value += 1
        }
    }
}

In this example, we’ve created a CounterViewModel class that contains a StateFlow object to store a counter value. The incrementCounter() method is used to update the counter.

Step 3: Collect StateFlow in an Activity

Collect the StateFlow object in your Activity or Fragment.

In this example, we’re using a CoroutineScope to collect the StateFlow object and update the UI whenever the counter value changes.


StateFlow in Jetpack Compose

StateFlow integrates seamlessly with Jetpack Compose, making it an excellent choice for managing state in declarative UIs.

Example: Using StateFlow in Jetpack Compose

Let’s implement the same counter example using Jetpack Compose.

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel

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

@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
    val counter by viewModel.counter.collectAsState()

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = "Counter: $counter", modifier = Modifier.padding(16.dp))
        Button(onClick = { viewModel.incrementCounter() }) {
            Text(text = "Increment")
        }
    }
}

In this example, we’re using the collectAsState() function to convert StateFlow into Compose state, allowing the UI to react to changes in the counter value.


Best Practices for Using StateFlow

  1. Use ViewModel for State Management: Always use ViewModel to hold StateFlow objects, ensuring separation of concerns.
  2. Leverage collectAsState in Compose: Use collectAsState() to integrate StateFlow with Jetpack Compose.
  3. Avoid Direct State Mutations: Always update StateFlow values within a coroutine scope to ensure thread safety.
  4. Combine with Other Flows: Use StateFlow in combination with other Kotlin flows for complex data streams.

Conclusion: Why StateFlow is a Game-Changer

StateFlow is a powerful tool for managing UI state in modern Android development. Its seamless integration with Kotlin Coroutines and Jetpack Compose makes it an excellent choice for building reactive and maintainable apps. Whether you’re working on a simple counter app or a complex UI, StateFlow ensures your app remains responsive and scalable.

Key Takeaways

  • StateFlow is a reactive state holder that integrates with Kotlin Coroutines.
  • It works seamlessly with Jetpack Compose for declarative UI updates.
  • Use ViewModel to hold StateFlow objects and ensure separation of concerns.

Call-to-Action

Ready to take your Android development skills to the next level? Start integrating StateFlow into your projects today! Share your experiences and questions in the comments below, and don’t forget to subscribe for more in-depth tutorials on Android development.

LiveData vs StateFlow in Jetpack: Which One Should You Use?

Introduction

When building Android applications, managing UI-related data efficiently is crucial. Jetpack provides two primary tools for reactive programming: LiveData and StateFlow. While both serve similar purposes, they differ in behavior, lifecycle awareness, and use cases.

In this article, we’ll compare LiveData vs StateFlow in Jetpack, discuss their key differences, and provide code samples to help you decide which one to use in your Android projects.

What is LiveData?

LiveData is an observable data holder that is lifecycle-aware, meaning it respects the lifecycle of Android components like Activities and Fragments. It is part of Android Architecture Components and is commonly used in MVVM (Model-View-ViewModel) architecture.

Features of LiveData

  • Lifecycle-aware: Automatically updates observers only when they are in an active state.
  • Single source of truth: Ensures data consistency by avoiding multiple sources for the same data.
  • Automatic cleanup: Prevents memory leaks by removing observers when the lifecycle is destroyed.
  • Thread-safe: Can be updated from background threads using postValue() or from the main thread using setValue().

Example Usage of LiveData

1. Define LiveData in ViewModel

class MyViewModel : ViewModel() {
    private val _message = MutableLiveData<String>()
    val message: LiveData<String> get() = _message

    fun updateMessage(newMessage: String) {
        _message.value = newMessage
    }
}

2. Observe LiveData in Fragment or Activity

class MyFragment : Fragment() {
    private val viewModel: MyViewModel by viewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewModel.message.observe(viewLifecycleOwner) { newMessage ->
            textView.text = newMessage
        }
    }
}

What is StateFlow?

StateFlow is a hot flow introduced in Kotlin’s Flow API. It is designed to handle state management in a predictable and reactive manner. Unlike LiveData, StateFlow does not have lifecycle awareness but offers better coroutine-based state handling.

Features of StateFlow

  • State holder: Always has a current state and emits updates when the state changes.
  • Cold start behavior: Collectors receive the latest value immediately upon subscription.
  • Works with coroutines: Designed for reactive programming with structured concurrency.
  • Thread-safe: Can be updated safely across multiple threads.

Example Usage of StateFlow

1. Define StateFlow in ViewModel

class MyViewModel : ViewModel() {
    private val _message = MutableStateFlow("Initial Message")
    val message: StateFlow<String> get() = _message

    fun updateMessage(newMessage: String) {
        _message.value = newMessage
    }
}

2. Collect StateFlow in Fragment or Activity

class MyFragment : Fragment() {
    private val viewModel: MyViewModel by viewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        lifecycleScope.launch {
            viewModel.message.collect { newMessage ->
                textView.text = newMessage
            }
        }
    }
}

For a deeper dive into how StateFlow works in Jetpack Compose, check out our comprehensive guide on StateFlow.

Key Differences Between LiveData and StateFlow

FeatureLiveDataStateFlow
Lifecycle-awareYesNo
Initial value requiredNoYes
Thread safetyYesYes
Coroutines supportNo (can be used with coroutines)Yes (fully coroutine-based)
Hot/Cold natureCold (only emits when observed)Hot (always emits latest state)
Observers behaviorOnly active observers get updatesAll collectors receive updates

When to Use LiveData vs StateFlow

Use LiveData When:

  • You need lifecycle-aware data handling.
  • Your UI component (Fragment/Activity) directly consumes the data.
  • You want automatic memory management with no manual cleanup.

Use StateFlow When:

  • You work extensively with Kotlin Coroutines.
  • You need to handle UI state in a predictable, consistent manner.
  • You require hot flow behavior, where the latest value is always emitted.

Migration from LiveData to StateFlow

If you are transitioning from LiveData to StateFlow, you can do so easily:

Convert LiveData to StateFlow

val messageFlow: StateFlow<String> = _message.asStateFlow()

Convert StateFlow to LiveData

val messageLiveData: LiveData<String> = messageFlow.asLiveData()

Conclusion

Both LiveData and StateFlow are powerful tools in Jetpack for managing UI-related data. LiveData is lifecycle-aware and easy to use in traditional MVVM patterns, while StateFlow is more suitable for coroutine-based applications that require predictable state management.

Key Takeaways

  • Use LiveData if you need lifecycle-aware data handling.
  • Use StateFlow if you prefer Kotlin Coroutines and need a consistent, hot state container.
  • Migrating between LiveData and StateFlow is straightforward using asStateFlow() and asLiveData().

What are your thoughts on LiveData vs StateFlow in Jetpack? Have you migrated to StateFlow yet? Share your experiences in the comments!


Understanding ViewModel in Jetpack Compose: A Comprehensive Guide for Android Developers

Jetpack Compose has revolutionized Android UI development by introducing a declarative and reactive way to build user interfaces. However, managing UI-related data remains a critical aspect of app development. This is where ViewModel comes into play. In this blog post, we’ll explore how to use ViewModel in Jetpack Compose, its benefits, and best practices. We’ll also include plenty of code samples to help you integrate ViewModel seamlessly into your Compose-based apps.


What is a ViewModel in Jetpack Compose?

The ViewModel is a class designed to store and manage UI-related data in a lifecycle-conscious way. In Jetpack Compose, ViewModel works hand-in-hand with Compose’s state management to ensure that your UI remains consistent and responsive, even during configuration changes like screen rotations.

Key Benefits of Using ViewModel in Compose

  1. Lifecycle Awareness: ViewModel survives configuration changes, ensuring data persistence.
  2. Separation of Concerns: ViewModel separates UI logic from the UI components, making your code cleaner and more maintainable.
  3. State Management: ViewModel integrates seamlessly with Compose’s state management, enabling reactive UIs.
  4. Improved Testability: By decoupling business logic from the UI, ViewModel makes your app easier to test.

How Does ViewModel Work in Jetpack Compose?

In Jetpack Compose, ViewModel is used to manage state and business logic outside of composable functions. Composable functions are designed to be stateless, meaning they should not hold any data that needs to persist across recompositions. Instead, the state is hoisted to a ViewModel, which is then observed by the composable functions.

ViewModel Lifecycle in Compose

The lifecycle of a ViewModel in Compose is tied to the lifecycle of the composable that uses it. When the composable is removed from the composition, the ViewModel is cleared from memory. However, during configuration changes, the ViewModel persists, ensuring data consistency.


Implementing ViewModel in Jetpack Compose

Let’s walk through the steps to implement a ViewModel in a Jetpack Compose application. We’ll start with a simple example and gradually build on it.

Step 1: Add Dependencies

To use ViewModel in your Compose project, add the necessary dependencies to your build.gradle file:

dependencies {
    implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1"
    implementation "androidx.activity:activity-compose:1.7.0"
    implementation "androidx.compose.runtime:runtime-livedata:1.4.0"
}

Step 2: Create a ViewModel Class

Create a new class that extends ViewModel. This class will hold the data that you want to preserve during configuration changes.

import androidx.lifecycle.ViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.LiveData

class CounterViewModel : ViewModel() {
    private val _counter = MutableLiveData(0)
    val counter: LiveData<Int> get() = _counter

    fun incrementCounter() {
        _counter.value = (_counter.value ?: 0) + 1
    }
}

In this example, we’ve created a CounterViewModel class that contains a LiveData object to store a counter value. The incrementCounter() method is used to update the counter.

Step 3: Use ViewModel in a Composable

To use the ViewModel in your composable function, you need to obtain an instance of it using the viewModel() function.

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel

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

@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
    val counter by viewModel.counter.observeAsState(0)

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = "Counter: $counter", modifier = Modifier.padding(16.dp))
        Button(onClick = { viewModel.incrementCounter() }) {
            Text(text = "Increment")
        }
    }
}

In this example, we’re using the viewModel() function to obtain an instance of CounterViewModel. The observeAsState() extension function is used to convert LiveData into Compose’s state, allowing the UI to react to changes in the counter value.


ViewModel and State Hoisting in Compose

State hoisting is a key concept in Jetpack Compose, where state is moved to a higher-level composable or ViewModel to make the composable stateless. This approach ensures that the composable is reusable and easier to test.

Example: State Hoisting with ViewModel

Let’s extend our previous example to include a list of items stored in the ViewModel.

class ItemViewModel : ViewModel() {
    private val _itemList = MutableLiveData<List<String>>(listOf("Item 1", "Item 2", "Item 3"))
    val itemList: LiveData<List<String>> get() = _itemList

    fun addItem(item: String) {
        val currentList = _itemList.value?.toMutableList() ?: mutableListOf()
        currentList.add(item)
        _itemList.value = currentList
    }
}

@Composable
fun ItemScreen(viewModel: ItemViewModel = viewModel()) {
    val itemList by viewModel.itemList.observeAsState(emptyList())

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        itemList.forEach { item ->
            Text(text = item, modifier = Modifier.padding(8.dp))
        }
        Button(onClick = { viewModel.addItem("New Item") }) {
            Text(text = "Add Item")
        }
    }
}

In this example, the ItemViewModel manages a list of items, and the ItemScreen composable observes the list and updates the UI accordingly.


Best Practices for Using ViewModel in Compose

  1. Keep Composables Stateless: Hoist state to the ViewModel to make your composables reusable and testable.
  2. Use observeAsState for LiveData: Convert LiveData to Compose state using observeAsState for seamless integration.
  3. Avoid Business Logic in Composables: Move all business logic to the ViewModel to keep your UI layer clean.
  4. Leverage SavedStateHandle for Critical Data: Use SavedStateHandle to persist data that must survive process death.

Conclusion

ViewModel is an essential component for managing UI-related data in Jetpack Compose. By separating state and business logic from the UI layer, ViewModel helps you build scalable, maintainable, and testable apps. Combined with Compose’s state management, ViewModel provides a powerful solution for handling data in modern Android development.

By following the examples and best practices outlined in this post, you’ll be well-equipped to integrate ViewModel in Jetpack Compose into your projects. Whether you’re building a simple counter app or a complex list-based UI, ViewModel ensures your app remains robust and responsive.


With these tips and examples, you’re ready to harness the power of ViewModel in Jetpack Compose and take your Android development skills to the next level. Happy coding!

Introduction to Jetpack Compose

Jetpack Compose is Google’s modern, fully declarative UI toolkit for building native Android apps. Introduced to simplify UI development, it enables developers to design beautiful and responsive interfaces with less boilerplate code. By using Kotlin as its foundation, Jetpack Compose seamlessly integrates with existing Android apps, empowering developers to create dynamic and maintainable UI components.

In this blog post, we’ll explore the core features, advantages, best practices, and tips for adopting Jetpack Compose in your projects. If you’re looking for an efficient way to build Android UIs, this guide is for you.


Key Features of Jetpack Compose

1. Declarative UI Paradigm

Jetpack Compose employs a declarative programming model, which means you describe what your UI should look like rather than how to build it. This simplifies the process of creating complex UI structures and makes your code more readable.

@Composable
fun Greeting(name: String) {
    Text(text = "Hello, $name!")
}

2. Composable Functions

Composable functions are the building blocks of Jetpack Compose. Marked with the @Composable annotation, they let you define reusable UI components.

3. State Management

Jetpack Compose introduces a reactive programming model where UI updates automatically in response to state changes.

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

    Column {
        Text("Count: $count")
        Button(onClick = { count++ }) {
            Text("Increment")
        }
    }
}

4. Integration with Jetpack Libraries

Jetpack Compose integrates seamlessly with popular Android libraries like Navigation, Room, and ViewModel. This ensures consistency across the app’s architecture.

5. Custom Modifiers

Modifiers in Jetpack Compose allow you to style, position, and add behavior to UI components.

Text(
    text = "Stylish Text",
    modifier = Modifier.padding(16.dp).background(Color.Gray)
)

Advantages of Jetpack Compose

1. Simplified UI Development

Jetpack Compose significantly reduces the amount of boilerplate code required for building UIs, enabling faster development cycles.

2. Improved Code Reusability

Composable functions are modular and reusable, making it easier to maintain and test components.

3. Reactive Programming

Compose’s reactive state management ensures that the UI automatically updates when the underlying data changes, eliminating the need for manual UI updates.

4. Better Performance

Optimized rendering processes ensure that Jetpack Compose performs efficiently, even for complex UIs.

5. Modern and Future-Proof

As Google’s recommended UI toolkit, Jetpack Compose is designed to stay relevant and evolve with the Android ecosystem.


Best Practices for Jetpack Compose

1. Use State Wisely

Avoid placing too much logic in composables. Use state only where necessary, and leverage remember and rememberSaveable to preserve state efficiently.

@Composable
fun Example() {
    var text by rememberSaveable { mutableStateOf("") }
    TextField(value = text, onValueChange = { text = it })
}

2. Keep UI and Business Logic Separate

Use ViewModels to handle business logic and expose state to the UI. This ensures better separation of concerns.

class MyViewModel : ViewModel() {
    private val _count = MutableLiveData(0)
    val count: LiveData<Int> get() = _count

    fun increment() {
        _count.value = (_count.value ?: 0) + 1
    }
}

@Composable
fun Counter(viewModel: MyViewModel) {
    val count by viewModel.count.observeAsState(0)
    Text("Count: $count")
}

3. Optimize for Performance

Minimize recompositions by using appropriate scopes for remember and avoid heavy computations in composables.

4. Leverage Previews

Use Android Studio’s Compose Previews to test and iterate on your UI without needing to build and run the app.

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    Greeting(name = "Compose")
}

5. Adopt Theming and Material Design

Jetpack Compose makes it easy to implement consistent theming using Material Design principles.

@Composable
fun MyAppTheme(content: @Composable () -> Unit) {
    MaterialTheme(
        colors = lightColors(
            primary = Color.Blue,
            secondary = Color.Green
        )
    ) {
        content()
    }
}

Code Example: Building a Simple Todo App

Here’s a basic example of a Todo App built using Jetpack Compose:

@Composable
fun TodoApp() {
    var todos by remember { mutableStateOf(listOf<String>()) }
    var newTask by remember { mutableStateOf("") }

    Column(modifier = Modifier.padding(16.dp)) {
        TextField(
            value = newTask,
            onValueChange = { newTask = it },
            label = { Text("Add a new task") },
            modifier = Modifier.fillMaxWidth()
        )
        Spacer(modifier = Modifier.height(8.dp))
        Button(
            onClick = {
                if (newTask.isNotBlank()) {
                    todos = todos + newTask
                    newTask = ""
                }
            },
            modifier = Modifier.fillMaxWidth()
        ) {
            Text("Add Task")
        }
        Spacer(modifier = Modifier.height(16.dp))
        LazyColumn {
            items(todos) { task ->
                Text(task, modifier = Modifier.padding(8.dp))
            }
        }
    }
}

Conclusion

Jetpack Compose is a game-changer for Android UI development. Its declarative approach, reactive state management, and integration with Kotlin make it a powerful tool for building modern apps. By adopting Compose, developers can create more readable, maintainable, and efficient UI components, ensuring a smoother development experience. As the Android ecosystem evolves, Jetpack Compose is poised to become the cornerstone of Android UI design.

Whether you’re starting a new project or migrating an existing one, there’s no better time to dive into Jetpack Compose. Experiment with its features, follow best practices, and watch your productivity soar.


Kotlin Functions: From Basics to Advanced Usage

Kotlin, a modern programming language for the JVM, has become immensely popular due to its concise and expressive syntax. Functions in Kotlin are at the core of its programming model. This blog explores Kotlin functions, starting from the basics and advancing to more complex concepts, complete with code examples to help you understand and implement them effectively.

What Are Functions in Kotlin?

A function in Kotlin is a reusable block of code designed to perform a specific task. Functions make code modular, readable, and maintainable. Kotlin provides a variety of features to define and use functions efficiently.

Syntax for Defining a Function

fun functionName(parameterName: ParameterType): ReturnType {
    // Function body
    return result
}

For example:

fun greet(name: String): String {
    return "Hello, $name!"
}

fun main() {
    println(greet("Kotlin"))
}

Types of Functions in Kotlin

1. Top-Level Functions

These functions are defined outside of any class.

fun add(a: Int, b: Int): Int {
    return a + b
}

fun main() {
    println(add(3, 5)) // Output: 8
}

2. Member Functions

Functions that are defined within a class or object.

class Calculator {
    fun multiply(a: Int, b: Int): Int {
        return a * b
    }
}

fun main() {
    val calc = Calculator()
    println(calc.multiply(4, 5)) // Output: 20
}

3. Extension Functions

These add functionality to existing classes.

fun String.reverse(): String {
    return this.reversed()
}

fun main() {
    val text = "Kotlin"
    println(text.reverse()) // Output: niltoK
}

4. Higher-Order Functions

A function that takes another function as a parameter or returns a function.

fun calculate(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
    return operation(a, b)
}

fun main() {
    val sum = calculate(10, 5) { x, y -> x + y }
    println(sum) // Output: 15
}

5. Inline Functions

Inline functions improve performance by reducing overhead caused by higher-order functions.

inline fun operate(action: () -> Unit) {
    action()
}

fun main() {
    operate { println("Inline function executed.") }
}

Default and Named Parameters

Kotlin allows you to provide default values for function parameters.

fun greet(name: String = "Guest") {
    println("Welcome, $name!")
}

fun main() {
    greet() // Output: Welcome, Guest!
    greet("John") // Output: Welcome, John!
}

Named arguments improve readability:

fun formatText(text: String, upperCase: Boolean, prefix: String = ""): String {
    val result = if (upperCase) text.uppercase() else text
    return "$prefix$result"
}

fun main() {
    println(formatText(text = "hello", upperCase = true, prefix = "Message: "))
    // Output: Message: HELLO
}

Recursion in Kotlin

Functions can call themselves for tasks like factorial computation.

fun factorial(n: Int): Int {
    return if (n == 1) 1 else n * factorial(n - 1)
}

fun main() {
    println(factorial(5)) // Output: 120
}

Lambda Expressions and Anonymous Functions

Lambda Expression

val double = { x: Int -> x * 2 }
fun main() {
    println(double(4)) // Output: 8
}

Anonymous Function

val triple = fun(x: Int): Int {
    return x * 3
}

fun main() {
    println(triple(4)) // Output: 12
}

Coroutines and Suspending Functions

Kotlin supports asynchronous programming using coroutines.

import kotlinx.coroutines.*

suspend fun fetchData(): String {
    delay(1000L)
    return "Data fetched"
}

fun main() = runBlocking {
    println("Fetching...")
    println(fetchData()) // Output: Data fetched
}
}

Conclusion

Kotlin functions are versatile and powerful, making them a key feature of the language. By understanding the basics and exploring advanced topics like higher-order functions, lambdas, and coroutines, you can write efficient and maintainable Kotlin code.

Exploring Kotlin’s Extension Functions in Android

Understanding Kotlin Extension Functions

Kotlin, a modern programming language that runs on the Java Virtual Machine (JVM), offers a powerful feature known as extension functions. These functions allow developers to extend the functionality of existing classes without modifying their source code. This is particularly useful for Android developers looking to enhance the functionality of Android’s numerous classes effortlessly.

What are Extension Functions?

Extension functions in Kotlin are a way to add new functionality to existing classes. They enable you to call new methods on an instance of a class as if they were part of the original class definition. This helps in writing clean, readable, and maintainable code.

fun String.hasVowels(): Boolean {    return this.any { it in "aeiouAEIOU" }}

In the above example, we have defined an extension function hasVowels() for the String class, which checks if a string contains any vowels.

Why Use Extension Functions in Android?

In Android development, extension functions can simplify complex APIs, enhance readability, and provide a more expressive syntax. They allow you to:

  • Add utility functions to existing Android classes.
  • Improve code reusability and organization.
  • Create cleaner and more readable code.

Implementing Extension Functions in Android

Let’s explore an example of using extension functions to simplify an Android API:

fun View.show() {    this.visibility = View.VISIBLE}fun View.hide() {    this.visibility = View.GONE}

The above code adds two extension functions to the View class, allowing you to change the visibility of a view with simple calls like view.show() or view.hide().

Best Practices

While extension functions are powerful, it’s crucial to use them judiciously:

  • Ensure that the function logically belongs to the class being extended.
  • Be cautious of name clashes with existing methods.
  • Document your extension functions for clarity.

Conclusion

Extension functions in Kotlin provide Android developers with a robust tool to enhance code functionality and readability. By understanding and implementing them effectively, you can streamline your development process and create more maintainable applications.

Sealed Classes and When Statements in Kotlin: A Comprehensive Guide

Kotlin, with its modern features and concise syntax, has become the go-to programming language for Android development and beyond. Among its numerous powerful features, sealed classes and when statements stand out as tools that significantly improve code readability, maintainability, and safety. In this blog post, we’ll dive deep into sealed classes and when statements, explore their synergy, and illustrate their practical use cases with real-world examples.


What Are Sealed Classes in Kotlin?

Sealed classes are a special kind of class in Kotlin that are used to represent a restricted hierarchy. Unlike regular classes, they allow you to define a finite set of subclasses within the same file. This feature makes them ideal for modeling scenarios where a type can have a limited set of possible states.

Key Characteristics of Sealed Classes:

  • Restricted Subclassing: Subclasses of a sealed class must be defined in the same file as the sealed class declaration.
  • Type Safety: The compiler knows all possible subclasses at compile time, enabling exhaustive checks.
  • Improved Readability: They make code more readable by clearly representing all possible states.

Syntax:

sealed class Shape {
    data class Circle(val radius: Double) : Shape()
    data class Rectangle(val width: Double, val height: Double) : Shape()
    object Triangle : Shape()
}

In this example, the Shape class is sealed, and its subclasses Circle, Rectangle, and Triangle are defined within the same file. This hierarchy explicitly defines all possible shapes.


The Power of When Statements

The when statement in Kotlin is a more powerful and expressive alternative to the traditional switch statement found in other languages. When used with sealed classes, it shines even brighter by enabling exhaustive pattern matching.

Key Features of When Statements:

  • Pattern Matching: Matches values or types against a set of patterns.
  • Exhaustive Checks: Ensures all possible cases are handled when used with sealed classes.
  • Concise Syntax: Reduces boilerplate code while improving readability.

Syntax:

fun describeShape(shape: Shape): String = when (shape) {
    is Shape.Circle -> "Circle with radius ${shape.radius}"
    is Shape.Rectangle -> "Rectangle with width ${shape.width} and height ${shape.height}"
    Shape.Triangle -> "Triangle"
    // No need for an `else` branch as all cases are covered.
}

Combining Sealed Classes and When Statements

The true power of sealed classes and when statements emerges when they are used together. Let’s explore some real-world scenarios to illustrate their synergy.

1. Modeling UI States in an Application

Sealed classes are perfect for representing different states in a UI, such as loading, success, and error.

Example:

sealed class UiState {
    object Loading : UiState()
    data class Success(val data: String) : UiState()
    data class Error(val message: String) : UiState()
}

fun renderUi(state: UiState): String = when (state) {
    is UiState.Loading -> "Loading..."
    is UiState.Success -> "Data: ${state.data}"
    is UiState.Error -> "Error: ${state.message}"
}

// Usage
val currentState: UiState = UiState.Success("Welcome to Kotlin!")
println(renderUi(currentState))

This approach ensures that all states are accounted for, preventing runtime errors caused by unhandled cases.

2. Handling API Responses

Sealed classes and when statements can simplify error handling and response parsing in API calls.

Example:

sealed class ApiResponse<out T> {
    data class Success<T>(val data: T) : ApiResponse<T>()
    data class Error(val errorCode: Int, val message: String) : ApiResponse<Nothing>()
    object Loading : ApiResponse<Nothing>()
}

fun handleApiResponse(response: ApiResponse<String>) = when (response) {
    is ApiResponse.Success -> "Data received: ${response.data}"
    is ApiResponse.Error -> "Error ${response.errorCode}: ${response.message}"
    ApiResponse.Loading -> "Loading..."
}

// Usage
val response: ApiResponse<String> = ApiResponse.Error(404, "Not Found")
println(handleApiResponse(response))

3. Command Processing

In applications that require processing user commands, sealed classes can represent the command hierarchy.

Example:

sealed class Command {
    object Start : Command()
    object Stop : Command()
    data class SendMessage(val message: String) : Command()
}

fun processCommand(command: Command): String = when (command) {
    Command.Start -> "Starting..."
    Command.Stop -> "Stopping..."
    is Command.SendMessage -> "Sending message: ${command.message}"
}

// Usage
val command: Command = Command.SendMessage("Hello, Kotlin!")
println(processCommand(command))

4. State Management in Finite State Machines

Finite state machines (FSM) can be elegantly modeled using sealed classes.

Example:

sealed class TrafficLight {
    object Red : TrafficLight()
    object Yellow : TrafficLight()
    object Green : TrafficLight()
}

fun nextLight(current: TrafficLight): TrafficLight = when (current) {
    TrafficLight.Red -> TrafficLight.Green
    TrafficLight.Yellow -> TrafficLight.Red
    TrafficLight.Green -> TrafficLight.Yellow
}

// Usage
val currentLight: TrafficLight = TrafficLight.Red
println(nextLight(currentLight))

Advantages of Using Sealed Classes with When

  1. Compile-Time Safety: The compiler ensures all cases are handled, reducing the chances of runtime errors.
  2. Readability: Both sealed classes and when statements provide a clear and concise way to represent complex logic.
  3. Maintainability: Adding new states or cases is straightforward and less error-prone.
  4. Type-Specific Behavior: Each subclass can carry its own data and behavior, making it easier to handle diverse scenarios.

Best Practices

  1. Keep Sealed Classes in a Single File: This enforces the finite hierarchy and improves code organization.
  2. Leverage Data Classes: Use data classes as subclasses to store additional information.
  3. Avoid Overloading: Keep the hierarchy simple and avoid excessive nesting.
  4. Ensure Exhaustiveness: Always use when statements with sealed classes to benefit from compile-time exhaustiveness checks.

Conclusion

Sealed classes and when statements are a match made in Kotlin heaven. They enable developers to write safer, more readable, and maintainable code, especially when dealing with restricted hierarchies and state management. By incorporating these features into your Kotlin projects, you can create robust and expressive codebases that are easier to extend and debug.

Whether you’re managing UI states, handling API responses, or processing commands, sealed classes combined with when statements are tools you’ll reach for time and again. Start using them in your projects today, and experience the Kotlin advantage firsthand!

Exit mobile version