Back to Blog

Koin DI, Repository Pattern & ViewModel in Kotlin Compose Desktop | Lesson 16

Sandy LaneSandy Lane

Video: Koin DI, Repository Pattern & ViewModel in Kotlin Compose Desktop | Lesson 16 by Taught by Celeste AI - AI Coding Coach

Watch full page →

Koin DI, Repository Pattern & ViewModel in Kotlin Compose Desktop

This lesson demonstrates how to build a clean architecture Book Library app using Kotlin Compose Desktop with Koin for dependency injection. You’ll learn to implement the repository pattern, manage UI state reactively with StateFlow in a ViewModel, and wire everything together using Koin modules and injection.

Code

import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.dsl.module

// Data model representing a book
data class Book(val id: Int, val title: String, val author: String, val isFavorite: Boolean = false)

// Repository interface for book data
interface BookRepository {
  fun getBooks(): List<Book>
  fun searchBooks(query: String): List<Book>
  fun toggleFavorite(bookId: Int)
}

// In-memory repository implementation
class InMemoryBookRepository : BookRepository {
  private val books = mutableListOf(
    Book(1, "1984", "George Orwell"),
    Book(2, "Brave New World", "Aldous Huxley"),
    // ... more books
  )

  override fun getBooks() = books.toList()

  override fun searchBooks(query: String) =
    books.filter { it.title.contains(query, ignoreCase = true) || it.author.contains(query, ignoreCase = true) }

  override fun toggleFavorite(bookId: Int) {
    books.find { it.id == bookId }?.let {
      val index = books.indexOf(it)
      books[index] = it.copy(isFavorite = !it.isFavorite)
    }
  }
}

// UI state data class
data class LibraryUiState(
  val books: List<Book> = emptyList(),
  val searchQuery: String = "",
  val selectedBook: Book? = null
)

// ViewModel managing UI state and repository interaction
class LibraryViewModel(private val repository: BookRepository) : KoinComponent {
  private val _uiState = MutableStateFlow(LibraryUiState())
  val uiState: StateFlow<LibraryUiState> = _uiState

  init {
    loadBooks()
  }

  private fun loadBooks() {
    _uiState.value = _uiState.value.copy(books = repository.getBooks())
  }

  fun onSearch(query: String) {
    val filtered = if (query.isBlank()) repository.getBooks() else repository.searchBooks(query)
    _uiState.value = _uiState.value.copy(books = filtered, searchQuery = query)
  }

  fun onToggleFavorite(bookId: Int) {
    repository.toggleFavorite(bookId)
    // Refresh book list after toggle
    onSearch(_uiState.value.searchQuery)
  }

  fun onSelectBook(book: Book) {
    _uiState.value = _uiState.value.copy(selectedBook = book)
  }
}

// Koin modules for DI
val dataModule = module {
  single { InMemoryBookRepository() }
}

val uiModule = module {
  single { LibraryViewModel(get()) }
}

// In Compose, inject ViewModel with koinInject()
// @Composable
// fun LibraryScreen(viewModel: LibraryViewModel = koinInject()) {
//   val uiState by viewModel.uiState.collectAsState()
//   // UI implementation here
// }

// Main.kt initializes KoinApplication with modules
// startKoin {
//   modules(dataModule, uiModule)
// }

Key Points

  • Koin simplifies dependency injection by declaring modules and injecting dependencies like the repository and ViewModel.
  • The repository pattern abstracts data access, allowing easy swapping of implementations and facilitating testing.
  • LibraryViewModel uses StateFlow to expose reactive UI state, enabling Compose to update automatically on changes.
  • ViewModel constructor injection with Koin allows clean separation of concerns and easier unit testing.
  • Compose’s koinInject() function provides seamless injection of ViewModels inside composables for concise UI code.