From cd679f0a3c90c05e7928d76e1ca50c3f1bbcf1b8 Mon Sep 17 00:00:00 2001 From: tabidachinokaze Date: Thu, 2 Oct 2025 16:55:08 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20compose-mvi=20?= =?UTF-8?q?=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- compose-mvi/.gitignore | 1 + compose-mvi/build.gradle.kts | 49 +++++++++++++++++++ compose-mvi/consumer-rules.pro | 0 compose-mvi/proguard-rules.pro | 21 ++++++++ .../compose/mvi/ExampleInstrumentedTest.kt | 22 +++++++++ compose-mvi/src/main/AndroidManifest.xml | 4 ++ .../tabidachi/compose/mvi/BaseViewModel.kt | 41 ++++++++++++++++ .../compose/mvi/UnidirectionalViewModel.kt | 29 +++++++++++ .../tabidachi/compose/mvi/ExampleUnitTest.kt | 16 ++++++ settings.gradle.kts | 1 + 10 files changed, 184 insertions(+) create mode 100644 compose-mvi/.gitignore create mode 100644 compose-mvi/build.gradle.kts create mode 100644 compose-mvi/consumer-rules.pro create mode 100644 compose-mvi/proguard-rules.pro create mode 100644 compose-mvi/src/androidTest/java/moe/tabidachi/compose/mvi/ExampleInstrumentedTest.kt create mode 100644 compose-mvi/src/main/AndroidManifest.xml create mode 100644 compose-mvi/src/main/java/moe/tabidachi/compose/mvi/BaseViewModel.kt create mode 100644 compose-mvi/src/main/java/moe/tabidachi/compose/mvi/UnidirectionalViewModel.kt create mode 100644 compose-mvi/src/test/java/moe/tabidachi/compose/mvi/ExampleUnitTest.kt diff --git a/compose-mvi/.gitignore b/compose-mvi/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/compose-mvi/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/compose-mvi/build.gradle.kts b/compose-mvi/build.gradle.kts new file mode 100644 index 0000000..0718606 --- /dev/null +++ b/compose-mvi/build.gradle.kts @@ -0,0 +1,49 @@ +import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "moe.tabidachi.compose.mvi" + compileSdk = 36 + + defaultConfig { + minSdk = 28 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } +} + +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_17 + } + explicitApi = ExplicitApiMode.Strict +} + +dependencies { + api(libs.lifecycle.runtime.compose) + api(libs.lifecycle.viewmodel) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.ext.junit) + androidTestImplementation(libs.espresso.core) +} diff --git a/compose-mvi/consumer-rules.pro b/compose-mvi/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/compose-mvi/proguard-rules.pro b/compose-mvi/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/compose-mvi/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/compose-mvi/src/androidTest/java/moe/tabidachi/compose/mvi/ExampleInstrumentedTest.kt b/compose-mvi/src/androidTest/java/moe/tabidachi/compose/mvi/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..eb6dd89 --- /dev/null +++ b/compose-mvi/src/androidTest/java/moe/tabidachi/compose/mvi/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package moe.tabidachi.compose.mvi + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.* +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("moe.tabidachi.compose.mvi.test", appContext.packageName) + } +} diff --git a/compose-mvi/src/main/AndroidManifest.xml b/compose-mvi/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a5918e6 --- /dev/null +++ b/compose-mvi/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/compose-mvi/src/main/java/moe/tabidachi/compose/mvi/BaseViewModel.kt b/compose-mvi/src/main/java/moe/tabidachi/compose/mvi/BaseViewModel.kt new file mode 100644 index 0000000..20d2c13 --- /dev/null +++ b/compose-mvi/src/main/java/moe/tabidachi/compose/mvi/BaseViewModel.kt @@ -0,0 +1,41 @@ +package moe.tabidachi.compose.mvi + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +public abstract class BaseViewModel( + initialState: STATE +) : ViewModel(), UnidirectionalViewModel { + private val _state: MutableStateFlow = MutableStateFlow(initialState) + override val state: StateFlow = _state.asStateFlow() + + private val _effect: MutableSharedFlow = MutableSharedFlow() + override val effect: SharedFlow = _effect.asSharedFlow() + + private val handledOneTimeEvents: MutableSet = mutableSetOf() + + protected fun updateState(block: (STATE) -> STATE) { + _state.update(block) + } + + protected fun emitEffect(effect: EFFECT) { + viewModelScope.launch { + _effect.emit(effect) + } + } + + protected fun handleOneTimeEvent(event: EVENT, block: () -> Unit) { + if (event !in handledOneTimeEvents) { + handledOneTimeEvents.add(event) + block() + } + } +} diff --git a/compose-mvi/src/main/java/moe/tabidachi/compose/mvi/UnidirectionalViewModel.kt b/compose-mvi/src/main/java/moe/tabidachi/compose/mvi/UnidirectionalViewModel.kt new file mode 100644 index 0000000..92c0726 --- /dev/null +++ b/compose-mvi/src/main/java/moe/tabidachi/compose/mvi/UnidirectionalViewModel.kt @@ -0,0 +1,29 @@ +package moe.tabidachi.compose.mvi + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow + +public interface UnidirectionalViewModel { + public val state: StateFlow + public val effect: SharedFlow + public fun event(event: EVENT) + + @Composable + public operator fun component1(): State = state.collectAsStateWithLifecycle() + public operator fun component2(): (EVENT) -> Unit = ::event +} + +@Composable +public inline fun UnidirectionalViewModel.observe( + crossinline handleEffect: (EFFECT) -> Unit +): UnidirectionalViewModel = apply { + LaunchedEffect(key1 = effect) { + effect.collect { + handleEffect(it) + } + } +} diff --git a/compose-mvi/src/test/java/moe/tabidachi/compose/mvi/ExampleUnitTest.kt b/compose-mvi/src/test/java/moe/tabidachi/compose/mvi/ExampleUnitTest.kt new file mode 100644 index 0000000..1603e85 --- /dev/null +++ b/compose-mvi/src/test/java/moe/tabidachi/compose/mvi/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package moe.tabidachi.compose.mvi + +import org.junit.Assert.* +import org.junit.Test + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 9187e08..93d6feb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,3 +17,4 @@ dependencyResolutionManagement { } rootProject.name = "Electro" include(":app") +include(":compose-mvi")