Compose MultiplatformSeries • 4/4

Adding Real State Management (Favorites Feature)

Refine your Quotes app with proper state management using ViewModel-style approach in shared code. Learn how to centralize state and manage favorites consistently across all platforms.

Introduction

We've got our Quotes app running across Android, iOS, and Desktop with shared UI. In this final part, let's refine the favorite/unfavorite feature and introduce proper state management.

Instead of holding mutable lists directly in the composable, we'll manage state using a ViewModel-style approach in shared code.

Step 1: Create a Quote Model

We already have a basic model:

Quote.ktkotlin
1data class Quote(
2 val id: Int,
3 val text: String,
4 val author: String,
5 val isFavorite: Boolean = false
6)

Adding an id helps with stable updates.

Step 2: Create a ViewModel (Shared Logic)

Inside commonMain, add:

QuotesViewModel.ktkotlin
1class QuotesViewModel {
2 private val _quotes = mutableStateListOf(
3 Quote(1, "Compose once, run anywhere.", "JetBrains"),
4 Quote(2, "Simplicity is the soul of efficiency.", "Austin Freeman"),
5 Quote(3, "Stay hungry, stay foolish.", "Steve Jobs")
6 )
7 val quotes: SnapshotStateList<Quote> = _quotes
8
9
10 fun toggleFavorite(id: Int) {
11 val index = _quotes.indexOfFirst { it.id == id }
12 if (index != -1) {
13 _quotes[index] = _quotes[index].copy(isFavorite = !_quotes[index].isFavorite)
14 }
15 }
16}

👉 This centralizes the app's state in one place.

Step 3: Update the Screen

Now our UI observes the ViewModel's state instead of owning it directly.

QuotesScreen.ktkotlin
1@Composable
2fun QuotesScreen(viewModel: QuotesViewModel = QuotesViewModel()) {
3 val quotes = viewModel.quotes
4
5 LazyColumn {
6 items(quotes.size) { index ->
7 val quote = quotes[index]
8 QuoteCard(
9 text = quote.text,
10 author = quote.author,
11 isFavorite = quote.isFavorite
12 ) {
13 viewModel.toggleFavorite(quote.id)
14 }
15 }
16 }
17}

Step 4: Run It

On Desktop → Run Window { QuotesScreen() }.

On Android → setContent { QuotesScreen() }.

On iOS → ComposeUIViewController { QuotesScreen() }.

Now, toggling a favorite updates everywhere consistently.

Outro

And that's a wrap 🎉 You've built a cross-platform Quotes App with real state management:

  • One shared UI codebase.
  • ViewModel-style state handling.
  • Favorites that persist across recompositions.

From here, you could extend this with persistence (SQLDelight), or live data sources (Ktor), but the foundation is already solid.

Final Result

Here's what our completed Quotes app looks like on Android:

Quotes of the Day app showing three quotes with favorite/unfavorite buttons

🎉 Series Complete! You've successfully built a complete Compose Multiplatform app with shared UI and state management across Android, iOS, and Desktop.