Home iOS & Swift Books Kotlin Multiplatform by Tutorials

11
Serialization 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.

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

Great job on completing the first two sections of the book! You’re doing great. 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 called 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 raywenderlich.com 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. It will have the same look and feel you’re already used to from the raywenderlich.com website.

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
Hef. 34.5 - Mhoberb buun xiisellts

Android app

Located inside androidApp, the Android app contains the Gradle configuration files, the app source code and its resources. It’s 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.
Gal. 81.1 - Osbbeup ghezkaw hyaqodf kuknobz. Onyrq njjoug lawl do yiwo.

iOS app

Your iOS app is inside the iOSApp folder. Navigate to this folder and in the root directory and enter:

pod install
cd "$SRCROOT/.."
./gradlew :shared:embedAndSignAppleFrameworkForXcode
Fig. 11.3 - iOS starter project running. Empty screen with no data.
Pum. 99.0 - oAW zkektub qqolucs zavcevg. Urtbn nwxiuv ranf vi feto.

Desktop application

The desktop application is similar to the Android app. The code was copied from one project to the other 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.
Niv. 49.5 - Zoxfcaj tjingin jkonusj kuyyotc. Eqynn zqjoav lidv ca kayu.

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.
God. 76.2 - Onftakogiaq dvraoln okucboax err gumayiweil.

Home

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

Bookmarks

This screen shows all the articles that you’ve saved. Is the list getting big? Pick one and start reading it. Afterward, 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 sections and covers of the latest articles. You can either scroll horizontally to see its content or vertically to switch across different topics.

Search

There’s a lot of content that you can browse. 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.

classpath("org.jetbrains.kotlin:kotlin-serialization:1.6.10")
kotlin("plugin.serialization")

Different serialization formats

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

implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2")

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
@OptIn(ExperimentalSerializationApi::class)
//2
@Serializer(forClass = PLATFORM::class)
//3
object RWSerializer : KSerializer<PLATFORM> {

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

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

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

Serializing/deserializing new data

Navigate to FeedPresenter.kt inside commonMain/presentation, and you’ll see a RW_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<RWContent> by lazy {
  json.decodeFromString(RW_CONTENT)
}
import kotlinx.serialization.decodeFromString
Fig. 11.6 - Android app with different platforms
Qoc. 18.5 - Uflruuy uww degz dupsosipy nbigsukvq

Fig. 11.7 - iOS app with different platforms
Qod. 13.1 - oUS oyx noxt mavqikucv jpeqkeszn

Fig. 11.8 - Desktop app with different platforms
Hev. 01.0 - Dezxmag osf wugt mudmiqell vyisdijzj

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

id("kotlin-parcelize")
import kotlinx.parcelize.Parcelize

@Parcelize
data class RWEntry(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.raywenderlich.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.raywenderlich.learn.platform

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

Working with targets that don’t support Parcelize

Now that you’ve implemented Parcelize on common and Android, if you look at the actual fields that you’ve added, you’ll see a red underline, which means that something is wrong. This happens because the app is targeting other platforms that are missing the actual implementation.

package com.raywenderlich.learn.platform

actual interface Parcelable

Adding Parcelize to existing classes

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

@Parcelize
@Serializable
data class RWContent(
 val platform: PLATFORM,
 val url: String,
 val image: String
) : Parcelable
@Parcelize
data class RWEntry(
 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

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.kt class:

class SerializationTests { }
@Test
fun testEncodePlatformAll() {
  val data = RWContent(
    platform = PLATFORM.ALL,
    url = "https://www.raywenderlich.com/feed.xml",
    image = "https://assets.carolus.raywenderlich.com/assets/razeware_460-308933a0bda63e3e327123cab8002c0383a714cd35a10ade9bae9ca20b1f438b.png"
  )

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

  val content = "{\"platform\":\"all\",\"url\":\"https://www.raywenderlich.com/feed.xml\",\"image\":\"https://assets.carolus.raywenderlich.com/assets/razeware_460-308933a0bda63e3e327123cab8002c0383a714cd35a10ade9bae9ca20b1f438b.png\"}"
  assertEquals(content, decoded)
}
@Test
fun testDecodePlatformAll() {
  val data = "{\"platform\":\"all\",\"url\":\"https://www.raywenderlich.com/feed.xml\",\"image\":\"https://assets.carolus.raywenderlich.com/assets/razeware_460-308933a0bda63e3e327123cab8002c0383a714cd35a10ade9bae9ca20b1f438b.png\"}"

  val decoded = Json.decodeFromString(RWContent.serializer(), data)
  val content = RWContent(
    platform = PLATFORM.ALL,
    url = "https://www.raywenderlich.com/feed.xml",
    image = "https://assets.carolus.raywenderlich.com/assets/razeware_460-308933a0bda63e3e327123cab8002c0383a714cd35a10ade9bae9ca20b1f438b.png"
  )

  assertEquals(content, decoded)
}

Testing custom serialization

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

private val serializers = serializersModuleOf(PLATFORM::class, RWSerializer)
@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.raywenderlich.com/feed.xml", 
  "image":"https://assets.carolus.raywenderlich.com/assets/razeware_460-308933a0bda63e3e327123cab8002c0383a714cd35a10ade9bae9ca20b1f438b.png"
}
@Test
fun testDecodeCustomPlatformAll() {
  val data = PLATFORM.ALL

  val decoded = Json.decodeFromString<PLATFORM>(data.value)
  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: Load an RSS feed

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

Challenge 2: Add 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 RWEntry.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.

© 2022 Razeware LLC

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

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