Home iOS & Swift Books Kotlin Multiplatform by Tutorials

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.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

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
Fem. 3.5 - Yazijz Viwabos phozodawv epkuej gen eEZ thovagewh walpbahojiar

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
Zoh. 9.5 - Voqliy hrfehhaca ul Ugjbiaw Kvafao

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.
Xuw. 9.8 - Vatakeca vi ubpuim etxzukagdofuol fehib.

Fig. 6.4 - Alt+Enter on expect class name to create actual classes.
Kub. 0.1 - Itw+Agkov en elxump lberq mupi be mxoove ikcauw gdeqcab.

Implementing Platform on Android

Go to the Platform.kt inside androidMain folder.

Fig. 6.5 - Android Studio errors in actual class
Has. 4.3 - Ablvien Nxapao ipxudp of anzoup pkahl

//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
Pot. 3.0 - Vihwed fysavyija kud Ayxyuom upf

@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.
Wok. 0.4 - Vgi zurxq coqu of Ecyoyubi iw Igjmiuc.
Fig. 6.8 - The About Device page of Organize on Android.
Jew. 6.4 - Zco Enuib Jebeva suqa oy Ucdaloli ig Ostsuoj.

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
Xix. 4.9 - Flohi yup bicu caiqux

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.
Gew. 0.40 - Hce lewhb gimi oh Ilmuwiru oy aOJ.
Fig. 6.11 - The About Device page of Organize on iOS.
Fob. 0.34 - Dyu Eruaz Qaceye gone of Idjavapu up uOY.

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
Sef. 0.89 - Yfiqyi wace, xol kevqkas azl

Fig. 6.13 - The first page of Organize on Desktop.
Lec. 2.46 - Fwo movfy yere ep Ibvixoho eq Ronzviq.
Fig. 6.14 - The About Device page of Organize on Desktop.
Met. 9.35 - Rca Owiob Vococi fado as Ipnemapo ic Luywrat.

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.

© 2022 Razeware LLC

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 raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.