Chapters

Hide chapters

Kotlin Multiplatform by Tutorials

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

A. Appendix A: Kotlin: A Primer for Swift Developers
Written by Carlos Mota

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

Kotlin is a language developed by JetBrains that gained popularity and wide adoption when Google announced that from that point on, all of their Android libraries would no longer be written in Java. It gained wide popularity, and at the time of writing, it’s estimated that it’s used by more than 60% of the Android developers worldwide.

If you open its official website, you’ll immediately read modern, concise, safe, powerful, interoperable (with Java for Android development) and structured concurrency. All of these keywords are functionalities that developers look for in any programming language, and Kotlin has all of them.

Even more importantly, Kotlin is not only for Android. It also supports Web front-end, server-side and — the focus of your work throughout this book — Multiplatform.

Kotlin and Swift: Comparing both languages

The syntax between both languages is quite similar. If you’re a Swift developer, you can easily program in Kotlin. This appendix shows you how to start.

The examples shown in this appendix are code snippets from learn. A final version of the project is available in the materials repository.

Basics

In this section, you’ll learn the Kotlin basics — or as Swift developers are familiar with, its foundations. :]

Package declaration

The extension of a Kotlin file is .kt. The tree hierarchy of a Multiplatform project typically follows the Android naming convention for package names — you’ve got three folder levels. In learn, it’s com/raywenderlich/learn, and they usually correspond to:

package com.raywenderlich.learn.presentation

Imports

Typically, when an import is missing, Android Studio shows you a prompt with one or more suggestions, so you shouldn’t have any issues. In any case, if you want to add one manually, you need to add it after the package declaration:

import com.raywenderlich.learn.data.model.GravatarEntry

Comments

Similar to Swift, you can add three types of comments:

public fun fetchMyGravatar(cb: FeedData) {
  //Logger.d(TAG, "fetchMyGravatar")

  //Update the current listener with the new one.
  listener = cb
  fetchMyGravatar()
}
 /*
 * Copyright (c) 2021 Razeware LLC
 *
 */ 
/**
 * This method fetches your Gravatar profile.
 *
 * @property cb, the callback used to notify the UI that the 
 * profile was successfully fetched or not.
 */
public fun fetchMyGravatar(cb: FeedData) {
  //Your code goes here
}

Variables

Similar to Swift, in Kotlin you also have two types of variables:

private val scope = PresenterCoroutineScope(defaultDispatcher)
private var listener: FeedData? = null
private var listener: FeedData? = nil

Lazy initialization

Kotlin supports lazy initialization through the use of the lazy keyword. This variable needs to be immutable — in other words, you need to declare it as val.

val content: List<RWContent> by lazy {
  json.decodeFromString(RW_CONTENT)
} 

Late initialization

You can delay the initialization of a variable until your app needs it. For that, you need to set it as lateinit, and it can’t be set as immutable or null.

private lateinit var listener: FeedData
onSuccess = { listener?.onNewDataAvailable(it, platform, null) },
onFailure = { listener?.onNewDataAvailable(emptyList(), platform, it) }
if (::listener.isInitialized) {
  //Do something      
}
private var listener: FeedData!
if listener != nil {
  //Do something
}

Nullability

Perhaps the most known trait of Kotlin is its nullability. Ideally, there are no more NullPointerExceptions — in other words, exceptions triggered by calls to objects that don’t exist. The word “ideally” is needed here since developers have the final word and can always go against what the language advises.

listener.onMyGravatarData(GravatarEntry())
listener!!.onMyGravatarData(GravatarEntry())
listener?.onMyGravatarData(GravatarEntry())
listener?.let {
  it.onMyGravatarData(GravatarEntry())
}
if let listener = listener {
  listener.onMyGravatarData(GravatarEntry())
}

String interpolation

With string interpolation, you can easily concatenate strings and variables together. On fetchAllFeeds, you’ll iterate over content and call fetchFeed with the platform and feed URL. Before this block of code, add:

Logger.d(TAG, "Fetching feed: ${feed.platform}")
Logger.d(TAG, "Fetching feed: $feed.platform")

Type inference

If you declare a variable and assign it a specific value, you don’t need to define its type. Kotlin is capable of inferring it in most cases. If you look at the variables declared at FeedPresenter.kt, you can see that json uses type inference, but content doesn’t.

val content by lazy {
  json.decodeFromString<List<RWContent>>(RW_CONTENT)
} 

Type checks

Both languages use the is to check if an object is from a specific type.

Cast

Casting a variable is similar in both languages. You just need to use the keyword as followed by the type of the class that you want to cast.

Converting between different types

You can easily convert between primitive types by calling .to*() for the type that you want:

// Convert String to Integer
"raywenderlich".toInt()

// Convert String to Long
"raywenderlich".toLong()

// Convert String to Float
"raywenderlich".toFloat()

// Convert Int to String
42.toString()

// Convert Int to Long
42.toLong()

// Convert Int to Float
42.toFloat()

Extension functions

As the name suggests, extension functions allow you to create additional behaviors for existing classes. Imagine that you want to add a method that needs to be available for all String objects, and it should return “Ray Wenderlich” when called:

fun String.toRW(): String {
  return "Ray Wenderlich"
}
init {
  Logger.d(TAG, "content=${RW_CONTENT.toRW()}")
}
FeedPresenter | content=Ray Wenderlich

Comparing objects

You can compare objects by reference through the use of === or by content ==.

Control flow

Although the syntax is quite similar in both languages, you’ll find that Kotlin gives you powerful expressions that you can use.

ifelse

This condition check is similar in both languages. If you open GetFeedData.kt, you can see different functions that use ifelse.

if (parsed != null) {
  feed += parsed
}
if (parsed != null)
  feed += parsed
if (parsed != null) feed += parsed

switch

It doesn’t exist in Kotlin. Alternatively, you can use when which is similar.

when

when is a condition expression that supports multiple and different expressions. You can see an example of how to use it on the ImagePreview.kt file, which is inside the components folder of the androidApp:

when (painter.state) {
  is ImagePainter.State.Loading -> {
    AddImagePreviewEmpty(modifier)
  }
  is ImagePainter.State.Error -> {
    AddImagePreviewError(modifier)
  }
  else -> {
    // Do nothing
  }
}

for

Back to the FeedPresenter.kt file from the shared module. You can find the for loop on fetchAllFeeds:

for (feed in content) {
  fetchFeed(feed.platform, feed.url)
}
for (index in content.indices) {
  val feed = content[index]
  fetchFeed(feed.platform, feed.url)
}
for ((index, feed) in content.withIndex()) {
  fetchFeed(feed.platform, feed.url)
}
for (index in 0..content.size) {
  val feed = content[index]
  fetchFeed(feed.platform, feed.url)
}

while

The while and dowhile loops are similar to Swift. You just need to add the condition that should end the cycle and the code that should run while it isn’t met.

while (condition) {
  //Do something
}

do {
  //Something
} while (condition)
while condition {
  //Do something
}

repeat {
  //Something
} while condition

Ternary operator

It doesn’t exist in Kotlin. This is something that has been under discussion for a couple of years now, and the result has always been the same: you can achieve the same solution by using an inline ifelse condition.

Collections

Kotlin supports different types of collections: arrays, lists and maps. These are immutable by default, but you can use their mutable counterpart by using: mutableList and mutableMap.

Lists

You can easily create a list in Kotlin from a source set by calling listOf and add the items as parameters. You can see an example where this is done on the MainScreen.kt file inside the androidApp/main folder:

val bottomNavigationItems = listOf(
  BottomNavigationScreens.Home,
  BottomNavigationScreens.Bookmark,
  BottomNavigationScreens.Latest,
  BottomNavigationScreens.Search
)
val bottomNavigationItems = mutableListOf(
  BottomNavigationScreens.Home,
  BottomNavigationScreens.Bookmark,
  BottomNavigationScreens.Latest,
  BottomNavigationScreens.Search
)
val bottomNavigationItems = listOf(
  BottomNavigationScreens.Home,
  BottomNavigationScreens.Bookmark,
  BottomNavigationScreens.Latest,
  BottomNavigationScreens.Search
).toMutableList()
bottomNavigationItems.remove(BottomNavigationScreens.Search)
bottomNavigationItems -= BottomNavigationScreens.Search

Arrays

Arrays are mutable, but they have fixed size. Once you’ve created one, you can’t add or remove elements. Instead, you change its content. Using the previous example, you can create an arrayOf with an initial number of items:

val bottomNavigationItems = arrayOf(
  BottomNavigationScreens.Home,
  BottomNavigationScreens.Bookmark,
  BottomNavigationScreens.Latest,
  BottomNavigationScreens.Search
) 
bottomNavigationItems[0] = BottomNavigationScreens.Bookmark
bottomNavigationItems[1] = BottomNavigationScreens.Home

Maps (Swift dictionaries)

Similar to what you’ve read in the examples above, you can create a map using mapOf function. It receives a Pair of objects that you can add or remove.

val bottomNavigationItems = mapOf(
  0 to BottomNavigationScreens.Home,
  1 to BottomNavigationScreens.Bookmark,
  2 to BottomNavigationScreens.Latest,
  3 to BottomNavigationScreens.Search
)
// Returns BottomNavigationScreens.HOME
bottomNavigationItems[0]

// Returns BottomNavigationScreens.HOME
bottomNavigationItems.get(0)
val bottomNavigationItems = mutableMapOf(
  0 to BottomNavigationScreens.Home,
  1 to BottomNavigationScreens.Bookmark,
  2 to BottomNavigationScreens.Latest,
  3 to BottomNavigationScreens.Search
)
val bottomNavigationItems = mapOf(
  0 to BottomNavigationScreens.Home,
  1 to BottomNavigationScreens.Bookmark,
  2 to BottomNavigationScreens.Latest,
  3 to BottomNavigationScreens.Search
).toMutableMap()

Extra functionalities

All of these collections also provide a set of functions that allow you to easily iterate and filter objects. Here’s a short list of the ones that you might use daily:

Classes and objects

You can use different approaches to define class and objects in Kotlin depending on your use case.

Classes

You can create a class by using the keyword class followed by its name and any parameters that it might receive. If you open the FeedPresenter.kt file, you’ll see:

class FeedPresenter(private val feed: GetFeedData)

Data classes

You can create a data class by using the keyword data before declaring a class. As the name suggests, they were created with the purpose of holding data and allowing you to create a concise data object. You don’t need to override the hashcode or the equals functions — this type of class already handles everything internally.

data class RWContent(
  val platform: PLATFORM,
  val url: String,
  val image: String
)

Sealed classes

If you define a class or an interface as sealed, you can’t extend it outside its package. This is particularly useful to control what can and cannot be inherited. Open the BottomNavigationScreens.kt file inside ui/main in the androidApp:

sealed class BottomNavigationScreens(
  val route: String,
  @StringRes val stringResId: Int,
  @DrawableRes val drawResId: Int
) 
object Home : BottomNavigationScreens("Home", R.string.navigation_home, R.drawable.ic_home)

object Search : BottomNavigationScreens("Search", R.string.navigation_search, R.drawable.ic_search)
enum BottomNavigationScreens {
  struct Content {
    let route: String
    let stringResId: Int
    let drawResId: Int
  }
}
enum BottomNavigationScreens {
  ...

  case home(route: String, stringResId: Int, drawResId: Int)

  case search(route: String, stringResId: Int, drawResId: Int)
}

Default arguments

Kotlin allows you to define default arguments for class properties or function arguments. For instance, you can define the default value for platform to always be PLATFORM.ALL. With this, you don’t necessarily need to define the platform value when creating a RWContent object. In these scenarios, the system will use the default one.

data class RWContent(
  val platform: PLATFORM = PLATFORM.ALL,
  val url: String,
  val image: String = ""
)
val content = RWContent(
  url = "https://www.raywenderlich.com"
)
// > ALL
Logger.d(TAG, "platform=${content.platform}")
// > https://www.raywenderlich.com
Logger.d(TAG, "url=${content.url}")
// > ""
Logger.d(TAG, "image=${content.image}")

Singletons

To create a singleton in Kotlin, you need to use the keyword object. The ServiceLocator.kt file — since it deals with object initialization — is one such example:

public object ServiceLocator

Interfaces

Interfaces are similar to Swift protocols. They define a set of functions that any class or variable that uses them needs to declare.

public interface FeedData {

  public fun onNewDataAvailable(items: List<RWEntry>, platform: PLATFORM, e: Exception?)

  public fun onNewImageUrlAvailable(id: String, url: String, platform: PLATFORM, e: Exception?)

  public fun onMyGravatarData(item: GravatarEntry)
}

Functions

Kotlin supports different types of functions:

(non-line) functions

These functions are the ones that are more common to find in any source base. They’re quite similar to Swift func, but in Kotlin, this keyword loses a letter because it’s fun. :]

public fun fetchAllFeeds(cb: FeedData) {

  listener = cb

  for (feed in content) {
    fetchFeed(feed.platform, feed.url)
  }
}
public suspend fun fetchRWEntry(feedUrl: String): HttpResponse = client.get(feedUrl)

Lambda expressions

Lambda expressions allow you to execute specific code blocks as functions. They can receive parameters and even return a specific type of object. You can see two of them on fetchFeed: onSuccess and onFailure parameters on FeedPresenter.kt.

private fun fetchFeed(platform: PLATFORM, feedUrl: String) {
  GlobalScope.apply {
    MainScope().launch {
      feed.invokeFetchRWEntry(
        platform = platform,
        feedUrl = feedUrl,
        onSuccess = { listener?.onNewDataAvailable(it, platform, null) },
        onFailure = { listener?.onNewDataAvailable(emptyList(), platform, it) }
      )
    }
  }
}
onSuccess = { list ->
  listener?.onNewDataAvailable(list, platform, null)
}

Higher-order functions

Higher-order functions support receiving a function as an argument.

public fun onNewDataAvailable(items: List<RWEntry>, platform: PLATFORM, e: Exception?)
public suspend fun invokeFetchRWEntry(
  platform: PLATFORM,
  feedUrl: String,
  onSuccess: (List<RWEntry>) -> Unit,
  onFailure: (Exception) -> Unit
) 

Inline functions

If your app calls a high-level function multiple times, it can have an associated performance cost. Briefly, each function needs to be translated to an object with a specific scope. Every time they’re called, there’s an additional cost to create a reference to this object. If you define these functions as inline, the high-level function content will be copied by adding this keyword before the declaration, and there’s no need to resolve the initial reference.

Suspend functions

To use the suspend function, you need to add the Coroutines library to your project. You can run, stop, resume and pause a suspended function. This is why they’re ideal for asynchronous operations — and why the app network requests use it:

public suspend fun fetchRWEntry(feedUrl: String): HttpResponse = client.get(feedUrl)

Kotlin and Swift syntax table

You can find a comparison table between both languages in the materials repository.

Where to go from here?

Are you looking to write code in Kotlin without the IDE just to test its power? JetBrains has the Kotlin Playground that allows you to test some basic 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