Satchel
:school_satchel: A fast, secure and modular key-value storage with batteries-included for Android and JVM.
Install / Use
/learn @adrielcafe/SatchelREADME
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
- [x]
DoubleandList<Double> - [x]
FloatandList<Float> - [x]
IntandList<Int> - [x]
LongandList<Long> - [x]
BooleanandList<Boolean> - [x]
StringandList<String> - [x]
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
:warning: Satchel doesn't store your crypto keys, it only uses it. So make sure to store them in a safe place.
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. `
Related Skills
feishu-drive
343.1k|
things-mac
343.1kManage Things 3 via the `things` CLI on macOS (add/update projects+todos via URL scheme; read/search/list from the local Things database)
clawhub
343.1kUse the ClawHub CLI to search, install, update, and publish agent skills from clawhub.com
codebase-memory-mcp
1.1kHigh-performance code intelligence MCP server. Indexes codebases into a persistent knowledge graph — average repo in milliseconds. 66 languages, sub-ms queries, 99% fewer tokens. Single static binary, zero dependencies.
