• Latest Code...

    Featured Post

    Implementing Hilt in a Kotlin Android Jetpack Compose Project with MVVM Architecture

     In modern Android development, maintaining a scalable codebase can be challenging, especially when it comes to dependency management. Hilt,...

    How to Implement Ktor in Android Apps Using MVVM Clean Architecture, Jetpack Compose, and Kotlin Coroutines

    Ktor is a Kotlin-native framework that enables developers to create asynchronous HTTP clients and servers. In this article, we'll walk you through implementing Ktor in an Android project structured around MVVM Clean Architecture, leveraging Jetpack Compose for UI and Kotlin Coroutines and Flow for data handling.



    Prerequisites

    Before starting, ensure you are familiar with:

    • Kotlin programming
    • MVVM Clean Architecture
    • Jetpack Compose for UI
    • Dependency Injection with Hilt
    • Coroutines and Flows

    Step 1: Set Up Your Android Project

    1. Create a new Android project in Android Studio.

      • Choose Jetpack Compose in the project setup wizard.
      • Select Kotlin as the programming language.
    2. Add Dependencies
      Open your app/build.gradle file and include the following libraries:

        
    dependencies {
        // Ktor for HTTP requests
        implementation("io.ktor:ktor-client-core:2.0.0")
        implementation("io.ktor:ktor-client-cio:2.0.0")
        implementation("io.ktor:ktor-client-content-negotiation:2.0.0")
        implementation("io.ktor:ktor-serialization-kotlinx-json:2.0.0")
    
        // Kotlin Coroutines
        implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
    
        // Jetpack Compose
        implementation("androidx.compose.ui:ui:1.5.0")
        implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.1")
    
        // Hilt for Dependency Injection
        implementation("com.google.dagger:hilt-android:2.47")
        kapt("com.google.dagger:hilt-compiler:2.47")
    
        // Testing libraries (optional)
        testImplementation("junit:junit:4.13.2")
        androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.5.0")
    }
    1. Enable Kotlin Serialization
      In your build.gradle (project-level), enable the Kotlin serialization plugin:
    plugins {
        id("org.jetbrains.kotlin.plugin.serialization") version "1.9.10"
    }

    Step 2: Define the Clean Architecture Layers

    Clean Architecture organizes the code into layers to improve scalability and maintainability. These layers include Presentation, Domain, and Data.


    Data Layer

    The Data Layer handles communication with APIs and maps responses into domain models.

    1. Define API Endpoints

    Create a KtorService interface to abstract Ktor HTTP calls:

    interface KtorService {
        suspend fun fetchItems(): List<ItemDto>
    }
    1. Implement KtorService

    Here, we configure Ktor and handle the API interaction:

    class KtorServiceImpl : KtorService {
        private val client = HttpClient(CIO) {
            install(ContentNegotiation) {
                json() // Enable JSON serialization
            }
        }
    
        override suspend fun fetchItems(): List&lt;ItemDto&gt; {
            return client.get("https://api.example.com/items").body()
        }
    }
    1. Define DTO and Mappers

    Define a Data Transfer Object (DTO) to represent API responses and map it to a domain model.

    @Serializable
    data class ItemDto(
        val id: String,
        val name: String
    )
    
    fun ItemDto.toDomain(): Item = Item(id, name)
    1. Repository

    Create a repository to abstract data sources and expose flows:

    class ItemRepository(private val service: KtorService) {
        suspend fun getItems(): Flow<List<Item>> = flow {
            try {
                val response = service.fetchItems()
                emit(response.map { it.toDomain() }) // Convert DTO to domain model
            } catch (e: Exception) {
                emit(emptyList()) // Handle errors gracefully
            }
        }
    }

    Domain Layer

    The Domain Layer contains business logic and is independent of the framework.

    1. Define Domain Model

    Create the domain model for your app:

    data class Item(
        val id: String,
        val name: String
    )
    1. Implement Use Case

    A use case encapsulates a single piece of functionality:

    class GetItemsUseCase(private val repository: ItemRepository) {
        operator fun invoke(): Flow<List<Item>> {
            return repository.getItems()
        }
    }

    Presentation Layer

    The Presentation Layer manages UI state and user interactions.

    1. Create ViewModel

    Use ViewModel to expose data to the UI:

    @HiltViewModel
    class ItemViewModel @Inject constructor(
        private val getItemsUseCase: GetItemsUseCase
    ) : ViewModel() {
        private val _itemsState = MutableStateFlow<List<Item>>(emptyList())
        val itemsState: StateFlow<List<Item>> = _itemsState
    
        init {
            fetchItems()
        }
    
        private fun fetchItems() {
            viewModelScope.launch {
                getItemsUseCase().collect { items ->
                    _itemsState.value = items
                }
            }
        }
    }
    1. Compose UI

    Use Jetpack Compose to create the UI:

    @Composable
    fun ItemListScreen(viewModel: ItemViewModel = hiltViewModel()) {
        val items by viewModel.itemsState.collectAsState()
    
        LazyColumn {
            items(items) { item ->
                Text(
                    text = item.name,
                    style = MaterialTheme.typography.body1,
                    modifier = Modifier.padding(16.dp)
                )
            }
        }
    }

    Step 3: Set Up Dependency Injection

    Use Hilt to inject dependencies across layers.

    1. Setup Hilt

    Annotate your application class:

    @HiltAndroidApp
    class MyApp : Application()
    1. Provide Dependencies

    Create an Hilt module:

    @Module
    @InstallIn(SingletonComponent::class)
    object AppModule {
        @Provides
        fun provideKtorService(): KtorService = KtorServiceImpl()
    
        @Provides
        fun provideRepository(service: KtorService): ItemRepository = ItemRepository(service)
    
        @Provides
        fun provideGetItemsUseCase(repository: ItemRepository): GetItemsUseCase = GetItemsUseCase(repository)
    }

    Step 4: Handle State and Error Management

    In a real-world app, you must handle API states (loading, success, error) gracefully.

    1. Update ViewModel

    Add a state to track the API status:

    data class UiState(
        val isLoading: Boolean = false,
        val items: List<Item> = emptyList(),
        val error: String? = null
    )
    
    @HiltViewModel
    class ItemViewModel @Inject constructor(
        private val getItemsUseCase: GetItemsUseCase
    ) : ViewModel() {
        private val _uiState = MutableStateFlow(UiState())
        val uiState: StateFlow<UiState> = _uiState
    
        init {
            fetchItems()
        }
    
        private fun fetchItems() {
            viewModelScope.launch {
                _uiState.value = UiState(isLoading = true)
                getItemsUseCase().collect { items ->
                    _uiState.value = UiState(items = items)
                }
            }
        }
    }
    1. Compose UI with State

    Display states in the UI:

    @Composable
    fun ItemListScreen(viewModel: ItemViewModel = hiltViewModel()) {
        val state by viewModel.uiState.collectAsState()
    
        when {
            state.isLoading -> CircularProgressIndicator()
            state.error != null -> Text("Error: ${state.error}")
            else -> LazyColumn {
                items(state.items) { item ->
                    Text(text = item.name)
                }
            }
        }
    }

    Step 5: Test Your App

    Run the app and verify that:

    • Items load from the API.
    • UI updates automatically when data changes.

    Conclusion

    By combining Ktor, MVVM Clean Architecture, Jetpack Compose, and Kotlin Coroutines, you create a scalable, testable, and maintainable Android app. Expand on this foundation by adding advanced features like offline caching, user authentication, or detailed error reporting.

    Happy coding! 🚀

    Contact Form

    Name

    Email *

    Message *