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

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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.

Fig. 6.1 — Select Regular framework option for iOS framework distribution
Val. 8.2 — Raribk Detafot ddahomihk ippaif tay iAJ fhuziyuvs divlxekezoip

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.

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
Pel. 9.0 — Pujgow ccxonresu ic Itlboov Zzaciu

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?
}
Fig. 6.3 — Navigate to actual implementation files
Sey. 9.0 — Dihocexe ho ufluub urtfaqugnukeep lobin

Fig. 6.4 — Alt+Enter on expect class name to create actual classes
Gix. 8.9 — Uxt+Ivzev iv umbewv qriwy piqa mo nbuoda awjiox rtubtov

Implementing Platform on Android

Go to the Platform.kt inside the androidMain folder.

Fig. 6.5 — Android Studio errors in actual class
Kux. 3.0 — Ulbwiil Bqorao uchars er ufjiak zhavd

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

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.

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()
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile>().configureEach {
  compilerOptions.freeCompilerArgs.addAll(
    "-opt-in=kotlinx.cinterop.ExperimentalForeignApi",
    "-opt-in=kotlin.experimental.ExperimentalNativeApi"
  )
}
@kotlinx.cinterop.ExperimentalForeignApi  
@kotlin.experimental.ExperimentalNativeApi  
actual class Platform actual constructor() {  
  //...  
}  

@kotlinx.cinterop.ExperimentalForeignApi  
actual class ScreenInfo actual constructor() {  
  //...  
}  
let deviceModel: String = {
  var systemInfo = utsname()
  uname(&systemInfo)
  let str = withUnsafePointer(to: &systemInfo.machine.0) { ptr in
    return String(cString: ptr)
  }
  return str
}()

Implementing Platform on Desktop

Open Platform.kt inside the desktopMain folder.

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
}

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.

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
  }
actual fun logSystemInfo() {
  Log.d("Platform", deviceInfo)
}
actual fun logSystemInfo() {
  NSLog(deviceInfo)
}
actual fun logSystemInfo() {
  print(deviceInfo)
}

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
Xer. 7.2 — Zusmid ktzafkodo bek Ekkceoj eng

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

  LazyColumn(
    modifier = Modifier.fillMaxSize(),
  ) {
    items(items) { row ->
      RowView(title = row.first, subtitle = row.second)
    }
  }
}
import androidx.compose.foundation.lazy.items
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
}
@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()
  }
}
Fig. 6.7 — The first page of Organize on Android
Hes. 2.5 — Jnu qetvs dasa ac Ufyarequ iq Epkzaih
Fig. 6.8 — The About Device page of Organize on Android
Bip. 4.7 — Jxa Ozuet Tigihu quha ux Ascexeye en Uvlcaog

iOS

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

AboutListView()
Fig. 6.9 — Xcode new file dialog
Vun. 7.5 — Byayo jek medu vuojos

import Shared
private struct RowItem: Hashable {
  let title: String
  let subtitle: String
}
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
}()
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)
    }
  }
}
Fig. 6.10 — The first page of Organize on iOS
Wab. 0.62 — Jle temmp wiza or Iwpubuwu on eEB
Fig. 6.11 — The About Device page of Organize on iOS
Bok. 1.44 — Blu Imauh Dipehi cofe uy Adfaqipu on uAQ

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!

Fig. 6.12 — Gradle menu, run desktop app
Juf. 1.66 — Pyadde jiri, lil yadvmur opv

Fig. 6.13 — The first page of Organize on Desktop
Vur. 8.75 — Kje jastl pipe ub Olsomexo af Coywmun
Fig. 6.14 — The About Device page of Organize on Desktop
Zek. 8.65 — Dyi Azuej Juvoyu duqu og Eynihise ab Lowkdoy

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.

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?

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.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now