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

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.

Once the project synchronizes, you’ll see a set of subfolders and other important files:

Fig. 11.1 — Project view hierarchy
Fig. 11.1 — Project view hierarchy

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:

  • components: generic Composable functions that are used in different screens.
  • ui: contains all the UI. The sub packages correspond to specific screens (bookmark, home, latest or search), the navigation bar (main), or the application design system (theme).
  • utils: utility class to format a date.
  • KodecoApplication.kt: the Application class that initializes the Context needed by SQLDelight.

Build and run the app.

Fig. 11.2 — Android starter project running. Empty screen with no data.
Fig. 11.2 — Android starter project running. Empty screen with no data.

This is your app skeleton. It doesn’t look much, but that’s because there’s no data to show. You’ll load the app data in the next sections.

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.

Use Xcode or AppCode to open the file iosApp.xcodeproj located inside the iosApp folder.

Open the iosApp target, navigate to the Build Phases tab, and then select the Compile Kotlin dropdown. Here you have:

cd "$SRCROOT/.."
./gradlew :shared:embedAndSignAppleFrameworkForXcode

This task compiles the SharedKit framework and adds it to your project when necessary.

Build and run the app. You’ll see a screen like this:

Fig. 11.3 — iOS starter project running. Empty screen with no data.
Fig. 11.3 — iOS starter project running. Empty screen with no data.

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:

  • precompose: A community library that lets you use Jetpack Lifecycle, ViewModel, LiveData and Navigation in a desktop application.

To run the desktop application, go to the command line and in the project root folder, enter:

./gradlew desktopApp:run

After it finishes, a new window will open with the app. You’ll see a screen like this:

Fig. 11.4 — Desktop starter project running. Empty screen with no data.
Fig. 11.4 — Desktop starter project running. Empty screen with no data.

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:

  • kotlin: Where the application business logic is.
  • sqldelight: Contains the SQL file that’s going to be used by SQLDelight to create the app’s database, along with the corresponding CRUD methods to interact with it.

In the next section, you’ll see what learn will look like after you implement all the required features.

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.
Fig. 11.5 — Application screens overview and navigation.

learn has four different screens that you can navigate to from the app’s bottom bar:

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.

Clicking any topic redirects the user to the latest screen where they can see the most recent articles written, read or shared, then add them to the bookmarks list or remove them once they’re done.

To open an article, click on the card and you’ll be automatically redirected to the browser. If you click on the three dots inside the card instead, the app will show a bottom sheet. From there, you can add it to your bookmarks, or send it to a friend.

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.

Now that you’re familiar with the app and the project, it’s time to start learning how to implement these features.

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.

So, do we need to implement serialization from scratch then? No, this is already available on kotlinx.serialization, a library created and maintained by JetBrains.

Time to import this library into the app. Open Android Studio and wait for the project to finish synchronizing. learn is using Gradle version catalogs to add and maintain its dependencies. Open the libs.versions.toml file located in the gradle folder and inside the [versions] section define the library version that you’re going to use:

kotlinx-serialization-json = "1.6.0"

To use kotlinx.serialization you need to define the plugin and the library that’s going to be added to the build.gradle.kts files. To add the first one, scroll down to [plugins] and add:

jetbrains-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

The version.ref attribute indicates the version that the app should use, which in this case is the same as kotlin.

Locate the [libraries] section and add:

kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" }

Click Sync Now to start using these libraries and plugins.

Open the build.gradle.kts file located in the in root folder and in the plugins section add:

alias(libs.plugins.jetbrains.kotlin.serialization) apply false

Now, open the build.gradle.kts file inside shared and load the serialization plugin by adding the following code inside the plugins block:

alias(libs.plugins.jetbrains.kotlin.serialization)

Click Sync Now and wait for this process to finish.

There are four different build.gradle.kts files in the project:

  • build.gradle.kts: This is located in the project root folder and configures the Android app, desktop app, and the shared module. It contains the repositories from which dependencies will be downloaded.

  • shared/build.gradle.kts: This is the shared module configuration file. It contains the plugins and libraries that you’re going to use, as well as the platforms you’re going to target.

  • androidApp/build.gradle.kts: The configuration file for the Android app. Defines a set of parameters used to compile the project, namely the SDK version, dependencies and compilations flags.

  • desktopApp/build.gradle.kts: This is the configuration file for the desktop application. It defines the same configuration as the Android build.gradle.kts file.

Different Serialization Formats

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

  • JSON: A lightweight human-readable data-interchange format. learn uses JSON to load the Kodeco RSS feed links from a local file and later to deserialize the data received from the server (kotlinx-serialization-json).

  • JSON-Okio: A set of extensions for JSON that allow to integrate with Okio.

  • Protocol buffers: A cross-platform mechanism for serialized structured data (kotlinx-serialization-protobuf).

  • CBOR: A binary data serialization format (kotlinx-serialization-cbor).

  • Properties: A key-value file that saves the configuration parameters of an application used in Java-related technologies (kotlinx-serialization-properties).

  • HOCON: A superset of JSON that’s more human-readable and typically used in configuration files (kotlinx-serialization-hocon).

Note: There are a set of community-maintained libraries that support additional formats.

Except for kotlinx-serialization-json, all of these libraries are still experimental. Although they all seem robust and are used in a set of applications, it’s worth mentioning that the API can (drastically) change in future releases — meaning that you might end up refactoring its usage.

During the scope of this book, you’ll only need to add kotlinx-serialization-json. Navigate to shared, open the build.gradle.kts file and search for the commonMain field. There’s already a set of dependencies on the project.

After kotlinx.datetime, add:

implementation(libs.kotlinx.serialization.json)

Synchronize the project, and now you’re ready to use serialization for JSON format.

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.

A good example of this is on the KodecoContent.kt data class, inside the shared module. The platform field is of type PLATFORM, an enum created to identify which section the article belongs to.

If you don’t create a custom serializer, kotlinx-serialization-json won’t be able to identify what the keywords “All,” “Android,” “iOS,” “Unity” or “Flutter” are. This happens because the attribute on the JSON is type String, and it should be PLATFORM instead. You need to implement a custom serializer/deserializer to provide this support.

In the commonMain package inside the shared module, go over to data and create a KodecoSerializer.kt file.

Start by adding the findByKey method:

private fun findByKey(key: String, default: PLATFORM = PLATFORM.ALL): PLATFORM {
 return PLATFORM.values().find { it.value == key } ?: default
}

This will receive a key and return the corresponding value in the enum PLATFORM.

Now that you’ve added the mapping function, create the KodecoSerializer object. In the same file, add the following code and import the required classes:

//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
    }
  }
}

Here’s a step-by-step breakdown of this logic:

  1. You extend the KSerializer class and define the type of object that you are going to serialize/deserialize.

  2. It’s necessary to define the PrimitiveSerialDescriptior that contains the class name — PLATFORM — and how the parameter should be read. In this case, it’s going to be from a String.

  3. Now that everything is ready, it’s time to define the serialize method. Since, the content type of PLATFORM is String, you just need to call encodeString and send the value of the received object.

  4. Finally, to deserialize, you’re going to call the opposite method, which is decodeString, and with the raw value (the key), you’ll call findByKey to see which value of the enum it corresponds to.

That’s it! KodecoSerializer is ready. You just need to add it to the class. Open the KodecoContent.kt file again, and above the declaration of PLATFORM add:

@Serializable(with = KodecoSerializer::class)

This associates the new device serializer to the PLATFORM class. Remember to import from kotlinx.serialization.Serializable.

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:

  • platform: The different areas covered by Kodeco articles: Android, iOS, Unity and Flutter. There’s a fifth value — all. Once set, it removes this filter and shows everything published.

  • url: Contains the RSS feed URL from where the articles should be fetched.

  • image: A cover image that corresponds to the platform that was selected.

These three attributes are already mapped into the data class KodecoContent, which is inside data/model. Open it and add to the top of its declaration:

@Serializable

The KodcoContent data class uses @Serializable, so when decoding the KODECO_CONTENT property, it easily generates a list of KodecoContent that maps the attributes of the JSON string into the fields of the data class.

With everything defined, navigate to FeedPresenter.kt and first add to the class:

private val json = Json { ignoreUnknownKeys = true }

This will create the Json object that you’ll use to decode the file content. It’s important to set ignoreUnknownKeys to true to avoid any exceptions that might be thrown in case one of the fields inside KodecoContent.kt doesn’t have a direct attribute in the JSON file. Remember to import kotlinx.serialization.json.Json.

Now, update the content property to decode the KODECO_CONTENT instead of returning an emptyList:

val content: List<KodecoContent> by lazy {
  json.decodeFromString(KODECO_CONTENT)
}

content is lazily initialized. In other words, it will open the file and read its content only when accessed. When done, it calls decodeFromString to generate a list of KodecoContent objects.

Build and run the project and see what’s new in learn. You’ll see screens similar to the following ones on different platforms:

Fig. 11.6 — Android app with different platforms
Fig. 11.6 — Android app with different platforms

Fig. 11.7 — iOS app with different platforms
Fig. 11.7 — iOS app with different platforms

Fig. 11.8 — Desktop app with different platforms
Fig. 11.8 — Desktop app with different platforms

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.

On the other hand Parcelable, an Android specific equivalent for Serializable, requires all the object types to be declared. This makes it a faster solution, since there’s no need to use reflection to understand the object type.

One might argue that Parcelable is more complex to implement. This was true some years ago, since it was necessary to override a couple of methods and create the read/write methods according to the object fields. But, you don’t have to do all that now, as the kotlin-parcelize plugin generates this code automatically. So the only effort here is to add an annotation to the top of the class — @Parcelize — and extend Parcelable.

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

Parcelize is part of a plugin named kotlin-parcelize, which contains the Parcelable code generator. Before writing the Android declaration for this class, you’ll need to define it in libs.versions.toml, located inside the gradle folder. Here go to the [plugins] section and add:

jetbrains-kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }

Now open the build.gradle.kts file on the root folder and add to the plugins section the declaration that you’ve just defined:

alias(libs.plugins.jetbrains.kotlin.parcelize) apply false

Finally, go to the shared module’s build.gradle.kts file. Open it, and in the plugin section add:

alias(libs.plugins.jetbrains.kotlin.parcelize)

Synchronize the project.

To set a data class as Parcelable, one would add the annotation @Parcelize to the top of the class declaration and then extend the Parcelable generator. It should be something similar to:

import kotlinx.parcelize.Parcelize

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

However, since this platform-specific code shouldn’t exist on commonMain, you’ll need to define this behavior at the platform level — in this case in androidMain.

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.

To implement Parcelize, you need to declare:

  • The Parcelable interface, which is extended by the data class.

  • The Parcelize annotation, which is used to activate the kotlin-parcelize plugin.

Go to commonMain in the shared module, navigate to platform and create a file named Parcelable.common.kt . Open it and add:

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()

Let’s break this code snippet into parts:

  1. The Parcelable interface that needs to be defined.

  2. Declaring an annotation that it’s still experimental, meaning that the API might change in the future. These two annotations notify the user about this behavior and prevent Android Studio from warning the developer every time there’s a call to this Parcelize class.

  3. Target and Retention annotations are part of Kotlin’s Parcelize annotation. To keep the same behavior as the native one, they’re also added here.

  4. The Parcelize annotation that will be defined.

Note: When using the OptIn annotation, you might see warning messages on your build log stating:

This class can only be used with the compiler argument ‘-opt-in=kotlin.RequiresOptIn’

Open the shared build.gradle.kts file and add the following code at the end of this file:

kotlin.sourceSets.all {
  languageSettings.optIn("kotlin.RequiresOptIn")
}

Depending on the data classes that you’re using, you might need to create an expect/ actual class for other annotations. One of these examples is @RawValue, which is used along with default serializers for custom types. You can follow the same approach used on Parcelize to achieve the same goal:

@OptIn(ExperimentalMultiplatform::class)
@OptionalExpectation
@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.BINARY)
expect annotation class RawValue()

With the expected declarations defined, go over to androidMain and create the corresponding actual implementation. It should be located in the same directory as the file you’ve just added on commonMain.

Navigate to the platform directory and create the corresponding Parcelable.android.kt file. Open it and add:

package com.kodeco.learn.platform

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

Since Parcelable and Parcelize already exist on the Android platform, there’s no need to create them. Instead, use typealias to create a reference link between the expected class and the actual class itself. In other words, it’s as if you’re calling android.os.Parcelable or kotlinx.android.parcel.Parcelize directly.

Note: Type aliases don’t create a new type of data, but instead create a link to an existing one. Get more information about Kotlin’s typealias directly from the official documentation.

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.

Note: Press Alt + Enter, and Android Studio automatically suggests creating the files for the remaining platforms. Generate these files by pressing OK.

Open Parcelable.ios.kt and add:

package com.kodeco.learn.platform

actual interface Parcelable

That’s it! iOS doesn’t have a corresponding Parcelable interface, so define an empty declaration. The compiler automatically removes both the annotation and class since there’s no declaration set. The generated framework doesn’t contain any reference to Parcelize and Parcelable.

Create the file Parcelable.desktop.kt inside desktopMain/platform and add the same declaration.

This is the expected behavior, since this is an Android-only feature.

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.

In each of these files, add the @Parcelize annotation above the class declaration and extend Parcelable:

The KodecoContent.kt file will look like this:

@Parcelize
@Serializable
data class KodecoContent(
 val platform: PLATFORM,
 val url: String,
 val image: String
) : Parcelable

And, KodecoEntry.kt will look like this:

@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

Don’t forget to add the necessary imports. Now, you can start sending this object across different Activities without any problems.

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.

To create a new shared module, go to File ▸ New ▸ New Module… and select Kotlin Multiplatform Shared Module. Here, define the module name as shared-dto and the package name as com.kodeco.learn. Click Next and select No Activity.

Click on Finish to add the module to the project.

After the synchronization ends, open the build.gradle.kts from the newly added shared-dto and inside the kotlin section add the JVM target.

jvm()

The current version of your module template might not be using the most recent APIs, so you might need to make a couple of updates:

  1. If it’s calling android() to set the Android target, replace the code block with androidTarget().
  2. In the sourceSets section, instead of declaring the commonMain and commonTest properties, you can access them by calling getByName():
sourceSets {
  getByName("commonMain") {
    dependencies {
      implementation(libs.kotlinx.serialization.json)
    }
  }
  getByName("commonTest") {
    dependencies {
      implementation(kotlin("test"))
    }
  }
}
  1. Set the compatibilityOptions to version 17 on the android section:
compileOptions {
  sourceCompatibility = JavaVersion.VERSION_17
  targetCompatibility = JavaVersion.VERSION_17
}

With these changes, you’ll no longer have warnings in your new module.

Since desktop and server are JVM platforms, you can just add a single target.

Synchronize the project.

Add a new jvmMain directory. To easily create this target, right-click on shared-dto folder and go-to File ▸ New ▸ Directory and select jvmMain/kotlin. Now go to kotlin folder and, once again, right-click and select File ▸ New ▸ Package and write com.kodeco.learn.

Now you’ll need to move some files from shared to shared-dto:

  1. Go to shared/commonMain and move the folder data to shared-dto/commonMain.

  2. Create a platform directory on androidMain, commonMain, iosMain and jvmMain

  3. Move the Parcelable.*.kt files from shared to the corresponding platform folders. Note, that jvmMain corresponds to desktopMain.

  4. Since, shared-dto now contains Parcelable and KSerializer objects, you need to add the following plugins to build.gradle.kts:

    alias(libs.plugins.jetbrains.kotlin.parcelize) alias(libs.plugins.jetbrains.kotlin.serialization)

    And on commonMain/dependencies: implementation(libs.kotlinx.serialization.json)

  5. Remove the Platform.*.kt and Greeting.kt files from androidMain, commonMain and iosMain which were generated from the template.

  6. Finally, open shared/build.gradle.kts to add the shared-dto to the project. Go to commonMain/dependencies and write:

    api(project(":shared-dto"))

    With this the Android and desktop applications can easily access these objects. For iOS, you need to take an extra step. If you now try to access KodecoEntry from Swift, you’ll see that this object doesn’t exist. Instead you need to call SharedDto_KodecoEntry. This is how Kotlin Multiplatform handles external libraries – it adds the module prefix. To overcome this and provide a better experience to iOS developers, you can export shared-dto when building the framework and then access KodecoEntry directly. Inside it.binaries.framework add:

    export(project(":shared-dto"))

Synchronize and compile the project.

With the clients updated, if you want to share the data classes with the server, you just need to compile the JVM version:

./gradlew :shared-dto:jvmJar

You’ll see how to publish a library in Chapter 14, “Creating Your KMP Library”.

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 { }

You’ll need to create encode and decode tests to validate that everything is working as expected. Start by writing the encoder. Add the following method to the class:

@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)
}

Here, you’re validating that your JSON serialization is capable of encoding a KodecoContent object, data, to a string. This property corresponds to the all section in learn. If the serializer is working correctly, the result of encodeToString needs to be the same as content — otherwise the test will fail. Click the green arrow by the function on the left to run the test and select android (:testDebugUnitTest). You’ll see that the test passes.

For assertEquals you should import kotlin.test.assertEquals.

Now to test if the deserialization is correct, add the following method:

@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)
}

Essentially, you do the opposite. Starting with the JSON response, you’ll need to call decodeFromString so kotlinx.serialization builds your KodecoContent object, decoded, and then you’ll compare it with the one that you’re expecting - content. If the content is the same, the test successfully passes. Run the test and see that it passes.

Testing Custom Serialization

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

private val serializers = serializersModuleOf(PLATFORM::class, KodecoSerializer)

This serializers property contains the serializer you’ll need to encode and decode your data.

Start by creating an encoding test:

@Test
fun testEncodeCustomPlatformAll() {
  val data = PLATFORM.ALL

  val encoded = Json.encodeToString(serializers.serializer(), data)
  val expectedString = "\"all\""
  assertEquals(expectedString, encoded)
}

When you receive a response, the body is a string response in a JSON format:

{
  "platform":"all",
  "url":"https://www.kodeco.com/feed.xml",
  "image":"https://play-lh.googleusercontent.com/CAa4g9UbOJambautjl7lOfdiwjYoX04ORbivxdkPDZNirQd23TXQAfbFYPTN1VBWyzDt"
}

To test if KodecoSerializer is working correctly on the test above, check if the result corresponds to the JSON response "all" after encoding the PLATFORM.ALL property into a string.

If it does, the assertEquals function will return true, otherwise the test will fail.

Now, add the decoder test:

@Test
fun testDecodeCustomPlatformAll() {
  val data = PLATFORM.ALL
  val jsonString = "\"${data.value}\""

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

Here, you’re doing the opposite. From the string “all”, returned from data.value, you want the corresponding PLATFORM enum value. For that, you call decodeFromString and confirm if the returned object is the one you’re expecting.

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:

  • Create an KODECO_ALL_FEED property that contains the RSS feed content of one of the feed URLs from KODECO_CONTENT.

  • Read this property and parse its content in the shared module so it can be available for all apps to use. Keep in mind that for this KodecoEntry needs to be serializable.

  • In androidApp and desktopApp, open the FeedViewModel.kt file inside ui/home, and populate items with this new data.

  • In iOSApp, open FeedClient inside extensions, and in the fetchFeeds function add the new feeds.

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.

Note: You should be able to read a JSON string containing the content to be serialized and access the object afterward.

Key Points

  • Exchanging data between local and remote applications requires the content that’s transferred to be serialized and deserialized, depending on whether it’s being sent or received, respectively.

  • kotlinx.serialization is a multiplatform library that supports serialization. It allows serializing/ deserializing JSON, Protocol buffers, CBOR, Properties, HOCON, YAML and Apache Avro.

  • You can create custom serializers by implementing the serialize/ deserialize for a custom type and then associating it with that class.

  • You can use typealias with actual to automatically link a class declaration to an existing one at the platform level.

Where to Go From Here?

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

The next chapter starts with the features that you’ve implemented in this one, but instead of only loading the data locally, you’ll also fetch it from the network.

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.
© 2026 Kodeco Inc.