We’ve previously shared how to use the Proto DataStore and Preferences DataStore. Both DataStore versions serialize data behind the scenes using Protos. You can also use Kotlin serialization, combining DataStore with custom data classes. This helps reduce boilerplate code and does not require learning or relying on the Protobuf library while still providing architecture for the data.
You need to complete the following operations:
- Defining data classes
- Make sure your data classes are immutable
- Implement DataStore serializer using Kotlin serialization
- Begin to use
Defining data classes
The Kotlin data classes are ideal for use with DataStore because they work seamlessly with the Kotlin serialization. DataStore relies on equals and hashCode, which are automatically generated by the data class. Data classes also generate toString and copy functions that are easy to debug and update data.
/* Copyright 2021 Google LLC.spdx-license-Identifier: Apache-2.0 */
data class UserPreferences(
val showCompleted: Boolean.val sortOrder: SortOrder
)
Copy the code
Make sure your data classes are immutable
It is important to ensure that your data classes are immutable, because DataStore is not compatible with mutable types. Using mutable types in conjunction with datastores can lead to hard-to-catch errors and race conditions. Data classes are not necessarily immutable.
Vars are mutable, so you should use vals instead:
/* Copyright 2021 Google LLC.spdx-license-Identifier: Apache-2.0 */
data class MyData(-var num: Int
+ val num: Int
)
- myObj.num = 5 // Fails to compile when num is val
+ val newObj = myObj.copy(num = 5)
Copy the code
Arrays are mutable, so you should not expose them.
/* Copyright 2021 Google LLC.spdx-license-Identifier: Apache-2.0 */
data class MyData(-var num: IntArray
)
- myObj.num = 5 // This would mutate your object
Copy the code
Even if a read-only list is used as part of a data class, the data class is still mutable. You should consider using immutable/persistent collections instead:
/* Copyright 2021 Google LLC.spdx-license-Identifier: Apache-2.0 */
data class MyData(-val nums: List<Int>
+ val nums: PersistentList<Int>) -val myInts = mutableListOf(1.2.3.4)
- val myObj = MyData(myInts)
- myInts.add(5) // Fails to compile with PersistentList, but mutates with List
+ val newData = myObj.copy(
+ nums = myObj.nums.mutate { it += 5 } // Mutate returns a new PersistentList+)Copy the code
Using a mutable type as part of a data class puts the data class into a mutable state. Instead of doing this, make sure everything is immutable.
/* Copyright 2021 Google LLC.spdx-license-Identifier: Apache-2.0 */
data class MyData(-val mutableType: MutableType
)
- val myType = MutableType()
- val myObj = MyData(myType)
- myType.mutate()
Copy the code
Implement DataStore serializer
Kotlin serialization supports a variety of formats including JSON and protocol buffers. I’ll use JSON here because it’s common, easy to use, and stored in clear text for debugging. Protobuf is also a good choice as it is smaller, faster and protobuf-Lite compatible.
To read the data class and write it to JSON using Kotlin serialization, You need to use the @SerialIZABLE annotated data class and use json.decodeFromString
(string) and json.encodeToString (data). Here is an example with UserPreferences:
/* Copyright 2021 Google LLC.spdx-license-Identifier: Apache-2.0 */
@Serializable
data class UserPreferences(
val showCompleted: Boolean = false.val sortOrder: SortOrder = SortOrder.None
)
object UserPreferencesSerializer : Serializer<UserPreferences> {
override val defaultValue = UserPreferences()
override suspend fun readFrom(input: InputStream): UserPreferences {
try {
return Json.decodeFromString(
UserPreferences.serializer(), input.readBytes().decodeToString())
} catch (serialization: SerializationException) {
throw CorruptionException("Unable to read UserPrefs", serialization)
}
}
override suspend fun writeTo(t: UserPreferences, output: OutputStream) {
output.write(Json.encodeToString(UserPreferences.serializer(), t).encodeToByteArray())
}
}
Copy the code
⚠️ It is not safe to use Parcelables with DataStore because the data format may change between Android versions.
Using serializer
When you build, pass the serializer you created to the DataStore:
/* Copyright 2021 Google LLC.spdx-license-Identifier: Apache-2.0 */
val Context.dataStore by dataStore("my_file.json", serializer = UserPreferencesSerializer)
Copy the code
The read looks the same as the read using protos:
/* Copyright 2021 Google LLC.spdx-license-Identifier: Apache-2.0 */
suspend fun getShowCompleted(a): Boolean {
context.dataStore.data.first().showCompleted
}
Copy the code
You can update the data using the generated.copy() function:
/* Copyright 2021 Google LLC.spdx-license-Identifier: Apache-2.0 */
suspend fun setShowCompleted(newShowCompleted: Boolean) {
// This will leave the sortOrder value untouched:
context.dataStore.updateData { it.copy(newShowCompleted = showCompleted) }
}
Copy the code
conclusion
Using DataStore in combination with Kotlin serialization and data classes reduces boilerplate and helps simplify code, but you must be careful not to introduce errors due to variability. You only need to define the data classes and implement the serializer. Come and try it!
To learn more about DataStore, check out our documentation and get some hands-on experience using Proto DataStore and Preferences DataStore Codelab.