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).
Features
- Fast: see the Benchmark results
- Small: the core library has ~35kb and contains everything you need to get started
- Simple: has an easy to use API
- Modular: 10 (optional) built-in modules to choose from
- Extensible: create your own Storer, Encrypter and Serializer
Supported types
-
Double
andList<Double>
-
Float
andList<Float>
-
Int
andList<Int>
-
Long
andList<Long>
-
Boolean
andList<Boolean>
-
String
andList<String>
-
Serializable
¹
¹ Not supported by satchel-serializer-protobuf-lite
Setup
- Add the JitPack repository to your project level
build.gradle
:
allprojects {
repositories {
maven { url 'https://jitpack.io' }
}
}
- 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"
}
Usage
Take a look at the sample app for a working example.
Global instance
First initialize Satchel's global instance by calling Satchel.init()
:
Satchel.init(
storer = FileSatchelStorer(storageFile),
encrypter = BypassSatchelEncrypter,
serializer = RawSatchelSerializer
)
Now you can use Satchel.storage
everywhere:
Satchel.storage["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"
API
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") -> { /* ... */ }
}
remove("favoritePostIds")
clear()
}
But unlike SharedPreferences
, there's no apply()
or commit()
. Changes are saved asynchronously every time a write operation (set()
, remove()
and clear()
) happens.
Delegates
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)
showFavoritePosts(favoritePostIds)
If you doesn't specify a default value, it will return a nullable value:
private var username by satchel.value<String>("username")
username?.let(::showProfile)
Events
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 -> { /* ... */ }
}
}
Modules
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.
Storers
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, "satchel.storage")
FileSatchelStorer
Uses the FileOutputStream
and FileInputStream
to read and write without do any modification.
val storer = FileSatchelStorer(file)
EncryptedFileSatchelStorer
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
}
}
Encrypters
BypassSatchelEncrypter
Just bypass the encryption/decryption.
val encrypter = BypassSatchelEncrypter
CipherSatchelEncrypter
Uses the Cipher for encryption/decryption.
val transformation = "AES"
val key = KeyGenerator
.getInstance(transformation)
.apply { init(256) }
.generateKey()
val cipherKey = CipherKey.SecretKey(key)
val encrypter = CipherSatchelEncrypter.with(cipherKey, transformation)
Jose4jSatchelEncrypter
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
}
}
Serializers
RawSatchelSerializer
Uses the ObjectOutputStream
/ObjectInputStream
for serialization/deserialization.
val serializer = RawSatchelSerializer
GzipSatchelSerializer
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
KryoSatchelSerializer
Uses the Kryo library for serialization/deserialization.
val serializer = KryoSatchelSerializer
ProtobufLiteSatchelSerializer
Uses the Protocol Buffers Java Lite library for serialization/deserialization.
val serializer = ProtobufLiteSatchelSerializer
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
}
}
Benchmark
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 |
Storers
Read (ns) | Write (ns) | |
---|---|---|
FileSatchelStorer | 55.302 | 47.811 |
EncryptedFileSatchelStorer | 261.962 | 322.577 |
Encrypters
Read (ns) | Write (ns) | |
---|---|---|
BypassSatchelEncrypter | 0 | 0 |
CipherSatchelEncrypter | 189.423 | 202.577 |
Jose4jSatchelEncrypter | 394.654 | 498.538 |
TinkSatchelEncrypter | 46.439 | 55.134 |
Serializers
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 |