├── .github ├── ci-gradle.properties └── workflows │ ├── development.yaml │ └── release.yaml ├── .gitignore ├── README.md ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts ├── settings.gradle.kts └── src │ └── main │ └── kotlin │ ├── BasicSetup.kt │ ├── JsConfig.kt │ ├── KotlinExtensions.kt │ ├── KtorBuildProperties.kt │ ├── NativeUtils.kt │ └── TargetsConfig.kt ├── compose ├── android │ └── src │ │ └── dev │ │ └── programadorthi │ │ └── state │ │ └── compose │ │ ├── ComposeValueManagerSavedStateHandle.kt │ │ └── ValueManagerAsState.android.kt ├── build.gradle.kts ├── common │ ├── src │ │ └── dev │ │ │ └── programadorthi │ │ │ └── state │ │ │ └── compose │ │ │ ├── ComposeValueManagerState.kt │ │ │ ├── RememberValueManager.kt │ │ │ ├── ValidatorManagerAsState.kt │ │ │ ├── ValidatorManagerState.kt │ │ │ ├── ValueManagerAsState.kt │ │ │ └── ValueManagerSaver.kt │ └── test │ │ └── dev │ │ └── programadorthi │ │ └── state │ │ └── compose │ │ ├── ComposeValueManagerTest.kt │ │ ├── fake │ │ ├── FakeEntry.kt │ │ └── FakeSaveableStateRegistry.kt │ │ └── helper │ │ ├── TestApplier.kt │ │ └── runComposeTest.kt └── gradle.properties ├── core ├── android │ └── src │ │ ├── AndroidManifest.xml │ │ └── dev │ │ └── programadorthi │ │ └── state │ │ └── core │ │ ├── AndroidValueManager.kt │ │ ├── AndroidValueManagerState.kt │ │ ├── AndroidValueManagerStateFactory.kt │ │ └── AndroidValueManagerStateSaver.kt ├── build.gradle.kts ├── common │ ├── src │ │ └── dev │ │ │ └── programadorthi │ │ │ └── state │ │ │ └── core │ │ │ ├── BaseValueManager.kt │ │ │ ├── BasicValueManager.kt │ │ │ ├── ValueManager.kt │ │ │ ├── action │ │ │ ├── ChangeAction.kt │ │ │ ├── CollectAction.kt │ │ │ ├── ErrorAction.kt │ │ │ └── UpdateAction.kt │ │ │ ├── extension │ │ │ ├── ValidatorManagerDelegate.kt │ │ │ ├── ValueManagerCreator.kt │ │ │ └── ValueManagerDelegate.kt │ │ │ └── validation │ │ │ ├── Validator.kt │ │ │ ├── ValidatorAction.kt │ │ │ └── ValidatorManager.kt │ └── test │ │ └── dev │ │ └── programadorthi │ │ └── state │ │ └── core │ │ └── ValueManagerTest.kt └── gradle.properties ├── coroutines ├── build.gradle.kts ├── common │ ├── src │ │ └── dev │ │ │ └── programadorthi │ │ │ └── state │ │ │ └── coroutines │ │ │ ├── ValidatorManagerFlow.kt │ │ │ ├── ValidatorResult.kt │ │ │ ├── ValueManagerAsFlow.kt │ │ │ └── ValueManagerFlow.kt │ └── test │ │ └── dev │ │ └── programadorthi │ │ └── state │ │ └── coroutines │ │ └── FlowValueManagerTest.kt └── gradle.properties ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── samples ├── compose │ └── norris-facts │ │ ├── android │ │ ├── build.gradle.kts │ │ └── src │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ └── dev │ │ │ │ └── programadorthi │ │ │ │ └── android │ │ │ │ ├── ComposeActivity.kt │ │ │ │ ├── ComposeViewModel.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ └── MainViewModel.kt │ │ │ └── res │ │ │ └── layout │ │ │ └── activity_main.xml │ │ ├── common │ │ ├── build.gradle.kts │ │ └── src │ │ │ └── commonMain │ │ │ └── kotlin │ │ │ └── dev │ │ │ └── programadorthi │ │ │ └── common │ │ │ ├── App.kt │ │ │ ├── api │ │ │ ├── Fact.kt │ │ │ ├── NorrisApi.kt │ │ │ └── Result.kt │ │ │ ├── login │ │ │ ├── LoginScreen.kt │ │ │ └── LoginViewModel.kt │ │ │ ├── mvi │ │ │ ├── MVIScreen.kt │ │ │ └── MVIViewModel.kt │ │ │ └── state │ │ │ ├── CategoriesValueManager.kt │ │ │ ├── FactsValueManager.kt │ │ │ └── State.kt │ │ └── desktop │ │ ├── build.gradle.kts │ │ └── src │ │ └── jvmMain │ │ └── kotlin │ │ └── Main.kt └── iosSample │ ├── iosSample.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── iosSample │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── ContentView.swift │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ └── iosSampleApp.swift ├── serialization ├── build.gradle.kts ├── common │ ├── src │ │ └── dev │ │ │ └── programadorthi │ │ │ └── state │ │ │ └── serialization │ │ │ ├── ValueManagerSerializable.kt │ │ │ └── ValueManagerSerializer.kt │ └── test │ │ └── dev │ │ └── programadorthi │ │ └── state │ │ └── serialization │ │ ├── SerializableValueManager.kt │ │ └── SerializableValueManagerTest.kt └── gradle.properties ├── settings.gradle.kts └── validators ├── build.gradle.kts ├── common ├── src │ └── dev │ │ └── programadorthi │ │ └── state │ │ └── validator │ │ ├── any │ │ ├── InValidator.kt │ │ ├── IsEqualToValidator.kt │ │ ├── IsNotEqualToValidator.kt │ │ ├── IsNotNullValidator.kt │ │ ├── IsNullValidator.kt │ │ └── NotInValidator.kt │ │ ├── character │ │ ├── InValidator.kt │ │ ├── IsDigitValidator.kt │ │ ├── IsEqualToValidator.kt │ │ ├── IsLetterOrDigitValidator.kt │ │ ├── IsLetterValidator.kt │ │ ├── IsLowerCaseValidator.kt │ │ ├── IsNotDigitValidator.kt │ │ ├── IsNotEqualToValidator.kt │ │ ├── IsNotLetterOrDigitValidator.kt │ │ ├── IsNotLetterValidator.kt │ │ ├── IsNotWhitespaceValidator.kt │ │ ├── IsUpperCaseValidator.kt │ │ ├── IsWhitespaceValidator.kt │ │ └── NotInValidator.kt │ │ ├── comparable │ │ ├── InValidator.kt │ │ ├── IsGreaterThanOrEqualToValidator.kt │ │ ├── IsGreaterThanValidator.kt │ │ ├── IsLessThanOrEqualToValidator.kt │ │ └── IsLessThanValidator.kt │ │ ├── number │ │ ├── IsFiniteValidator.kt │ │ ├── IsInfiniteValidator.kt │ │ ├── IsNaNValidator.kt │ │ ├── IsNegativeValidator.kt │ │ ├── IsNotOneValidator.kt │ │ ├── IsNotZeroValidator.kt │ │ ├── IsOneValidator.kt │ │ ├── IsPositiveValidator.kt │ │ └── IsZeroValidator.kt │ │ └── string │ │ ├── AllDigitValidator.kt │ │ ├── AllLetterOrDigitValidator.kt │ │ ├── AllLetterValidator.kt │ │ ├── AllLowerCaseValidator.kt │ │ ├── AllUpperCaseValidator.kt │ │ ├── AllWhitespaceValidator.kt │ │ ├── AnyDigitValidator.kt │ │ ├── AnyLetterOrDigitValidator.kt │ │ ├── AnyLetterValidator.kt │ │ ├── AnyLowerCaseValidator.kt │ │ ├── AnyUpperCaseValidator.kt │ │ ├── AnyWhitespaceValidator.kt │ │ ├── ContainsAllValidator.kt │ │ ├── ContainsAnyValidator.kt │ │ ├── ContainsValidator.kt │ │ ├── EndsWithValidator.kt │ │ ├── HasSizeValidator.kt │ │ ├── InValidator.kt │ │ ├── IsEqualToValidator.kt │ │ ├── IsNotBlankValidator.kt │ │ ├── IsNotEmptyValidator.kt │ │ ├── IsNotEqualToValidator.kt │ │ ├── IsNullOrBlankValidator.kt │ │ ├── IsNullOrEmptyValidator.kt │ │ ├── NoneDigitValidator.kt │ │ ├── NoneLetterOrDigitValidator.kt │ │ ├── NoneLetterValidator.kt │ │ ├── NoneLowerCaseValidator.kt │ │ ├── NoneUpperCaseValidator.kt │ │ ├── NoneWhitespaceValidator.kt │ │ ├── NotContainsAllValidator.kt │ │ ├── NotContainsAnyValidator.kt │ │ ├── NotContainsValidator.kt │ │ ├── NotEndsWithValidator.kt │ │ ├── NotInValidator.kt │ │ ├── NotStartsWithValidator.kt │ │ ├── RegexContainsValidator.kt │ │ ├── RegexMatchValidator.kt │ │ ├── RegexNotContainsValidator.kt │ │ ├── RegexNotMatchValidator.kt │ │ └── StartsWithValidator.kt └── test │ └── dev │ └── programadorthi │ └── state │ └── validator │ ├── AnyValidatorsTest.kt │ ├── CharacterValidatorsTest.kt │ ├── ComparableValidatorsTest.kt │ ├── NumberValidatorsTest.kt │ └── StringValidatorsTest.kt └── gradle.properties /.github/ci-gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.daemon=false 2 | 3 | org.gradle.parallel=true 4 | org.gradle.jvmargs=-Xmx6g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:MaxMetaspaceSize=1536m 5 | #org.gradle.workers.max=2 6 | 7 | kotlin.compiler.execution.strategy=in-process -------------------------------------------------------------------------------- /.github/workflows/development.yaml: -------------------------------------------------------------------------------- 1 | name: Development 2 | on: [push] 3 | 4 | jobs: 5 | build: 6 | name: Tests 7 | runs-on: ubuntu-latest 8 | timeout-minutes: 60 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | 13 | - name: Setup Java 14 | uses: actions/setup-java@v3 15 | with: 16 | java-version: 17 17 | distribution: 'temurin' 18 | 19 | - name: Tests 20 | run: | 21 | ./gradlew runJvmTests --stacktrace -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | release: 4 | types: [prereleased, released] 5 | 6 | jobs: 7 | 8 | release: 9 | name: Publish State Manager 10 | runs-on: macOS-latest 11 | timeout-minutes: 60 12 | steps: 13 | 14 | - name: Fetch Sources 15 | uses: actions/checkout@v3 16 | with: 17 | ref: ${{ github.event.release.tag_name }} 18 | 19 | - name: Copy CI gradle.properties 20 | run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties 21 | 22 | - name: Setup Java 23 | uses: actions/setup-java@v3 24 | with: 25 | java-version: 17 26 | distribution: 'temurin' 27 | 28 | - name: Deploy to Sonatype 29 | run: | 30 | NEW_VERSION=$(echo "${GITHUB_REF}" | cut -d "/" -f3) 31 | echo "New version: ${NEW_VERSION}" 32 | ./gradlew -Pversion=${NEW_VERSION} kotlinUpgradeYarnLock publishAllPublicationsToCentralPortal --stacktrace 33 | env: 34 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.ORG_GRADLE_PROJECT_mavenCentralUsername }} 35 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.ORG_GRADLE_PROJECT_mavenCentralPassword }} 36 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.ORG_GRADLE_PROJECT_signingInMemoryKey }} 37 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.ORG_GRADLE_PROJECT_signingInMemoryKeyPassword }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | .idea 4 | .DS_Store 5 | build 6 | */build 7 | captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | xcuserdata/ 12 | Pods/ 13 | /androidApp/key 14 | *.jks 15 | *yarn.lock -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kotlin-state-manager 2 | A multiplatform and extensible state manager. Its wrapper the managed value to deliver a better, easy and extensible way. It's like a Value class with powerS. 3 | 4 | > The project is not a replacement for Coroutines Flow or Compose State. Your origin is from 2022 when Compose wasn't multiplatform. 5 | 6 | ## How it works 7 | There are a lot of ways to use a Value Manager 8 | 9 | ### As a basic variable 10 | ```kotlin 11 | class CounterViewModel { 12 | val counter = basicValueManager(initialValue = 0) 13 | val counterFlow = counter.asMutableStateFlow() // StateFlow version 14 | 15 | var value by basicValueManager(initialValue = 0) // Delegate property version available 16 | } 17 | ``` 18 | 19 | ### Updating it value 20 | ```kotlin 21 | class CounterViewModel { 22 | fun increment() { 23 | anyValueManagerType.update { current -> current + 1 } 24 | anyValueManager = anyValueManager + 1 // update method as a Delegate property 25 | anyValueManager++ // same as previous 26 | } 27 | } 28 | ``` 29 | 30 | ### Collecting value changes 31 | ```kotlin 32 | class CounterViewModel { 33 | fun listen() { 34 | anyValueManagerType.collect { 35 | // collect without suspend is available 36 | } 37 | 38 | coroutinesScope.launch { 39 | flowValueManagerType.collect { 40 | // suspend collect available in Flow 41 | } 42 | } 43 | } 44 | } 45 | ``` 46 | 47 | ### Inside Jetpack Compose 48 | ```kotlin 49 | @Composable 50 | fun HomeScreen() { 51 | val counter = remember { basicValueManager(initialValue = 0) } 52 | var counterState by remember { counter.asState() } // remember or rememberSaveable are available 53 | 54 | // Update and listen operations are the same 55 | } 56 | ``` 57 | 58 | ### Listening for errors 59 | ```kotlin 60 | class CounterViewModel { 61 | val counter = basicValueManager(initialValue = 0) 62 | 63 | init { 64 | counter.onError { 65 | 66 | } 67 | } 68 | } 69 | ``` 70 | 71 | ### Listening for changes 72 | ```kotlin 73 | class CounterViewModel { 74 | val counter = basicValueManager(initialValue = 0) 75 | 76 | init { 77 | counter.onChanged { 78 | 79 | } 80 | } 81 | } 82 | ``` 83 | 84 | ### Validations are supported 85 | ```kotlin 86 | class PositiveValidator( 87 | override val message: (Int) -> String = { "Value $it should be positive" } 88 | ) : Validator { 89 | override fun isValid(value: Int): Boolean = value > 0 90 | } 91 | 92 | val counter = basicValueManager(initialValue = 0) 93 | 94 | counter.addValidator(PositiveValidator()) 95 | // or 96 | counter += PositiveValidator() 97 | 98 | counter.onValidated { 99 | // Listen on each validation operation 100 | } 101 | 102 | // Put a value don't trigger validations 103 | counter.value = -1 104 | // Call validate() to trigger validations 105 | counter.validate() 106 | 107 | // Calling update always trigger validations and don't need call validate() 108 | counter.update { -1 } 109 | 110 | // Checking is valid 111 | counter.isValid() 112 | 113 | // Getting validators messages 114 | counter.messages() 115 | ``` 116 | 117 | ### Serialization 118 | 119 | A value manager can be encoded or decoded using Kotlin Serialization and `serialization` module. 120 | It is a good use case whether you have model shared with serialization infrastructure as network requests 121 | 122 | ```kotlin 123 | import dev.programadorthi.state.serialization.ValueManager // Not from core package 124 | 125 | @Serializable 126 | data class MyClass( 127 | val count: ValueManager, 128 | ) 129 | 130 | val data = MyClass(count = basicValueManager(1)) 131 | val json = Json.encodeToString(data) 132 | println(json) // {"count": 1} 133 | 134 | val decoded = Json.decodeFromString(json) 135 | println(data == decoded) // true 136 | ``` 137 | 138 | ### State Restoration 139 | 140 | #### Compose 141 | 142 | ```kotlin 143 | val counter by rememberSaveableValueManager { ... } 144 | ``` 145 | 146 | Without remember function or using a class to manager, you need to pass a `SaveableStateRegistry` 147 | 148 | ```kotlin 149 | class MyComposeViewModel(stateRegistry: SaveableStateRegistry) { 150 | private var counter by composeValueManager(0, stateRegistry = stateRegistry) 151 | } 152 | ``` 153 | 154 | Checkout [MVIViewModel](https://github.com/programadorthi/kotlin-state-manager/blob/master/samples/compose/norris-facts/common/src/commonMain/kotlin/dev/programadorthi/common/mvi/MVIViewModel.kt) sample for more details 155 | 156 | #### Android 157 | 158 | ```kotlin 159 | class MyActivity : ComponentActivity { 160 | private var counter by androidValueManager(0) 161 | } 162 | ``` 163 | 164 | ```kotlin 165 | class MyFragment : AndroidXFragment { 166 | private var counter by androidValueManager(0) 167 | } 168 | ``` 169 | 170 | ```kotlin 171 | class MyViewModel(savedStateHandle: SavedStateHandle) : AndroidXViewModel { 172 | private var counter by androidValueManager(0, savedStateHandle = savedStateHandle) 173 | } 174 | ``` 175 | 176 | Checkout [MainActivity](https://github.com/programadorthi/kotlin-state-manager/blob/master/samples/compose/norris-facts/android/src/main/java/dev/programadorthi/android/MainActivity.kt) sample for more details 177 | 178 | ### Do you prefer inheritance over composition? 179 | 180 | ```kotlin 181 | class CounterValueManager : BaseValueManager(initialValue = 0) { 182 | // Now all operations is available here 183 | } 184 | ``` 185 | 186 | ### Samples 187 | 188 | Samples folder have a mix of usage. 189 | 190 | Close usage to real project [here](https://github.com/programadorthi/full-stack-kotlin/blob/main/domain-model/interactors/src/commonMain/kotlin/dev/programadorthi/full/stack/interactors/user/LoginInteractor.kt) 191 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("plugin.serialization") version "1.9.22" apply false 3 | id("org.jetbrains.compose") version "1.6.0-beta02" apply false 4 | id("com.vanniktech.maven.publish") version "0.27.0" apply false 5 | id("com.gradleup.nmcp") version "0.0.4" 6 | } 7 | 8 | group = "dev.programadorthi.state" 9 | 10 | tasks.register("runJvmTests") { 11 | group = "other" 12 | description = "Execute JVM tests in each module only" 13 | } 14 | 15 | nmcp { 16 | publishAllProjectsProbablyBreakingProjectIsolation { 17 | username = project.providers.gradleProperty("mavenCentralUsername") 18 | password = project.providers.gradleProperty("mavenCentralPassword") 19 | publicationType = "AUTOMATIC" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | dependencies { 6 | implementation(kotlin("gradle-plugin", version = "1.9.22")) 7 | implementation("com.android.tools.build:gradle:8.2.2") 8 | } -------------------------------------------------------------------------------- /buildSrc/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/BasicSetup.kt: -------------------------------------------------------------------------------- 1 | import org.gradle.api.JavaVersion 2 | import org.gradle.api.Project 3 | import org.gradle.api.tasks.testing.AbstractTestTask 4 | import org.gradle.api.tasks.testing.logging.TestExceptionFormat 5 | import org.gradle.kotlin.dsl.withType 6 | import org.jetbrains.kotlin.gradle.dsl.KotlinCommonOptions 7 | import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension 8 | import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation 9 | import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType 10 | import org.jetbrains.kotlin.gradle.targets.js.KotlinJsTarget 11 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 12 | 13 | fun Project.applyBasicSetup() { 14 | configureTargets() 15 | setupJvmToolchain() 16 | 17 | kotlin { 18 | explicitApi() 19 | 20 | setCompilationOptions() 21 | configureSourceSets() 22 | } 23 | 24 | tasks.withType { 25 | testLogging { 26 | events("PASSED", "FAILED", "SKIPPED") 27 | exceptionFormat = TestExceptionFormat.FULL 28 | showStandardStreams = true 29 | showStackTraces = true 30 | } 31 | } 32 | 33 | rootProject.tasks.named("runJvmTests") { 34 | dependsOn(tasks.named("jvmTest")) 35 | } 36 | } 37 | 38 | 39 | fun KotlinMultiplatformExtension.setCompilationOptions() { 40 | targets.all { 41 | if (this is KotlinJsTarget) { 42 | irTarget?.compilations?.all { 43 | configureCompilation() 44 | } 45 | } 46 | compilations.all { 47 | configureCompilation() 48 | } 49 | } 50 | } 51 | 52 | fun KotlinCompilation.configureCompilation() { 53 | kotlinOptions { 54 | if (platformType == KotlinPlatformType.jvm) { 55 | allWarningsAsErrors = true 56 | } 57 | 58 | freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn" 59 | freeCompilerArgs += "-Xexpect-actual-classes" 60 | } 61 | } 62 | 63 | fun KotlinMultiplatformExtension.configureSourceSets() { 64 | sourceSets 65 | .matching { it.name !in listOf("main", "test") } 66 | .all { 67 | val srcDir = if (name.endsWith("Main")) "src" else "test" 68 | val resourcesPrefix = if (name.endsWith("Test")) "test-" else "" 69 | val platform = name.dropLast(4) 70 | 71 | kotlin.srcDir("$platform/$srcDir") 72 | resources.srcDir("$platform/${resourcesPrefix}resources") 73 | 74 | languageSettings.apply { 75 | progressiveMode = true 76 | } 77 | } 78 | } 79 | 80 | fun Project.setupJvmToolchain() { 81 | tasks.withType { 82 | kotlinOptions { 83 | jvmTarget = JavaVersion.VERSION_1_8.toString() 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/JsConfig.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2021 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | @file:Suppress("UNUSED_VARIABLE") 5 | 6 | import org.gradle.api.* 7 | import org.gradle.kotlin.dsl.* 8 | import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension 9 | import java.io.* 10 | 11 | fun Project.configureJs() { 12 | configureJsTasks() 13 | 14 | kotlin { 15 | sourceSets { 16 | val jsTest by getting { 17 | dependencies { 18 | implementation(npm("puppeteer", "*")) 19 | } 20 | } 21 | } 22 | } 23 | 24 | configureJsTestTasks() 25 | } 26 | 27 | private fun Project.configureJsTasks() { 28 | configure { 29 | js(IR) { 30 | nodejs { 31 | testTask { 32 | useMocha { 33 | timeout = "10000" 34 | } 35 | } 36 | } 37 | 38 | browser { 39 | testTask { 40 | useKarma { 41 | useChromeHeadless() 42 | useConfigDirectory(File(project.rootProject.projectDir, "karma")) 43 | } 44 | } 45 | } 46 | 47 | val main by compilations.getting 48 | main.kotlinOptions.apply { 49 | metaInfo = true 50 | sourceMap = true 51 | moduleKind = "umd" 52 | this.main = "noCall" 53 | sourceMapEmbedSources = "always" 54 | } 55 | 56 | val test by compilations.getting 57 | test.kotlinOptions.apply { 58 | metaInfo = true 59 | sourceMap = true 60 | moduleKind = "umd" 61 | this.main = "call" 62 | sourceMapEmbedSources = "always" 63 | } 64 | } 65 | } 66 | } 67 | 68 | private fun Project.configureJsTestTasks() { 69 | val shouldRunJsBrowserTest = !hasProperty("teamcity") || hasProperty("enable-js-tests") 70 | if (shouldRunJsBrowserTest) return 71 | 72 | val cleanJsBrowserTest by tasks.getting 73 | val jsBrowserTest by tasks.getting 74 | cleanJsBrowserTest.onlyIf { false } 75 | jsBrowserTest.onlyIf { false } 76 | } 77 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/KotlinExtensions.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2021 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | import org.gradle.api.NamedDomainObjectContainer 6 | import org.gradle.api.Project 7 | import org.gradle.kotlin.dsl.* 8 | import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension 9 | import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet 10 | import org.jetbrains.kotlin.gradle.plugin.mpp.DefaultCInteropSettings 11 | import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget 12 | 13 | fun Project.kotlin(block: KotlinMultiplatformExtension.() -> Unit) { 14 | configure(block) 15 | } 16 | 17 | val Project.kotlin: KotlinMultiplatformExtension get() = the() 18 | 19 | fun KotlinMultiplatformExtension.createCInterop( 20 | name: String, 21 | cinteropTargets: List, 22 | block: DefaultCInteropSettings.() -> Unit 23 | ) { 24 | cinteropTargets.mapNotNull { targets.findByName(it) }.filterIsInstance().forEach { 25 | val main by it.compilations 26 | main.cinterops.create(name, block) 27 | } 28 | } 29 | 30 | fun NamedDomainObjectContainer.jvmAndNixMain(block: KotlinSourceSet.() -> Unit) { 31 | val sourceSet = findByName("jvmAndNixMain") ?: getByName("jvmMain") 32 | block(sourceSet) 33 | } 34 | 35 | fun NamedDomainObjectContainer.jvmAndNixTest(block: KotlinSourceSet.() -> Unit) { 36 | val sourceSet = findByName("jvmAndNixTest") ?: getByName("jvmTest") 37 | block(sourceSet) 38 | } 39 | 40 | fun NamedDomainObjectContainer.nixTest(block: KotlinSourceSet.() -> Unit) { 41 | val sourceSet = findByName("nixTest") ?: return 42 | block(sourceSet) 43 | } 44 | 45 | fun NamedDomainObjectContainer.posixMain(block: KotlinSourceSet.() -> Unit) { 46 | val sourceSet = findByName("posixMain") ?: return 47 | block(sourceSet) 48 | } 49 | 50 | fun NamedDomainObjectContainer.darwinMain(block: KotlinSourceSet.() -> Unit) { 51 | val sourceSet = findByName("darwinMain") ?: return 52 | block(sourceSet) 53 | } 54 | 55 | fun NamedDomainObjectContainer.darwinTest(block: KotlinSourceSet.() -> Unit) { 56 | val sourceSet = findByName("darwinTest") ?: return 57 | block(sourceSet) 58 | } 59 | 60 | fun NamedDomainObjectContainer.jsMain(block: KotlinSourceSet.() -> Unit) { 61 | val sourceSet = findByName("jsMain") ?: return 62 | block(sourceSet) 63 | } 64 | 65 | fun NamedDomainObjectContainer.jsTest(block: KotlinSourceSet.() -> Unit) { 66 | val sourceSet = findByName("jsTest") ?: return 67 | block(sourceSet) 68 | } 69 | 70 | fun NamedDomainObjectContainer.desktopMain(block: KotlinSourceSet.() -> Unit) { 71 | val sourceSet = findByName("desktopMain") ?: return 72 | block(sourceSet) 73 | } 74 | 75 | fun NamedDomainObjectContainer.desktopTest(block: KotlinSourceSet.() -> Unit) { 76 | val sourceSet = findByName("desktopTest") ?: return 77 | block(sourceSet) 78 | } 79 | 80 | fun NamedDomainObjectContainer.windowsMain(block: KotlinSourceSet.() -> Unit) { 81 | val sourceSet = findByName("windowsMain") ?: return 82 | block(sourceSet) 83 | } 84 | 85 | fun NamedDomainObjectContainer.windowsTest(block: KotlinSourceSet.() -> Unit) { 86 | val sourceSet = findByName("windowsTest") ?: return 87 | block(sourceSet) 88 | } 89 | 90 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/KtorBuildProperties.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2021 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | val OS_NAME = System.getProperty("os.name").lowercase() 5 | 6 | val HOST_NAME = when { 7 | OS_NAME.startsWith("linux") -> "linux" 8 | OS_NAME.startsWith("windows") -> "windows" 9 | OS_NAME.startsWith("mac") -> "macos" 10 | else -> error("Unknown os name `$OS_NAME`") 11 | } 12 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/NativeUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2021 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | import org.gradle.api.Project 6 | import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension 7 | import org.jetbrains.kotlin.gradle.plugin.mpp.Framework 8 | import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget 9 | 10 | fun Project.fastOr(block: () -> List): List { 11 | return block() 12 | } 13 | 14 | fun Project.posixTargets(): List = fastOr { 15 | nixTargets() + windowsTargets() 16 | } 17 | 18 | fun Project.nixTargets(): List = fastOr { 19 | darwinTargets() //+ kotlin.linuxX64().name 20 | } 21 | 22 | fun Project.darwinTargets(): List = fastOr { 23 | macosTargets() + iosTargets() + watchosTargets() + tvosTargets() 24 | } 25 | 26 | fun Project.macosTargets(): List = fastOr { 27 | with(kotlin) { 28 | listOf( 29 | macosX64(), 30 | macosArm64() 31 | ).map { it.name } 32 | } 33 | } 34 | 35 | fun Project.iosTargets(): List = fastOr { 36 | with(kotlin) { 37 | listOf( 38 | iosX64(), 39 | iosArm64(), 40 | iosSimulatorArm64(), 41 | ).map { it.name } 42 | } 43 | } 44 | 45 | fun Project.watchosTargets(): List = fastOr { 46 | with(kotlin) { 47 | listOf( 48 | watchosX64(), 49 | watchosArm32(), 50 | watchosArm64(), 51 | watchosSimulatorArm64(), 52 | ).map { it.name } 53 | } 54 | } 55 | 56 | fun Project.tvosTargets(): List = fastOr { 57 | with(kotlin) { 58 | listOf( 59 | tvosX64(), 60 | tvosArm64(), 61 | tvosSimulatorArm64(), 62 | ).map { it.name } 63 | } 64 | } 65 | 66 | fun Project.desktopTargets(): List = fastOr { 67 | with(kotlin) { 68 | listOf( 69 | macosX64(), 70 | macosArm64(), 71 | linuxX64(), 72 | mingwX64() 73 | ).map { it.name } 74 | } 75 | } 76 | 77 | fun Project.windowsTargets(): List = fastOr { 78 | with(kotlin) { 79 | listOf( 80 | mingwX64() 81 | ).map { it.name } 82 | } 83 | } 84 | 85 | fun Project.darwinTargetsFramework( 86 | targets: KotlinMultiplatformExtension.() -> List = { 87 | listOf( 88 | macosX64(), 89 | macosArm64(), 90 | iosX64(), 91 | iosArm64(), 92 | iosSimulatorArm64(), 93 | ) 94 | }, 95 | action: Framework.() -> Unit = {} 96 | ) { 97 | val projectName = name 98 | val specialCharacters = """\W""".toRegex() 99 | 100 | with(kotlin) { 101 | targets().forEach { iosTarget -> 102 | val moduleName = projectName 103 | .lowercase() 104 | .split(specialCharacters) 105 | .joinToString(separator = "") { splitName -> 106 | splitName.replaceFirstChar { it.uppercase() } 107 | } 108 | iosTarget.binaries.framework { 109 | baseName = "${moduleName}Shared" 110 | action() 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/TargetsConfig.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2021 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | import org.gradle.api.Project 6 | import org.gradle.kotlin.dsl.configure 7 | import org.gradle.kotlin.dsl.creating 8 | import org.gradle.kotlin.dsl.extra 9 | import org.gradle.kotlin.dsl.getValue 10 | import org.gradle.kotlin.dsl.getting 11 | import org.gradle.kotlin.dsl.invoke 12 | import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension 13 | import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet 14 | import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl 15 | import java.io.File 16 | 17 | val Project.files: Array get() = project.projectDir.listFiles() ?: emptyArray() 18 | val Project.hasCommon: Boolean get() = files.any { it.name == "common" } 19 | val Project.hasJvmAndNix: Boolean get() = hasCommon || files.any { it.name == "jvmAndNix" } 20 | val Project.hasPosix: Boolean get() = hasCommon || files.any { it.name == "posix" } 21 | val Project.hasDesktop: Boolean get() = hasPosix || files.any { it.name == "desktop" } 22 | val Project.hasNix: Boolean get() = hasPosix || hasJvmAndNix || files.any { it.name == "nix" } 23 | val Project.hasDarwin: Boolean get() = hasNix || files.any { it.name == "darwin" } 24 | val Project.hasWindows: Boolean get() = hasPosix || files.any { it.name == "windows" } 25 | val Project.hasJs: Boolean get() = hasCommon || files.any { it.name == "js" } 26 | val Project.hasJvm: Boolean get() = hasCommon || hasJvmAndNix || files.any { it.name == "jvm" } 27 | val Project.hasNative: Boolean get() = hasCommon || hasNix || hasPosix || hasDarwin || hasDesktop || hasWindows 28 | 29 | fun Project.configureTargets() { 30 | kotlin { 31 | jvm() 32 | 33 | if (hasJs) { 34 | js(IR) { 35 | nodejs() 36 | browser() 37 | } 38 | 39 | @OptIn(ExperimentalWasmDsl::class) 40 | wasmJs { 41 | moduleName = project.name 42 | browser { 43 | commonWebpackConfig { 44 | outputFileName = "${project.name}.js" 45 | } 46 | } 47 | binaries.executable() 48 | } 49 | 50 | configureJs() 51 | } 52 | 53 | if (hasPosix || hasDarwin || hasWindows) extra.set("hasNative", true) 54 | 55 | sourceSets { 56 | val commonTest by getting { 57 | dependencies { 58 | implementation(kotlin("test")) 59 | } 60 | } 61 | 62 | if (hasPosix) { 63 | val posixMain by creating 64 | val posixTest by creating 65 | } 66 | 67 | if (hasNix) { 68 | val nixMain by creating 69 | val nixTest by creating 70 | } 71 | 72 | if (hasDarwin) { 73 | val darwinMain by creating 74 | val darwinTest by creating { 75 | dependencies { 76 | implementation(kotlin("test")) 77 | } 78 | } 79 | 80 | val macosMain by creating 81 | val macosTest by creating 82 | 83 | val watchosMain by creating 84 | val watchosTest by creating 85 | 86 | val tvosMain by creating 87 | val tvosTest by creating 88 | 89 | val iosMain by creating 90 | val iosTest by creating 91 | } 92 | 93 | if (hasDesktop) { 94 | val desktopMain by creating 95 | val desktopTest by creating { 96 | dependencies { 97 | implementation(kotlin("test")) 98 | } 99 | } 100 | } 101 | 102 | if (hasWindows) { 103 | val windowsMain by creating 104 | val windowsTest by creating 105 | } 106 | 107 | if (hasJvmAndNix) { 108 | val jvmAndNixMain by creating { 109 | findByName("commonMain")?.let { dependsOn(it) } 110 | } 111 | 112 | val jvmAndNixTest by creating { 113 | findByName("commonTest")?.let { dependsOn(it) } 114 | } 115 | } 116 | 117 | if (hasJvm) { 118 | val jvmMain by getting { 119 | findByName("jvmAndNixMain")?.let { dependsOn(it) } 120 | } 121 | 122 | val jvmTest by getting { 123 | findByName("jvmAndNixTest")?.let { dependsOn(it) } 124 | } 125 | } 126 | 127 | if (hasPosix) { 128 | val posixMain by getting { 129 | findByName("commonMain")?.let { dependsOn(it) } 130 | } 131 | 132 | val posixTest by getting { 133 | findByName("commonTest")?.let { dependsOn(it) } 134 | 135 | dependencies { 136 | implementation(kotlin("test")) 137 | } 138 | } 139 | 140 | posixTargets().forEach { 141 | getByName("${it}Main").dependsOn(posixMain) 142 | getByName("${it}Test").dependsOn(posixTest) 143 | } 144 | } 145 | 146 | if (hasNix) { 147 | val nixMain by getting { 148 | findByName("posixMain")?.let { dependsOn(it) } 149 | findByName("jvmAndNixMain")?.let { dependsOn(it) } 150 | } 151 | 152 | val nixTest by getting { 153 | findByName("posixTest")?.let { dependsOn(it) } 154 | findByName("jvmAndNixTest")?.let { dependsOn(it) } 155 | } 156 | 157 | nixTargets().forEach { 158 | getByName("${it}Main").dependsOn(nixMain) 159 | getByName("${it}Test").dependsOn(nixTest) 160 | } 161 | } 162 | 163 | if (hasDarwin) { 164 | val nixMain: KotlinSourceSet? = findByName("nixMain") 165 | val darwinMain by getting 166 | val darwinTest by getting 167 | val macosMain by getting 168 | val macosTest by getting 169 | val iosMain by getting 170 | val iosTest by getting 171 | val watchosMain by getting 172 | val watchosTest by getting 173 | val tvosMain by getting 174 | val tvosTest by getting 175 | 176 | nixMain?.let { darwinMain.dependsOn(it) } 177 | macosMain.dependsOn(darwinMain) 178 | tvosMain.dependsOn(darwinMain) 179 | iosMain.dependsOn(darwinMain) 180 | watchosMain.dependsOn(darwinMain) 181 | 182 | macosTargets().forEach { 183 | getByName("${it}Main").dependsOn(macosMain) 184 | getByName("${it}Test").dependsOn(macosTest) 185 | } 186 | 187 | iosTargets().forEach { 188 | getByName("${it}Main").dependsOn(iosMain) 189 | getByName("${it}Test").dependsOn(iosTest) 190 | } 191 | 192 | watchosTargets().forEach { 193 | getByName("${it}Main").dependsOn(watchosMain) 194 | getByName("${it}Test").dependsOn(watchosTest) 195 | } 196 | 197 | tvosTargets().forEach { 198 | getByName("${it}Main").dependsOn(tvosMain) 199 | getByName("${it}Test").dependsOn(tvosTest) 200 | } 201 | 202 | darwinTargets().forEach { 203 | getByName("${it}Main").dependsOn(darwinMain) 204 | getByName("${it}Test").dependsOn(darwinTest) 205 | } 206 | } 207 | 208 | if (hasDesktop) { 209 | val desktopMain by getting { 210 | findByName("posixMain")?.let { dependsOn(it) } 211 | } 212 | 213 | val desktopTest by getting 214 | 215 | desktopTargets().forEach { 216 | getByName("${it}Main").dependsOn(desktopMain) 217 | getByName("${it}Test").dependsOn(desktopTest) 218 | } 219 | } 220 | 221 | if (hasWindows) { 222 | val windowsMain by getting { 223 | findByName("posixMain")?.let { dependsOn(it) } 224 | } 225 | 226 | val windowsTest by getting { 227 | dependencies { 228 | implementation(kotlin("test")) 229 | } 230 | } 231 | 232 | windowsTargets().forEach { 233 | getByName("${it}Main").dependsOn(windowsMain) 234 | getByName("${it}Test").dependsOn(windowsTest) 235 | } 236 | } 237 | 238 | if (hasNative) { 239 | tasks.findByName("linkDebugTestLinuxX64")?.onlyIf { HOST_NAME == "linux" } 240 | tasks.findByName("linkDebugTestMingwX64")?.onlyIf { HOST_NAME == "windows" } 241 | } 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /compose/android/src/dev/programadorthi/state/compose/ComposeValueManagerSavedStateHandle.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.compose 2 | 3 | import androidx.compose.runtime.SnapshotMutationPolicy 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.saveable.Saver 6 | import androidx.compose.runtime.saveable.SaverScope 7 | import androidx.lifecycle.SavedStateHandle 8 | import dev.programadorthi.state.core.ValueManager 9 | 10 | internal class ComposeValueManagerSavedStateHandle( 11 | private val valueManager: ValueManager, 12 | private val policy: SnapshotMutationPolicy, 13 | private val savedStateHandle: SavedStateHandle, 14 | stateRestorationKey: String, 15 | saver: Saver, 16 | ) : SaverScope, ValueManager by valueManager { 17 | 18 | private val valueManagerSaver = ValueManagerSaver(saver) { valueManager } 19 | private val state = mutableStateOf(valueManager.value, policy) 20 | private val key = "$KEY:$stateRestorationKey" 21 | private val canBeSaved: Boolean 22 | 23 | init { 24 | val restoredValue = savedStateHandle.remove(key) 25 | 26 | // By default SaveStateHandle throw exceptions when can't save the value in a Bundle 27 | savedStateHandle[key] = valueManager.value 28 | canBeSaved = true 29 | 30 | valueManager.collect { newValue -> 31 | state.value = newValue 32 | savedStateHandle[key] = with(valueManagerSaver) { 33 | save(valueManager) 34 | } 35 | } 36 | 37 | if (restoredValue != null) { 38 | valueManagerSaver.restore(restoredValue) 39 | } 40 | } 41 | 42 | override var value: T 43 | get() = state.value 44 | set(value) { 45 | val current = state.value 46 | if (policy.equivalent(current, value).not()) { 47 | valueManager.value = value 48 | } 49 | } 50 | 51 | override fun canBeSaved(value: Any): Boolean = canBeSaved 52 | 53 | private companion object { 54 | private const val KEY = "dev.programadorthi.state.compose.ComposeValueManagerState.android" 55 | } 56 | } -------------------------------------------------------------------------------- /compose/android/src/dev/programadorthi/state/compose/ValueManagerAsState.android.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.compose 2 | 3 | import androidx.compose.runtime.SnapshotMutationPolicy 4 | import androidx.compose.runtime.saveable.Saver 5 | import androidx.compose.runtime.saveable.autoSaver 6 | import androidx.compose.runtime.structuralEqualityPolicy 7 | import androidx.lifecycle.SavedStateHandle 8 | import dev.programadorthi.state.core.ValueManager 9 | import dev.programadorthi.state.core.extension.basicValueManager 10 | import kotlin.properties.PropertyDelegateProvider 11 | import kotlin.properties.ReadWriteProperty 12 | import kotlin.reflect.KProperty 13 | 14 | public fun composeValueManager( 15 | initialValue: T, 16 | stateRestorationKey: String, 17 | savedStateHandle: SavedStateHandle, 18 | policy: SnapshotMutationPolicy = structuralEqualityPolicy(), 19 | saver: Saver = autoSaver(), 20 | ): ValueManager = ComposeValueManagerSavedStateHandle( 21 | valueManager = basicValueManager(initialValue), 22 | policy = policy, 23 | savedStateHandle = savedStateHandle, 24 | stateRestorationKey = stateRestorationKey, 25 | saver = saver, 26 | ) 27 | 28 | public fun composeValueManager( 29 | initialValue: T, 30 | savedStateHandle: SavedStateHandle, 31 | policy: SnapshotMutationPolicy = structuralEqualityPolicy(), 32 | saver: Saver = autoSaver(), 33 | ): ComposeValueManager = basicValueManager(initialValue).asState( 34 | savedStateHandle = savedStateHandle, 35 | policy = policy, 36 | saver = saver, 37 | ) 38 | 39 | public fun ValueManager.asState( 40 | savedStateHandle: SavedStateHandle, 41 | policy: SnapshotMutationPolicy = structuralEqualityPolicy(), 42 | saver: Saver = autoSaver(), 43 | ): ComposeValueManager = PropertyDelegateProvider { _, property -> 44 | val state = asState( 45 | stateRestorationKey = property.name, 46 | savedStateHandle = savedStateHandle, 47 | policy = policy, 48 | saver = saver, 49 | ) 50 | object : ReadWriteProperty { 51 | override fun getValue(thisRef: Any?, property: KProperty<*>): T = state.value 52 | 53 | override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { 54 | state.value = value 55 | } 56 | } 57 | } 58 | 59 | public fun ValueManager.asState( 60 | stateRestorationKey: String, 61 | savedStateHandle: SavedStateHandle, 62 | policy: SnapshotMutationPolicy = structuralEqualityPolicy(), 63 | saver: Saver = autoSaver(), 64 | ): ValueManager = ComposeValueManagerSavedStateHandle( 65 | valueManager = this, 66 | policy = policy, 67 | savedStateHandle = savedStateHandle, 68 | stateRestorationKey = stateRestorationKey, 69 | saver = saver, 70 | ) -------------------------------------------------------------------------------- /compose/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("multiplatform") 3 | id("org.jetbrains.compose") 4 | id("com.android.library") 5 | id("com.vanniktech.maven.publish") 6 | } 7 | 8 | applyBasicSetup() 9 | 10 | darwinTargetsFramework() 11 | 12 | kotlin { 13 | androidTarget { 14 | publishLibraryVariants("release") 15 | } 16 | 17 | sourceSets { 18 | commonMain { 19 | dependencies { 20 | api(project(":coroutines")) 21 | api(compose.runtime) 22 | api(compose.runtimeSaveable) 23 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") 24 | } 25 | } 26 | commonTest { 27 | dependencies { 28 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0") 29 | } 30 | } 31 | androidMain { 32 | dependencies { 33 | implementation("androidx.activity:activity-ktx:1.8.2") 34 | implementation("androidx.fragment:fragment-ktx:1.6.2") 35 | } 36 | } 37 | } 38 | } 39 | 40 | android { 41 | namespace = "dev.programadorthi.state.compose" 42 | compileSdk = 34 43 | 44 | defaultConfig { 45 | minSdk = 23 46 | } 47 | 48 | compileOptions { 49 | sourceCompatibility = JavaVersion.VERSION_1_8 50 | targetCompatibility = JavaVersion.VERSION_1_8 51 | } 52 | 53 | buildTypes { 54 | getByName("release") { 55 | isMinifyEnabled = false 56 | } 57 | } 58 | 59 | packaging { 60 | resources { 61 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 62 | } 63 | } 64 | 65 | sourceSets["main"].manifest.srcFile("src/AndroidManifest.xml") 66 | sourceSets["main"].res.srcDirs("src/res") 67 | } 68 | -------------------------------------------------------------------------------- /compose/common/src/dev/programadorthi/state/compose/ComposeValueManagerState.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.compose 2 | 3 | import androidx.compose.runtime.RememberObserver 4 | import androidx.compose.runtime.SnapshotMutationPolicy 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.saveable.SaveableStateRegistry 7 | import androidx.compose.runtime.saveable.Saver 8 | import androidx.compose.runtime.saveable.SaverScope 9 | import dev.programadorthi.state.core.ValueManager 10 | 11 | internal class ComposeValueManagerState( 12 | private val valueManager: ValueManager, 13 | private val policy: SnapshotMutationPolicy, 14 | private val stateRegistry: SaveableStateRegistry?, 15 | private val stateRestorationKey: String?, 16 | saver: Saver, 17 | ) : SaverScope, RememberObserver, ValueManager by valueManager { 18 | 19 | private val valueManagerSaver = ValueManagerSaver(saver) { valueManager } 20 | private val state = mutableStateOf(valueManager.value, policy) 21 | private val key = "$KEY:$stateRestorationKey" 22 | 23 | private var entry: SaveableStateRegistry.Entry? = null 24 | 25 | init { 26 | valueManager.collect { newValue -> 27 | state.value = newValue 28 | } 29 | tryRestoreAndRegister() 30 | } 31 | 32 | override var value: T 33 | get() = state.value 34 | set(value) { 35 | val current = state.value 36 | if (policy.equivalent(current, value).not()) { 37 | valueManager.value = value 38 | } 39 | } 40 | 41 | override fun canBeSaved(value: Any): Boolean { 42 | val registry = stateRegistry 43 | return registry == null || registry.canBeSaved(value) 44 | } 45 | 46 | override fun onAbandoned() { 47 | entry?.unregister() 48 | } 49 | 50 | override fun onForgotten() { 51 | entry?.unregister() 52 | } 53 | 54 | override fun onRemembered() { 55 | register() 56 | } 57 | 58 | private fun tryRestoreAndRegister() { 59 | if (stateRestorationKey.isNullOrBlank()) return 60 | val registry = stateRegistry ?: return 61 | registry.consumeRestored(key)?.let { consumed -> 62 | valueManagerSaver.restore(consumed) 63 | } 64 | 65 | register() 66 | } 67 | 68 | private fun register() { 69 | if (stateRestorationKey.isNullOrBlank()) return 70 | val registry = stateRegistry ?: return 71 | 72 | val saveable = { 73 | with(valueManagerSaver) { 74 | save(valueManager) 75 | } 76 | } 77 | val toSave = requireNotNull(saveable()) { 78 | "$value cannot be saved using the current SaveableStateRegistry" 79 | } 80 | if (registry.canBeSaved(toSave)) { 81 | entry = registry.registerProvider(key, saveable) 82 | } 83 | } 84 | 85 | private companion object { 86 | private const val KEY = "dev.programadorthi.state.compose.ComposeValueManagerState" 87 | } 88 | } -------------------------------------------------------------------------------- /compose/common/src/dev/programadorthi/state/compose/RememberValueManager.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.compose 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import androidx.compose.runtime.saveable.Saver 6 | import androidx.compose.runtime.saveable.autoSaver 7 | import androidx.compose.runtime.saveable.rememberSaveable 8 | import dev.programadorthi.state.core.ValueManager 9 | 10 | @Composable 11 | public fun rememberValueManager( 12 | valueManager: () -> ValueManager 13 | ): ValueManager = remember { 14 | valueManager().asState() 15 | } 16 | 17 | @Composable 18 | public fun rememberSaveableValueManager( 19 | saver: Saver = autoSaver(), 20 | valueManager: () -> ValueManager 21 | ): ValueManager = rememberSaveable( 22 | init = valueManager, 23 | saver = ValueManagerSaver( 24 | saver = saver, 25 | manager = valueManager, 26 | ), 27 | ) 28 | -------------------------------------------------------------------------------- /compose/common/src/dev/programadorthi/state/compose/ValidatorManagerAsState.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.compose 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import androidx.compose.runtime.saveable.mapSaver 6 | import androidx.compose.runtime.saveable.rememberSaveable 7 | import dev.programadorthi.state.core.validation.ValidatorManager 8 | 9 | public fun ValidatorManager.asValidatorState(): ValidatorManagerState = 10 | ValidatorManagerStateImpl(this) 11 | 12 | @Composable 13 | public fun rememberValidatorState( 14 | validatorManager: () -> ValidatorManager 15 | ): ValidatorManagerState = remember { 16 | validatorManager().asValidatorState() 17 | } 18 | 19 | @Composable 20 | public fun rememberSaveableValidatorState( 21 | validatorManager: () -> ValidatorManager 22 | ): ValidatorManagerState { 23 | val managerState = remember { ValidatorManagerStateImpl(validatorManager()) } 24 | return rememberSaveable( 25 | init = { managerState }, 26 | saver = mapSaver( 27 | save = { it.toSave() }, 28 | restore = { managerState.toRestore(it) } 29 | ) 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /compose/common/src/dev/programadorthi/state/compose/ValidatorManagerState.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.compose 2 | 3 | import androidx.compose.runtime.State 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import dev.programadorthi.state.core.validation.ValidatorManager 8 | 9 | public interface ValidatorManagerState : State { 10 | 11 | public operator fun component1(): Boolean 12 | 13 | public operator fun component2(): List 14 | 15 | } 16 | 17 | internal class ValidatorManagerStateImpl( 18 | validatorManager: ValidatorManager 19 | ) : ValidatorManagerState { 20 | 21 | private var valid by mutableStateOf(validatorManager.isValid) 22 | private var messages by mutableStateOf(validatorManager.messages) 23 | 24 | init { 25 | validatorManager.onValidated { other -> 26 | valid = other.isEmpty() 27 | messages = other 28 | } 29 | } 30 | 31 | override val value: Boolean 32 | get() = valid 33 | 34 | override fun component1(): Boolean = value 35 | 36 | override fun component2(): List = messages 37 | 38 | @Suppress("UNCHECKED_CAST") 39 | internal fun toRestore(items: Map): ValidatorManagerStateImpl { 40 | val msgs = items[MESSAGES_KEY] as? Array ?: emptyArray() 41 | valid = items[VALID_KEY] as? Boolean ?: false 42 | messages = msgs.toList() 43 | return this 44 | } 45 | 46 | internal fun toSave(): Map = mapOf( 47 | VALID_KEY to value, 48 | MESSAGES_KEY to messages.toTypedArray() 49 | ) 50 | 51 | private companion object { 52 | private const val MESSAGES_KEY = "MESSAGES_KEY" 53 | private const val VALID_KEY = "VALID_KEY" 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /compose/common/src/dev/programadorthi/state/compose/ValueManagerAsState.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.compose 2 | 3 | import androidx.compose.runtime.SnapshotMutationPolicy 4 | import androidx.compose.runtime.saveable.SaveableStateRegistry 5 | import androidx.compose.runtime.saveable.Saver 6 | import androidx.compose.runtime.saveable.autoSaver 7 | import androidx.compose.runtime.structuralEqualityPolicy 8 | import dev.programadorthi.state.core.ValueManager 9 | import dev.programadorthi.state.core.extension.basicValueManager 10 | import kotlin.properties.PropertyDelegateProvider 11 | import kotlin.properties.ReadWriteProperty 12 | import kotlin.reflect.KProperty 13 | 14 | public typealias ComposeValueManager = PropertyDelegateProvider> 15 | 16 | public fun composeValueManager( 17 | initialValue: T, 18 | policy: SnapshotMutationPolicy = structuralEqualityPolicy(), 19 | ): ValueManager = basicValueManager(initialValue).asState(policy = policy) 20 | 21 | public fun composeValueManager( 22 | initialValue: T, 23 | stateRegistry: SaveableStateRegistry, 24 | policy: SnapshotMutationPolicy = structuralEqualityPolicy(), 25 | saver: Saver = autoSaver(), 26 | ): ComposeValueManager = basicValueManager(initialValue).asState( 27 | stateRegistry = stateRegistry, 28 | policy = policy, 29 | saver = saver, 30 | ) 31 | 32 | public fun composeValueManager( 33 | initialValue: T, 34 | stateRestorationKey: String, 35 | stateRegistry: SaveableStateRegistry, 36 | policy: SnapshotMutationPolicy = structuralEqualityPolicy(), 37 | saver: Saver = autoSaver(), 38 | ): ValueManager = ComposeValueManagerState( 39 | valueManager = basicValueManager(initialValue), 40 | stateRestorationKey = stateRestorationKey, 41 | stateRegistry = stateRegistry, 42 | policy = policy, 43 | saver = saver, 44 | ) 45 | 46 | public fun ValueManager.asState( 47 | policy: SnapshotMutationPolicy = structuralEqualityPolicy(), 48 | ): ValueManager = ComposeValueManagerState( 49 | valueManager = this, 50 | policy = policy, 51 | stateRegistry = null, 52 | stateRestorationKey = null, 53 | saver = autoSaver(), 54 | ) 55 | 56 | public fun ValueManager.asState( 57 | stateRegistry: SaveableStateRegistry, 58 | policy: SnapshotMutationPolicy = structuralEqualityPolicy(), 59 | saver: Saver = autoSaver(), 60 | ): ComposeValueManager = PropertyDelegateProvider { _, property -> 61 | val state = asState( 62 | stateRestorationKey = property.name, 63 | stateRegistry = stateRegistry, 64 | policy = policy, 65 | saver = saver, 66 | ) 67 | object : ReadWriteProperty { 68 | override fun getValue(thisRef: Any?, property: KProperty<*>): T = state.value 69 | 70 | override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { 71 | state.value = value 72 | } 73 | } 74 | } 75 | 76 | public fun ValueManager.asState( 77 | stateRestorationKey: String, 78 | stateRegistry: SaveableStateRegistry, 79 | policy: SnapshotMutationPolicy = structuralEqualityPolicy(), 80 | saver: Saver = autoSaver(), 81 | ): ValueManager = ComposeValueManagerState( 82 | valueManager = this, 83 | policy = policy, 84 | stateRegistry = stateRegistry, 85 | stateRestorationKey = stateRestorationKey, 86 | saver = saver, 87 | ) 88 | -------------------------------------------------------------------------------- /compose/common/src/dev/programadorthi/state/compose/ValueManagerSaver.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.compose 2 | 3 | import androidx.compose.runtime.saveable.Saver 4 | import androidx.compose.runtime.saveable.SaverScope 5 | import androidx.compose.runtime.saveable.autoSaver 6 | import dev.programadorthi.state.core.ValueManager 7 | 8 | internal class ValueManagerSaver( 9 | private val saver: Saver = autoSaver(), 10 | private val manager: () -> ValueManager, 11 | ) : Saver, Any> { 12 | 13 | override fun restore(value: Any): ValueManager { 14 | @Suppress("UNCHECKED_CAST") 15 | (saver as Saver) 16 | 17 | val result = manager() 18 | saver.restore(value)?.let { other -> 19 | result.value = other 20 | } 21 | return result 22 | } 23 | 24 | override fun SaverScope.save(value: ValueManager): Any? = with(saver) { 25 | save(value.value) 26 | } 27 | } -------------------------------------------------------------------------------- /compose/common/test/dev/programadorthi/state/compose/ComposeValueManagerTest.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.compose 2 | 3 | import androidx.compose.runtime.LaunchedEffect 4 | import androidx.compose.runtime.remember 5 | import dev.programadorthi.state.compose.fake.FakeSaveableStateRegistry 6 | import dev.programadorthi.state.compose.helper.runComposeTest 7 | import dev.programadorthi.state.core.extension.getValue 8 | import dev.programadorthi.state.core.extension.setValue 9 | import kotlin.test.Test 10 | import kotlin.test.assertEquals 11 | 12 | internal class ComposeValueManagerTest { 13 | @Test 14 | fun shouldCurrentValueBeEqualsToInitialValue() = runComposeTest { composition, recomposer -> 15 | val manager = composeValueManager(0) 16 | var result = -1 17 | 18 | composition.setContent { 19 | val value by remember { manager } 20 | result = value 21 | } 22 | 23 | recomposer() 24 | 25 | assertEquals(0, result, "Current value is not equals to initial value") 26 | } 27 | 28 | @Test 29 | fun shouldCurrentValueBeEqualsToInitialValue_WhenUsingDelegateProperty() = 30 | runComposeTest { composition, recomposer -> 31 | val value by composeValueManager(0) 32 | var result = -1 33 | 34 | composition.setContent { 35 | result = value 36 | } 37 | 38 | recomposer() 39 | 40 | assertEquals(0, result, "Current value is not equals to initial value") 41 | } 42 | 43 | @Test 44 | fun shouldChangeCurrentValue_WhenCallUpdate() = runComposeTest { composition, recomposer -> 45 | val manager = composeValueManager(0) 46 | var result = -1 47 | 48 | composition.setContent { 49 | val value by remember { manager.asState() } 50 | result = value 51 | } 52 | 53 | manager.update { value -> 54 | value + 1 55 | } 56 | recomposer() 57 | 58 | assertEquals(1, result, "Call to update function is not updating current value") 59 | } 60 | 61 | @Test 62 | fun shouldChangeCurrentValue_WhenCallUpdateUsingDelegateProperty() = 63 | runComposeTest { composition, recomposer -> 64 | var value by composeValueManager(0) 65 | var result = -1 66 | 67 | composition.setContent { 68 | result = value 69 | } 70 | 71 | value += 1 72 | recomposer() 73 | 74 | assertEquals(1, result, "Updating by delegate property is not updating current value") 75 | } 76 | 77 | @Test 78 | fun shouldChangeCurrentValue_WhenCallRememberedVersion() = 79 | runComposeTest { composition, recomposer -> 80 | var update = false 81 | var result = -1 82 | 83 | composition.setContent { 84 | val (value, setValue) = remember { composeValueManager(0) } 85 | 86 | LaunchedEffect(update) { 87 | setValue(value + 1) 88 | } 89 | 90 | result = value 91 | } 92 | 93 | update = true 94 | recomposer() 95 | 96 | assertEquals(1, result, "Updating by delegate property is not updating current value") 97 | } 98 | 99 | @Test 100 | fun shouldNotRegisterToRestorationWithoutAKey() { 101 | val stateRegistry = FakeSaveableStateRegistry() 102 | 103 | composeValueManager( 104 | initialValue = 0, 105 | stateRestorationKey = "", 106 | stateRegistry = stateRegistry, 107 | ) 108 | 109 | assertEquals( 110 | "", 111 | stateRegistry.consumeRestoredKey, 112 | "Should not have a restored key when not provided" 113 | ) 114 | } 115 | 116 | @Test 117 | fun shouldRegisterToRestoration() { 118 | val stateRestorationKey = "key#123" 119 | val stateRegistry = FakeSaveableStateRegistry() 120 | val expectKey = "dev.programadorthi.state.compose.ComposeValueManagerState:$stateRestorationKey" 121 | 122 | composeValueManager( 123 | initialValue = 0, 124 | stateRegistry = stateRegistry, 125 | stateRestorationKey = stateRestorationKey, 126 | ) 127 | 128 | assertEquals( 129 | expectKey, 130 | stateRegistry.consumeRestoredKey, 131 | "Consuming different keys is wrong" 132 | ) 133 | assertEquals(0, stateRegistry.canBeSavedValue, "Saved values different") 134 | } 135 | 136 | @Test 137 | fun shouldRegisterToRestorationByPropertyDelegate() { 138 | val stateRegistry = FakeSaveableStateRegistry() 139 | val myName by composeValueManager( 140 | initialValue = 0, 141 | stateRegistry = stateRegistry, 142 | ) 143 | 144 | assertEquals( 145 | "dev.programadorthi.state.compose.ComposeValueManagerState:myName", 146 | stateRegistry.consumeRestoredKey, 147 | "Consuming different property keys is wrong" 148 | ) 149 | assertEquals(0, stateRegistry.canBeSavedValue, "Saved values different") 150 | assertEquals(0, myName, "Saved values different in properties") 151 | } 152 | 153 | @Test 154 | fun shouldRegisterRestorationProvider() { 155 | val stateRestorationKey = "key#123" 156 | val stateRegistry = FakeSaveableStateRegistry() 157 | val expectKey = "dev.programadorthi.state.compose.ComposeValueManagerState:$stateRestorationKey" 158 | stateRegistry.canBeSaved = true 159 | 160 | composeValueManager( 161 | initialValue = 0, 162 | stateRegistry = stateRegistry, 163 | stateRestorationKey = stateRestorationKey, 164 | ) 165 | 166 | assertEquals( 167 | expectKey, 168 | stateRegistry.consumeRestoredKey, 169 | "Consuming different keys is wrong" 170 | ) 171 | assertEquals(0, stateRegistry.canBeSavedValue, "Saved values are different") 172 | assertEquals( 173 | expectKey, 174 | stateRegistry.entry?.key, 175 | "Registering providers with different keys" 176 | ) 177 | assertEquals(0, stateRegistry.entry?.valueProvider?.invoke(), "Not provided correct value") 178 | } 179 | 180 | @Test 181 | fun shouldRestorePreviousValue() { 182 | val stateRestorationKey = "key#123" 183 | val stateRegistry = FakeSaveableStateRegistry() 184 | val expectKey = "dev.programadorthi.state.compose.ComposeValueManagerState:$stateRestorationKey" 185 | stateRegistry.canBeSaved = true 186 | stateRegistry.consumeRestored = 2024 187 | 188 | val manager = composeValueManager( 189 | initialValue = 0, 190 | stateRegistry = stateRegistry, 191 | stateRestorationKey = stateRestorationKey, 192 | ) 193 | 194 | assertEquals(2024, manager.value, "Value not restored correctly") 195 | assertEquals( 196 | expectKey, 197 | stateRegistry.consumeRestoredKey, 198 | "Consuming different keys is wrong" 199 | ) 200 | assertEquals(2024, stateRegistry.canBeSavedValue, "Different can be saved values") 201 | assertEquals( 202 | expectKey, 203 | stateRegistry.entry?.key, 204 | "Registering providers with different keys" 205 | ) 206 | assertEquals( 207 | 2024, 208 | stateRegistry.entry?.valueProvider?.invoke(), 209 | "Not provided correct value" 210 | ) 211 | } 212 | } -------------------------------------------------------------------------------- /compose/common/test/dev/programadorthi/state/compose/fake/FakeEntry.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.compose.fake 2 | 3 | import androidx.compose.runtime.saveable.SaveableStateRegistry 4 | 5 | internal class FakeEntry( 6 | val key: String, 7 | val valueProvider: () -> Any?, 8 | ) : SaveableStateRegistry.Entry { 9 | var unregistered: Boolean = false 10 | private set 11 | 12 | override fun unregister() { 13 | check(!unregistered) { 14 | "Entry is already unregistered" 15 | } 16 | unregistered = true 17 | } 18 | } -------------------------------------------------------------------------------- /compose/common/test/dev/programadorthi/state/compose/fake/FakeSaveableStateRegistry.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.compose.fake 2 | 3 | import androidx.compose.runtime.saveable.SaveableStateRegistry 4 | 5 | internal class FakeSaveableStateRegistry : SaveableStateRegistry { 6 | var entry: FakeEntry? = null 7 | 8 | var canBeSaved: Boolean = false 9 | var canBeSavedValue: Any? = null 10 | private set 11 | 12 | var consumeRestoredKey: String = "" 13 | private set 14 | var consumeRestored: Any? = null 15 | 16 | val valueToSave: Map> = mutableMapOf() 17 | 18 | override fun canBeSaved(value: Any): Boolean { 19 | canBeSavedValue = value 20 | return canBeSaved 21 | } 22 | 23 | override fun consumeRestored(key: String): Any? { 24 | consumeRestoredKey = key 25 | return consumeRestored 26 | } 27 | 28 | override fun performSave(): Map> = valueToSave 29 | 30 | override fun registerProvider( 31 | key: String, 32 | valueProvider: () -> Any? 33 | ): SaveableStateRegistry.Entry { 34 | entry = FakeEntry( 35 | key = key, 36 | valueProvider = valueProvider 37 | ) 38 | return entry!! 39 | } 40 | } -------------------------------------------------------------------------------- /compose/common/test/dev/programadorthi/state/compose/helper/TestApplier.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.compose.helper 2 | 3 | import androidx.compose.runtime.Applier 4 | 5 | internal class TestApplier : Applier { 6 | override val current: Unit 7 | get() = Unit 8 | 9 | override fun down(node: Unit) {} 10 | 11 | override fun up() {} 12 | 13 | override fun insertTopDown( 14 | index: Int, 15 | instance: Unit, 16 | ) {} 17 | 18 | override fun insertBottomUp( 19 | index: Int, 20 | instance: Unit, 21 | ) {} 22 | 23 | override fun remove( 24 | index: Int, 25 | count: Int, 26 | ) {} 27 | 28 | override fun move( 29 | from: Int, 30 | to: Int, 31 | count: Int, 32 | ) {} 33 | 34 | override fun clear() {} 35 | } -------------------------------------------------------------------------------- /compose/common/test/dev/programadorthi/state/compose/helper/runComposeTest.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.compose.helper 2 | 3 | import androidx.compose.runtime.BroadcastFrameClock 4 | import androidx.compose.runtime.Composition 5 | import androidx.compose.runtime.Recomposer 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.ExperimentalCoroutinesApi 8 | import kotlinx.coroutines.Job 9 | import kotlinx.coroutines.launch 10 | import kotlinx.coroutines.test.TestScope 11 | import kotlinx.coroutines.test.advanceTimeBy 12 | import kotlinx.coroutines.test.runTest 13 | 14 | @OptIn(ExperimentalCoroutinesApi::class) 15 | internal fun runComposeTest( 16 | content: TestScope.(Composition, () -> Unit) -> Unit 17 | ) = runTest { 18 | val job = Job() 19 | val clock = BroadcastFrameClock() 20 | val scope = CoroutineScope(coroutineContext + job + clock) 21 | val recomposer = Recomposer(scope.coroutineContext) 22 | val runner = 23 | scope.launch { 24 | recomposer.runRecomposeAndApplyChanges() 25 | } 26 | val composition = Composition(TestApplier(), recomposer) 27 | val invalidate: () -> Unit = { 28 | advanceTimeBy(99) 29 | clock.sendFrame(0L) 30 | } 31 | try { 32 | content(composition, invalidate) 33 | } finally { 34 | runner.cancel() 35 | recomposer.close() 36 | job.cancel() 37 | } 38 | } -------------------------------------------------------------------------------- /compose/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=Compose 2 | POM_ARTIFACT_ID=compose -------------------------------------------------------------------------------- /core/android/src/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /core/android/src/dev/programadorthi/state/core/AndroidValueManager.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.core 2 | 3 | import androidx.activity.ComponentActivity 4 | import androidx.fragment.app.Fragment 5 | import androidx.lifecycle.SavedStateHandle 6 | import androidx.lifecycle.ViewModelProvider 7 | import androidx.lifecycle.ViewModelStoreOwner 8 | import dev.programadorthi.state.core.action.ChangeAction 9 | import dev.programadorthi.state.core.action.ErrorAction 10 | import dev.programadorthi.state.core.extension.basicValueManager 11 | import dev.programadorthi.state.core.validation.Validator 12 | import kotlin.properties.PropertyDelegateProvider 13 | import kotlin.properties.ReadWriteProperty 14 | import kotlin.reflect.KProperty 15 | 16 | private const val KEY = "dev.programadorthi.state.core.AndroidValueManagerState" 17 | 18 | private fun String.compoundKey(): String = "$KEY:$this" 19 | 20 | public typealias AndroidValueManager = PropertyDelegateProvider> 21 | 22 | public fun androidValueManager( 23 | initialValue: T, 24 | stateRestorationKey: String, 25 | savedStateHandle: SavedStateHandle, 26 | saver: AndroidValueManagerStateSaver? = null, 27 | validators: List>? = null, 28 | changeHandler: ChangeAction? = null, 29 | errorHandler: ErrorAction? = null, 30 | ): Lazy> = lazy { 31 | val valueManager = basicValueManager( 32 | initialValue = initialValue, 33 | validators = validators, 34 | changeHandler = changeHandler, 35 | errorHandler = errorHandler, 36 | ) 37 | val state = AndroidValueManagerState( 38 | valueManager = valueManager, 39 | stateRestorationKey = stateRestorationKey.compoundKey(), 40 | savedStateHandle = savedStateHandle, 41 | saver = saver, 42 | ) 43 | state.valueManager 44 | } 45 | 46 | public fun androidValueManager( 47 | initialValue: T, 48 | savedStateHandle: SavedStateHandle, 49 | saver: AndroidValueManagerStateSaver? = null, 50 | validators: List>? = null, 51 | changeHandler: ChangeAction? = null, 52 | errorHandler: ErrorAction? = null, 53 | ): AndroidValueManager = PropertyDelegateProvider { _, property -> 54 | object : ReadWriteProperty { 55 | val valueManager by androidValueManager( 56 | initialValue = initialValue, 57 | stateRestorationKey = property.name, 58 | savedStateHandle = savedStateHandle, 59 | saver = saver, 60 | validators = validators, 61 | changeHandler = changeHandler, 62 | errorHandler = errorHandler 63 | ) 64 | 65 | override fun getValue(thisRef: Any?, property: KProperty<*>): T = valueManager.value 66 | 67 | override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { 68 | valueManager.value = value 69 | } 70 | } 71 | } 72 | 73 | public fun ComponentActivity.androidValueManager( 74 | initialValue: T, 75 | stateRestorationKey: String, 76 | saver: AndroidValueManagerStateSaver? = null, 77 | validators: List>? = null, 78 | changeHandler: ChangeAction? = null, 79 | errorHandler: ErrorAction? = null, 80 | ): Lazy> = ownerValueManager( 81 | initialValue = initialValue, 82 | stateRestorationKey = stateRestorationKey, 83 | saver = saver, 84 | validators = validators, 85 | changeHandler = changeHandler, 86 | errorHandler = errorHandler, 87 | ) 88 | 89 | public fun ComponentActivity.androidValueManager( 90 | initialValue: T, 91 | saver: AndroidValueManagerStateSaver? = null, 92 | validators: List>? = null, 93 | changeHandler: ChangeAction? = null, 94 | errorHandler: ErrorAction? = null, 95 | ): AndroidValueManager = PropertyDelegateProvider { _, property -> 96 | object : ReadWriteProperty { 97 | val valueManager by ownerValueManager( 98 | initialValue = initialValue, 99 | stateRestorationKey = property.name, 100 | saver = saver, 101 | validators = validators, 102 | changeHandler = changeHandler, 103 | errorHandler = errorHandler, 104 | ) 105 | 106 | override fun getValue(thisRef: Any?, property: KProperty<*>): T = valueManager.value 107 | 108 | override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { 109 | valueManager.value = value 110 | } 111 | } 112 | } 113 | 114 | public fun Fragment.androidValueManager( 115 | initialValue: T, 116 | stateRestorationKey: String, 117 | saver: AndroidValueManagerStateSaver? = null, 118 | validators: List>? = null, 119 | changeHandler: ChangeAction? = null, 120 | errorHandler: ErrorAction? = null, 121 | ): Lazy> = ownerValueManager( 122 | initialValue = initialValue, 123 | stateRestorationKey = stateRestorationKey, 124 | saver = saver, 125 | validators = validators, 126 | changeHandler = changeHandler, 127 | errorHandler = errorHandler, 128 | ) 129 | 130 | public fun Fragment.androidValueManager( 131 | initialValue: T, 132 | saver: AndroidValueManagerStateSaver? = null, 133 | validators: List>? = null, 134 | changeHandler: ChangeAction? = null, 135 | errorHandler: ErrorAction? = null, 136 | ): AndroidValueManager = PropertyDelegateProvider { _, property -> 137 | object : ReadWriteProperty { 138 | val valueManager by ownerValueManager( 139 | initialValue = initialValue, 140 | stateRestorationKey = property.name, 141 | saver = saver, 142 | validators = validators, 143 | changeHandler = changeHandler, 144 | errorHandler = errorHandler, 145 | ) 146 | 147 | override fun getValue(thisRef: Any?, property: KProperty<*>): T = valueManager.value 148 | 149 | override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { 150 | valueManager.value = value 151 | } 152 | } 153 | } 154 | 155 | private fun ViewModelStoreOwner.ownerValueManager( 156 | initialValue: T, 157 | stateRestorationKey: String, 158 | saver: AndroidValueManagerStateSaver? = null, 159 | validators: List>? = null, 160 | changeHandler: ChangeAction? = null, 161 | errorHandler: ErrorAction? = null, 162 | ): Lazy> = lazy { 163 | val valueManager = basicValueManager( 164 | initialValue = initialValue, 165 | validators = validators, 166 | changeHandler = changeHandler, 167 | errorHandler = errorHandler, 168 | ) 169 | val key = stateRestorationKey.compoundKey() 170 | val provider = ViewModelProvider( 171 | owner = this, 172 | factory = AndroidValueManagerStateFactory( 173 | stateRestorationKey = key, 174 | valueManager = valueManager, 175 | saver = saver, 176 | ) 177 | ) 178 | val state = provider[key, AndroidValueManagerState::class.java] 179 | @Suppress("UNCHECKED_CAST") 180 | state as AndroidValueManagerState 181 | state.valueManager 182 | } 183 | -------------------------------------------------------------------------------- /core/android/src/dev/programadorthi/state/core/AndroidValueManagerState.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.core 2 | 3 | import android.os.Bundle 4 | import androidx.lifecycle.SavedStateHandle 5 | import androidx.lifecycle.ViewModel 6 | 7 | internal class AndroidValueManagerState( 8 | val valueManager: BaseValueManager, 9 | stateRestorationKey: String, 10 | savedStateHandle: SavedStateHandle, 11 | saver: AndroidValueManagerStateSaver?, 12 | ) : ViewModel() { 13 | 14 | init { 15 | val restored: T? = when (saver) { 16 | null -> savedStateHandle[stateRestorationKey] 17 | else -> savedStateHandle.get(stateRestorationKey)?.let { bundle -> 18 | saver.restore(bundle) 19 | } 20 | } 21 | 22 | if (restored != null) { 23 | valueManager.value = restored 24 | } 25 | 26 | if (saver == null) { 27 | valueManager.collect { value -> 28 | savedStateHandle[stateRestorationKey] = value 29 | } 30 | } else { 31 | savedStateHandle.setSavedStateProvider(stateRestorationKey) { 32 | saver.save(valueManager.value) 33 | } 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /core/android/src/dev/programadorthi/state/core/AndroidValueManagerStateFactory.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.core 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import androidx.lifecycle.createSavedStateHandle 6 | import androidx.lifecycle.viewmodel.CreationExtras 7 | 8 | internal class AndroidValueManagerStateFactory( 9 | private val valueManager: BaseValueManager, 10 | private val stateRestorationKey: String, 11 | private val saver: AndroidValueManagerStateSaver?, 12 | ) : ViewModelProvider.Factory { 13 | 14 | @Suppress("UNCHECKED_CAST") 15 | override fun create(modelClass: Class, extras: CreationExtras): T { 16 | return AndroidValueManagerState( 17 | valueManager = valueManager, 18 | stateRestorationKey = stateRestorationKey, 19 | savedStateHandle = extras.createSavedStateHandle(), 20 | saver = saver, 21 | ) as T 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /core/android/src/dev/programadorthi/state/core/AndroidValueManagerStateSaver.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.core 2 | 3 | import android.os.Bundle 4 | 5 | public interface AndroidValueManagerStateSaver { 6 | 7 | public fun save(value: Original): Bundle 8 | 9 | public fun restore(bundle: Bundle): Original? 10 | } 11 | 12 | public fun AndroidValueManagerStateSaver( 13 | save: (value: Original) -> Bundle, 14 | restore: (value: Bundle) -> Original? 15 | ): AndroidValueManagerStateSaver { 16 | return object : AndroidValueManagerStateSaver { 17 | override fun save(value: Original) = save.invoke(value) 18 | 19 | override fun restore(bundle: Bundle) = restore.invoke(bundle) 20 | } 21 | } -------------------------------------------------------------------------------- /core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("multiplatform") 3 | id("com.android.library") 4 | id("com.vanniktech.maven.publish") 5 | } 6 | 7 | applyBasicSetup() 8 | 9 | darwinTargetsFramework() 10 | 11 | kotlin { 12 | androidTarget { 13 | publishLibraryVariants("release") 14 | } 15 | 16 | sourceSets { 17 | androidMain { 18 | dependencies { 19 | implementation("androidx.activity:activity-ktx:1.8.2") 20 | implementation("androidx.fragment:fragment-ktx:1.6.2") 21 | } 22 | } 23 | } 24 | } 25 | 26 | android { 27 | namespace = "dev.programadorthi.state.core" 28 | compileSdk = 34 29 | 30 | defaultConfig { 31 | minSdk = 23 32 | } 33 | 34 | compileOptions { 35 | sourceCompatibility = JavaVersion.VERSION_1_8 36 | targetCompatibility = JavaVersion.VERSION_1_8 37 | } 38 | 39 | buildTypes { 40 | getByName("release") { 41 | isMinifyEnabled = false 42 | } 43 | } 44 | 45 | packaging { 46 | resources { 47 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 48 | } 49 | } 50 | 51 | sourceSets["main"].manifest.srcFile("src/AndroidManifest.xml") 52 | sourceSets["main"].res.srcDirs("src/res") 53 | } -------------------------------------------------------------------------------- /core/common/src/dev/programadorthi/state/core/BaseValueManager.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.core 2 | 3 | import dev.programadorthi.state.core.action.ChangeAction 4 | import dev.programadorthi.state.core.action.CollectAction 5 | import dev.programadorthi.state.core.action.ErrorAction 6 | import dev.programadorthi.state.core.action.UpdateAction 7 | import dev.programadorthi.state.core.validation.Validator 8 | import dev.programadorthi.state.core.validation.ValidatorAction 9 | import dev.programadorthi.state.core.validation.ValidatorManager 10 | 11 | public abstract class BaseValueManager( 12 | initialValue: T 13 | ) : ValueManager, ValidatorManager { 14 | 15 | private val changeActions = mutableListOf>() 16 | private val collectorActions = mutableListOf>() 17 | private val errorActions = mutableListOf() 18 | private val validatorActions = mutableListOf() 19 | private val validators = mutableListOf>() 20 | private val localMessages = mutableListOf() 21 | 22 | private var opened: Boolean = true 23 | private var valid = true 24 | 25 | override val closed: Boolean 26 | get() = !opened 27 | 28 | override val isValid: Boolean 29 | get() = valid 30 | 31 | override val messages: List 32 | get() = localMessages.toList() 33 | 34 | override var value: T = initialValue 35 | set(value) { 36 | check(!closed) { 37 | "Manager is closed and can't update the value" 38 | } 39 | val previous = field 40 | if (previous == value) return 41 | field = value 42 | runCatching { 43 | notifyChanged(previous = previous, next = field) 44 | }.onFailure(::notifyError) 45 | runCatching { 46 | notifyCollector(field) 47 | }.onFailure(::notifyError) 48 | } 49 | 50 | override fun equals(other: Any?): Boolean = 51 | other is ValueManager<*> && other.value == value 52 | 53 | override fun hashCode(): Int = value?.hashCode() ?: 0 54 | 55 | override fun component1(): T = value 56 | 57 | override fun component2(): (T) -> Unit = { value = it } 58 | 59 | override fun close() { 60 | opened = false 61 | changeActions.clear() 62 | collectorActions.clear() 63 | errorActions.clear() 64 | validators.clear() 65 | localMessages.clear() 66 | } 67 | 68 | override fun collect(action: CollectAction) { 69 | collectorActions += action 70 | } 71 | 72 | override fun addValidator(validator: Validator) { 73 | validators += validator 74 | } 75 | 76 | override fun removeValidator(validator: Validator) { 77 | validators -= validator 78 | } 79 | 80 | override fun update(action: UpdateAction) { 81 | runCatching { 82 | val previous = value 83 | val newValue = action(previous) 84 | if (previous == newValue) { 85 | return 86 | } 87 | validate(newValue) 88 | newValue 89 | }.onFailure(::notifyError) 90 | .onSuccess { newValue -> 91 | if (isValid) { 92 | value = newValue 93 | } 94 | } 95 | } 96 | 97 | override fun validate(): Boolean { 98 | runCatching { 99 | validate(value) 100 | }.onFailure(::notifyError) 101 | return isValid 102 | } 103 | 104 | override fun onChanged(action: ChangeAction) { 105 | changeActions += action 106 | } 107 | 108 | override fun onError(action: ErrorAction) { 109 | errorActions += action 110 | } 111 | 112 | override fun onValidated(action: ValidatorAction) { 113 | validatorActions += action 114 | } 115 | 116 | private fun notifyChanged(previous: T, next: T) { 117 | changeActions.forEach { action -> action(previous, next) } 118 | } 119 | 120 | private fun notifyCollector(value: T) { 121 | collectorActions.forEach { action -> action(value) } 122 | } 123 | 124 | private fun notifyError(throwable: Throwable) { 125 | errorActions.forEach { action -> action(throwable) } 126 | } 127 | 128 | private fun notifyValidator(messages: List) { 129 | validatorActions.forEach { action -> action(messages) } 130 | } 131 | 132 | private fun validate(value: T) { 133 | val mappedMessages = validators 134 | .filter { validator -> validator.isValid(value).not() } 135 | .map { validator -> validator.message(value) } 136 | localMessages.clear() 137 | localMessages.addAll(mappedMessages) 138 | valid = mappedMessages.isEmpty() 139 | notifyValidator(localMessages.toList()) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /core/common/src/dev/programadorthi/state/core/BasicValueManager.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.core 2 | 3 | internal class BasicValueManager(initialValue: T) : BaseValueManager(initialValue) -------------------------------------------------------------------------------- /core/common/src/dev/programadorthi/state/core/ValueManager.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.core 2 | 3 | import dev.programadorthi.state.core.action.ChangeAction 4 | import dev.programadorthi.state.core.action.CollectAction 5 | import dev.programadorthi.state.core.action.ErrorAction 6 | import dev.programadorthi.state.core.action.UpdateAction 7 | 8 | @OptIn(ExperimentalStdlibApi::class) 9 | public interface ValueManager : AutoCloseable { 10 | public val closed: Boolean 11 | 12 | public var value: T 13 | 14 | public operator fun component1(): T 15 | 16 | public operator fun component2(): (T) -> Unit 17 | 18 | public fun collect(action: CollectAction) 19 | 20 | public fun update(action: UpdateAction) 21 | 22 | public fun onChanged(action: ChangeAction) 23 | 24 | public fun onError(action: ErrorAction) 25 | } 26 | -------------------------------------------------------------------------------- /core/common/src/dev/programadorthi/state/core/action/ChangeAction.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.core.action 2 | 3 | public typealias ChangeAction = (previous: T, next: T) -> Unit -------------------------------------------------------------------------------- /core/common/src/dev/programadorthi/state/core/action/CollectAction.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.core.action 2 | 3 | public typealias CollectAction = (value: T) -> Unit -------------------------------------------------------------------------------- /core/common/src/dev/programadorthi/state/core/action/ErrorAction.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.core.action 2 | 3 | public typealias ErrorAction = (exception: Throwable) -> Unit -------------------------------------------------------------------------------- /core/common/src/dev/programadorthi/state/core/action/UpdateAction.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.core.action 2 | 3 | public typealias UpdateAction = (value: T) -> T -------------------------------------------------------------------------------- /core/common/src/dev/programadorthi/state/core/extension/ValidatorManagerDelegate.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.core.extension 2 | 3 | import dev.programadorthi.state.core.validation.Validator 4 | import dev.programadorthi.state.core.validation.ValidatorManager 5 | 6 | public operator fun ValidatorManager.minusAssign(element: Validator) { 7 | removeValidator(element) 8 | } 9 | 10 | public operator fun ValidatorManager.minusAssign(elements: Iterable>) { 11 | elements.forEach { validator -> removeValidator(validator) } 12 | } 13 | 14 | public operator fun ValidatorManager.plusAssign(element: Validator) { 15 | addValidator(element) 16 | } 17 | 18 | public operator fun ValidatorManager.plusAssign(elements: Iterable>) { 19 | elements.forEach { validator -> addValidator(validator) } 20 | } -------------------------------------------------------------------------------- /core/common/src/dev/programadorthi/state/core/extension/ValueManagerCreator.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.core.extension 2 | 3 | import dev.programadorthi.state.core.BaseValueManager 4 | import dev.programadorthi.state.core.BasicValueManager 5 | import dev.programadorthi.state.core.action.ChangeAction 6 | import dev.programadorthi.state.core.action.ErrorAction 7 | import dev.programadorthi.state.core.validation.Validator 8 | 9 | public fun basicValueManager( 10 | initialValue: T, 11 | validators: List>? = null, 12 | changeHandler: ChangeAction? = null, 13 | errorHandler: ErrorAction? = null, 14 | ): BaseValueManager = BasicValueManager(initialValue = initialValue).apply { 15 | validators?.forEach { addValidator(it) } 16 | changeHandler?.let { onChanged(it) } 17 | errorHandler?.let { onError(it) } 18 | } 19 | -------------------------------------------------------------------------------- /core/common/src/dev/programadorthi/state/core/extension/ValueManagerDelegate.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.core.extension 2 | 3 | import dev.programadorthi.state.core.ValueManager 4 | import kotlin.reflect.KProperty 5 | 6 | public operator fun ValueManager.getValue(thisObj: Any?, property: KProperty<*>): T = value 7 | 8 | public operator fun ValueManager.setValue( 9 | thisObj: Any?, 10 | property: KProperty<*>, 11 | value: T, 12 | ) { 13 | this.value = value 14 | } 15 | -------------------------------------------------------------------------------- /core/common/src/dev/programadorthi/state/core/validation/Validator.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.core.validation 2 | 3 | public interface Validator { 4 | public val message: (T) -> String 5 | 6 | public fun isValid(value: T): Boolean 7 | } -------------------------------------------------------------------------------- /core/common/src/dev/programadorthi/state/core/validation/ValidatorAction.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.core.validation 2 | 3 | public typealias ValidatorAction = (messages: List) -> Unit -------------------------------------------------------------------------------- /core/common/src/dev/programadorthi/state/core/validation/ValidatorManager.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.core.validation 2 | 3 | public interface ValidatorManager { 4 | 5 | public val isValid: Boolean 6 | 7 | public val messages: List 8 | 9 | public fun addValidator(validator: Validator) 10 | 11 | public fun removeValidator(validator: Validator) 12 | 13 | public fun validate(): Boolean 14 | 15 | public fun onValidated(action: ValidatorAction) 16 | } -------------------------------------------------------------------------------- /core/common/test/dev/programadorthi/state/core/ValueManagerTest.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.core 2 | 3 | import dev.programadorthi.state.core.extension.basicValueManager 4 | import dev.programadorthi.state.core.extension.getValue 5 | import dev.programadorthi.state.core.extension.setValue 6 | import dev.programadorthi.state.core.validation.Validator 7 | import kotlin.random.Random 8 | import kotlin.test.Test 9 | import kotlin.test.assertContentEquals 10 | import kotlin.test.assertEquals 11 | import kotlin.test.assertFails 12 | import kotlin.test.assertFalse 13 | import kotlin.test.assertIs 14 | import kotlin.test.assertTrue 15 | 16 | @OptIn(ExperimentalStdlibApi::class) 17 | internal class ValueManagerTest { 18 | @Test 19 | fun shouldCurrentValueBeEqualsToInitialValue() { 20 | val manager = basicValueManager(0) 21 | assertEquals(0, manager.value, "Current value is not equals to initial value") 22 | } 23 | 24 | @Test 25 | fun shouldCurrentValueBeEqualsToInitialValue_WhenUsingDelegateProperty() { 26 | val value by basicValueManager(0) 27 | assertEquals(0, value, "Current value is not equals to initial value") 28 | } 29 | 30 | @Test 31 | fun shouldChangeCurrentValue_WhenCallUpdate() { 32 | val manager = basicValueManager(0) 33 | manager.update { value -> 34 | value + 1 35 | } 36 | assertEquals(1, manager.value, "Call to update function is not updating current value") 37 | } 38 | 39 | @Test 40 | fun shouldChangeCurrentValue_WhenCallUpdateUsingDelegateProperty() { 41 | var value by basicValueManager(0) 42 | value++ // or value = value + 1 43 | assertEquals(1, value, "Updating by delegate property is not updating current value") 44 | } 45 | 46 | @Test 47 | fun shouldNotInitiateClosed() { 48 | val manager = basicValueManager(0) 49 | assertEquals(false, manager.closed, "Value manager has started in closed state") 50 | } 51 | 52 | @Test 53 | fun shouldCloseAfterRequestedToClose() { 54 | val manager = basicValueManager(0) 55 | manager.close() 56 | assertEquals(true, manager.closed, "Value manager still opened after request to close") 57 | } 58 | 59 | @Test 60 | fun shouldCollectAllEmittedValue() { 61 | val expected = listOf(1, 2, 3, 4, 5) 62 | val result = mutableListOf() 63 | 64 | val manager = basicValueManager(0) 65 | manager.collect(result::add) 66 | repeat(times = 5) { 67 | manager.update { value -> 68 | value + 1 69 | } 70 | } 71 | 72 | assertContentEquals( 73 | expected, 74 | result, 75 | "Collect function is not collecting all updated values" 76 | ) 77 | } 78 | 79 | @Test 80 | fun shouldNotUpdateValueAfterRequestedToClose() { 81 | val manager = basicValueManager(initialValue = 0) 82 | manager.close() 83 | 84 | val exception = assertFails { 85 | manager.update { value -> 86 | value + 1 87 | } 88 | } 89 | 90 | assertIs( 91 | exception, 92 | "Update value after closed is not a IllegalStateException" 93 | ) 94 | assertEquals( 95 | "Manager is closed and can't update the value", 96 | exception.message, 97 | "Missing exception on update value after close manager" 98 | ) 99 | } 100 | 101 | @Test 102 | fun shouldUpdateValueAndClose() { 103 | val manager = basicValueManager(initialValue = 0) 104 | 105 | manager.use { 106 | it.update { value -> 107 | value + 1 108 | } 109 | } 110 | 111 | assertEquals(1, manager.value, "Value should be updated before close") 112 | assertTrue(manager.closed, "Manager should be closed") 113 | } 114 | 115 | @Test 116 | fun shouldCallErrorHandler_WhenErrorHappens() { 117 | val random = Random.Default 118 | val expected = mutableListOf() 119 | val exceptions = mutableListOf() 120 | val manager = basicValueManager( 121 | initialValue = 0, 122 | errorHandler = { 123 | exceptions.add(it) 124 | }, 125 | ) 126 | 127 | repeat(times = 10) { 128 | if (random.nextBoolean()) { 129 | val ex = Exception("Exception number $it") 130 | expected += ex 131 | manager.update { 132 | throw ex 133 | } 134 | } 135 | } 136 | 137 | assertEquals(0, manager.value, "Value would be not updated when crashing") 138 | assertEquals( 139 | expected.size, 140 | exceptions.size, 141 | "Missing exceptions on update value crashing always" 142 | ) 143 | assertContentEquals( 144 | expected, 145 | exceptions, 146 | "Missing exceptions on update value crashing always" 147 | ) 148 | } 149 | 150 | @Test 151 | fun shouldCallLifecycleHandler_WhenUpdatingValue() { 152 | val expected = listOf( 153 | 0 to 1, 154 | 1 to 2, 155 | 2 to 1, 156 | ) 157 | val events = mutableListOf>() 158 | 159 | var value by basicValueManager( 160 | initialValue = 0, 161 | changeHandler = { previous, next -> 162 | events += previous to next 163 | }, 164 | ) 165 | 166 | value += 1 167 | value += 1 168 | value -= 1 169 | 170 | assertContentEquals( 171 | expected, 172 | events, 173 | "Lifecycle events was ignored in the update value flow" 174 | ) 175 | } 176 | 177 | @Test 178 | fun shouldNotUpdateTheValue_WhenHaveInvalidValue() { 179 | val manager = basicValueManager(0) 180 | manager.addValidator(object : Validator { 181 | override val message: (Int) -> String = { "Value $it should be positive" } 182 | 183 | override fun isValid(value: Int): Boolean = value > 0 184 | }) 185 | manager.update { value -> 186 | value - 1 187 | } 188 | assertFalse(manager.isValid, "Value {${manager.value}} should be invalid") 189 | assertEquals("Value -1 should be positive", manager.messages.first()) 190 | assertEquals(0, manager.value, "Value should be equals to initial value") 191 | } 192 | } -------------------------------------------------------------------------------- /core/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=Core 2 | POM_ARTIFACT_ID=core -------------------------------------------------------------------------------- /coroutines/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("multiplatform") 3 | id("com.vanniktech.maven.publish") 4 | } 5 | 6 | applyBasicSetup() 7 | 8 | darwinTargetsFramework() 9 | 10 | kotlin { 11 | sourceSets { 12 | val commonMain by getting { 13 | dependencies { 14 | api(project(":core")) 15 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") 16 | } 17 | } 18 | val commonTest by getting { 19 | dependencies { 20 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0") 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /coroutines/common/src/dev/programadorthi/state/coroutines/ValidatorManagerFlow.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.coroutines 2 | 3 | import dev.programadorthi.state.core.validation.ValidatorManager 4 | import kotlinx.coroutines.flow.FlowCollector 5 | import kotlinx.coroutines.flow.MutableStateFlow 6 | import kotlinx.coroutines.flow.StateFlow 7 | 8 | internal class ValidatorManagerFlow( 9 | validatorManager: ValidatorManager 10 | ) : StateFlow { 11 | 12 | private val stateFlow = MutableStateFlow( 13 | ValidatorResult( 14 | isValid = validatorManager.isValid, 15 | messages = validatorManager.messages, 16 | ) 17 | ) 18 | 19 | init { 20 | validatorManager.onValidated { messages -> 21 | stateFlow.tryEmit( 22 | ValidatorResult( 23 | isValid = messages.isEmpty(), 24 | messages = messages, 25 | ) 26 | ) 27 | } 28 | } 29 | 30 | override val replayCache: List 31 | get() = stateFlow.replayCache 32 | 33 | override val value: ValidatorResult 34 | get() = stateFlow.value 35 | 36 | override suspend fun collect(collector: FlowCollector): Nothing { 37 | stateFlow.collect(collector) 38 | } 39 | } -------------------------------------------------------------------------------- /coroutines/common/src/dev/programadorthi/state/coroutines/ValidatorResult.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.coroutines 2 | 3 | public data class ValidatorResult( 4 | val isValid: Boolean, 5 | val messages: List, 6 | ) 7 | -------------------------------------------------------------------------------- /coroutines/common/src/dev/programadorthi/state/coroutines/ValueManagerAsFlow.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.coroutines 2 | 3 | import dev.programadorthi.state.core.ValueManager 4 | import dev.programadorthi.state.core.validation.ValidatorManager 5 | import kotlinx.coroutines.flow.MutableStateFlow 6 | import kotlinx.coroutines.flow.StateFlow 7 | import kotlin.reflect.KProperty 8 | 9 | public operator fun MutableStateFlow.getValue(thisObj: Any?, property: KProperty<*>): T = 10 | value 11 | 12 | public operator fun MutableStateFlow.setValue( 13 | thisObj: Any?, 14 | property: KProperty<*>, 15 | value: T, 16 | ) { 17 | tryEmit(value) 18 | } 19 | 20 | public fun ValueManager.asMutableStateFlow(): MutableStateFlow = ValueManagerFlow(this) 21 | 22 | public fun ValidatorManager.asStateFlow(): StateFlow = 23 | ValidatorManagerFlow(this) 24 | -------------------------------------------------------------------------------- /coroutines/common/src/dev/programadorthi/state/coroutines/ValueManagerFlow.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.coroutines 2 | 3 | import dev.programadorthi.state.core.ValueManager 4 | import kotlinx.coroutines.ExperimentalCoroutinesApi 5 | import kotlinx.coroutines.flow.FlowCollector 6 | import kotlinx.coroutines.flow.MutableStateFlow 7 | import kotlinx.coroutines.flow.StateFlow 8 | 9 | internal class ValueManagerFlow( 10 | private val valueManager: ValueManager 11 | ) : MutableStateFlow { 12 | 13 | private val stateFlow = MutableStateFlow(valueManager.value) 14 | 15 | init { 16 | valueManager.collect { newValue -> 17 | stateFlow.tryEmit(newValue) 18 | } 19 | } 20 | 21 | override val replayCache: List 22 | get() = stateFlow.replayCache 23 | 24 | override val subscriptionCount: StateFlow 25 | get() = stateFlow.subscriptionCount 26 | 27 | override var value: T 28 | get() = stateFlow.value 29 | set(value) { 30 | valueManager.value = value 31 | } 32 | 33 | override suspend fun collect(collector: FlowCollector): Nothing { 34 | stateFlow.collect(collector) 35 | } 36 | 37 | override fun compareAndSet(expect: T, update: T): Boolean { 38 | if (stateFlow.value != expect) { 39 | return false 40 | } 41 | value = update 42 | return true 43 | } 44 | 45 | override suspend fun emit(value: T) { 46 | this.value = value 47 | } 48 | 49 | @OptIn(ExperimentalCoroutinesApi::class) 50 | override fun resetReplayCache() { 51 | stateFlow.resetReplayCache() 52 | } 53 | 54 | override fun tryEmit(value: T): Boolean { 55 | this.value = value 56 | return true 57 | } 58 | } -------------------------------------------------------------------------------- /coroutines/common/test/dev/programadorthi/state/coroutines/FlowValueManagerTest.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.state.coroutines 2 | 3 | import dev.programadorthi.state.core.extension.basicValueManager 4 | import kotlinx.coroutines.ExperimentalCoroutinesApi 5 | import kotlinx.coroutines.Job 6 | import kotlinx.coroutines.cancelAndJoin 7 | import kotlinx.coroutines.flow.toList 8 | import kotlinx.coroutines.launch 9 | import kotlinx.coroutines.test.advanceTimeBy 10 | import kotlinx.coroutines.test.runTest 11 | import kotlin.test.Test 12 | import kotlin.test.assertContentEquals 13 | import kotlin.test.assertEquals 14 | import kotlin.test.assertFails 15 | import kotlin.test.assertIs 16 | 17 | @OptIn(ExperimentalCoroutinesApi::class) 18 | class FlowValueManagerTest { 19 | 20 | @Test 21 | fun shouldCurrentValueBeEqualsToInitialValue() = runTest { 22 | val manager = basicValueManager(0).asMutableStateFlow() 23 | assertEquals(0, manager.value, "Current value is not equals to initial value") 24 | } 25 | 26 | @Test 27 | fun shouldCurrentValueBeEqualsToInitialValue_WhenUsingDelegateProperty() = runTest { 28 | val value by basicValueManager(0).asMutableStateFlow() 29 | assertEquals(0, value, "Current value is not equals to initial value") 30 | } 31 | 32 | @Test 33 | fun shouldChangeCurrentValue_WhenCallUpdate() = runTest { 34 | val manager = basicValueManager(0).asMutableStateFlow() 35 | manager.value += 1 36 | assertEquals(1, manager.value, "Call to update function is not updating current value") 37 | } 38 | 39 | @Test 40 | fun shouldChangeCurrentValue_WhenCallUpdateUsingDelegateProperty() = runTest { 41 | var value by basicValueManager(0).asMutableStateFlow() 42 | value++ // or value = value + 1 43 | assertEquals(1, value, "Updating by delegate property is not updating current value") 44 | } 45 | 46 | @Test 47 | fun shouldCollectAllEmittedValue_WhenCollectIsNotSuspend() = runTest { 48 | val expected = listOf(1, 2, 3, 4, 5) 49 | val result = mutableListOf() 50 | 51 | val manager = basicValueManager(0).asMutableStateFlow() 52 | val job = launch(coroutineContext + Job()) { 53 | manager.collect(result::add) 54 | } 55 | repeat(times = 5) { 56 | manager.value += 1 57 | advanceTimeBy(500) 58 | } 59 | 60 | assertContentEquals( 61 | expected, 62 | result, 63 | "Collect function is not collecting all updated values" 64 | ) 65 | job.cancelAndJoin() 66 | } 67 | 68 | @Test 69 | fun shouldCollectAllEmittedValue_WhenCollectIsSuspend() = runTest { 70 | val expected = listOf(1, 2, 3, 4, 5) 71 | val result = mutableListOf() 72 | 73 | val manager = basicValueManager(0).asMutableStateFlow() 74 | val job = launch { 75 | manager.toList(result) 76 | } 77 | 78 | repeat(times = 5) { 79 | manager.value += 1 80 | advanceTimeBy(500) 81 | } 82 | 83 | job.cancelAndJoin() 84 | 85 | assertContentEquals( 86 | expected, 87 | result, 88 | "Collect function is not collecting all updated values" 89 | ) 90 | } 91 | 92 | @Test 93 | fun shouldNotUpdateValueAfterRequestedToClose() = runTest { 94 | val manager = basicValueManager(0) 95 | manager.close() 96 | val state = manager.asMutableStateFlow() 97 | 98 | val exception = assertFails { 99 | state.value += 1 100 | } 101 | 102 | assertIs( 103 | exception, 104 | "Update value after closed is not a IllegalStateException" 105 | ) 106 | assertEquals( 107 | "Manager is closed and can't update the value", 108 | exception.message, 109 | "Missing exception on update value after close manager" 110 | ) 111 | } 112 | 113 | } -------------------------------------------------------------------------------- /coroutines/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=Coroutines 2 | POM_ARTIFACT_ID=coroutines -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | android.useAndroidX=true 2 | kotlin.code.style=official 3 | kotlin.mpp.applyDefaultHierarchyTemplate=false 4 | 5 | # Maven 6 | RELEASE_SIGNING_ENABLED=true 7 | 8 | GROUP=dev.programadorthi.state 9 | 10 | POM_NAME=Kotlin State Manager 11 | POM_DESCRIPTION=A multiplatform and extensible kotlin value manager 12 | POM_INCEPTION_YEAR=2022 13 | POM_URL=https://github.com/programadorthi/kotlin-state-manager 14 | 15 | POM_LICENSE_NAME=Apache License Version 2.0 16 | POM_LICENSE_URL=https://www.apache.org/licenses/LICENSE-2.0 17 | POM_LICENSE_DIST=https://www.apache.org/licenses/LICENSE-2.0 18 | 19 | POM_SCM_URL=https://github.com/programadorthi/kotlin-state-manager 20 | POM_SCM_CONNECTION=scm:git:ssh://git@github.com/programadorthi/kotlin-state-manager.git 21 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/programadorthi/kotlin-state-manager.git 22 | 23 | POM_DEVELOPER_ID=programadorthi 24 | POM_DEVELOPER_NAME=Thiago Santos 25 | POM_DEVELOPER_URL=https://programadorthi.dev 26 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programadorthi/kotlin-state-manager/fc407d93fd193dca6978513eeef072c00ae5cef1/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programadorthi/kotlin-state-manager/fc407d93fd193dca6978513eeef072c00ae5cef1/gradlew -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /samples/compose/norris-facts/android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("org.jetbrains.compose") 4 | kotlin("android") 5 | } 6 | 7 | android { 8 | namespace = "dev.programadorthi.android" 9 | compileSdk = 34 10 | 11 | defaultConfig { 12 | applicationId = "dev.programadorthi.android" 13 | minSdk = 24 14 | versionCode = 1 15 | versionName = "1.0" 16 | } 17 | compileOptions { 18 | sourceCompatibility = JavaVersion.VERSION_11 19 | targetCompatibility = JavaVersion.VERSION_11 20 | } 21 | kotlinOptions { 22 | jvmTarget = JavaVersion.VERSION_11.toString() 23 | } 24 | buildTypes { 25 | getByName("release") { 26 | isMinifyEnabled = false 27 | } 28 | } 29 | } 30 | 31 | dependencies { 32 | implementation(project(":samples:compose:norris-facts:common")) 33 | implementation("androidx.activity:activity-compose:1.8.2") 34 | implementation("androidx.appcompat:appcompat:1.6.1") 35 | implementation("androidx.core:core-ktx:1.12.0") 36 | implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1") 37 | } -------------------------------------------------------------------------------- /samples/compose/norris-facts/android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /samples/compose/norris-facts/android/src/main/java/dev/programadorthi/android/ComposeActivity.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.android 2 | 3 | import android.os.Bundle 4 | import androidx.activity.compose.setContent 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.compose.foundation.layout.Arrangement 7 | import androidx.compose.foundation.layout.Column 8 | import androidx.compose.foundation.layout.Spacer 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.foundation.layout.height 11 | import androidx.compose.material.Button 12 | import androidx.compose.material.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.unit.dp 17 | import androidx.lifecycle.viewmodel.compose.viewModel 18 | 19 | class ComposeActivity : AppCompatActivity() { 20 | 21 | override fun onCreate(savedInstanceState: Bundle?) { 22 | super.onCreate(savedInstanceState) 23 | setContent { 24 | Content() 25 | } 26 | } 27 | 28 | @Composable 29 | private fun Content( 30 | viewModel: ComposeViewModel = viewModel() 31 | ) { 32 | Column( 33 | modifier = Modifier.fillMaxSize(), 34 | verticalArrangement = Arrangement.Center, 35 | horizontalAlignment = Alignment.CenterHorizontally, 36 | ) { 37 | Text("Counter: ${viewModel.state}") 38 | Spacer(modifier = Modifier.height(16.dp)) 39 | Button(onClick = viewModel::decrement) { 40 | Text("Decrement") 41 | } 42 | Spacer(modifier = Modifier.height(16.dp)) 43 | Button(onClick = viewModel::increment) { 44 | Text("Increment") 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /samples/compose/norris-facts/android/src/main/java/dev/programadorthi/android/ComposeViewModel.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.android 2 | 3 | import androidx.lifecycle.SavedStateHandle 4 | import androidx.lifecycle.ViewModel 5 | import dev.programadorthi.state.compose.composeValueManager 6 | 7 | class ComposeViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { 8 | 9 | private var composeState by composeValueManager(0, savedStateHandle) 10 | 11 | val state: Int 12 | get() = composeState 13 | 14 | fun increment() { 15 | composeState++ 16 | } 17 | 18 | fun decrement() { 19 | composeState-- 20 | } 21 | } -------------------------------------------------------------------------------- /samples/compose/norris-facts/android/src/main/java/dev/programadorthi/android/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.android 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.widget.Button 6 | import android.widget.TextView 7 | import androidx.activity.viewModels 8 | import androidx.appcompat.app.AppCompatActivity 9 | import dev.programadorthi.state.core.androidValueManager 10 | 11 | class MainActivity : AppCompatActivity() { 12 | private val model: MainViewModel by viewModels() 13 | 14 | private var year by androidValueManager(2024) 15 | 16 | override fun onCreate(savedInstanceState: Bundle?) { 17 | super.onCreate(savedInstanceState) 18 | /*setContent { 19 | MaterialTheme { 20 | App() 21 | } 22 | }*/ 23 | 24 | setContentView(R.layout.activity_main) 25 | 26 | val textView = findViewById(R.id.nameText) 27 | val decrementButton = findViewById