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

0 comments:

Post a Comment