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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Android Data Serialization Tutorial with the Kotlin Serialization Library
25 mins
- Getting Started
- Understanding Data Encoding and Serialization
- Understanding Data Decoding and Deserialization
- Kotlin Serialization
- The Compiler Plugin
- The JSON Encoder Module
- Comparing Kotlin Serialization Library to Moshi and Gson
- Modeling Data
- Encoding Data Manually
- Serializing Composite Types
- Customizing Property Names
- Marking Transient Data
- Integrating With Retrofit
- Adding the Retrofit Converter for Kotlin Serialization
- Switching to the Real Data Source
- Writing Serializers Manually
- Writing a Descriptor
- Writing the Serialize Method
- Writing the Deserialize Method
- Connecting the Serializer to the Class
- Bonus: Tests
- Limitations
- Where to Go From Here?
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.
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.
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
. enum
s 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.
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.