Skip to content

KotPreferences#

Open in GitHub

Latest Release Last Update License

Stars Forks

With this library you can declare preferences via kotlin delegates and observe and update them via kotlin Flows. This works with any storage implementation, an implementation for JetPack DataStore is provided already.

Features#

With this library you can declare preferences via kotlin delegates and observe and update them via kotlin Flows. This works with any storage implementation, an implementation for JetPack DataStore is provided already. Additionally there's also an extension to easily integrate this library into your compose app.

All features are splitted into separate modules, just include the modules you want to use!

🔗 Dependencies#

This library does not have any dependencies!

Setup Gradle#

This library is distributed via JitPack.io.

1/2: Add jitpack to your project's build.gradle
repositories {
    maven { url "https://jitpack.io" }
}
2/2: Add dependencies to your module's build.gradle
// use the latest version of the library
val kotpreferences = "<LATEST-VERSION>" 

// include necessary modules

// core module
implementation("com.github.MFlisar.KotPreferences:core:$kotpreferences")

// storage module
implementation("com.github.MFlisar.KotPreferences:datastore:$kotpreferences")
implementation("com.github.MFlisar.KotPreferences:encryption-aes:$kotpreferences")

// extension module
implementation("com.github.MFlisar.KotPreferences:compose:$kotpreferences")

⌨ Usage#

Define Preferences
object UserSettingsModel : SettingsModel(DataStoreStorage(name = "user")) {

    // main data types
    val someString by stringPref("value")
    val someBool by boolPref(false)
    val someInt by intPref(123)
    val someLong by intPref(123L)
    val someFloat by intPref(123f)
    val someDouble by intPref(123.0)

    // enum
    val someEnum by enumPref(Enum.Value1)

    // custom
    val someCustomClass by anyStringPref(TestClass.CONVERTER, TestClass()) // converts TestClass to a string and saves this string
    val someCustomClass by anyIntPref(TestClass.CONVERTER, TestClass())    // converts TestClass to an int and saves this int
    val someCustomClass by anyLongPref(TestClass.CONVERTER, TestClass())   // converts TestClass to a long and saves this long

    // sets
    val someStringSet by stringSetPref(setOf("a"))
    val someIntSet by intSetPref(setOf(1))
    val someLongSet by longSetPref(setOf(1L))
    val someFloatSet by floatSetPref(setOf(1f))
    val someDoubleSet by doubleSetPref(setOf(1.0))

    // NULLABLE vs NON NULLABLE
    val nonNullableString by stringPref()
    val nullableString by nullableStringPref()
    val nonNullableInt by intPref()
    val nullableInt by nullableIntPref()
    val nonNullableFloat by floatPref()
    val nullableFloat by nullableFloatPref()
    val nonNullableDouble by doublePref()
    val nullableDouble by nullableDoublePref()
    val nonNullableLong by longPref()
    val nullableLong by nullableLongPref()
    val nonNullableBool by boolPref()
    val nullableBool by nullableBoolPref()
    // custom
    val someCustomClass by nullableAnyStringPref(TestClass.CONVERTER, TestClass())
    val someCustomClass by nullableAnyIntPref(TestClass.CONVERTER, TestClass())
    val someCustomClass by nullableAnyLongPref(TestClass.CONVERTER, TestClass())
}
Observe/Read Preferences
// 1) simply observe a setting
UserSettingsModel.name.observe(lifecycleScope) {
    L.d { "name = $it"}
}

// 2) direct read (not recommended if not necessary but may be useful in many cases => simply returns flow.first() in a blocking way)
val name = UserSettingsModel.name.value

// 3) observe a setting once
UserSettingsModel.name.observeOnce(lifecycleScope) {
    L.d { "name = $it"}
}

// 4) observe ALL settings
UserSettingsModel.changes.onEach {
    L.d { "[ALL SETTINGS OBSERVER] Setting '${it.setting.key}' changed its value to ${it.value}" }
}.launchIn(lifecycleScope)

// 5) observe SOME settings
UserSettingsModel.changes
    .filter {
        it.setting == UserSettingsModel.name ||
        it.setting == UserSettingsModel.age
    }.onEach {
        // we know that either the name or the age changes
        L.d { "[SOME SETTINGS OBSERVER] Setting '${it.setting.key}' changed its value to ${it.value}" }
    }.launchIn(lifecycleScope)

// 6) read multiple settings in a suspending way
lifecycleScope.launch(Dispatchers.IO) {
    val name = UserSettingsModel.childName1.flow.first()
    val alive = DemoSettingsModel.alive.flow.first()
    val hairColor = DemoSettingsModel.hairColor.flow.first()
    withContext(Dispatchers.Main) {
        textView.text = "Informations: $name, $alive, $hairColor"
    }
}
Lifedata
val lifedata = UserSettingsModel.name.flow.asLiveData()
Update preferences
lifecycleScope.launch(Dispatchers.IO)  {
    UserSettingsModel.name.update("Some new name")
    UserSettingsModel.age.update(30)
}
Compose

Add the compose module to get following extensions for compose.

val name = UserSettingsModel.name.collectAsState()
val name = UserSettingsModel.name.collectAsStateWithLifecycle()

// simply use the state inside your composables, the state will change whenever the setting behind it will change

🧬 Demo#

A full demo is included inside the demo module, it shows nearly every usage with working examples.

Modules and Extensions#

Encryption Module

Currently there only exists the AES encryption module. It simple implements a predefined interface that encrypts and decrypts all data before it get's persisted by a storage implementation.

This module is placed inside the encrpytion-aes artifact and can simply be used like following:

Step 1/2: define the encryption

private const val ALGORITHM = StorageEncryptionAES.DEFAULT_ALGORITHM
private const val KEY_ALGORITHM = StorageEncryptionAES.DEFAULT_KEY_ALGORITHM
// also check out StorageEncryptionAES::generateKey and StorageEncryptionAES::generateIv if you need some helper functions
private val KEY = StorageEncryptionAES.getKeyFromPassword(KEY_ALGORITHM, "my key", "my salt")
private val BYTE_ARRAY = listOf(0x16, 0x09, 0xc0, 0x4d, 0x4a, 0x09, 0xd2, 0x46, 0x71, 0xcc, 0x32, 0xb7, 0xd2, 0x91, 0x8a, 0x9c)
    .map { it.toByte() }
    .toByteArray()
private val IV = StorageEncryptionAES.getIv(BYTE_ARRAY) // byte array must be 16 bytes!
val ENCRYPTION = StorageEncryptionAES(ALGORITHM, KEY, IV)

Step 2/2: attach the encryption to your storage instance

object MyEncryptedSettingsModel : SettingsModel(
    DataStoreStorage(
        name = "encrypted",
        encryption = ENCRYPTION
    )
) {
    // ...
}
Storage DataStore Module

The Storage is an abstraction to support any storage implementation. The datastore module provides an implementation based on the Android JetPack DataStore.

This module is placed inside the datastore artifact and can simple be used like following:

object SettingsModel : SettingsModel(
    DataStoreStorage(
        name = "demo_settings" // file name of preference file
    )
) {
    // ...
}

It supports following data types:

  • String
  • Bool
  • Int
  • Long
  • Float
  • Double
  • Sets:
    • Set<String>
    • Set<Int>
    • Set<Long>
    • Set<Float>
    • Set<Double>