:blowfish: An Android & JVM key-value storage powered by Protobuf and Coroutines

Overview

JitPack Android API Bitrise Codecov Codacy kotlin ktlint License MIT

logo PufferDB

PufferDB is a key-value storage powered by Protocol Buffers (aka Protobuf) and Coroutines.

The purpose of this library is to provide an efficient, reliable and Android independent storage.

Why Android independent? The SharedPreferences and many great third-party libraries (like Paper and MMKV) requires the Android Context to work. But if you are like me and want a kotlin-only data module (following the principles of Clean Architecture), this library is for you!

This project started as a library module in one of my personal projects, but I decided to open source it and add more features for general use. Hope you like!

About Protobuf

Protocol Buffers are a language-neutral, platform-neutral extensible mechanism for serializing structured data. Compared to JSON, Protobuf files are smaller and faster to read/write because the data is stored in an efficient binary format.

Features

Supported types

So far, PufferDB supports the following 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>

Getting Started

Import to your project

  1. Add the JitPack repository in your root build.gradle at the end of repositories:
allprojects {
    repositories {
        maven { url 'https://jitpack.io' }
    }
}
  1. Next, add the desired dependencies to your module:
dependencies {
    // Core library
    implementation "com.github.adrielcafe.pufferdb:core:$currentVersion"

    // Android helper
    implementation "com.github.adrielcafe.pufferdb:android:$currentVersion"

    // Coroutines wrapper
    implementation "com.github.adrielcafe.pufferdb:coroutines:$currentVersion"

    // RxJava wrapper
    implementation "com.github.adrielcafe.pufferdb:rxjava:$currentVersion"
}

Current version: JitPack

Platform compatibility

core android coroutines rxjava
Android
JVM

Core

As the name suggests, Core is a standalone module and all other modules depends on it.

To create a new Puffer instance you must tell which file to use.

val pufferFile = File("path/to/puffer/file")
val puffer = PufferDB.with(pufferFile)
// or
val puffer = PufferDB.with(pufferFile, myCoroutineScope, myCoroutineDispatcher)

If you are on 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.

Its API is similar to SharedPreferences:

puffer.apply {
    val myValue = get<String>("myKey")
    val myValueWithDefault = get("myKey", "defaultValue")
    
    put("myOtherKey", 123)

    getKeys().forEach { key ->
        // ...
    }

    if(contains("myKey")){
        // ...
    }

    remove("myOtherKey")

    removeAll()
}

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

Threading

PufferDB uses a ConcurrentHashMap to manage a thread-safe in-memory cache for fast read and write operations.

Changes are saved asynchronously with the help of a StateFlow (to save the most recent state in a race condition) and Mutex locker (to prevent simultaneous writes).

It is possible to run the API methods on the Android Main Thread, but you should avoid that. You can use one of the wrapper modules or built in extension functions for that (listed below).

Android

The Android module contains an AndroidPufferDB helper class:

class MyApp : Application() {

    override fun onCreate() {
        super.onCreate()
        // Init the PufferDB when your app starts
        AndroidPufferDB.init(this)
    }
}

// Now you can use it anywhere in your app
class MyActivity : AppCompatActivity() {

    // Returns a default Puffer instance, the file is located on Context.filesDir
    val corePuffer = AndroidPufferDB.withDefault()

    // Returns a File that should be used to create a Puffer instance
    val pufferFile = AndroidPufferDB.getInternalFile("my.db")
    val coroutinePuffer = CoroutinePufferDB.with(pufferFile)
}

Coroutines

The Coroutines module contains a CoroutinePufferDB wrapper class and some useful extension functions:

val pufferFile = File("path/to/puffer/file")
val puffer = CoroutinePufferDB.with(pufferFile)

// All methods are suspend functions that runs on Dispatchers.IO context
launch {
    puffer.apply {
        val myValue = get<String>("myKey")
        val myValueWithDefault = get("myKey", "defaultValue")
        
        put("myOtherKey", 123)

        getKeys().forEach { key ->
            // ...
        }

        if(contains("myKey")){
            // ...
        }

        remove("myOtherKey")

        removeAll()
    }
}

If you don't want to use this wrapper class, there's some built in extension functions that can be used with the Core module:

val pufferFile = File("path/to/puffer/file")
val puffer = PufferDB.with(pufferFile) // <- Note that we're using the Core PufferDB

launch {
    puffer.apply {
        val myValue = getSuspend<String>("myKey")

        val myValue = getAsync<String>("myKey").await()
        
        // You can use your own coroutine scope and dispatcher
        putSuspend("myOtherKey", 123, myCoroutineScope, myCoroutineDispatcher)

        putAsync("myOtherKey", 123, myActivityScope).await()
    }
}

RxJava

The RxJava module contains a RxPufferDB wrapper class and some useful extension functions:

val pufferFile = File("path/to/puffer/file")
val puffer = RxPufferDB.with(pufferFile)

puffer.apply {
    // Some methods returns Single<T>...
    get<String>("myKey") // OR get("myKey", "defaultValue")
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe { myValue ->
            // ...
        }

    // ... And others returns Completable
    put("myOtherKey", 123)
        // ...
        .subscribe {
            // ...
        }

    getKeys()
        // ...
        .subscribe { keys ->
            // ...
        }

    contains("myKey")
        // ...
        .subscribe { contains ->
            // ...
        }

    remove("myOtherKey")
        // ...
        .subscribe {
            // ...
        }

    removeAll()
        // ...
        .subscribe {
            // ...
        }
}

Like the Coroutines module, the RxJava module also provides some useful built in extension functions that can be used with the Core module:

val pufferFile = File("path/to/puffer/file")
val puffer = PufferDB.with(pufferFile) // <- Note that we're using the Core PufferDB

puffer.apply {
    val myValue = getSingle<String>("myKey").blockingGet()

    putCompletable("myOtherKey", 123).blockingAwait()

    getKeysObservable().blockingSubscribe { keys ->
        // ...
    }
}

Benchmark

Write & Read

Write 1k strings (ms) Read 1k strings (ms)
PufferDB 20 5
SharedPreferences 278 7
MMKV 13 8
Paper 818 169
Binary Prefs 121 9
Hawk 15183 207
Write 100k strings (ms) Read 100k strings (ms)
PufferDB 259 32
SharedPreferences 💥 💥
MMKV 871 516
Paper 💥 💥
Binary Prefs 1082 101
Hawk 💥 💥

File size

1k strings (kb)
PufferDB 25
SharedPreferences 20
MMKV 40
Paper 61
Binary Prefs 53
Hawk 27
100k strings (kb)
PufferDB 2.907
SharedPreferences 💥
MMKV 4.104
Paper 💥
Binary Prefs 5.175
Hawk 💥

Tested on Moto Z2 Plus

You can run the Benchmark through the sample app.

Comments
  • Closing file streams

    Closing file streams

    Was having an issue with a warning of "a resource failed to call close". With the right strict mode configuration, this can also cause a crash for a leaked Closeable. These changes just close the streams after reading/writing from/to them

    opened by daviscodesbugs 0
  • InvalidProtocolBufferException

    InvalidProtocolBufferException

    // CRASH: // Short Msg: com.google.protobuf.InvalidProtocolBufferException // Long Msg: com.google.protobuf.InvalidProtocolBufferException: Protocol message contained an invalid tag (zero). // Build Label: Infinix/X680-OP/Infinix-X680:10/QP1A.190711.020/DE-200107V96:user/release-keys // Build Changelist: DE-200107V96 // Build Time: 1578388730000 // java.lang.RuntimeException: Unable to create application : cafe.adriel.pufferdb.core.PufferException: Unable to read /data/user/0/****/files/puffer.db // at android.app.ActivityThread.handleBindApplication(ActivityThread.java:6642) // at android.app.ActivityThread.access$1400(ActivityThread.java:236) // at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1915) // at android.os.Handler.dispatchMessage(Handler.java:107) // at android.os.Looper.loop(Looper.java:264) // at android.app.ActivityThread.main(ActivityThread.java:7547) // at java.lang.reflect.Method.invoke(Native Method) // at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492) // at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:980) // Caused by: cafe.adriel.pufferdb.core.PufferException: Unable to read /data/user/0/*****/files/puffer.db // at cafe.adriel.pufferdb.core.PufferDB.loadProto(PufferDB.kt:106) // at cafe.adriel.pufferdb.core.PufferDB.(PufferDB.kt:46) // at cafe.adriel.pufferdb.core.PufferDB.(PufferDB.kt:24) // at cafe.adriel.pufferdb.core.PufferDB$Companion.with(PufferDB.kt:33) // at cafe.adriel.pufferdb.android.AndroidPufferDB.init(AndroidPufferDB.kt:17)

    // at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1189) // at android.app.ActivityThread.handleBindApplication(ActivityThread.java:6637) // ... 8 more // Caused by: com.google.protobuf.InvalidProtocolBufferException: Protocol message contained an invalid tag (zero). // at com.google.protobuf.CodedInputStream.readTag(CodedInputStream.java:148) // at cafe.adriel.pufferdb.proto.Pufferroto.dynamicMethod(PufferProto.java:337) // at com.google.protobuf.GeneratedMessageLite.parsePartialFrom(GeneratedMessageLite.java:1355) // at com.google.protobuf.GeneratedMessageLite.parseFrom(GeneratedMessageLite.java:1464) // at cafe.adriel.pufferdb.proto.PufferProto.parseFrom(PufferProto.java:153) // at cafe.adriel.pufferdb.core.PufferDB.loadProtoFile(PufferDB.kt:116) // at cafe.adriel.pufferdb.core.PufferDB.loadProto(PufferDB.kt:100) // ... 17 more

    opened by tmxdyf 0
  • com.google.protobuf.InvalidProtocolBufferException

    com.google.protobuf.InvalidProtocolBufferException

    Motorola Moto G (5th Gen) (cedric), 2048MB RAM, Android 8.1

    Caused by: cafe.adriel.pufferdb.core.PufferException: 
      at cafe.adriel.pufferdb.core.PufferDB.loadProto (PufferDB.java:6)
      at cafe.adriel.pufferdb.core.PufferDB.<init> (PufferDB.java:6)
      at cafe.adriel.pufferdb.core.PufferDB.<init> (PufferDB.java:6)
      at cafe.adriel.pufferdb.core.PufferDB$Companion.with (PufferDB.java:3)
      at cafe.adriel.pufferdb.android.AndroidPufferDB.init (AndroidPufferDB.java:3)
      at com.talpa.translate.Initialize.initialize (Initialize.java:3)
      at com.talpa.translate.Initialize.<init> (Initialize.java:3)
      at com.talpa.translate.HiApplication.onCreate (HiApplication.java:11)
      at android.app.Instrumentation.callApplicationOnCreate (Instrumentation.java:1141)
      at android.app.ActivityThread.handleBindApplication (ActivityThread.java:5854)
      at android.app.ActivityThread.-wrap1 (Unknown Source)
      at android.app.ActivityThread$H.handleMessage (ActivityThread.java:1697)
      at android.os.Handler.dispatchMessage (Handler.java:106)
      at android.os.Looper.loop (Looper.java:164)
      at android.app.ActivityThread.main (ActivityThread.java:6626)
      at java.lang.reflect.Method.invoke (Method.java)
      at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:438)
      at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:811)
    Caused by: com.google.protobuf.InvalidProtocolBufferException: 
      at com.google.protobuf.CodedInputStream.readTag (CodedInputStream.java:25)
      at cafe.adriel.pufferdb.proto.PufferProto.dynamicMethod (PufferProto.java:2)
      at com.google.protobuf.GeneratedMessageLite.parsePartialFrom (GeneratedMessageLite.java:10)
      at com.google.protobuf.GeneratedMessageLite.parseFrom (GeneratedMessageLite.java:8)
      at cafe.adriel.pufferdb.proto.PufferProto.parseFrom (PufferProto.java:1)
      at cafe.adriel.pufferdb.core.PufferDB.loadProtoFile (PufferDB.java:1)
      at cafe.adriel.pufferdb.core.PufferDB.loadProto (PufferDB.java:2)
      at cafe.adriel.pufferdb.core.PufferDB.<init> (PufferDB.java:2)
      at cafe.adriel.pufferdb.core.PufferDB.<init> (PufferDB.java:2)
      at cafe.adriel.pufferdb.core.PufferDB$Companion.with (PufferDB.java:3)
      at cafe.adriel.pufferdb.android.AndroidPufferDB.init (AndroidPufferDB.java:3)
    
    opened by tmxdyf 0
  • PufferException#AndroidPufferDB.withDefault

    PufferException#AndroidPufferDB.withDefault

    Samsung Galaxy J6+ (j6primelte), 3072MB RAM, Android 9 报告 1

    java.lang.RuntimeException: 
      at android.app.ActivityThread.performLaunchActivity (ActivityThread.java:3126)
      at android.app.ActivityThread.handleLaunchActivity (ActivityThread.java:3269)
      at android.app.servertransaction.LaunchActivityItem.execute (LaunchActivityItem.java:78)
      at android.app.servertransaction.TransactionExecutor.executeCallbacks (TransactionExecutor.java:108)
      at android.app.servertransaction.TransactionExecutor.execute (TransactionExecutor.java:68)
      at android.app.ActivityThread$H.handleMessage (ActivityThread.java:1960)
      at android.os.Handler.dispatchMessage (Handler.java:106)
      at android.os.Looper.loop (Looper.java:214)
      at android.app.ActivityThread.main (ActivityThread.java:7094)
      at java.lang.reflect.Method.invoke (Method.java)
      at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:494)
      at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:975)
    Caused by: cafe.adriel.pufferdb.core.PufferException: 
      at cafe.adriel.pufferdb.android.AndroidPufferDB.withDefault (AndroidPufferDB.java:3)
    ```java
    opened by tmxdyf 0
  • PufferException

    PufferException

    Huawei Honor 7A (HWAUM-Q), 2048MB RAM, Android 8.0 报告 2

    java.lang.RuntimeException: 
      at android.app.ActivityThread.handleBindApplication (ActivityThread.java:6637)
      at android.app.ActivityThread.-wrap2 (Unknown Source)
      at android.app.ActivityThread$H.handleMessage (ActivityThread.java:2066)
      at android.os.Handler.dispatchMessage (Handler.java:108)
      at android.os.Looper.loop (Looper.java:166)
      at android.app.ActivityThread.main (ActivityThread.java:7529)
      at java.lang.reflect.Method.invoke (Method.java)
      at com.android.internal.os.Zygote$MethodAndArgsCaller.run (Zygote.java:245)
      at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:921)
    Caused by: cafe.adriel.pufferdb.core.PufferException: 
      at cafe.adriel.pufferdb.core.PufferDB.loadProto (PufferDB.java:6)
      at cafe.adriel.pufferdb.core.PufferDB.<init> (PufferDB.java:6)
      at cafe.adriel.pufferdb.core.PufferDB.<init> (PufferDB.java:6)
      at cafe.adriel.pufferdb.core.PufferDB$Companion.with (PufferDB.java:3)
      at cafe.adriel.pufferdb.android.AndroidPufferDB.init (AndroidPufferDB.java:3)
      at com.talpa.translate.Initialize.initialize (Initialize.java:3)
      at com.talpa.translate.Initialize.<init> (Initialize.java:3)
      at com.talpa.translate.HiApplication.onCreate (HiApplication.java:11)
      at android.app.Instrumentation.callApplicationOnCreate (Instrumentation.java:1122)
      at android.app.ActivityThread.handleBindApplication (ActivityThread.java:6619)
      at android.app.ActivityThread.-wrap2 (Unknown Source)
      at android.app.ActivityThread$H.handleMessage (ActivityThread.java:2066)
      at android.os.Handler.dispatchMessage (Handler.java:108)
      at android.os.Looper.loop (Looper.java:166)
      at android.app.ActivityThread.main (ActivityThread.java:7529)
      at java.lang.reflect.Method.invoke (Method.java)
      at com.android.internal.os.Zygote$MethodAndArgsCaller.run (Zygote.java:245)
      at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:921)
    Caused by: com.google.protobuf.InvalidProtocolBufferException: 
      at com.google.protobuf.CodedInputStream.readTag (CodedInputStream.java:25)
      at cafe.adriel.pufferdb.proto.PufferProto.dynamicMethod (PufferProto.java:2)
      at com.google.protobuf.GeneratedMessageLite.parsePartialFrom (GeneratedMessageLite.java:10)
      at com.google.protobuf.GeneratedMessageLite.parseFrom (GeneratedMessageLite.java:8)
      at cafe.adriel.pufferdb.proto.PufferProto.parseFrom (PufferProto.java:1)
      at cafe.adriel.pufferdb.core.PufferDB.loadProtoFile (PufferDB.java:1)
      at cafe.adriel.pufferdb.core.PufferDB.loadProto (PufferDB.java:2)
      at cafe.adriel.pufferdb.core.PufferDB.<init> (PufferDB.java:2)
      at cafe.adriel.pufferdb.core.PufferDB.<init> (PufferDB.java:2)
      at cafe.adriel.pufferdb.core.PufferDB$Companion.with (PufferDB.java:3)
      at cafe.adriel.pufferdb.android.AndroidPufferDB.init (AndroidPufferDB.java:3)
    ```java
    opened by tmxdyf 0
Releases(1.1.1)
Owner
Adriel Café
Remote Android Developer
Adriel Café
Flutter plugin that leverages Storage Access Framework (SAF) API to get access and perform the operations on files and folders

Flutter plugin that leverages Storage Access Framework (SAF) API to get access and perform the operations on files and folders.

Vehement 8 Nov 26, 2022
A local storage management library for Kotlin Multiplatform Mobile iOS and android

A local storage management library for Kotlin Multiplatform Mobile iOS and android Features iOS and Android local storage in one interface Provides ge

LINE 20 Oct 30, 2022
Easy to use cryptographic framework for data protection: secure messaging with forward secrecy and secure data storage. Has unified APIs across 14 platforms.

Themis provides strong, usable cryptography for busy people General purpose cryptographic library for storage and messaging for iOS (Swift, Obj-C), An

Cossack Labs 1.6k Jan 8, 2023
Beautifully designed Pokémon Database app for Android based on PokéAPI and powered by Kotlin.

PokéFacts PokéFacts is an open-source Pokémon Database app for Android based on PokéAPI and powered by Kotlin. The app allows users to view Pokémons,

Arjun Mehta 9 Oct 22, 2022
Recycler-coroutines - RecyclerView Auto Add Data Using Coroutines

Sample RecyclerView Auto Add With Coroutine Colaborator Very open to anyone, I'l

Faisal Amir 8 Dec 1, 2022
MangaKu App Powered by Kotlin Multiplatform Mobile, Jetpack Compose, and SwiftUI

MangaKu ?? Introduction MangaKu App Powered by Kotlin Multiplatform Mobile, Jetpack Compose, and SwiftUI Module core: data and domain layer iosApp: io

Uwais Alqadri 132 Jan 8, 2023
A discord bot made in Kotlin powered by JDA and Realms.

A discord bot made in Kotlin powered by JDA and Realms.

null 1 Jun 30, 2022
An application for tracking the value of crypto currency

CriptoApp an application for tracking the value of crypto currency API https://m

Alex92w 0 Dec 19, 2021
ConstraintSetChangesTest - Simple project showing Changes of ConstraintSet value as part of mutable state in JetpackCompose.

ConstraintSetChangesTest Simple project showing Changes of ConstraintSet value as part of mutable state in JetpackCompose. Version: implementation

Mateusz Perlak 1 Feb 13, 2022
🔥🖼 Display images stored in Cloud Storage for Firebase using Coil

firecoil firecoil allows you to load images from Cloud Storage for Firebase in your Android app (through a StorageReference) , using the image loading

Rosário Pereira Fernandes 35 Oct 4, 2022
A modular object storage framework for Kotlin multiplatform projects.

ObjectStore A modular object storage framework for Kotlin multiplatform projects. Usage ObjectStore provides a simple key/value storage interface whic

Drew Carlson 4 Nov 10, 2022
A simple solution to handling persistent data storage in your Minecraft server.

Modern Data Stores A simple solution to handling persistent data storage in your Minecraft server. This plugin will be used throughout the Modern Plug

Modern Plugins 2 Nov 7, 2022
A simple solution to handling persistent data storage in your Minecraft server.

Modern Data Stores A simple solution to handling persistent data storage in your Minecraft server. This plugin will be used throughout the Modern Plug

Modern Plugins 2 Nov 7, 2022
A highly customizable calendar library for Android, powered by RecyclerView.

CalendarView A highly customizable calendar library for Android, powered by RecyclerView. With this library, your calendar will look however you want

Kizito Nwose 3.4k Jan 3, 2023
A libre smart powered comic book reader for Android.

Seeneva A libre smart powered comic book reader for Android. Translation: Русский • • Features • Speech balloons zooming • OCR and TTS • Performance •

Seeneva comic book reader 130 Jan 7, 2023
Type-safe time calculations in Kotlin, powered by generics.

Time This library is made for you if you have ever written something like this: val duration = 10 * 1000 to represent a duration of 10 seconds(in mill

Kizito Nwose 958 Dec 10, 2022
On-device wake word detection powered by deep learning.

Porcupine Made in Vancouver, Canada by Picovoice Porcupine is a highly-accurate and lightweight wake word engine. It enables building always-listening

Picovoice 2.8k Dec 30, 2022
MyAndroidTools, but powered by Sui

MyAndroidTools 便捷地管理您的 Android 设备 简介 与另一款 MyAndroidTools 一样,本应用使你能够便捷地管理 Android 设备中的应用和组件。但与之不同的是,本应用通过 Sui 来调用高权限 API,所以不会在使用过程中频繁弹出 root 授权的 Toast

null 7 Sep 17, 2022
Create an application with Kotlin/JVM and Kotlin/JS, and explore features around code sharing, serialization, server- and client

Practical Kotlin Multiplatform on the Web 본 저장소는 코틀린 멀티플랫폼 기반 웹 프로그래밍 워크숍(강좌)을 위해 작성된 템플릿 프로젝트가 있는 곳입니다. 워크숍 과정에서 코틀린 멀티플랫폼을 기반으로 프론트엔드(front-end)는 Ko

SpringRunner 14 Nov 5, 2022