Let’s explore how to build your first Kotlin Multiplatform Mobile (KMM) app!

What is Kotlin Multiplatform Mobile?

Think of KMM is like a code-sharing wand that lets you write business logic once in Kotlin and use it on both iOS and Android. It’s like having a universal translator for your code - write once, run everywhere (well, on mobile at least)!

When you use KMM, you get to:

  • Share business logic across platforms
  • Keep native UI for the best user experience
  • Reduce duplicate code and potential bugs
  • Speed up development time

Setting Up Your Environment

First, let’s get your development environment ready. You’ll need:

// Root build.gradle
buildscript {
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20")
        classpath("com.android.tools.build:gradle:8.1.0")
    }
}

Your project structure should look like this:

MyKMMProject/
β”œβ”€β”€ androidApp/
β”œβ”€β”€ iosApp/
└── shared/
    β”œβ”€β”€ src/
        β”œβ”€β”€ commonMain/
        β”œβ”€β”€ androidMain/
        └── iosMain/

Your First Shared Code

Let’s start with something simple - a shared data model:

// commonMain/kotlin/com/example/User.kt
data class User(
    val id: String,
    val name: String,
    val email: String
)

Now, let’s add some platform-specific code:

// commonMain/kotlin/com/example/Platform.kt
expect class Platform() {
    val platform: String
}

// androidMain/kotlin/com/example/Platform.kt
actual class Platform actual constructor() {
    actual val platform: String = "Android ${android.os.Build.VERSION.SDK_INT}"
}

// iosMain/kotlin/com/example/Platform.kt
actual class Platform actual constructor() {
    actual val platform: String = UIDevice.currentDevice.systemName()
}

Networking Made Easy

Here’s how to set up a shared network layer using Ktor:

class ApiClient {
    private val httpClient = HttpClient {
        install(ContentNegotiation) {
            json()
        }
    }
    
    suspend fun fetchUser(id: String): User {
        return httpClient.get("https://api.example.com/users/$id").body()
    }
}

Platform-Specific UI

Android UI

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        setContent {
            UserProfile()
        }
    }
}

@Composable
fun UserProfile() {
    var user by remember { mutableStateOf<User?>(null) }
    
    LaunchedEffect(Unit) {
        user = ApiClient().fetchUser("123")
    }
    
    Column {
        Text(user?.name ?: "Loading...")
        Text(user?.email ?: "")
    }
}

iOS UI

struct ContentView: View {
    @State private var user: User? = nil
    
    var body: some View {
        VStack {
            Text(user?.name ?? "Loading...")
            Text(user?.email ?? "")
        }
        .onAppear {
            ApiClient().fetchUser(id: "123") { fetchedUser in
                user = fetchedUser
            }
        }
    }
}

Common Pitfalls to Watch Out For

  1. The Threading Trap: Remember that each platform handles threading differently. Use Dispatchers.Default for shared code:
class UserRepository {
    suspend fun getUser(id: String) = withContext(Dispatchers.Default) {
        // Safe to call from any platform
        api.fetchUser(id)
    }
}
  1. The Memory Maze: iOS and Android handle memory differently. Use weak references when needed:
// In shared code
class SharedViewModel {
    private val _state = MutableStateFlow<User?>(null)
    val state = _state.asStateFlow()
}
  1. The Platform Puzzle: Don’t access platform-specific APIs in common code:
// DON'T do this in commonMain
fun getPlatformVersion() = Build.VERSION.SDK_INT // Won't work!

// DO this instead
expect fun getPlatformVersion(): String
actual fun getPlatformVersion() = Build.VERSION.SDK_INT.toString()

Real-World Example: Image Loading

Here’s a practical example of an image loader that works across platforms:

interface ImageLoader {
    suspend fun loadImage(url: String): ImageBitmap
}

class AndroidImageLoader : ImageLoader {
    override suspend fun loadImage(url: String): ImageBitmap =
        withContext(Dispatchers.IO) {
            URL(url).openStream().use { 
                BitmapFactory.decodeStream(it).asImageBitmap()
            }
        }
}

class IosImageLoader : ImageLoader {
    override suspend fun loadImage(url: String): ImageBitmap =
        withContext(Dispatchers.Default) {
            NSUrl(string = url)?.let { nsUrl ->
                NSData.dataWithContentsOfURL(nsUrl)?.toUIImage()?.toImageBitmap()
            } ?: throw IllegalArgumentException("Invalid URL")
        }
}

Testing Your KMM Code

Here’s how to test your shared code:

class UserRepositoryTest {
    @Test
    fun testFetchUser() = runTest {
        val repository = UserRepository(mockApi)
        val user = repository.getUser("123")
        assertEquals("John Doe", user.name)
    }
}

Conclusion

KMM can easily help you reduce your cross-platform app development time and effort by a lot!

Also don’t forget to check out the official KMM documentation for more advanced topics.