A.
Appendix A: Kotlin: A Primer for Swift Developers
Written by Carlos Mota
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. :]
One good thing about Android Studio is that in most cases, if you’re missing an import or using a wrong type for a variable, it will automatically warn you and suggest a fix.
Note: Every time Android Studio underlines your code or shows a tooltip box, you can automatically accept its suggestion by pressing Alt-Enter.
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/kodeco/learn, and they usually correspond to:
- com, domain
- kodeco, company name
- learn, app name
Since the package name is unique — you can’t have two apps on the Google Play Store with the same one — this convention guarantees there is no conflict between apps from different companies.
Every time you declare a new class or object, you must define the package declaration. This should be the first instruction of a new file. Or, if you have a copyright header, right after it.
If you open the FeedPresenter.kt inside the presentation folder from the shared module, you can see that the import is:
package com.kodeco.learn.presentation
In this case, presentation is the subfolder where this class is. The package definition should correspond to the same tree hierarchy — otherwise, you might end up importing the wrong files.
There’s no package declaration to add in Swift.
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.kodeco.learn.data.model.GravatarEntry
This is different from Swift. There’s no need to add classes — you just need to import the framework you’re going to use.
Comments
Similar to Swift, you can add three types of comments:
-
Line, where you just need to add
//before the code or text that you want to comment. In this example,Loggerwon’t be executed:
public fun fetchMyGravatar(cb: FeedData) {
//Logger.d(TAG, "fetchMyGravatar")
//Update the current listener with the new one.
listener = cb
fetchMyGravatar()
}
-
Block, where you need to surround your code or text with
/* */. This is also used for adding the copyright section at the beginning of a file:
/*
* Copyright (c) 2021 Razeware LLC
*
*/
-
KDoc, which corresponds to the documentation that’s going to be generated for your project. You can use tags like
@property,@param,@return,@constructor, etc. to provide additional information about a function:
/**
* 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
}
Note: The equivalent version of Kdoc for Swift is Jazzy.
Variables
Similar to Swift, in Kotlin you also have two types of variables:
-
val, which corresponds to Swift’s let. It’s a read-only (immutable) variable that can only be set once – in its declaration. In this case,
scopeis initialized with a specific value when declared. This value can never change throughout the app execution:
private val scope = PresenterCoroutineScope(defaultDispatcher)
-
var is the same keyword as in Swift. It’s a mutable variable, so you can set it as many times as you need. In this example, the initial value of
listenerisnull. When the UI makes a new request for data, it’s going to be updated with a new callback reference:
private var listener: FeedData? = null
In Swift, the above declaration is:
private var listener: FeedData? = nil
You can have optional values in both languages. The only difference is Kotlin uses null, whereas Swift uses nil to represent the absence of a value.
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.
The value of this variable will only be calculated when it’s first accessed. You should only define a variable as lazy if you don’t need to access it right away and the variable does some heavy work.
Open the FeedPresenter.kt file inside shared/presentation and search for content declaration:
val content: List<KodecoContent> by lazy {
json.decodeFromString(KODECO_CONTENT)
}
As you can see, it’s defined as lazy. Do this to avoid decoding KODECO_CONTENT immediately when the app starts. It’s one less thing to process.
If your app has a heavy startup, following this approach will give you a faster and smoother initialization of the app. The value will only be set when there’s a call to 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.
Change the listener on FeedPresenter.kt to:
private lateinit var listener: FeedData
Removing the ? and null defines this object as non-null. You’ll immediately see a couple of warnings through this file:
onSuccess = { listener?.onNewDataAvailable(it, platform, null) },
onFailure = { listener?.onNewDataAvailable(emptyList(), platform, it) }
In particular, you’ll see warnings, on the ? in the above lambda expressions. Since, listener is not null, you can call the callback directly now that the value won’t be null. This shouldn’t be a problem since all the functions that can be called from the UI like fetchAllFeeds and fetchMyGravatar receive a non-null FeedData that updates the listener before any network call.
You need to be careful when using lateinit; if you try to access its value without having it initialized, your app will crash with the exception:
UninitializedPropertyAccessException: lateinit property has not been initialized
Additionally, you can check if it’s initialized:
if (::listener.isInitialized) {
//Do something
}
But this is seen as smelly code and not advised.
In Swift, there’s no lateinit keyword for initialization. Instead, you need to use the operator !:
private var listener: FeedData!
Under the hood, listener is defined as optional. Be careful — before accessing its value you need to define it. Otherwise your app will crash.
The equivalent to see if it’s initialized:
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.
To define if a variable can be null, you need to use the ? operator.
You can see that listener has the type of FeedData, but its value can be null. Now, try to make any operation on this object. On fetchAllFeeds, before the Loggercall, add:
listener.onMyGravatarData(GravatarEntry())
You’re sending an empty GravatarEntry since this parameter cannot be null. Looking at this instruction, you can see there’s a red underline under the . with the message:
Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type
FeedData?
Since this variable might be null, you shouldn’t do any operation before checking its value. There are two different possibilities here:
- Explicitly say that it won’t be null. You can use the character
!!to tell the compiler that this value will never benull, so you can make any call that you need:
listener!!.onMyGravatarData(GravatarEntry())
Going against the language rules is never a good idea, so try to run away from this implementation.
The equivalent in Swift to this annotation is just to use a single !.
- Only call the method if the value is not null. In this case,
listeneris mutable, so you can’t just add anifcondition to see if it’s not null (since it might be changed by another thread). The solution is to use the?operator again. In this scenario it will only callonMyGravatarDataiflisteneris not null:
listener?.onMyGravatarData(GravatarEntry())
Well, there might be a third possibility here. Don’t make listener as nullable in the first place. :]
Additionally, you can also use *?.let { ... } as a verification to only run the code between brackets if the variable that you’re accessing is not null:
listener?.let {
it.onMyGravatarData(GravatarEntry())
}
it corresponds to listener.
Similarly, in Swift you could do this:
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}")
To print the result of feed.platform, you need to add brackets to the instruction that you want to execute.
What happens if you don’t add those brackets, and instead you have:
Logger.d(TAG, "Fetching feed: $feed.platform")
Compile your app and switch to the Logcat view to confirm that this log will show you the feed object followed by “.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.
Try to remove the type from content declaration. Android Studio immediately underlines this expression, and if you check the error it says:
Not enough information to infer type variable T
This is because decodeFromString doesn’t know which type of object it should return. When you define the type at the variable level, decodeFromString uses it to know which objects it should return. You can define this type directly on the function if you want to use type inference on the variable declaration:
val content by lazy {
json.decodeFromString<List<KodecoContent>>(KODECO_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
"kodeco".toInt()
// Convert String to Long
"kodeco".toLong()
// Convert String to Float
"kodeco".toFloat()
// Convert Int to String
42.toString()
// Convert Int to Long
42.toLong()
// Convert Int to Float
42.toFloat()
If you’re dealing with custom objects, you can always create an extension function for it.
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 “Kodeco” when called:
fun String.toKodeco(): String {
return "Kodeco"
}
This is it. You use the type that you want to extend, followed by the method name. Now this function is available for all String objects.
You can try this by adding the previous function and a new log to a String variable on FeedPresenter.kt — for instance to KODECO_CONTENT:
init {
Logger.d(TAG, "content=${KODECO_CONTENT.toKodeco()}")
}
You can confirm in the Logcat that the output of this call will be similar to:
FeedPresenter | content=Kodeco
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.
if… else
This condition check is similar in both languages. If you open GetFeedData.kt, you can see different functions that use if… else.
The invokeFetchKodecoEntry only adds the parsed object if it isn’t null:
if (parsed != null) {
feed += parsed
}
Moreover, you don’t need to add brackets when it’s a single instruction.
Alternatively, you could just write:
if (parsed != null)
feed += parsed
Or even inline:
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
}
}
In this case, you’re checking the current state of an image that’s being downloaded from the internet, and adding different composable depending on if its value is either Loading or Error.
Since all of these expressions are single-line, you could drop the brackets.
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)
}
Here, you’re iterating through all the values of content. Starting with the first element in the list, on each iteration you’ll get a different element that you can access through feed.
Additionally, there are other possibilities to write the same for cycle:
for (index in content.indices) {
val feed = content[index]
fetchFeed(feed.platform, feed.url)
}
That uses the index to go through all elements. Or, you could get the index and the feeddirectly via:
for ((index, feed) in content.withIndex()) {
fetchFeed(feed.platform, feed.url)
}
Or, you can even get the feed from:
for (index in 0..content.size) {
val feed = content[index]
fetchFeed(feed.platform, feed.url)
}
These are all possibilities that iterate through the list of all the elements from content to get the same result .
while
The while and do… while 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)
The difference between both is the same as in Swift: if the condition is false on while the code block will never run, while do… while will run once.
This is similar in Swift to the while and repeat-while loop:
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 if… else 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.
Although on Swift you can change the mutability of a list or a dictionary if you declare it with let (immutable) or var (mutable), the same is not valid for Kotlin. As mentioned above, you’ve got the list and map for immutable variables, and mutableList and mutableMap for mutable.
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
)
In this case, this is the list of items in the navigation bar.
Imagine that you want to add a new item to this list. You can’t. There’s no add or remove method, since the list object is immutable. What you can do is create a mutable list:
val bottomNavigationItems = mutableListOf(
BottomNavigationScreens.Home,
BottomNavigationScreens.Bookmark,
BottomNavigationScreens.Latest,
BottomNavigationScreens.Search
)
Or, you can convert the existing list to mutableList:
val bottomNavigationItems = listOf(
BottomNavigationScreens.Home,
BottomNavigationScreens.Bookmark,
BottomNavigationScreens.Latest,
BottomNavigationScreens.Search
).toMutableList()
Now you can add or remove elements to the list. Try removing the Search option:
bottomNavigationItems.remove(BottomNavigationScreens.Search)
Or, you could just use the minus and equal sign:
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
)
And then if you want to change the value of one of its indexes:
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.
Modify the previous example to create a map containing the index as key and the screen as value:
val bottomNavigationItems = mapOf(
0 to BottomNavigationScreens.Home,
1 to BottomNavigationScreens.Bookmark,
2 to BottomNavigationScreens.Latest,
3 to BottomNavigationScreens.Search
)
You can get any value on the map by using its key:
// Returns BottomNavigationScreens.HOME
bottomNavigationItems[0]
// Returns BottomNavigationScreens.HOME
bottomNavigationItems.get(0)
Or, you can create a mutable map:
val bottomNavigationItems = mutableMapOf(
0 to BottomNavigationScreens.Home,
1 to BottomNavigationScreens.Bookmark,
2 to BottomNavigationScreens.Latest,
3 to BottomNavigationScreens.Search
)
Or by converting toMutableMap:
val bottomNavigationItems = mapOf(
0 to BottomNavigationScreens.Home,
1 to BottomNavigationScreens.Bookmark,
2 to BottomNavigationScreens.Latest,
3 to BottomNavigationScreens.Search
).toMutableMap()
Maps are equivalent to Swift’s dictionaries.
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:
-
*.isEmpty()returnstrueif the collection is empty,falseotherwise. On the contrary, you also have*. isNotEmpty()that returns the opposite values. -
*.filter { ... }allows filtering your collection according to a specific predicate. -
*.first { ... }returns the first object that meets the condition between brackets. There’s also*.firstOrNull { }that returnsnullif there’s no object that matches the predicate. -
*.forEach { ... }iterates over the collection. -
*.last { ... }is similar tofirst, but this time the last object found is returned. -
*.sortBy { ... }returns a new ordered list according to the predicate defined. You also can get the list on its descending order by calling:*.sortByDescending { ... }.
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)
Typically, each word of a class has an uppercase letter. In this case, feed has val set, so it can be accessed from any function on FeedPresenter scope.
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.
You can see an example of a data class if you open KodecoContent.kt from the data/model folder on the shared module:
data class KodecoContent(
val platform: PLATFORM,
val url: String,
val image: String
)
However, they have a couple of differences when compared with a generic class: you can’t inherit a data class or define it as abstract.
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
)
If you try to extend this class in any other class in the project, you’ll see an error similar to the following:
Inheritor of sealed class or interface declared in package com.kodeco.learn.ui.home but it must be in package com.kodeco.learn.ui.main where base class is declared
To create an object of this sealed class:
object Home : BottomNavigationScreens("Home", R.string.navigation_home, R.drawable.ic_home)
object Search : BottomNavigationScreens("Search", R.string.navigation_search, R.drawable.ic_search)
In this case, they represent the navigation tabs.
Although sealed classes don’t exist in Swift, you can create a similar concept with enum:
enum BottomNavigationScreens {
struct Content {
let route: String
let stringResId: Int
let drawResId: Int
}
}
There’s no @StringRes or @DrawableRes, since these annotations are Android-specific.
Additionally, to create the corresponding objects, you can do something similar to:
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 KodecoContent object. In these scenarios, the system will use the default one.
data class KodecoContent(
val platform: PLATFORM = PLATFORM.ALL,
val url: String,
val image: String = ""
)
And to create this object:
val content = KodecoContent(
url = "https://www.kodeco.com"
)
Now, if you print its content:
// > ALL
Logger.d(TAG, "platform=${content.platform}")
// > https://www.kodeco.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
This guarantees that at all times, you’ll only have one reference to ServiceLocator throughout the scope of your app.
Interfaces
Interfaces are similar to Swift protocols. They define a set of functions that any class or variable that uses them needs to declare.
Open FeedData.kt from domain/cb in the shared module:
public interface FeedData {
public fun onNewDataAvailable(items: List<KodecoEntry>, platform: PLATFORM, e: Exception?)
public fun onNewImageUrlAvailable(id: String, url: String, platform: PLATFORM, e: Exception?)
public fun onMyGravatarData(item: GravatarEntry)
}
FeedData defines three different functions that will be called when there’s a network response. They’re declared on FeedViewModel.kt (inside androidMain/home) and used to notify the UI that there are new data available.
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. :]
You can see different examples of functions in FeedPresenter.kt:
public fun fetchAllFeeds(cb: FeedData) {
listener = cb
for (feed in content) {
fetchFeed(feed.platform, feed.url)
}
}
A function can also return an object. If you open FeedAPI.kt and look at fetchKodecoEntry, you can see that it’s returning a HttpResponse object. Moreover, since there’s only one instruction, you don’t need to add brackets and the return can be written on the same line. You just need to add the = sign:
public suspend fun fetchKodecoEntry(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.invokeFetchKodecoEntry(
platform = platform,
feedUrl = feedUrl,
onSuccess = { listener?.onNewDataAvailable(it, platform, null) },
onFailure = { listener?.onNewDataAvailable(emptyList(), platform, it) }
)
}
}
}
Both of these expressions receive an it parameter. In the first case, it’s a list of KodecoEntry, and in the second it’s an Exception. Alternatively, you could define this expression like the following to better identify what it really is:
onSuccess = { list ->
listener?.onNewDataAvailable(list, platform, null)
}
Higher-Order Functions
Higher-order functions support receiving a function as an argument.
A good example of this type of function is the onSuccess and onFailure arguments on fetchFeed. If you analyze these instructions, you can see that onSuccess and onFailure receive different it objects.
Open FeedData.kt and look for the onDataAvailable:
public fun onNewDataAvailable(items: List<KodecoEntry>, platform: PLATFORM, e: Exception?)
The it in onSuccess is a list of KodecoEntry, while onFailure is an Exception. Navigate to invokeFetchKodecoEntry in GetFeedData.kt and look for the function:
public suspend fun invokeFetchKodecoEntry(
platform: PLATFORM,
feedUrl: String,
onSuccess: (List<KodecoEntry>) -> Unit,
onFailure: (Exception) -> Unit
)
You can see that both parameters receive a function, but in one the type is a list of KodecoEntry and in the other it’s an exception.
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 fetchKodecoEntry(feedUrl: String): HttpResponse = client.get(feedUrl)
This is similar to Swift’s async… await functions.
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.
If you want to learn more about both languages, you’ve got the Kotlin and Swift Apprentice books that teach you everything you need to know about both languages in detail.