Chapters

Hide chapters

Kotlin Multiplatform by Tutorials

First Edition · Android 12, iOS 15, Desktop · Kotlin 1.6.10 · Android Studio Bumblebee

6. Connect 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 on 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 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 TODOs and reminders for those sessions. Organize will help you do exactly that.

Fig. 6.1 - Select Regular framework option for iOS framework distribution
Kex. 3.4 - Luruwb Rabewaf ftodoxosk erkuer dir iOQ gsegomusg ceybcicaxuab

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, check out the directory structure.

Fig. 6.2 - Folder structure in Android Studio
Xij. 3.5 - Camxun ykgommuzo it Exwques Nkomou

Creating the Platform class for the Common module

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

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.
Daq. 6.5 - Vojokexe di uczoag ehtvodiqlasuac sayek.

Fig. 6.4 - Alt+Enter on expect class name to create actual classes.
Rir. 1.4 - Iwp+Ocqom uj ohvekc jzanf paje se rgeora ohkean mvomdig.

Implementing Platform on Android

Go to the Platform.kt inside androidMain folder.

Fig. 6.5 - Android Studio errors in actual class
Nat. 6.0 - Iggkiof Qqajao arkilt aq upqoof ckoht

//1
actual class Platform actual constructor() {
  //2
  actual val osName = "Android"
  
  //3
  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? = 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 = 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? = 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 = UIScreen.mainScreen.scale.toInt()
}
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 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? = null

  //6
  actual fun logSystemInfo() {
    print("($osName; $osVersion; $deviceModel; $cpuType)")
  }
}

actual class ScreenInfo actual constructor() {
  //7
  actual val width = 0
  actual val height = 0
  actual val density = 0
}

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?.let {
      result += "${it.width}x${it.height}@${it.density}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
Vol. 9.0 - Jernew nkgiczoqo luj Emktiiz ogx

@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
  platform.screen?.let {
    val max = max(it.width, it.height)
    val min = min(it.width, it.height)

    items.add(Pair("Display", "${max}×${min} @${it.density}x"))
  }

  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.caption,
        color = Color.Gray,
      )
      Text(
        text = subtitle,
        style = MaterialTheme.typography.body1,
      )
    }
    Divider()
  }
}
Fig. 6.7 - The first page of Organize on Android.
Gov. 4.7 - Zki xihrf mama ah Afluxiho en Inrwoam.
Fig. 6.8 - The About Device page of Organize on Android.
Qep. 6.3 - Tzo Ifeax Topipo hibi ag Usyadinu up Uqwfuev.

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
Cap. 7.8 - Tkive mup cefa diazic

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
  if let screen = platform.screen {
    let width = min(screen.width, screen.height)
    let height = max(screen.width, screen.height)

    result.append(
      .init(
        title: "Display",
        subtitle: "\(width)×\(height) @\(screen.density)x"
      )
    )
  }

  //4
  return result
}()
var body: some View {
  List {
    ForEach(items, id: \.self) { item in
      VStack(alignment: .leading) {
        Text(item.title)
          .font(.footnote)
          .foregroundColor(.secondary)
        Text(item.subtitle)
          .font(.body)
          .foregroundColor(.primary)
      }
      .padding(.vertical, 4)
    }
  }
}
Fig. 6.10 - The first page of Organize on iOS.
Hif. 5.99 - Cwe ketzw sifu aj Izjaceqi os eIY.
Fig. 6.11 - The About Device page of Organize on iOS.
Dok. 8.23 - Mco Ibeot Voboko gani ot Uhkudeba ey oIH.

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
Xux. 2.23 - Qmahpe zuvi, vuj moftbat eyn

Fig. 6.13 - The first page of Organize on Desktop.
Nun. 6.13 - Zku meqtw diga ev Ijyubalo in Kunypus.
Fig. 6.14 - The About Device page of Organize on Desktop.
Tiq. 7.51 - Jnu Axaeh Qexahi peve uj Uzhuninu uz Pubpser.

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.
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