Android Data Serialization Tutorial with the Kotlin Serialization Library

Learn how to use the Kotlin Serialization library in your Android app and how it differs from other data serialization libraries available out there. By Kshitij Chauhan.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Modeling Data

The Kotlin Serialization library generates serializers for classes annotated with @Serializable.

A serializer is a class that handles an object’s serialization and deserialization. For every class annotated with @Serializable, the compiler generates a serializer on its companion object. Use this method to get access to the serializer for that class.

Go to Android Studio, and open BoredActivity.kt in data. In this file, you’ll find the BoredActivity data class that represents an activity to try when you’re bored.

Note: If you’re new to Android development, don’t confuse the BoredActivity class with an Android Activity. An Android Activity is a system component that represents a screen in the app presented to the user. BoredActivity is a model class for an activity to try when you’re bored. It’s not related to the Android Activity class.

Now you need to make this class known to the Kotlin Serialization library. Import kotlinx.serialization.Serializable and annotate BoredActivity with @Serializable:

import kotlinx.serialization.Serializable

@Serializable
data class BoredActivity(
  // The rest of the data class...
)

Then build the project and see it compile.

Now, if you list the methods on the BoredActivity companion object as shown in the image below, you’ll notice a new serializer() on it that returns an instance of KSerializer. You do not need to modify the code in this step, so you can remove the init method after you’ve observed the method list.

Image showing the auto generated serializer method on the BoredActivity companion object

For most use cases, that’s all you need to do. However, the library offers several customization options and utilities for more advanced use cases. The next sections describe these features.

Encoding Data Manually

You can use the auto-generated serializer() to gain access to a class’s serializer. Then you can use it with the JSON encoding module to manually serialize or deserialize data as shown in the code example below:

import kotlinx.serialization.json.Json
import kotlinx.serialization.Serializable

@Serializable
data class PlatformInfo(
  val platformName: String,
  val apiLevel: Int
)

fun main() {
  val lollipop = PlatformInfo("Lollipop", 21)
  val json = Json.encodeToString(PlatformInfo.serializer(), lollipop)
  println(json) // {"platformName":"Lollipop","apiLevel":21}
}

You could also access a class’s serializer by using the top-level generic serializer() as shown in the next example:

import kotlinx.serialization.serializer

val lollipop = PlatformInfo("Lollipop", 21)
val json = Json.encodeToString(serializer<PlatformInfo>(), lollipop)
println(json) // {"platformName":"Lollipop","apiLevel":21}

Serializing Composite Types

The library can serialize all primitive Kotlin values out of the box, including Boolean, Byte, Short, Int, Long, Float, Double, Char and String. It also supports composite types based on these primitives, such as List, Map, Set, Pair and Triple. enums work automatically, too!

You can access the serializer for composite types based on custom types by using the base serializer and passing to it the custom type’s serializer. For example, to serialize List, you can use ListSerializer along with PlatformInfo.serializer():

val platforms: List<PlatformInfo> = listOf(...)
val platformsSerializer = ListSerializer(PlatformInfo.serializer())

Similarly, you can use SetSerializer and MapSerializer when needed. If you’re unsure how to construct your serializer, you can always use the top level serializer function as illustrated earlier.

Customizing Property Names

Many encoding formats use snake case variable names to represent data. To model such data with a Kotlin class, you need to break the language’s camel-case style convention. For example, consider the following example of JSON data:

{
  "platform_name": "Android",
  "api_level": 30
}

To model such an object, you use the @SerialName annotation instead as shown below:

import kotlinx.serialization.SerialName

@Serializable
data class PlatformInfo(
  @SerialName("platform_name")
  val platformName: String,
  @SerialName("api_level")
  val apiLevel: Int
)

The @SerialName annotation lets you specify a custom name for the encoded property in a serializable class. It tells the library to map the value of an encoded object’s platform_name field into the PlatformInfo class’s platformName field, and vice versa.

Marking Transient Data

If your model class contains properties that you must not serialize, annotate them with @Transient. A transient property must have a default value. Consider the code example below:

import kotlinx.serialization.Transient

@Serializable
data class PlatformInfo(
  // ...
  @Transient
  val isCurrent: Boolean = apiLevel == Build.VERSION.SDK_INT
)

isCurrent is annoted as @Transient and assigned a default initial value.

Transient properties are neither serialized into encoded output nor read from decoded input. Consider the example below that utilizes PlatformInfo:

val lollipop = PlatformInfo("Lollipop", 21)
println(lollipop) // PlatformInfo(platformName=Lollipop, apiLevel=21, isCurrent=false)

val json = Json.encodeToString(PlatformInfo.serializer(), lollipop)
println(json) // {"platform_name":"Lollipop","api_level":21}

The above code creates a PlatformInfo object and prints it, the object includes all three properties. Then, the serializer() is used to encode the object into JSON. The transient property is not encoded or included in the JSON.

That’s a lot of information about building and customizing serializers! Now it’s time to put it to use. In the next section, you’ll learn how to add Retrofit to the mix.

Integrating With Retrofit

In its current state, the app uses static, preprogrammed data. In this section, you’ll use Retrofit with the Kotlin Serialization library to fetch BoredActivity objects from the Bored API, which uses JSON to communicate requests and responses.

The app uses a repository to supply BoredActivity objects to its viewmodels. The repository relies on BoredActivityDataSource to fetch those objects. The starter app ships with two implementations of this interface:

  • FakeDataSource: Returns static data hard coded into the app. It doesn’t communicate with the Bored API.
  • RealDataSource: Communicates with the Bored API using Retrofit.
Note: Retrofit is an HTTP client for Android. It’s a popular library used to communicate with web servers over HTTP requests. If you’re unfamiliar with Retrofit, check out this video course: Android Networking: Fundamentals.

In its current state, the app uses the fake data source by default. It’s unsafe to use the real data source right now, as Retrofit wouldn’t know how to parse the JSON responses returned by the API.

To fix this, you need to give Retrofit the ability to handle JSON responses.

Adding the Retrofit Converter for Kotlin Serialization

Retrofit uses a pluggable system for serializing API requests and responses. It delegates this responsibility to a set of Converter objects that transform data into whatever format applies to the API.

For your app, you’ll use the retrofit2-kotlinx-serialization-converter library by Jake Wharton. It lets Retrofit use the Kotlin Serialization library to convert API requests and responses.

First, open your app module’s build.gradle and add the following dependency:

implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0"

Sync the project to download the dependency. Then open Module.kt in data. It contains a Dagger module named ApiModule that provides various dependencies, including Retrofit. Replace retrofit with the following:

@Provides
@ExperimentalSerializationApi
fun retrofit(okHttpClient: OkHttpClient): Retrofit {
  val contentType = "application/json".toMediaType()
  val converterFactory = Json.asConverterFactory(contentType)
  return Retrofit.Builder()
    .client(okHttpClient)
    .addConverterFactory(converterFactory)
    .baseUrl("https://www.boredapi.com/api/")
    .build()
}

The above code creates converterFactory as a converter factory that uses JSON and adds it to the Retrofit instance. Now that Retrofit can communicate with the API through JSON objects, it’s safe to switch to the source of real data.