Persisting Android’s Parcelables to Android Arch Components’ Room DB

Ken Yee
4 min readApr 22, 2018

--

In most Android applications, you have data objects that you want to persist for a short time in case the application is killed off by the OS because of memory resource issues. In the past, an Activity/Fragment’s InstanceState was abused to do this, but this caused OS slowdowns and jankiness because restoring instance state is done on the UI thread and large amounts of it caused jankiness to the UI (this was addressed in Android 7.x by the OS checking for more than 512KB of instance state and throwing a TransactionTooLarge exception).

An alternative location for persistent storage is in SharedPreferences, but a better place is in a SQLite DB so you can add extra metadata to the Parcelable to track things like expiration time, etc. We can do this using the Arch Components’ Room DB. First, lets create a Room table:

@Entity(tableName = "parcelized_data")
class Parcelized internal constructor(
@field:ColumnInfo(name = "id")
@field:PrimaryKey
val id: String, @field:ColumnInfo(name = "creationTimestamp")
val creationDateTime: Long, @field:ColumnInfo(name = "expiresTimestamp")
val expiresDateTime: Long?, @field:ColumnInfo(name = "data", typeAffinity = ColumnInfo.BLOB)
val data: ByteArray)

The expiration time stamp gives us a way to purge old data from the DB. The data column lets us store a binary blob (SharedPreferences can only support strings so this saves us a little space compared to using SharedPreferences. Saving creation time is good practice for any table so you can easily see when something was written to a DB. Finally, the ID is how you would reference this object and is used for the primary key so you can’t have duplicate IDs.

Next we’ll create an API to access this table using Room’s DAO support. As you can see, it’s fairly simple to add CRUD support (deletion of multiple objects is done in a transaction for best practice as well, but is not strictly needed in this case), including support for the expiration date:

@Dao
interface ParcelizedDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun saveParcelized(parcelized: Parcelized)

@Query("SELECT * FROM parcelized_data WHERE id = :id")
fun getParcelized(id: String): Parcelized

@Transaction
@Query("DELETE FROM parcelized_data WHERE (expiresTimestamp IS NOT NULL) AND (expiresTimestamp < :nowUtcMillis)")
fun deleteExpiredParcelized(nowUtcMillis: Long)

@Transaction
@Query("DELETE FROM parcelized_data")
fun deleteAllParcelized()
}

Now we can create a wrapper for any Parcelable we want to persist to the DB. The inner ParcelizedObject class handles the being reading/writing the object into a byte array. The TimeSource class is just separated out so that we can mock it easily for unit testing the expires functionality.

Note that the save and load methods on the PersistableParcelable class are tagged with the @WorkerThread annotation because these methods (and any Room DB calls) should never be done on the UI thread. Writing an object to a Parcel requires calling recycle on Parcel when we’re done, so this class just enforces that requirement:

class PersistableParcelable<T : Parcelable>(private val db: MyRoomDatabase, private val timeSource: TimeSource = PersistableParcelable.TimeSource()) {

fun save(id: String, data: T) {
save(id, data, null)
}

@WorkerThread
fun save(id: String, data: T, expirationDurationMillis: Long?) {
val nowUtcMillis = timeSource.nowUtcMillis
val
expirationTimeMillis = if (expirationDurationMillis == null) null else nowUtcMillis + expirationDurationMillis
val parcelizedData = ParcelizedObject(data)
val parcelized = Parcelized(id, nowUtcMillis, expirationTimeMillis, parcelizedData.toBytes())
db.parcelizedDao().saveParcelized(parcelized)
parcelizedData.recycle()
}

@WorkerThread
fun load(id: String, creator: Parcelable.Creator<T>): T? {
val nowUtcMillis = timeSource.nowUtcMillis
db
.parcelizedDao().deleteExpiredParcelized(nowUtcMillis)

val parcelized = db.parcelizedDao().getParcelized(id) ?: return null

val
parcelizedObject = ParcelizedObject(parcelized.data)
val obj = creator.createFromParcel(parcelizedObject.getParcel())
parcelizedObject.recycle()
return obj
}

class TimeSource {
val nowUtcMillis: Long
get() = System.currentTimeMillis()
}

private class ParcelizedObject {
private val parcel = Parcel.obtain()

internal constructor(parcelable: Parcelable) {
parcelable.writeToParcel(parcel, 0)
}

internal constructor(data: ByteArray) {
parcel.unmarshall(data, 0, data.size)
parcel.setDataPosition(0)
}

internal fun toBytes(): ByteArray {
return parcel.marshall()
}

fun getParcel(): Parcel {
parcel.setDataPosition(0)
return parcel
}

// Always call to prevent memory leak!
fun recycle() {
parcel.recycle()
}
}
}

Now we can finally see an example of how this is used:

val persistableParcelable = PersistableParcelable<MyParcelable>(myDatabase)
persistableParcelable.save("something", aParcelable)
persistableParcelable.load("something", MyParcelable.CREATOR)

You create a PersistableParcelable to wrap your object and tell it what database to use. Then you can save or load it from the database using “something” as your object’s ID.

Remember that we can’t use this on the UI thread. If you’re using RxJava, you can easily wrap the call via Maybe.fromCallable() and use subscribeOn to run it on the I/O Scheduler. Using LiveData in this case doesn’t buy you that much because the delete and save methods are not async.

If you do use this to replace InstanceState, do NOT run any of this in a blocking way or you’ll cause UI jank. And of course, you all are using an async loading pattern that handles the standard Load/Content/Error states, because that’s best practice, right? ;-)

Hope this is helpful and if you’re using this for other things besides InstanceState, let me know.

--

--

Ken Yee
Ken Yee

Written by Ken Yee

Mobile (Native Android), Backend (Spring Boot, Quarkus), Devops (Jenkins, Docker, K8s) software engineer. Currently Kotlin All The Things!