It's backed by Coroutines and great third-party libraries (Tink, Kryo and Protobuf to name a few).
- 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
-
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
- 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"
}
Take a look at the sample app for a working example.
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
}
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") -> { /* ... */ }
}
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.
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)
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, "satchel.storage")
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)
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
}
}
Just bypass the encryption/decryption.
val encrypter = BypassSatchelEncrypter
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)
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)
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
Uses the Protocol Buffers Java Lite library for serialization/deserialization.
val serializer = ProtobufLiteSatchelSerializer
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.
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 |