Chapters

Hide chapters

Kotlin Multiplatform by Tutorials

Second Edition · Android 14, iOS 17, Desktop · Kotlin 1.9.10 · Android Studio Hedgehog

6. Connecting to Platform-Specific API
Written by Saeed Taheri

Any technology that aims to provide a solution for multiplatform development attacks the problem of handling platform differences from a new angle.

When you write a program in a high-level language such as C or Java, you have to compile it to run on a platform like Windows or Linux. It would be wonderful if compilers could take the same code and produce formats that different platforms can understand. However, this is easier said than done.

Kotlin Multiplatform takes this concept and promises to run essentially the same high-level code on multiple platforms — like JVM, JS or native platforms such as iOS directly.

Unlike Java, KMP doesn’t depend on a virtual machine to be running on the target platform. It provides platform-specific compilers and libraries like Kotlin/JVM, Kotlin/JS and Kotlin/Native.

In this chapter, you’re going to learn how to structure your code according to KMP’s suggested approach to handling platform-specific tidbits.

Reusing Code Between Platforms

Kotlin Multiplatform doesn’t compile the entire shared module for all platforms as a whole. Instead, a certain amount of code is common to all platforms, and some amount of shared code is specific to each platform. For this matter, it uses a mechanism called expect/actual.

In Chapter 1, you got acquainted with those two new keywords. Now, you’re going to dive deeper into this concept.

Think of expect as a glorified interface in Kotlin or protocol in Swift. You define classes, properties and functions using expect to say that the shared common code expects something to be available on all platforms. Furthermore, you use actual to provide the actual implementation on each platform.

Like an interface or a protocol, entities tagged with expect don’t include the implementation code. That’s where the actual comes in.

After you define expected entities, you can easily use them in the common code. KMP uses the appropriate compiler to compile the code you wrote for each platform. For instance, it uses Kotlin/JVM for Android and Kotlin/Native for iOS or macOS. Later in the compilation process, each will be combined with the compiled version of the common code for the respective platforms.

You may ask why you need this in the first place. Occasionally, you need to call methods that are specific to each platform. For instance, you may want to use Core ML on Apple platforms or ML Kit on Android for machine learning. You could define certain expect classes, methods and properties in the common code and provide the actual implementation differently for each platform.

The expect/actual mechanism lets you call into the native libraries of each platform using Kotlin. How cool is that!

Say Hello to Organize

After you create a great app to find an appropriate time for setting up your international meetings, you’ll need a way to make To-dos and reminders for those sessions. Organize will help you do exactly that.

As with many apps you use every day, Organize has a page that shows you the device information the app is running on. If you’ve ever faced a bug in your apps, you know how valuable this information can be when debugging.

As of writing this chapter, Android Studio and the KMM plugin do not use Gradle Version Catalogs by default. However, in order to adopt the most modern approach to managing dependencies with Android Studio, the Organize starter project employs them. It also includes additional platforms and pre-configured dependencies.

Furthermore, similar to the project you created in Section 1, you will be using the Regular framework instead of CocoaPods for iOS framework distribution. While Swift Package Manager(SPM) has become the standard method for managing dependencies when developing for Apple platforms, the KMP team has not yet made SPM an available option.

If you ever decide to create a new project yourself, you can change the iOS framework distribution option on the following page.

Fig. 6.1 — Select Regular framework option for iOS framework distribution
Fig. 6.1 — Select Regular framework option for iOS framework distribution

Updating the Platform Class

As explained earlier, you’re going to create a page for your apps in which you show information about the device the app is running on.

For that matter, most of your work in this chapter relates to the Platform class.

Folder Structure

In Android Studio, choose the Project view in Project Navigator. Inside the shared module, browse through the directory structure.

Fig. 6.2 — Folder structure in Android Studio
Fig. 6.2 — Folder structure in Android Studio

There’s already a file named Platform.kt inside the project, or to be more exact, there are four of them — one for each platform you support, plus one. To make the expect/actual mechanism work, you’ll need to define the expect and actual entities in exactly the same package in each platform.

In the image above, the expect class for Platform is inside the com.yourcompany.organize package under the commonMain directory. The actual implementations for iOS, Android and desktop are inside the same package under iosMain, androidMain and desktopMain, respectively.

Creating the Platform Class for the Common Module

Open Platform.kt inside the commonMain folder. Replace the expect class definition with the following:

expect class Platform() {
  val osName: String
  val osVersion: String

  val deviceModel: String
  val cpuType: String

  val screen: ScreenInfo

  fun logSystemInfo()
}

expect class ScreenInfo() {
  val width: Int
  val height: Int
  val density: Int?
}

By writing this, you’re making a promise to KMP that you’re going to provide this information. As you see, there’s no implementation for anything here. You define what you want, just like an interface or protocol.

Note: You’ve used Kotlin’s shorthand notation for constructor definition by using Platform(). This means KMP now expects you to provide an implementation for the constructor alongside the properties and methods.

You may be surprised that you didn’t define Platform or ScreenInfo as a data class; after all, these classes seem a perfect fit for a data class, since they’re essentially data holders.

The reason is that data classes in Kotlin automatically generate some implementations under the hood. Consequently, you can’t use them here, as expect classes shouldn’t have implementations.

You also can’t define nested classes inside an expect class. Hence, you defined the ScreenInfo class outside the Platform definition. You can also create a new file if you desire. Doing it in the same file would work, too.

In the code gutter, click the yellow rhombus with the letter A in it. This lets you navigate to the actual implementation file for the platforms you defined in the project.

Fig. 6.3 — Navigate to actual implementation files
Fig. 6.3 — Navigate to actual implementation files

If the files aren’t already in their respective places, or you haven’t implemented the actual definition yet, you can put the cursor on the expect class name and press Alt+Enter on the keyboard. Android Studio will help ease the process. This is the case for ScreenInfo, for instance:

Fig. 6.4 — Alt+Enter on expect class name to create actual classes
Fig. 6.4 — Alt+Enter on expect class name to create actual classes

Implementing Platform on Android

Go to the Platform.kt inside the androidMain folder.

You’ll see that Android Studio has already started nagging you to fulfill the promise. After all, KMP is in its infancy, and you know how toddlers are!

Fig. 6.5 — Android Studio errors in actual class
Fig. 6.5 — Android Studio errors in actual class

Replace the entire class definition with this block of code:

//1
actual class Platform actual constructor() {
  //2
  actual val osName = "Android"

  //3
  @androidx.annotation.ChecksSdkIntAtLeast(extension = 0)
  actual val osVersion = "${Build.VERSION.SDK_INT}"

  //4
  actual val deviceModel = "${Build.MANUFACTURER} ${Build.MODEL}"

  //5
  actual val cpuType = Build.SUPPORTED_ABIS.firstOrNull() ?: "---"

  //6
  actual val screen = ScreenInfo()

  //7
  actual fun logSystemInfo() {
    Log.d(
      "Platform",
      "($osName; $osVersion; $deviceModel; ${screen.width}x${screen.height}@${screen.density}x; $cpuType)"
    )
  }
}

// 8
actual class ScreenInfo actual constructor() {
  //9
  private val metrics = Resources.getSystem().displayMetrics

  //10
  actual val width = metrics.widthPixels
  actual val height = metrics.heightPixels
  actual val density: Int? = round(metrics.density).toInt()
}

This seems like a lot of code, but it’s pretty straightforward:

  1. You provide the actual implementation for the Platform as well as its default constructor. Here, you can’t use the shorthand notation as you did in the expect file. You need to explicitly put an actual keyword before the constructor.

  2. For the operating system name, you provided the value "Android" because you know this code will be compiled for the Android part of the shared module.

  3. For the operating system version, you used the SDK version from the Build class in Android. Make sure to let the Android Studio import the needed package: android.os.Build. Since you’re inside the Android part of the shared module, you can freely use any Android-specific API. Since you’re checking the SDK version in this property, you must annotate the property with @androidx.annotation.ChecksSdkIntAtLeast(extension = 0).

  4. For the device model, you used static properties of MANUFACTURER and MODEL from Build.

  5. Thankfully, Build can give you the CPU type of the device using the SUPPORTED_ABIS property. Since the result may be null on some older versions of Android, provide a default value as well.

  6. You initialize an instance of ScreenInfo and store it in screen property.

  7. Like in an interface, you provide function implementation here. For now, you’ll use the Log class in Android to output all the properties to the console. Make sure to import android.util.Log. You can safely unwrap the nullable screen property since you initialized it with a non-null value in the previous part.

  8. You provide the actual implementation for the ScreenInfo as well as its default constructor.

  9. For fetching the screen properties, you’ll need a DisplayMetrics object. You can get that using this block of code. As you see, you can have extra properties or functions inside the actual class. Make sure to import android.content.res.Resources.

  10. You get the screen width, height and density using the metrics property you defined earlier. Import kotlin.math.round to be able to use the round function. For the density property, you’ll need to explicitly write the type, since if you don’t, its type would be non-nullable. Not only that, but you promised this property to be nullable in the expect file, and one should always stick to their promises. The reason this is of a nullable type will be clear when you implement the desktop part.

Next, you’ll implement the iOS-specific code.

Implementing Platform on iOS

When you’re inside an actual file, you can click the yellow rhombus with the letter E in the gutter to go to the expect definition. While inside Platform.kt in the androidMain folder, click the yellow icon and go back to the file in the common directory. From there, click the A icon and go to the iOS actual file.

The basics of the code you’re going to add are the same as before. This time, though, you’re calling into iOS-specific frameworks such as UIKit, Foundation and CoreGraphics to fetch the needed information.

Replace the actual implementation with the following block of code.

actual class Platform actual constructor() {
  //1
  actual val osName = when (UIDevice.currentDevice.userInterfaceIdiom) {
    UIUserInterfaceIdiomPhone -> "iOS"
    UIUserInterfaceIdiomPad -> "iPadOS"
    else -> kotlin.native.Platform.osFamily.name
  }

  //2
  actual val osVersion = UIDevice.currentDevice.systemVersion

  //3
  actual val deviceModel: String
    get() {
      memScoped {
        val systemInfo: utsname = alloc()
        uname(systemInfo.ptr)
        return NSString.stringWithCString(systemInfo.machine, encoding = NSUTF8StringEncoding)
          ?: "---"
      }
    }

  //4
  actual val cpuType = kotlin.native.Platform.cpuArchitecture.name

  //5
  actual val screen = ScreenInfo()

  //6
  actual fun logSystemInfo() {
    NSLog(
      "($osName; $osVersion; $deviceModel; ${screen.width}x${screen.height}@${screen.density}x; $cpuType)"
    )
  }
}

actual class ScreenInfo actual constructor() {
  //7
  actual val width = CGRectGetWidth(UIScreen.mainScreen.nativeBounds).toInt()
  actual val height = CGRectGetHeight(UIScreen.mainScreen.nativeBounds).toInt()
  actual val density: Int? = UIScreen.mainScreen.scale.toInt()
}
  1. There’s a class in UIKit called UIDevice from which you can query information about the currentDevice. In this code, you’re asking for the interface idiom to differentiate between iOS and iPadOS. The UIUserInterfaceIdiom enum has a few more cases. For brevity, you used the Kotlin/Native Platform class to find information in the else block.

  2. You can also use UIDevice to get the OS version.

  3. This is by far the clunkiest piece of code you’ll encounter in this book. But don’t worry: KMP isn’t usually like this. It’s here to demonstrate where things can become intricate. Objective-C at its core is C. In C, Structures (also called structs) are a way to group several related variables into one place. Practically, whenever you want to use a C struct, you’ll need to utilize the cinterop (C Interoperability) package of Kotlin. Using that package has its perks and occasionally, it gets a bit challenging. Here, you’re going to employ a C struct called utsname. Unix fans, rejoice! In this block 3of code, you’re allocating memory using the memScoped block and the alloc() function call. Then, you pass a pointer to the allocated memory space to the uname function, which retrieves the operating system information and fills it inside systemInfo. Subsequently, you convert the C String filled with the machine name to NSString and return it. The cast from NSString to Kotlin String is automatic. Phew!

  4. To obtain the CPU type, you can once again dig into C code, or like here, simply use the Kotlin/Native Platform class.

  5. You initialize an instance of ScreenInfo and store it in screen property.

  6. The function implementation is essentially the same as the Android implementation, except that you’re passing the same string to NSLog function.

  7. You need to combine your knowledge of UIKit and CoreGraphics to get the screen properties. First, you utilize the UIScreen class to fetch information about the mainScreen of the device. Then you use CGRectGetWidth and CGRectGetHeight functions of CoreGraphics to extract the width and height from the nativeBounds property, which is a CGSize Objective-C, or at its core, a C struct. Just like you did on Android, make sure to explicitly specify the type of density. Go ahead and import all missing packages if you haven’t done so already.

Note: Whenever you use the cinterop package, or call into the Kotlin/Native package, you must opt-in, as those APIs are experimental. If you check out the build.gradle.kts file inside the shared module, at the end of the file, you’ll see that these lines are already there for you:

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile>().configureEach {
  compilerOptions.freeCompilerArgs.addAll(
    "-opt-in=kotlinx.cinterop.ExperimentalForeignApi",
    "-opt-in=kotlin.experimental.ExperimentalNativeApi"
  )
}

Also, you need to annotate the classes of functions in which you’ve used such API. Make sure to add these lines before each class definition:

@kotlinx.cinterop.ExperimentalForeignApi  
@kotlin.experimental.ExperimentalNativeApi  
actual class Platform actual constructor() {  
  //...  
}  

@kotlinx.cinterop.ExperimentalForeignApi  
actual class ScreenInfo actual constructor() {  
  //...  
}  

These may look a bit weird for Kotlin and Swift developers. The reason is that you’re using the Objective-C nomenclature for entities. This is how KMP works for Apple platforms. The interoperability is thereby creating a bridge between Kotlin and Objective-C.

The block of code in Section 3 is odd, even for Swift developers. If you were to write this section using Swift, there would also be some travels to the C world:

let deviceModel: String = {
  var systemInfo = utsname()
  uname(&systemInfo)
  let str = withUnsafePointer(to: &systemInfo.machine.0) { ptr in
    return String(cString: ptr)
  }
  return str
}()

Why Objective-C and not Swift, you may ask. Although Swift interoperability is in the works by KMP creators, they chose to go with Objective-C for a couple of reasons:

  1. Many of the iOS frameworks themselves are built with Objective-C. Even when you write Swift code, you’re using a bridge.

  2. Objective-C has a more flexible and dynamic runtime than Swift. Apparently, Swift’s stricter type-safety features would have made the creation of KMP interoperability with Apple technologies more difficult.

Using Objective-C instead of Swift has some issues, though. For instance, you can’t use some newly introduced frameworks such as AppIntents as they’re Swift-only. Furthermore, you can’t use Swift-only extension functions or properties — or even Swift enum cases — either. You have no choice other than to use the verbose naming of entities in Objective-C.

Implementing Platform on Desktop

Open Platform.kt inside the desktopMain folder.

Replace the actual implementation with this block:

actual class Platform actual constructor() {
  //1
  actual val osName = System.getProperty("os.name") ?: "Desktop"

  //2
  actual val osVersion = System.getProperty("os.version") ?: "---"

  //3
  actual val deviceModel = "Desktop"

  //4
  actual val cpuType = System.getProperty("os.arch") ?: "---"

  //5
  actual val screen = ScreenInfo()

  //6
  actual fun logSystemInfo() {
    print("($osName; $osVersion; $deviceModel; ${screen.width}x${screen.height}; $cpuType)")
  }
}

actual class ScreenInfo actual constructor() {
  //7
  private val toolkit = Toolkit.getDefaultToolkit()

  actual val width = toolkit.screenSize.width
  actual val height = toolkit.screenSize.height
  actual val density: Int? = null
}
  1. The desktop app is based on JVM. As a result, you can use JDK classes and methods to get information about the device. There’s a class in Java called System. You can get the operating system name by using the static getProperty method with the "os.name" parameter. Since this method may return null, you provided a default result.
  2. You use the same method as before, but this time with the "os.version" parameter.
  3. You hard-code the value "Desktop". JVM doesn’t provide a way to know anything about the manufacturer and model.
  4. Once again, System class to the rescue! Use "os.arch" as the parameter.
  5. You create an instance of ScreenInfo, as you did on the other platforms.
  6. Next, you use Kotlin’s print function to output the usual info to the console.
  7. You create an instance of Toolkit and query the screen size methods on that object. Unfortunately, this property doesn’t give us the screen density.

Java doesn’t have a UI toolkit in itself. If a platform owner wants to use JVM and provide developers with a way to develop user interfaces, it creates a UI toolkit or uses one already available.

You may have heard about Swing or Abstract Window Toolkit (AWT). Jetpack Compose for Desktop uses Swing internally to make window-based desktop applications. As of writing this book, Jetpack Compose for Desktop doesn’t provide a way to query screen information outside its composable methods. However, you can use AWT methods outside the Compose world. The Toolkit class you used, is inside the java.awt package.

Sharing More Code

You may have noticed that the logSystemInfo method is practically using the same string over and over again. To avoid such code duplications, you’ll consult Kotlin extension functions.

Open Platform.kt inside the commonMain folder. As you know, you can’t add implementation to the properties or functions you defined here. However, no one said you can’t use Kotlin extension functions.

At the end of the file, add this:

val Platform.deviceInfo: String
  get() {
    var result = "($osName; $osVersion; $deviceModel; ${screen.width}x${screen.height}"

    screen.density?.let {
      result += "@${it}x; "
    }

    result += "$cpuType)"
    return result
  }

You’re making the same string, this time, based on the fact that density may be null.

Now go back to the actual files and use this property inside logSystemInfo functions.

In Platform.kt inside androidMain:

actual fun logSystemInfo() {
  Log.d("Platform", deviceInfo)
}

In Platform.kt inside iosMain:

actual fun logSystemInfo() {
  NSLog(deviceInfo)
}

In Platform.kt inside desktopMain:

actual fun logSystemInfo() {
  print(deviceInfo)
}

With this technique, you’re able to share code between the actual implementations.

Updating the UI

Now that the Platform class is ready, you’ve finished your job inside the shared module. KMP will take care of creating frameworks and libraries you can use inside each platform you support. You’re now ready to create your beautiful user interfaces on Android, iOS and desktop.

Android

You’ll do all of your tasks inside the androidApp module. The basic structure of the app is ready for you. Some important files need explaining. These will help you in the coming chapters as well. Here’s what it looks like:

Fig. 6.6 — Folder structure for Android app
Fig. 6.6 — Folder structure for Android app

Inside the root folder, there are AppScaffold.kt and AppNavHost.kt. These two files set up the screens of the app and make the navigation between them work as intended. Please don’t hesitate to take a look if you’re interested.

The app has two main screens: RemindersView, which shows a simple “Hello World” for now, and the AboutView, which you’re going to set up in this chapter. Go ahead and open it.

Here, the ContentView is where everything essential happens. Replace its implementation with the following snippet:

@Composable
private fun ContentView() {
  val items = makeItems()

  LazyColumn(
    modifier = Modifier.fillMaxSize(),
  ) {
    items(items) { row ->
      RowView(title = row.first, subtitle = row.second)
    }
  }
}

Here, you get the items you’d like to show out of the function makeItems and put them inside a LazyColumn, which is basically a list view. Import Modifier, LazyColumn and fillMaxSize from the Compose library. Add the following import for the items method:

import androidx.compose.foundation.lazy.items

You’ll implement RowView soon.

Add the makeItems method below ContentView:

private fun makeItems(): List<Pair<String, String>> {
  //1
  val platform = Platform()

  //2
  val items = mutableListOf(
    Pair("Operating System", "${platform.osName} ${platform.osVersion}"),
    Pair("Device", platform.deviceModel),
    Pair("CPU", platform.cpuType)
  )

  //3
  val max = max(platform.screen.width, platform.screen.height)
  val min = min(platform.screen.width, platform.screen.height)

  var displayInfo = "${max}×${min}"
  platform.screen.density?.let {
    displayInfo += " ${it}x"
  }

  items.add(Pair("Display", displayInfo))


  return items
}
  1. First, you initialize an instance of the Platform class you created earlier. Import it.
  2. Next, you create pairs of data with titles and info from the platform and store them in a mutable list.
  3. You’ll create a textual representation for the screen. Although you know that density property isn’t null on Android, it’s better to be safe than sorry when facing nullable properties.

Import max and min from kotlin.math. And for the final piece of this app, add the RowView composable function as follows:

@Composable
private fun RowView(
  title: String,
  subtitle: String,
) {
  Column(modifier = Modifier.fillMaxWidth()) {
    Column(Modifier.padding(8.dp)) {
      Text(
        text = title,
        style = MaterialTheme.typography.bodySmall,
        color = Color.Gray,
      )
      Text(
        text = subtitle,
        style = MaterialTheme.typography.bodyLarge,
      )
    }
    Divider()
  }
}

This is a simple vertical stack of text items that shows a title and subtitle. You can use predefined material typography values to polish things up. These are similar to the predefined text styles in the iOS Dynamic Type feature. Import the needed classes and methods from the Compose library.

That’s the end of your journey on Android in this chapter. Build and run the app, and take a look at the result.

Tap the i button to take a look at the device properties.

Fig. 6.7 — The first page of Organize on Android
Fig. 6.7 — The first page of Organize on Android
Fig. 6.8 — The About Device page of Organize on Android
Fig. 6.8 — The About Device page of Organize on Android

Next, you’re going to build the iOS app.

iOS

Although no one can stop you from using Android Studio for editing Swift files, it would be smarter to open Xcode.

Inside the iosApp folder in the project’s root directory, open the Xcode project by double-clicking iosApp.xcodeproj.

The ContentView.swift file is the starting page of the application. It’s already there for you. Take a look if you’d like.

Open AboutView.swift and replace the line where it has Text("Hello World") with this:

AboutListView()

Next, create a new SwiftUI view file called AboutListView by pressing Command-N.

Fig. 6.9 — Xcode new file dialog
Fig. 6.9 — Xcode new file dialog

First, import the Shared module at the top of the file:

import Shared

This is the framework KMP created for you. If it gives you an error stating that it’s not found, don’t worry. Building the project will resolve the issue.

Second, add an inner struct in AboutListView to hold the data you’re going to show:

private struct RowItem: Hashable {
  let title: String
  let subtitle: String
}

The conformance to the Hashable protocol is a necessity for the ForEach structure in SwiftUI.

Third, inside the AboutListView struct, add a property to hold a reference to the items you’re going to show from the Platform class.

private let items: [RowItem] = {
  //1
  let platform = Platform()

  //2
  var result: [RowItem] = [
    .init(
      title: "Operating System",
      subtitle: "\(platform.osName) \(platform.osVersion)"
    ),
    .init(
      title: "Device",
      subtitle: platform.deviceModel
    ),
    .init(
      title: "CPU",
      subtitle: platform.cpuType
    )
  ]

  //3
  let width = min(platform.screen.width, platform.screen.height)
  let height = max(platform.screen.width, platform.screen.height)

  var displayValue = "\(width)×\(height)"

  if let density = platform.screen.density {
    displayValue += " @\(density)x"
  }

  result.append(
    .init(
      title: "Display",
      subtitle: displayValue
    )
  )

  //4
  return result
}()
  1. You create an instance of the Platform class.
  2. Next, you create an array of RowItem instances, containing the info from the platform instance.
  3. Then, you calculate the width and height and conditionally unwrap the density property and append the result to the array.
  4. At the end, you return the array you’d like to show on the page.

Finally, replace the content of the body property with this:

var body: some View {
  List {
    ForEach(items, id: \.self) { item in
      VStack(alignment: .leading) {
        Text(item.title)
          .font(.footnote)
          .foregroundStyle(.secondary)
        Text(item.subtitle)
          .font(.body)
          .foregroundStyle(.primary)
      }
      .padding(.vertical, 4)
    }
  }
}

This is a very basic list in SwiftUI. For each item inside the items property, you show a vertical stack of text elements consisting of the title and the subtitle. You also apply a bunch of formatting modifiers such as font and foregroundStyle to make it appear more pleasing to the eye.

Build and run. Then, tap the About button to see the page you created.

Fig. 6.10 — The first page of Organize on iOS
Fig. 6.10 — The first page of Organize on iOS
Fig. 6.11 — The About Device page of Organize on iOS
Fig. 6.11 — The About Device page of Organize on iOS

Desktop

In Section 1, you learned how to share your UI code between Android and desktop. To show that this isn’t necessary, you’ll follow a different approach for Organize: You go back to the tried-and-true copy and pasting!

The setup for the desktop app is a bit different from the Android app, though they both use Jetpack Compose. One difference is that you don’t use the Jetpack Navigation Component on the desktop app. You also open the About Device page in a new window to be more in line with desktop conventions.

Except for a few nuances in the design for the About page, like showing each data item in a Row instead of a Column, the code is the same. It’s there for you in the starter project. Back in Android Studio, Open AboutView.kt from the desktopApp module, locate //2 and //3 and uncomment the code below them.

There are multiple ways to run the desktop app. You can open the Gradle menu on the side under desktopApp ▸ compose desktop and click run.

Fig. 6.12 — Gradle menu, run desktop app
Fig. 6.12 — Gradle menu, run desktop app

The app runs, and it looks mostly like the Android app.

Fig. 6.13 — The first page of Organize on Desktop
Fig. 6.13 — The first page of Organize on Desktop
Fig. 6.14 — The About Device page of Organize on Desktop
Fig. 6.14 — The About Device page of Organize on Desktop

Challenge

Here’s a challenge for you to practice what you learned. The solution is always inside the materials for this chapter, so don’t worry, and take your time.

Challenge: Create a Common Logger

You can call other expect functions inside your expect/actual implementations. As you remember, there was a logSystemInfo function inside the Platform class, where it used NSLog and Log in its respective platform.

Refactor these calls into a new class called Logger. As a bonus, you can add log levels to your implementation.

Key Points

  • You can use the expect/actual mechanism to call into native libraries of each platform using Kotlin.
  • Expect entities behave so much like an interface or protocol.
  • On Apple platforms, Kotlin uses Objective-C for interoperability.
  • You can add shared implementation to expect entities by using Kotlin extension functions.

Where to Go From Here?

Congratulations! You’ve written multiplatform implementations for a class and then used them in native apps on three platforms.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.