Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Welcome back to our comprehensive Kotlin Multiplatform tutorial series! In Part 1, we covered the fundamentals and set up our development environment. Now it’s time for the exciting part – creating our very first KMP project and writing code that runs on both Android and iOS.
By the end of this tutorial, you’ll have:
Let’s dive in!
Before we start coding, let’s clarify what we’re building throughout this series. Our Movie Database app will be a simple but complete application that demonstrates all the key KMP concepts:
Core Features:
Technical Architecture:
This gives us a perfect balance of simplicity for learning and complexity for real-world application.
The easiest way to start a new Kotlin Multiplatform project is using the official project wizard. Let’s walk through it step by step.
MovieDatabase
com.yourname.moviedatabase
(replace yourname
with your actual name/company)Android
and iOS
If you prefer working directly in Android Studio:
Both methods create the same project structure, so use whichever feels more comfortable.
Once you have your project, let’s take a tour of what was created. Open the project in Android Studio and you’ll see this structure:
MovieDatabase/
├── shared/ # Our shared business logic
│ ├── src/
│ │ ├── commonMain/kotlin/ # Code for ALL platforms
│ │ ├── androidMain/kotlin/ # Android-specific code
│ │ ├── iosMain/kotlin/ # iOS-specific code
│ │ └── commonTest/kotlin/ # Shared tests
│ └── build.gradle.kts # Shared module configuration
├── androidApp/ # Android application
│ ├── src/main/java/ # Android UI and platform code
│ └── build.gradle.kts
├── iosApp/ # iOS application
│ ├── iosApp.xcodeproj
│ └── iosApp/ # Swift/SwiftUI code
├── gradle/ # Gradle wrapper files
└── build.gradle.kts # Root project configuration
Key folders to understand:
shared/commonMain/
: This is where we’ll spend most of our time. Any Kotlin code here can be used by both Android and iOS.androidApp/
: Your standard Android app. It imports the shared module and adds Android-specific UI.iosApp/
: Your iOS app written in Swift. It also uses the shared module but with native iOS UI.Now for the exciting part – let’s write our first piece of shared code! We’ll create a Movie
data class that both our Android and iOS apps can use.
shared/src/commonMain/kotlin/
domain.model
model
package, create a new Kotlin file called Movie.kt
Here’s our first shared data class:
// shared/src/commonMain/kotlin/domain/model/Movie.kt
package domain.model
/**
* Represents a movie with basic information.
* This data class is shared between Android and iOS platforms.
*/
data class Movie(
val id: Int,
val title: String,
val overview: String,
val releaseDate: String,
val posterPath: String?,
val backdropPath: String?,
val voteAverage: Double,
val voteCount: Int,
val isAdult: Boolean = false
) {
/**
* Returns the full poster image URL
*/
fun getPosterUrl(): String? {
return posterPath?.let { "https://image.tmdb.org/t/p/w500$it" }
}
/**
* Returns the full backdrop image URL
*/
fun getBackdropUrl(): String? {
return backdropPath?.let { "https://image.tmdb.org/t/p/w780$it" }
}
/**
* Returns a formatted rating string
*/
fun getFormattedRating(): String {
return "⭐ ${String.format("%.1f", voteAverage)} ($voteCount votes)"
}
/**
* Checks if this movie has a good rating
*/
fun isHighlyRated(): Boolean = voteAverage >= 7.0
}
What makes this code special?
Let’s also create some sample movies to test with. Create another file called MovieSamples.kt
:
// shared/src/commonMain/kotlin/domain/model/MovieSamples.kt
package domain.model
/**
* Sample movie data for testing and development
*/
object MovieSamples {
val sampleMovies = listOf(
Movie(
id = 1,
title = "The Shawshank Redemption",
overview = "Two imprisoned officers bond over a number of years, finding solace and eventual redemption through acts of common decency.",
releaseDate = "1994-09-23",
posterPath = "/q6y0Go1tsGEsmtFryDOJo3dEmqu.jpg",
backdropPath = "/iNh3BivHyg5sQRPP1KOkzguEX0H.jpg",
voteAverage = 9.3,
voteCount = 8358,
isAdult = false
),
Movie(
id = 2,
title = "The Godfather",
overview = "The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son.",
releaseDate = "1972-03-14",
posterPath = "/3bhkrj58Vtu7enYsRolD1fZdja1.jpg",
backdropPath = "/tmU7GeKVybMWFButWEGl2M4GeiP.jpg",
voteAverage = 9.2,
voteCount = 6024,
isAdult = false
),
Movie(
id = 3,
title = "The Dark Knight",
overview = "When the menace known as the Joker wreaks havoc and chaos on the people of Gotham, Batman must accept one of the greatest psychological and physical tests of his ability to fight injustice.",
releaseDate = "2008-07-18",
posterPath = "/qJ2tW6WMUDux911r6m7haRef0WH.jpg",
backdropPath = "/qlGoGQSVMzIjGbpvXzZUOH1FjNu.jpg",
voteAverage = 9.0,
voteCount = 12269,
isAdult = false
)
)
/**
* Get a random sample movie
*/
fun getRandomMovie(): Movie = sampleMovies.random()
/**
* Get highly rated movies only
*/
fun getHighlyRatedMovies(): List<Movie> {
return sampleMovies.filter { it.isHighlyRated() }
}
}
Now let’s see our shared code in action! First, we’ll use it in the Android app.
Navigate to androidApp/src/main/java/
and find your MainActivity.kt
. Replace its contents with:
// androidApp/src/main/java/com/yourname/moviedatabase/android/MainActivity.kt
package com.yourname.moviedatabase.android
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import domain.model.Movie
import domain.model.MovieSamples
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
MovieListScreen()
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MovieListScreen() {
Column {
TopAppBar(
title = { Text("Movie Database") }
)
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(MovieSamples.sampleMovies) { movie ->
MovieCard(movie = movie)
}
}
}
}
@Composable
fun MovieCard(movie: Movie) {
Card(
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = movie.title,
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = movie.overview,
style = MaterialTheme.typography.bodyMedium,
maxLines = 3
)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = movie.getFormattedRating(), // Using shared logic!
style = MaterialTheme.typography.bodySmall
)
if (movie.isHighlyRated()) { // Using shared logic!
AssistChip(
onClick = { },
label = { Text("Highly Rated") }
)
}
}
Text(
text = "Released: ${movie.releaseDate}",
style = MaterialTheme.typography.bodySmall
)
}
}
}
@Preview
@Composable
fun DefaultPreview() {
MyApplicationTheme {
MovieCard(movie = MovieSamples.sampleMovies.first())
}
}
Notice the magic happening:
Movie
class: import domain.model.Movie
MovieSamples.sampleMovies
movie.getFormattedRating()
and movie.isHighlyRated()
Now let’s use the same shared code in our iOS app!
First, let’s understand how iOS accesses our Kotlin code. When you build the project, Kotlin Multiplatform automatically generates a framework that iOS can use. All your shared classes become available as Swift classes.
Navigate to iosApp/iosApp/ContentView.swift
and replace its contents:
// iosApp/iosApp/ContentView.swift
import SwiftUI
import shared
struct ContentView: View {
// Access our shared sample data
private let movies = MovieSamples().sampleMovies
var body: some View {
NavigationView {
List(movies, id: \.id) { movie in
MovieRow(movie: movie)
}
.navigationTitle("Movie Database")
}
}
}
struct MovieRow: View {
let movie: Movie
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(movie.title)
.font(.headline)
.fontWeight(.bold)
Text(movie.overview)
.font(.body)
.lineLimit(3)
HStack {
// 🎯 Using shared logic!
Text(movie.getFormattedRating())
.font(.caption)
.foregroundColor(.secondary)
Spacer()
// Using shared logic!
if movie.isHighlyRated() {
Text("Highly Rated")
.font(.caption)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Color.blue.opacity(0.2))
.cornerRadius(8)
}
}
Text("Released: \(movie.releaseDate)")
.font(.caption)
.foregroundColor(.secondary)
}
.padding(.vertical, 4)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Key observations:
import shared
MovieSamples().sampleMovies
movie.getFormattedRating()
and movie.isHighlyRated()
Now for the moment of truth – let’s build and run both apps to see our shared code in action!
Here’s the amazing part – let’s modify our shared code and see it update on both platforms automatically.
Go back to your Movie.kt
file and add this method:
/**
* Returns a short description with rating
*/
fun getShortDescription(): String {
val rating = if (isHighlyRated()) "⭐ Excellent" else "👍 Good"
return "$rating • ${releaseDate.take(4)}"
}
Update your Android MovieCard
to include:
Text(
text = movie.getShortDescription(), // New shared method!
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary
)
Update your iOS MovieRow
to include:
Text(movie.getShortDescription()) // Same shared method!
.font(.caption)
.foregroundColor(.blue)
Build and run both apps again – you’ll see the same new functionality working on both platforms!
Let’s take a moment to appreciate the magic we just witnessed:
Movie
class exists in one place but works everywheregetFormattedRating()
and isHighlyRated()
are implemented onceHere are some issues you might encounter and how to fix them:
Solution: Make sure you’ve created the package structure correctly and the files are in the right location.
Solution: Clean and rebuild the project. In Android Studio: Build → Clean Project → Rebuild Project.
Solution: The shared framework needs to be built first. Run the Android app once, then try iOS.
Solution: Make sure you’re using compatible versions. The project wizard should set these correctly.
Congratulations! You’ve just:
-Created your first complete Kotlin Multiplatform project.
-Written shared data classes that work on both platforms.
-Implemented business logic that’s automatically available everywhere
-Built native UI on both Android and iOS
-Seen the same code running on multiple platforms
This is the foundation of everything we’ll build in this series. You now have a working KMP project with shared business logic and platform-specific UI.
In Part 3: Writing and Sharing Core Business Logic, we’ll expand our shared code significantly:
We’ll transform our simple movie list into a robust, well-architected foundation that can handle real-world complexity.
Before moving to Part 3, try these exercises to reinforce your learning:
genre: String
and update both UIs to display itisRecentMovie()
that returns true if released in the last 5 yearsMovieSamples
Great job! You’ve successfully created your first Kotlin Multiplatform project and seen shared code running on both platforms. This is just the beginning – in Part 3, we’ll dive deeper into sharing complex business logic and building a robust architecture.
Have questions about project setup or sharing code? Drop them in the comments, and I’ll help you get everything working perfectly!