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
-
Create a new Android project in Android Studio.
- Choose Jetpack Compose in the project setup wizard.
- Select Kotlin as the programming language.
-
Add Dependencies
Open yourapp/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")
}
- Enable Kotlin Serialization
In yourbuild.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.
- Define API Endpoints
Create a KtorService
interface to abstract Ktor HTTP calls:
interface KtorService {
suspend fun fetchItems(): List<ItemDto>
}
- 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<ItemDto> {
return client.get("https://api.example.com/items").body()
}
}
- 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)
- 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.
- Define Domain Model
Create the domain model for your app:
data class Item(
val id: String,
val name: String
)
- 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.
- 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
}
}
}
}
- 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.
- Setup Hilt
Annotate your application class:
@HiltAndroidApp
class MyApp : Application()
- 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.
- 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)
}
}
}
}
- 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! 🚀