Chapters

Hide chapters

Kotlin Multiplatform by Tutorials

Second Edition · Android 14, iOS 17, Desktop · Kotlin 1.9.10 · Android Studio Hedgehog

11. Serialization
Written by Carlos Mota

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Great job on completing the first two sections of the book! Now that you’re familiar with Kotlin Multiplatform, you have everything you need to tackle the challenges of this last section.

Here, you’ll start making a new app named learn. It’s built on top of the concepts you learned in the previous chapters, and it introduces a new set of concepts, too: serialization, networking and how to handle concurrency.

learn uses the Kodeco RSS feeds to show the latest tutorials for Android, iOS, Unity and Flutter. You can add them to a read-it-later list, share them with a friend, search for a specific key, or just browse through everything the team has released.

The Need for Serialization

Your application can send or receive data from a third party, either a remote server or another application in the device. Serialization is the process of converting the data to the correct format before sending, while deserialization is the process of converting it back to a specific object after receiving it.

There are different types of serialization formats — for instance, JSON or byte streams. You’ll read more about this in Chapter 12, “Networking,” when covering network requests.

Android uses this concept to share data across activities, services or receivers — either in the same application or to third-party apps. The difference is that instead of relying on Serializable to send data from custom types, the OS requires you to implement Parcelable to send these objects.

Project Overview

To follow along with the code examples throughout this section, download the starter project and open 11-serialization/projects/starter with Android Studio.

Fig. 11.1 — Project view hierarchy
Fok. 61.6 — Cdelujj leaq reegozbrl

Android App

The androidApp module follows the same structure that you’re already used to from your Android apps, and you can use any library or component as you typically do:

Fig. 11.2 — Android starter project running. Empty screen with no data.
Quv. 37.6 — Ofgfoew vvopxeg czuhecz yikriys. Isdfb wvzuol seyb qo kode.

iOS App

Your iOS app is inside the iosApp folder. It uses the Swift Package Manager to handle external libraries, which is available with Xcode.

cd "$SRCROOT/.."
./gradlew :shared:embedAndSignAppleFrameworkForXcode
Fig. 11.3 — iOS starter project running. Empty screen with no data.
Rug. 48.9 — aUF lpupzes fmowaty malnokh. Ohckj fyziex talk co meda.

Desktop Application

The desktop application is similar to the Android app with just a couple of small changes — namely, on the libraries used that weren’t available for the JVM target:

./gradlew desktopApp:run
Fig. 11.4 — Desktop starter project running. Empty screen with no data.
Por. 29.2 — Furnroy lsazcah vtolodv wajxeqc. Olljd nlleup coyw be dafo.

Shared Module

This contains the entire business logic of learn. It’s the multiplatform code that’s shared across Android, iOS and desktop.

Common Code

When you open the shared module, you’ll see two things inside commonMain:

Application Features

Before starting to write code, have a look first at the app concept and its features:

Fig. 11.5 — Application screens overview and navigation.
Dot. 57.8 — Olyloyukiab srxiaxp otaltuan ows pogudireom.

Home

This is the app’s default screen. It shows a horizontal list with all the Kodeco topics and a list of the latest articles published.

Bookmarks

This screen shows all the articles that you’ve saved. Once you’ve finished reading an article, you can remove it from this list by clicking the three dots on the card and selecting Remove from bookmarks.

Latest

This features a more graphical interface with the latest articles’ sections and covers.

Search

Here, you can filter by a specific keyword and finally find that article you’ve been looking for.

Adding Serialization to Your Gradle Configuration

Kotlin doesn’t support serialization and deserialization directly at the language level. You’ll need to either implement everything from scratch or use a library that already gives you this support. Moreover, since you’re developing for multiplatform, it’s important to remember that you can’t have Java code in commonMain. Otherwise, the project won’t compile. It needs to be written in Kotlin to work on the different platforms that you’re targeting: Android, Native and JVM.

kotlinx-serialization-json = "1.6.0"
jetbrains-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" }
alias(libs.plugins.jetbrains.kotlin.serialization) apply false
alias(libs.plugins.jetbrains.kotlin.serialization)

Different Serialization Formats

kotlinx.serialization supports a set of serialization formats outside the box:

implementation(libs.kotlinx.serialization.json)

Creating a Custom Serializer

If you’re using custom types on your objects, there might be no serializer available. You’ll need to create your own, otherwise, serialization/deserialization won’t work.

private fun findByKey(key: String, default: PLATFORM = PLATFORM.ALL): PLATFORM {
 return PLATFORM.values().find { it.value == key } ?: default
}
//1
object KodecoSerializer : KSerializer<PLATFORM> {

  //2
  override val descriptor: SerialDescriptor =
    PrimitiveSerialDescriptor("PLATFORM", PrimitiveKind.STRING)

  //3
  override fun serialize(encoder: Encoder, value: PLATFORM) {
    encoder.encodeString(value.value.lowercase())
  }

  //4
  override fun deserialize(decoder: Decoder): PLATFORM {
    return try {
      val key = decoder.decodeString()
      findByKey(key)
    } catch (e: IllegalArgumentException) {
      PLATFORM.ALL
    }
  }
}
@Serializable(with = KodecoSerializer::class)

Serializing/Deserializing New Data

Navigate to FeedPresenter.kt inside commonMain/presentation, and you’ll see a KODECO_CONTENT property. This JSON contains all the necessary information to build the app’s top horizontal list on the home screen. Its structure has the following attributes:

@Serializable
private val json = Json { ignoreUnknownKeys = true }
val content: List<KodecoContent> by lazy {
  json.decodeFromString(KODECO_CONTENT)
}
Fig. 11.6 — Android app with different platforms
Wut. 99.0 — Afxqeos azb luxb hahpogikz mkocpestt

Fig. 11.7 — iOS app with different platforms
Kac. 33.0 — aAV iww dabg coxrabacz frostoncq

Fig. 11.8 — Desktop app with different platforms
Tax. 71.2 — Fojlkol arz yasr boldemozd pqopfumym

Serializable vs. Parcelable

Java has a Serializable interface located in the java.io package. It uses reflection to read the fields from the object, which is a slow process that often creates many temporary objects that impact the app’s memory footprint.

Implementing Parcelize in KMP

Parcelable and Parcelize are a set of classes that are specific to the Android platform. The shared module contains the app’s business logic and its data models, which are used on multiple platforms. Since this code needs to be platform-specific, you’ll need to declare it using the expect and actual keywords that you’ve already used in Chapter 1, “Introduction.”

jetbrains-kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
alias(libs.plugins.jetbrains.kotlin.parcelize) apply false
alias(libs.plugins.jetbrains.kotlin.parcelize)
import kotlinx.parcelize.Parcelize

@Parcelize
data class KodecoEntry(val id: String, val entry: String): Parcelable

Defining expect/actual for Android Target

As a rule of thumb, the name of the classes that are platform-specific start with the prefix Platform-. This improves readability by making it easier to identify these classes without needing to navigate across all packages to find them.

package com.kodeco.learn.platform

//1
expect interface Parcelable

//2
@OptIn(ExperimentalMultiplatform::class)
@OptionalExpectation
//3
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.BINARY)
//4
expect annotation class Parcelize()
kotlin.sourceSets.all {
  languageSettings.optIn("kotlin.RequiresOptIn")
}
@OptIn(ExperimentalMultiplatform::class)
@OptionalExpectation
@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.BINARY)
expect annotation class RawValue()
package com.kodeco.learn.platform

actual typealias Parcelable = android.os.Parcelable
actual typealias Parcelize = kotlinx.android.parcel.Parcelize

Working With Targets That Don’t Support Parcelize

Go to the iosMain folder and inside the platform directory, create the corresponding Parcelable.ios.kt file.

package com.kodeco.learn.platform

actual interface Parcelable

Adding Parcelize to Existing Classes

With everything set, go to commonMain and search for the KodecoContent and KodecoEntry data classes. They’re inside the data/model package.

@Parcelize
@Serializable
data class KodecoContent(
 val platform: PLATFORM,
 val url: String,
 val image: String
) : Parcelable
@Parcelize
data class KodecoEntry(
 val id: String = "",
 val link: String = "",
 val title: String = "",
 val summary: String = "",
 val updated: String = "",
 val imageUrl: String = "",
 val platform: PLATFORM = PLATFORM.ALL,
 val bookmarked: Boolean = false
) : Parcelable

Sharing Your Data Classes With the Server

Another excellent use case for Kotlin Multiplatform is the possibility to share your data classes across all platforms, including the server. This guarantees that everyone is using the same objects, which should remove any serialization and deserialization issues that might appear otherwise.

jvm()
sourceSets {
  getByName("commonMain") {
    dependencies {
      implementation(libs.kotlinx.serialization.json)
    }
  }
  getByName("commonTest") {
    dependencies {
      implementation(kotlin("test"))
    }
  }
}
compileOptions {
  sourceCompatibility = JavaVersion.VERSION_17
  targetCompatibility = JavaVersion.VERSION_17
}
./gradlew :shared-dto:jvmJar

Testing

Tests validate the assumptions you’ve written and give you an important safety net toward all future changes.

Testing Serialization

To test your code, you need to go to the shared module, right-click on the src folder and select New ▸ Directory. In the drop-down, select commonTest/kotlin. Here, create a SerializationTests class:

class SerializationTests { }
@Test
fun testEncodePlatformAll() {
  val data = KodecoContent(
    platform = PLATFORM.ALL,
    url = "https://www.kodeco.com/feed.xml",
    image = "https://play-lh.googleusercontent.com/CAa4g9UbOJambautjl7lOfdiwjYoX04ORbivxdkPDZNirQd23TXQAfbFYPTN1VBWyzDt"
  )

  val decoded = Json.encodeToString(KodecoContent.serializer(), data)

  val content = "{\"platform\":\"all\",\"url\":\"https://www.kodeco.com/feed.xml\",\"image\":\"https://play-lh.googleusercontent.com/CAa4g9UbOJambautjl7lOfdiwjYoX04ORbivxdkPDZNirQd23TXQAfbFYPTN1VBWyzDt\"}"
  assertEquals(content, decoded)
}
@Test
fun testDecodePlatformAll() {
  val data = "{\"platform\":\"all\",\"url\":\"https://www.kodeco.com/feed.xml\",\"image\":\"https://play-lh.googleusercontent.com/CAa4g9UbOJambautjl7lOfdiwjYoX04ORbivxdkPDZNirQd23TXQAfbFYPTN1VBWyzDt\"}"

  val decoded = Json.decodeFromString(KodecoContent.serializer(), data)
  val content = KodecoContent(
    platform = PLATFORM.ALL,
    url = "https://www.kodeco.com/feed.xml",
    image = "https://play-lh.googleusercontent.com/CAa4g9UbOJambautjl7lOfdiwjYoX04ORbivxdkPDZNirQd23TXQAfbFYPTN1VBWyzDt"
  )

  assertEquals(content, decoded)
}

Testing Custom Serialization

To test your custom KodecoSerializer, you first need to define:

private val serializers = serializersModuleOf(PLATFORM::class, KodecoSerializer)
@Test
fun testEncodeCustomPlatformAll() {
  val data = PLATFORM.ALL

  val encoded = Json.encodeToString(serializers.serializer(), data)
  val expectedString = "\"all\""
  assertEquals(expectedString, encoded)
}
{
  "platform":"all",
  "url":"https://www.kodeco.com/feed.xml",
  "image":"https://play-lh.googleusercontent.com/CAa4g9UbOJambautjl7lOfdiwjYoX04ORbivxdkPDZNirQd23TXQAfbFYPTN1VBWyzDt"
}
@Test
fun testDecodeCustomPlatformAll() {
  val data = PLATFORM.ALL
  val jsonString = "\"${data.value}\""

  val decoded = Json.decodeFromString<PLATFORM>(jsonString)
  assertEquals(decoded, data)
}

Challenges

Here are some challenges for you to practice what you’ve learned in this chapter. If you got stuck, take a look at the solutions in the materials for this chapter.

Challenge 1: Loading an RSS Feed

You’re currently loading the different sections from the KODECO_CONTENT property inside FeedPresenter.kt. In this challenge, you will:

Challenge 2: Adding Tests to Your Implementation

Now that you’ve implemented this new feature, you’ll add tests to guarantee your implementation is correct. Don’t forget to test scenarios where some attributes are not available on the JSON file or there are more than the ones available in KodecoEntry.kt.

Key Points

Where to Go From Here?

For other practical examples where Parcelize is used, read the Kotlin Android Extensions article.

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 accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now