kotlin multiplatform for beginners

Kotlin Multiplatform for Absolute Beginners – Part 2: Creating Your First Kotlin Multiplatform Project

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:

  • Created a complete KMP project using the official wizard
  • Written your first shared Kotlin class
  • Seen the same code running on both platforms
  • Built the foundation for our Movie Database app

Let’s dive in!

What We’re Building: Movie Database App Overview

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:

  • Browse popular movies
  • Search for specific films
  • View detailed movie information
  • Save favorite movies locally
  • Work offline with cached data

Technical Architecture:

  • Shared Module: Data models, API calls, business logic, local storage
  • Android App: Material Design UI with Jetpack Compose
  • iOS App: Native SwiftUI interface

This gives us a perfect balance of simplicity for learning and complexity for real-world application.

kotlin multiplatform for beginners
kotlin multiplatform for beginners

Step 1: Creating Your KMP Project with the Wizard

The easiest way to start a new Kotlin Multiplatform project is using the official project wizard. Let’s walk through it step by step.

  1. Open your browser and go to kmp.jetbrains.com
  2. Configure your project settings:
    • Project name: MovieDatabase
    • Project ID: com.yourname.moviedatabase (replace yourname with your actual name/company)
    • Platforms: Select both Android and iOS
    • Dependencies: Leave default for now (we’ll add more later)
  3. Download the project:
    • Click “Download” to get a ZIP file
    • Extract it to your desired location
    • Remember where you saved it!

Alternative: Using Android Studio’s New Project Wizard

If you prefer working directly in Android Studio:

  1. Open Android Studio
  2. Click “New Project”
  3. Select “Kotlin Multiplatform” from the templates
  4. Choose “Kotlin Multiplatform App”
  5. Fill in the same details as above
  6. Click “Finish”

Both methods create the same project structure, so use whichever feels more comfortable.

Step 2: Understanding Your New Project Structure

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.

Step 3: Your First Shared Data Class

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.

Creating the Movie Data Class

  1. Navigate to the shared module: shared/src/commonMain/kotlin/
  2. Create a new package: Right-click → New → Package → name it domain.model
  3. Create the Movie class: In the 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?

  • It’s written once but works on both platforms
  • It includes business logic (URL formatting, rating checks)
  • It uses standard Kotlin features that compile to both Android and iOS

Creating Sample Data for Testing

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() }
    }
}

Step 4: Using Shared Code in Android

Now let’s see our shared code in action! First, we’ll use it in the Android app.

Updating the Android MainActivity

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:

  • We’re importing our shared Movie class: import domain.model.Movie
  • We’re using shared sample data: MovieSamples.sampleMovies
  • We’re calling shared methods: movie.getFormattedRating() and movie.isHighlyRated()

Step 5: Using Shared Code in iOS

Now let’s use the same shared code in our iOS app!

Understanding the iOS Bridge

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.

Updating the iOS ContentView

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:

  • We import our shared module: import shared
  • We access the same data: MovieSamples().sampleMovies
  • We call the same methods: movie.getFormattedRating() and movie.isHighlyRated()
  • The UI is completely native SwiftUI, but the logic is shared!

Step 6: Building and Running Your Apps

Now for the moment of truth – let’s build and run both apps to see our shared code in action!

Running the Android App

  1. Select the Android configuration in Android Studio
  2. Choose an emulator or connected device
  3. Click the Run button (green triangle)
  4. Watch your app launch with the movie list!

Running the iOS App (Mac only)

  1. Select the iOS configuration in Android Studio
  2. Click Run – this will open Xcode
  3. In Xcode, select a simulator
  4. Click the Run button
  5. See the same data in native iOS UI!

Step 7: Verifying the Magic

Here’s the amazing part – let’s modify our shared code and see it update on both platforms automatically.

Adding a New Method to Movie

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)}"
}

Using It in Android

Update your Android MovieCard to include:

Text(
    text = movie.getShortDescription(), // New shared method!
    style = MaterialTheme.typography.bodySmall,
    color = MaterialTheme.colorScheme.primary
)

Using It in iOS

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!

Understanding What Just Happened

Let’s take a moment to appreciate the magic we just witnessed:

  1. Single Source of Truth: Our Movie class exists in one place but works everywhere
  2. Shared Business Logic: Methods like getFormattedRating() and isHighlyRated() are implemented once
  3. Native UI: Despite sharing logic, each platform has its own native, platform-appropriate interface
  4. Type Safety: Both platforms get full type safety and IDE support for shared code

Common Issues and Solutions

Here are some issues you might encounter and how to fix them:

Build Error: “Cannot resolve symbol ‘domain'”

Solution: Make sure you’ve created the package structure correctly and the files are in the right location.

iOS App Won’t Build

Solution: Clean and rebuild the project. In Android Studio: Build → Clean Project → Rebuild Project.

Shared Code Not Visible in iOS

Solution: The shared framework needs to be built first. Run the Android app once, then try iOS.

Gradle Sync Issues

Solution: Make sure you’re using compatible versions. The project wizard should set these correctly.

What We’ve Accomplished

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.

What’s Coming Next

In Part 3: Writing and Sharing Core Business Logic, we’ll expand our shared code significantly:

  • Create a proper repository pattern for data management
  • Add more complex business logic with validation and data transformation
  • Implement proper error handling across platforms
  • Set up dependency injection for shared components
  • Create shared ViewModels that both platforms can use

We’ll transform our simple movie list into a robust, well-architected foundation that can handle real-world complexity.

Key Takeaways

  1. KMP projects follow a clear structure with shared and platform-specific code
  2. Writing shared code is just regular Kotlin – nothing special needed
  3. Both platforms get full access to shared logic with complete type safety
  4. UI remains completely native and platform-appropriate
  5. Changes to shared code immediately benefit all platforms

Practice Exercises

Before moving to Part 3, try these exercises to reinforce your learning:

  1. Add a new property to Movie: Add genre: String and update both UIs to display it
  2. Create a new shared method: Add isRecentMovie() that returns true if released in the last 5 years
  3. Add more sample data: Create more movies in MovieSamples
  4. Experiment with the UI: Try different layouts on both platforms using the shared data

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!