diff --git a/README.md b/README.md index d9b0b8f..47cf440 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,22 @@ -[![Platform](https://img.shields.io/badge/Platform-Android-brightgreen)](https://github.com/gmerinojimenez/tweaks) -[![Version](https://maven-badges.herokuapp.com/maven-central/io.github.gmerinojimenez/tweaks/badge.png)](https://search.maven.org/artifact/io.github.gmerinojimenez/tweaks) -[![Support](https://img.shields.io/badge/Support-%3E%3D%20Android%205.0-brightgreen)](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?) {