1.0.3 (Aug 13, 2020)
Jul 13, 2020
Aug 12, 2020 (Retired)
Adriel Café (adrielcafe)
Adriel Café (adrielcafe)
Source code


Satchel is a powerful and flexible key-value storage with batteries-included for Android and JVM.

It's backed by Coroutines and great third-party libraries (Tink, Kryo and Protobuf to name a few).


Supported types

  • Double and List<Double>
  • Float and List<Float>
  • Int and List<Int>
  • Long and List<Long>
  • Boolean and List<Boolean>
  • String and List<String>
  • Serializable¹

¹ Not supported by satchel-serializer-protobuf-lite


  1. Add the JitPack repository to your project level build.gradle:
allprojects {
    repositories {
        maven { url '' }
  1. Next, add the desired dependencies to the module build.gradle:
dependencies {
    // Core (required)
    implementation "com.github.adrielcafe.satchel:satchel-core:$currentVersion"

    // Storers
    implementation "com.github.adrielcafe.satchel:satchel-storer-encrypted-file:$currentVersion"

    // Encrypters
    implementation "com.github.adrielcafe.satchel:satchel-encrypter-cipher:$currentVersion"
    implementation "com.github.adrielcafe.satchel:satchel-encrypter-jose4j:$currentVersion"
    implementation "com.github.adrielcafe.satchel:satchel-encrypter-tink-android:$currentVersion"
    implementation "com.github.adrielcafe.satchel:satchel-encrypter-tink-jvm:$currentVersion"

    // Serializers
    implementation "com.github.adrielcafe.satchel:satchel-serializer-base64-android:$currentVersion"
    implementation "com.github.adrielcafe.satchel:satchel-serializer-base64-jvm:$currentVersion"
    implementation "com.github.adrielcafe.satchel:satchel-serializer-gzip:$currentVersion"
    implementation "com.github.adrielcafe.satchel:satchel-serializer-kryo:$currentVersion"
    implementation "com.github.adrielcafe.satchel:satchel-serializer-protobuf-lite:$currentVersion"

Current version:


Take a look at the sample app for a working example.

Global instance

First initialize Satchel's global instance by calling Satchel.init():

    storer = FileSatchelStorer(storageFile),
    encrypter = BypassSatchelEncrypter,
    serializer = RawSatchelSerializer

Now you can use everywhere:["key"] = "value"

It's also possible to check if Satchel was already initialized:

if (Satchel.isInitialized.not()) {
    // Init

Local instance

Use Satchel.with() to create a local instance:

val satchel = Satchel.with(
    storer = FileSatchelStorer(storageFile),
    encrypter = BypassSatchelEncrypter,
    serializer = RawSatchelSerializer

And start using it:

satchel["key"] = "value"


Satchel has a simple and familiar API based on MutableMap and SharedPreferences:

satchel.apply {
    val firstName = get<String>("firstName")

    val notificationsEnabled = getOrDefault("notificationsEnabled", false)

    val favoritePostIds = getOrDefault("favoritePostIds") { emptySet<Int>() }

    val registeredAt = getOrSet("registeredAt", currentTimestamp)

    val lastName = getOrSet("lastName") { "Doe" }

    set("username", "john.doe")

    setIfAbsent("lastName", lastName)

    keys.forEach { key ->
        // ...

    when {
        isEmpty -> { /* ... */ }
        size == 1 -> { /* ... */ }
        contains("username") -> { /* ... */ }



But unlike SharedPreferences, there's no apply() or commit(). Changes are saved asynchronously every time a write operation (set(), remove() and clear()) happens.


It's possible to delegate the job of get and set the value of a specific key:

private var favoritePostIds by satchel.value(key = "favoritePostIds", defaultValue = emptySet<Int>())

// Will call set(key, value)
favoritePostIds = setOf(1, 2, 3)

// Will call getOrDefault(key, defaultValue)

If you doesn't specify a default value, it will return a nullable value:

private var username by satchel.value<String>("username")



You can be notified every time the storage changes, just call addListener() to register a listener in the specified CoroutineScope:

satchel.addListener(lifecycleScope) { event ->
    when (event) {
        is SatchelEvent.Set -> { /* ... */ }
        is SatchelEvent.Remove -> { /* ... */ }
        is SatchelEvent.Clear -> { /* ... */ }


Satchel has 3 different categories of modules:

  • Storers: responsible for reading and writing to the file system
  • Encrypters: responsible for encryption and decryption
  • Serializers: responsible for serialization and deserialization

The core library comes with one stock module for each category: FileSatchelStorer, BypassSatchelEncrypter and RawSatchelSerializer. All the other libraries are optional.


If you are developing for Android, I recommend to use the Context.filesDir as the parent folder. If you want to save in the external storage remember to ask for write permission first.

val file = File(context.filesDir, "")


Uses the FileOutputStream and FileInputStream to read and write without do any modification.

val storer = FileSatchelStorer(file)


Uses the EncryptedFile from Jetpack Security to read/write and also takes care of encryption/decryption.

val storer = EncryptedFileSatchelStorer.with(applicationContext, file)

Build your own Storer

Create a class or object that implements the SatchelStorer interface:

object MySatchelStorer : SatchelStorer {
    suspend fun store(data: ByteArray) {
        // Save the ByteArray wherever you want

    fun retrieve(): ByteArray {
        // Load and return the stored ByteArray


⚠️ Satchel doesn't store your crypto keys, it only uses it. So make sure to store them in a safe place.


Just bypass the encryption/decryption.

val encrypter = BypassSatchelEncrypter


Uses the Cipher for encryption/decryption.

val transformation = "AES"
val key = KeyGenerator
    .apply { init(256) }
val cipherKey = CipherKey.SecretKey(key)
val encrypter = CipherSatchelEncrypter.with(cipherKey, transformation)


Uses the Jose4j library for encryption/decryption.

val jwk = RsaJwkGenerator.generateJwk(2048)
val encrypter = Jose4jSatchelEncrypter.with(jwk)

TinkSatchelEncrypter (JVM)

Uses the Tink JVM library for encryption/decryption.

val keyset = KeysetHandle.generateNew(AesGcmKeyManager.aes256GcmTemplate())
val encrypter = TinkSatchelEncrypter.with(keyset)

TinkSatchelEncrypter (Android)

Uses the Tink Android library for encryption/decryption.

val encrypter = TinkSatchelEncrypter.with(applicationContext)

Build your own Encrypter

Create a class or object that implements the SatchelEncrypter interface:

object MySatchelEncrypter : SatchelEncrypter {
    suspend fun encrypt(data: ByteArray): ByteArray {
        // Return a encrypted ByteArray

    fun decrypt(data: ByteArray): ByteArray {
        // Return a decrypted ByteArray



Uses the ObjectOutputStream/ObjectInputStream for serialization/deserialization.

val serializer = RawSatchelSerializer


Uses the GZIPOutputStream/GZIPInputStream for serialization/deserialization.

val serializer = GzipSatchelSerializer

Base64SatchelSerializer (JVM)

Uses the Base64 from Java 8 for serialization/deserialization.

val serializer = Base64SatchelSerializer

Base64SatchelSerializer (Android)

Uses the Base64 from Android for serialization/deserialization.

val serializer = Base64SatchelSerializer


Uses the Kryo library for serialization/deserialization.

val serializer = KryoSatchelSerializer

⚠️ At the moment Kryo 5 only works on Android API 26 and later, this issue explains how to make it work in previous versions.


Uses the Protocol Buffers Java Lite library for serialization/deserialization.

val serializer = ProtobufLiteSatchelSerializer

⚠️ The current implementation doesn't supports Serializable objects.

Build your own Serializer

Create a class or object that implements the SatchelSerializer interface:

object MySatchelSerializer : SatchelSerializer {

    override suspend fun serialize(data: Map<String, Any>): ByteArray {
        // Transform the Map into a ByteArray

    override fun deserialize(data: ByteArray): Map<String, Any> {
        // Transform the ByteArray into a Map


The following benchmark consists in reading and writing 1k strings on Satchel and similar libraries. Also we compared all modules (storers, encrypters and serializers) individually to help you choose the fastest ones (if performance is a must for you).

You can run the benchmark by yourself, just execute the following command:

./gradlew benchmark:connectedCheck

The benchmark below was made on a Samsung Galaxy S20.

Similar libraries

For this benchmark, we use a local Satchel instance with the stock modules (FileSatchelStorer, BypassSatchelEncrypter and RawSatchelSerializer) from the core library.

Keep in mind that by using different modules you can get best or worse performance results (see the modules benchmarks below for a detailed comparison).

Read (ns) Write (ns)
Satchel 23.054 217.000
SharedPreferences 341.693 279.346
MMKV 461.807 551.308
Paper 71.388.808 427.568.730
Hawk 18.698.000 1.829.687.614


Read (ns) Write (ns)
FileSatchelStorer 55.302 47.811
EncryptedFileSatchelStorer 261.962 322.577


Read (ns) Write (ns)
BypassSatchelEncrypter 0 0
CipherSatchelEncrypter 189.423 202.577
Jose4jSatchelEncrypter 394.654 498.538
TinkSatchelEncrypter 46.439 55.134


Read (ns) Write (ns)
RawSatchelSerializer 652.769 1.001.346
GzipSatchelSerializer 741.230 1.425.924
Base64SatchelSerializer (Android) 683.231 1.029.077
Base64SatchelSerializer (JVM) 703.769 1.041.000
KryoSatchelSerializer 209.923 170.654
ProtobufLiteSatchelSerializer 629.116 1.319.961