diff --git a/README.md b/README.md
index d9b0b8f..47cf440 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,22 @@
-[](https://github.com/gmerinojimenez/tweaks)
-[](https://search.maven.org/artifact/io.github.gmerinojimenez/tweaks)
-[](https://github.com/Telefonica/mistica-android)
-# Tweaks
+
+
+
+
+
+
+
+
+
+
A customizable debug screen to view and edit flags that can be used for development in **Jetpack Compose** applications
+
+
+
+
+
To include the library add to your app's `build.gradle`:
@@ -31,7 +42,7 @@ where `demoTweakGraph` is the structure you want to be rendered:
```kotlin
private fun demoTweakGraph() = tweaksGraph {
cover("Tweaks Demo") {
- label("cover-key", "Current user ID:") { flow { emit("1")} }
+ label("cover-key", "Current user ID:") { flowOf("1") }
}
category("Screen 1") {
group("Group 1") {
@@ -39,12 +50,7 @@ private fun demoTweakGraph() = tweaksGraph {
key = "timestamp",
name = "Current timestamp",
) {
- flow {
- while (true) {
- emit("${System.currentTimeMillis() / 1000}")
- delay(1000)
- }
- }
+ timestampState
}
editableString(
key = "value1",
@@ -53,24 +59,13 @@ private fun demoTweakGraph() = tweaksGraph {
editableBoolean(
key = "value2",
name = "Value 2",
- )
- editableInt(
- key = "value3",
- name = "Value 3",
- defaultValue = flow {
- while (true) {
- counter += 1
- emit(counter)
- delay(1000)
- }
- }
+ defaultValue = true,
)
editableLong(
key = "value4",
name = "Value 4",
- defaultValue = 0L,
+ defaultValue = 42L,
)
-
button(
key = "button1",
name = "Demo button"
@@ -238,5 +233,16 @@ addTweakGraph(
}
```
+## Shake gesture support:
+The tweaks can be opened when the user shakes the device, to do this you need to add to your navigation controller:
+```kotlin
+navController.navigateToTweaksOnShake()
+```
+And also, optionally
+```xml
+
+```
+to your `AndroidManifest.xml`
+
## Special thanks to contributors:
* [Yamal Al Mahamid](https://github.com/yamal-coding)
diff --git a/app/src/main/java/com/gmerinojimenez/tweaks/demo/TweakDemoApplication.kt b/app/src/main/java/com/gmerinojimenez/tweaks/demo/TweakDemoApplication.kt
index dcf5ef4..831da5d 100644
--- a/app/src/main/java/com/gmerinojimenez/tweaks/demo/TweakDemoApplication.kt
+++ b/app/src/main/java/com/gmerinojimenez/tweaks/demo/TweakDemoApplication.kt
@@ -1,14 +1,13 @@
package com.gmerinojimenez.tweaks.demo
import android.app.Application
+import android.util.Log
import android.widget.Toast
import com.gmerinojimenez.tweaks.Tweaks
import com.gmerinojimenez.tweaks.domain.*
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.launch
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOf
class TweakDemoApplication : Application() {
override fun onCreate() {
@@ -16,57 +15,53 @@ class TweakDemoApplication : Application() {
Tweaks.init(this@TweakDemoApplication, demoTweakGraph())
}
- var timestampState = MutableStateFlow("0")
-
- init {
- CoroutineScope(Dispatchers.Default).launch {
- while (true) {
- timestampState.value = "${System.currentTimeMillis() / 1000}"
- delay(1000)
- }
+ var timestampState = flow {
+ while (true) {
+ emit("${System.currentTimeMillis() / 1000}")
+ delay(1000)
}
}
- private fun demoTweakGraph() = tweaksGraph {
- cover("Tweaks Demo") {
- label("cover-key", "Current user ID:") { MutableStateFlow("1") }
- }
- category("Screen 1") {
- group("Group 1") {
- label(
- key = "timestamp",
- name = "Current timestamp",
- ) {
- timestampState
- }
- editableString(
- key = "value1",
- name = "Value 1",
- )
- editableBoolean(
- key = "value2",
- name = "Value 2",
- defaultValue = true,
- )
- editableLong(
- key = "value4",
- name = "Value 4",
- defaultValue = 42L,
- )
- button(
- key = "button1",
- name = "Demo button"
- ) {
- Toast.makeText(this@TweakDemoApplication, "Demo button", Toast.LENGTH_LONG)
- .show()
- }
-
- routeButton(
- key = "button2",
- name = "Custom screen button",
- route = "custom-screen"
- )
+private fun demoTweakGraph() = tweaksGraph {
+ cover("Tweaks Demo") {
+ label("cover-key", "Current user ID:") { flowOf("1") }
+ }
+ category("Screen 1") {
+ group("Group 1") {
+ label(
+ key = "timestamp",
+ name = "Current timestamp",
+ ) {
+ timestampState
+ }
+ editableString(
+ key = "value1",
+ name = "Value 1",
+ )
+ editableBoolean(
+ key = "value2",
+ name = "Value 2",
+ defaultValue = true,
+ )
+ editableLong(
+ key = "value4",
+ name = "Value 4",
+ defaultValue = 42L,
+ )
+ button(
+ key = "button1",
+ name = "Demo button"
+ ) {
+ Toast.makeText(this@TweakDemoApplication, "Demo button", Toast.LENGTH_LONG)
+ .show()
}
+
+ routeButton(
+ key = "button2",
+ name = "Custom screen button",
+ route = "custom-screen"
+ )
}
}
+}
}
\ No newline at end of file
diff --git a/library/src/enabled/java/com/gmerinojimenez/tweaks/Tweaks.kt b/library/src/enabled/java/com/gmerinojimenez/tweaks/Tweaks.kt
index 81ca762..c2ab3f2 100644
--- a/library/src/enabled/java/com/gmerinojimenez/tweaks/Tweaks.kt
+++ b/library/src/enabled/java/com/gmerinojimenez/tweaks/Tweaks.kt
@@ -30,7 +30,7 @@ import com.gmerinojimenez.tweaks.domain.TweaksGraph
import com.gmerinojimenez.tweaks.ui.TweaksCategoryScreen
import com.gmerinojimenez.tweaks.ui.TweaksScreen
import com.squareup.seismic.ShakeDetector
-import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
@@ -39,9 +39,9 @@ open class Tweaks {
@Inject
internal lateinit var tweaksBusinessLogic: TweaksBusinessLogic
- open fun getTweakValue(key: String): StateFlow = tweaksBusinessLogic.getValue(key)
+ open fun getTweakValue(key: String): Flow = tweaksBusinessLogic.getValue(key)
- open fun getTweakValue(entry: TweakEntry): StateFlow = tweaksBusinessLogic.getValue(entry)
+ open fun getTweakValue(entry: TweakEntry): Flow = tweaksBusinessLogic.getValue(entry)
open suspend fun setTweakValue(key: String, value: T?) {
tweaksBusinessLogic.setValue(key, value)
diff --git a/library/src/enabled/java/com/gmerinojimenez/tweaks/domain/TweaksBusinessLogic.kt b/library/src/enabled/java/com/gmerinojimenez/tweaks/domain/TweaksBusinessLogic.kt
index 20061a7..842af51 100644
--- a/library/src/enabled/java/com/gmerinojimenez/tweaks/domain/TweaksBusinessLogic.kt
+++ b/library/src/enabled/java/com/gmerinojimenez/tweaks/domain/TweaksBusinessLogic.kt
@@ -2,13 +2,15 @@ package com.gmerinojimenez.tweaks.domain
import androidx.datastore.preferences.core.*
import com.gmerinojimenez.tweaks.data.TweaksDataStore
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flatMapMerge
+import kotlinx.coroutines.flow.map
import javax.inject.Inject
import javax.inject.Singleton
+@Suppress("UNCHECKED_CAST")
@Singleton
class TweaksBusinessLogic @Inject constructor(
private val tweaksDataStore: TweaksDataStore,
@@ -40,49 +42,43 @@ class TweaksBusinessLogic @Inject constructor(
private fun checkIfRepeatedKey(
alreadyIntroducedKeys: MutableSet,
- entry: TweakEntry<*>
+ entry: TweakEntry<*>,
) {
if (alreadyIntroducedKeys.contains(entry.key)) {
- throw IllegalStateException("There is a repeated key in the tweaks, review your graph")
+ throw IllegalStateException("There is a repeated key in the tweaks: ${entry.key}, review your graph")
}
alreadyIntroducedKeys.add(entry.key)
}
- @Suppress("UNCHECKED_CAST")
- fun getValue(key: String): StateFlow {
+ fun getValue(key: String): Flow {
val tweakEntry = keyToEntryValueMap[key] as TweakEntry
return getValue(tweakEntry)
}
- fun getValue(entry: TweakEntry): StateFlow = when (entry as Modifiable) {
+ fun getValue(entry: TweakEntry): Flow = when (entry as Modifiable) {
is ReadOnly<*> -> (entry as ReadOnly).value
is Editable<*> -> getEditableValue(entry)
}
+ @FlowPreview
@OptIn(ExperimentalCoroutinesApi::class)
- private fun getEditableValue(entry: TweakEntry): StateFlow {
+ private fun getEditableValue(entry: TweakEntry): Flow {
val editableCasted = entry as Editable
- val defaultValueFlow: StateFlow? = editableCasted.defaultValue
- val initialValue = defaultValueFlow?.value
-
- val mergedFlow: Flow = if (defaultValueFlow != null) {
- merge(
- getFromStorage(entry)
- .filter { it != null },
- defaultValueFlow
- )
- } else {
- getFromStorage(entry)
- }
+ val defaultValue: Flow = editableCasted.defaultValue
- return mergedFlow.stateIn(
- scope = CoroutineScope(Dispatchers.Default),
- started = SharingStarted.Lazily,
- initialValue = initialValue
- )
+ return isOverriden(entry)
+ .flatMapMerge { overriden ->
+ when (overriden) {
+ true -> getFromStorage(entry)
+ else -> defaultValue
+ }
+ }
}
+ private fun isOverriden(entry: TweakEntry<*>): Flow = tweaksDataStore.data
+ .map { preferences -> preferences[buildIsOverridenKey(entry)] ?: OVERRIDEN_DEFAULT_VALUE }
+
private fun getFromStorage(entry: TweakEntry) =
tweaksDataStore.data
.map { preferences -> preferences[buildKey(entry)] }
@@ -91,8 +87,10 @@ class TweaksBusinessLogic @Inject constructor(
tweaksDataStore.edit {
if (value != null) {
it[buildKey(entry)] = value
+ it[buildIsOverridenKey(entry)] = true
} else {
it.remove(buildKey(entry))
+ it[buildIsOverridenKey(entry)] = false
}
}
}
@@ -105,6 +103,7 @@ class TweaksBusinessLogic @Inject constructor(
suspend fun clearValue(entry: TweakEntry) {
tweaksDataStore.edit {
it.remove(buildKey(entry))
+ it.remove(buildIsOverridenKey(entry))
}
}
@@ -123,4 +122,11 @@ class TweaksBusinessLogic @Inject constructor(
is ButtonTweakEntry -> throw java.lang.IllegalStateException("Buttons doesn't have keys")
is RouteButtonTweakEntry -> throw java.lang.IllegalStateException("Buttons doesn't have keys")
}
+
+ private fun buildIsOverridenKey(entry: TweakEntry<*>): Preferences.Key =
+ booleanPreferencesKey("${entry.key}.TweakOverriden")
+
+ companion object {
+ private const val OVERRIDEN_DEFAULT_VALUE = false
+ }
}
\ No newline at end of file
diff --git a/library/src/enabled/java/com/gmerinojimenez/tweaks/ui/TweakComponents.kt b/library/src/enabled/java/com/gmerinojimenez/tweaks/ui/TweakComponents.kt
index 3d76a86..8bc16ed 100644
--- a/library/src/enabled/java/com/gmerinojimenez/tweaks/ui/TweakComponents.kt
+++ b/library/src/enabled/java/com/gmerinojimenez/tweaks/ui/TweakComponents.kt
@@ -135,7 +135,7 @@ fun ReadOnlyStringTweakEntryBody(
tweakEntry = entry,
onClick = {
Toast
- .makeText(context, "Current value is $entry.", Toast.LENGTH_LONG)
+ .makeText(context, "${entry.key} = $value", Toast.LENGTH_LONG)
.show()
}) {
Text(
diff --git a/library/src/main/java/com/gmerinojimenez/tweaks/domain/tweakModels.kt b/library/src/main/java/com/gmerinojimenez/tweaks/domain/tweakModels.kt
index bfc3845..fe69a3f 100644
--- a/library/src/main/java/com/gmerinojimenez/tweaks/domain/tweakModels.kt
+++ b/library/src/main/java/com/gmerinojimenez/tweaks/domain/tweakModels.kt
@@ -1,9 +1,7 @@
package com.gmerinojimenez.tweaks.domain
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOf
fun tweaksGraph(block: TweaksGraph.Builder.() -> Unit): TweaksGraph {
val builder = TweaksGraph.Builder()
@@ -78,7 +76,7 @@ data class TweakGroup(val title: String, val entries: List>) {
fun label(
key: String,
name: String,
- value: () -> StateFlow,
+ value: () -> Flow,
) {
tweak(ReadOnlyStringTweakEntry(key, name, value()))
}
@@ -86,7 +84,7 @@ data class TweakGroup(val title: String, val entries: List>) {
fun editableString(
key: String,
name: String,
- defaultValue: StateFlow? = null,
+ defaultValue: Flow = flowOf(),
) {
tweak(EditableStringTweakEntry(key, name, defaultValue))
}
@@ -102,7 +100,7 @@ data class TweakGroup(val title: String, val entries: List>) {
fun editableBoolean(
key: String,
name: String,
- defaultValue: StateFlow? = null,
+ defaultValue: Flow = flowOf(),
) {
tweak(EditableBooleanTweakEntry(key, name, defaultValue))
}
@@ -118,7 +116,7 @@ data class TweakGroup(val title: String, val entries: List>) {
fun editableInt(
key: String,
name: String,
- defaultValue: StateFlow? = null,
+ defaultValue: Flow = flowOf(),
) {
tweak(EditableIntTweakEntry(key, name, defaultValue))
}
@@ -134,7 +132,7 @@ data class TweakGroup(val title: String, val entries: List>) {
fun editableLong(
key: String,
name: String,
- defaultValue: StateFlow? = null,
+ defaultValue: Flow = flowOf(),
) {
tweak(EditableLongTweakEntry(key, name, defaultValue))
}
@@ -162,68 +160,68 @@ class RouteButtonTweakEntry(key: String, name: String, val route: String) :
TweakEntry(key, name)
/** A non editable entry */
-class ReadOnlyStringTweakEntry(key: String, name: String, override val value: StateFlow) :
+class ReadOnlyStringTweakEntry(key: String, name: String, override val value: Flow) :
TweakEntry(key, name), ReadOnly
/** An editable entry. It can be modified by using long-press*/
class EditableStringTweakEntry(
key: String,
name: String,
- override val defaultValue: StateFlow? = null,
+ override val defaultValue: Flow = flowOf(),
) : TweakEntry(key, name), Editable {
constructor(
key: String,
name: String,
defaultUniqueValue: String,
- ) : this(key, name, MutableStateFlow(defaultUniqueValue))
+ ) : this(key, name, flowOf(defaultUniqueValue))
}
/** An editable entry. It can be modified by using long-press*/
class EditableBooleanTweakEntry(
key: String,
name: String,
- override val defaultValue: StateFlow? = null,
+ override val defaultValue: Flow = flowOf(),
) : TweakEntry(key, name), Editable {
constructor(
key: String,
name: String,
defaultUniqueValue: Boolean,
- ) : this(key, name, MutableStateFlow(defaultUniqueValue))
+ ) : this(key, name, flowOf(defaultUniqueValue))
}
/** An editable entry. It can be modified by using long-press*/
class EditableIntTweakEntry(
key: String,
name: String,
- override val defaultValue: StateFlow? = null,
+ override val defaultValue: Flow = flowOf(),
) : TweakEntry(key, name), Editable {
constructor(
key: String,
name: String,
defaultUniqueValue: Int,
- ) : this(key, name, MutableStateFlow(defaultUniqueValue))
+ ) : this(key, name, flowOf(defaultUniqueValue))
}
/** An editable entry. It can be modified by using long-press*/
class EditableLongTweakEntry(
key: String,
name: String,
- override val defaultValue: StateFlow? = null,
+ override val defaultValue: Flow = flowOf(),
) : TweakEntry(key, name), Editable {
constructor(
key: String,
name: String,
defaultUniqueValue: Long,
- ) : this(key, name, MutableStateFlow(defaultUniqueValue))
+ ) : this(key, name, flowOf(defaultUniqueValue))
}
sealed interface Modifiable
interface Editable : Modifiable {
- val defaultValue: StateFlow?
+ val defaultValue: Flow
}
interface ReadOnly : Modifiable {
- val value: StateFlow
+ val value: Flow
}
internal object Constants {
diff --git a/library/src/noop/java/com/gmerinojimenez/tweaks/Tweaks.kt b/library/src/noop/java/com/gmerinojimenez/tweaks/Tweaks.kt
index ea31747..15de983 100644
--- a/library/src/noop/java/com/gmerinojimenez/tweaks/Tweaks.kt
+++ b/library/src/noop/java/com/gmerinojimenez/tweaks/Tweaks.kt
@@ -5,23 +5,22 @@ import androidx.compose.runtime.Composable
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import com.gmerinojimenez.tweaks.domain.*
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.Flow
open class Tweaks {
private val keyToEntryValueMap: MutableMap> = mutableMapOf()
@Suppress("UNCHECKED_CAST")
- open fun getTweakValue(key: String): StateFlow {
+ open fun getTweakValue(key: String): Flow {
val entry= keyToEntryValueMap[key] as TweakEntry
return getTweakValue(entry)
}
@Suppress("UNCHECKED_CAST")
- open fun getTweakValue(entry: TweakEntry): StateFlow = when (entry as Modifiable) {
+ open fun getTweakValue(entry: TweakEntry): Flow = when (entry as Modifiable) {
is ReadOnly<*> -> (entry as ReadOnly).value
- is Editable<*> -> (entry as Editable).defaultValue ?: MutableStateFlow(null)
+ is Editable<*> -> (entry as Editable).defaultValue
}
open suspend fun setTweakValue(key: String, value: T?) {