Skip to content

Commit

Permalink
feat: SelfHostedViewModel adding
Browse files Browse the repository at this point in the history
  • Loading branch information
kramlex committed Nov 24, 2023
1 parent b853256 commit a0eb755
Show file tree
Hide file tree
Showing 20 changed files with 345 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,31 @@ kotlin {
iosArm64()
iosX64()
iosSimulatorArm64()
jvm()

sourceSets {
val commonMain by getting
val commonTest by getting

val iosX64Main by getting
val iosArm64Main by getting
val iosSimulatorArm64Main by getting
val nativeTargets = listOf(
"iosArm32",
"iosArm64",
"iosX64",
"iosSimulatorArm64",
)

val iosMain by creating {
dependsOn(commonMain)
iosX64Main.dependsOn(this)
iosArm64Main.dependsOn(this)
iosSimulatorArm64Main.dependsOn(this)
}
val targetWithoutAndroid = nativeTargets + listOf(
"jvm",
)

val nonAndroidMain by creating
nonAndroidMain.dependsOn(commonMain)

targetWithoutAndroid.mapNotNull { findByName("${it}Main") }
.forEach { it.dependsOn(nonAndroidMain) }

val nonAndroidTest by creating
nonAndroidTest.dependsOn(commonTest)

val iosX64Test by getting
val iosArm64Test by getting
Expand Down
2 changes: 1 addition & 1 deletion compose-annotation/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
plugins {
id("kmm-library-convention")
id("kmp-library-convention")
}

version = libs.versions.mvm.get()
Expand Down
2 changes: 1 addition & 1 deletion core/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
plugins {
id("kmm-library-convention")
id("kmp-library-convention")
}

version = libs.versions.mvm.get()
Expand Down
4 changes: 4 additions & 0 deletions core/src/commonMain/kotlin/app/meetacy/vm/ViewModel.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package app.meetacy.vm

import app.meetacy.vm.extension.launchIn
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch

public expect open class ViewModel() {

Expand Down
5 changes: 3 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ androidGradle = "7.4.2"
androidLifecycleVersion = "2.6.2"
kotlinxCoroutines = "1.7.3"

mvm = "0.0.6"
mvm = "0.0.7"

[libraries]

Expand All @@ -14,9 +14,10 @@ lifecycleKtx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.re
androidViewModel = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidLifecycleVersion" }
composeFoundation = { module = "androidx.compose.foundation:foundation" }
kotlinxCoroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" }
kotlinxCoroutinesTest = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" }

# https://developer.android.com/jetpack/compose/bom/bom-mapping
composeBOM = "androidx.compose:compose-bom:2023.09.00"
composeBOM = "androidx.compose:compose-bom:2023.10.01"

# gradle plugins
kotlinGradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
Expand Down
5 changes: 4 additions & 1 deletion mvi/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
plugins {
id("kmm-library-convention")
id("kmp-library-convention")
}

version = libs.versions.mvm.get()
Expand All @@ -12,4 +12,7 @@ dependencies {
commonMainApi(projects.vm.core)
commonMainApi(projects.vm.composeAnnotation)
androidMainApi(projects.vm.core)

commonTestImplementation(kotlin("test"))
commonTestImplementation(libs.kotlinxCoroutinesTest)
}
15 changes: 15 additions & 0 deletions mvi/src/commonMain/kotlin/app/meetacy/vm/mvi/Intent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package app.meetacy.vm.mvi

import kotlinx.coroutines.flow.Flow

public interface Intent<TState, out TEffect> {

public fun flowOf(state: TState): Flow<Update<TState, TEffect>>

public sealed interface Update<out TState, out TEffect> {

public data class State<TState>(public val state: TState): Update<TState, Nothing>

public data class Effect<TEffect>(public val effect: TEffect): Update<Nothing, TEffect>
}
}
31 changes: 31 additions & 0 deletions mvi/src/commonMain/kotlin/app/meetacy/vm/mvi/IntentBuilder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package app.meetacy.vm.mvi

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.FlowCollector
import kotlin.jvm.JvmName

@DslMarker
public annotation class IntentBuilderDsl

public class IntentBuilder<TState, TEffect>(
initial: TState,
public val scope: CoroutineScope,
private val collector: FlowCollector<Intent.Update<TState, TEffect>>,
) {
private var _state = initial

@IntentBuilderDsl
public val currentState: TState get() = _state

@IntentBuilderDsl
public suspend fun reduce(transform: suspend TState.() -> TState) {
_state = currentState.transform()
collector.emit(Intent.Update.State(currentState))
}

@JvmName("performEffect")
@IntentBuilderDsl
public suspend fun perform(effect: TEffect) {
collector.emit(Intent.Update.Effect(effect))
}
}
24 changes: 24 additions & 0 deletions mvi/src/commonMain/kotlin/app/meetacy/vm/mvi/IntentHost.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package app.meetacy.vm.mvi

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow

public interface IntentHost<TState, TEffect>

@IntentBuilderDsl
public inline fun <TState, TEffect> IntentHost<TState, TEffect>.intent(
crossinline builder: suspend IntentBuilder<TState, TEffect>.() -> Unit
): Intent<TState, TEffect> = buildIntent(builder)

public inline fun <TState, TEffect> buildIntent(
crossinline builder: suspend IntentBuilder<TState, TEffect>.() -> Unit
): Intent<TState, TEffect> = object : Intent<TState, TEffect> {
override fun flowOf(state: TState): Flow<Intent.Update<TState, TEffect>> = channelFlow {
val intent = IntentBuilder(
state,
scope = this,
collector = { this.send(it) }
)
intent.run { builder() }
}
}
6 changes: 3 additions & 3 deletions mvi/src/commonMain/kotlin/app/meetacy/vm/mvi/MviViewModel.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
package app.meetacy.vm.mvi

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.launch
import app.meetacy.vm.ViewModel
import app.meetacy.vm.extension.launchIn
import app.meetacy.vm.flow.CSharedFlow
import app.meetacy.vm.flow.CStateFlow
import app.meetacy.vm.flow.cSharedFlow
import app.meetacy.vm.flow.cStateFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch

public abstract class MviViewModel<State : Any, Action, Event>(initialState: State) : ViewModel() {

Expand Down
14 changes: 14 additions & 0 deletions mvi/src/commonMain/kotlin/app/meetacy/vm/mvi/StateHolder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package app.meetacy.vm.mvi

import app.meetacy.vm.flow.CFlow
import app.meetacy.vm.flow.CStateFlow

public abstract class StateHolder<TState, TEffect> {

public abstract val effects: CFlow<TEffect>
public abstract val states: CStateFlow<TState>

public abstract suspend fun accept(intent: Intent<TState, TEffect>)
public abstract suspend fun accept(effect: TEffect)
public abstract fun accept(newState: TState)
}
40 changes: 40 additions & 0 deletions mvi/src/commonMain/kotlin/app/meetacy/vm/mvi/StateHost.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package app.meetacy.vm.mvi

import app.meetacy.vm.flow.CFlow
import app.meetacy.vm.flow.CStateFlow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*

public interface StateHost<TState, TEffect> {

public val holder: StateHolder<TState, TEffect>
}

public fun <TState, TEffect> StateHost<TState, TEffect>.holder(
initial: TState
): StateHolder<TState, TEffect> = object : StateHolder<TState, TEffect>() {
private val _effects: Channel<TEffect> = Channel(Channel.BUFFERED)
private val _states: MutableStateFlow<TState> = MutableStateFlow(initial)

override val effects: CFlow<TEffect> = CFlow(_effects.receiveAsFlow())
override val states: CStateFlow<TState> = CStateFlow(_states.asStateFlow())

private val collector: FlowCollector<Intent.Update<TState, TEffect>> = FlowCollector { value ->
when (value) {
is Intent.Update.State -> _states.emit(value.state)
is Intent.Update.Effect -> _effects.send(value.effect)
}
}

override suspend fun accept(effect: TEffect) {
_effects.send(effect)
}

override fun accept(newState: TState) {
_states.update { newState }
}

override suspend fun accept(intent: Intent<TState, TEffect>) {
intent.flowOf(states.value).collect(collector)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package app.meetacy.vm.mvi

import app.meetacy.vm.ViewModel
import app.meetacy.vm.extension.launchIn
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch

public abstract class StateHostedViewModel<TState, TEffect>: ViewModel(), StateHost<TState, TEffect> {

protected fun viewModelScopeLaunch(block: suspend CoroutineScope.() -> Unit) {
viewModelScope.launch(block = block)
}

protected fun <T> Flow<T>.observe(block: suspend (T) -> Unit): Job = launchIn(viewModelScope, block)

protected fun accept(intent: Intent<TState, TEffect>) {
viewModelScope.launch { holder.accept(intent) }
}

protected fun mutateState(transform: TState.() -> TState) {
holder.accept(holder.states.value.transform())
}

protected fun accept(effect: TEffect) {
viewModelScope.launch { holder.accept(effect) }
}
}
49 changes: 49 additions & 0 deletions mvi/src/commonTest/kotlin/app/meetacy/vm/mvi/SignUpHost.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package app.meetacy.vm.mvi

import kotlinx.coroutines.CoroutineScope

object SignUpHost: IntentHost<SignUpHost.State, SignUpHost.SideEffect > {
interface RegisterUseCase {

suspend fun register(userName: String): Result<Unit>
}

sealed interface SideEffect {
object RouteMain : SideEffect
object ShowError : SideEffect
}

data class State(
val userName: String,
val isLoading: Boolean
) {
companion object {
val Initial = State(
userName = "",
isLoading = true
)
}
}

fun signUpIntent(
text: String,
useCase: RegisterUseCase
) = intent {
reduce {
copy(
isLoading = true,
userName = text
)
}

useCase.register(currentState.userName).onSuccess {
perform(SideEffect.RouteMain)
}.onFailure {
perform(SideEffect.ShowError)
}

reduce { copy(isLoading = false) }
}
}


8 changes: 8 additions & 0 deletions mvi/src/commonTest/kotlin/app/meetacy/vm/mvi/SomeUseCase.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package app.meetacy.vm.mvi

import kotlinx.coroutines.flow.Flow

interface SomeUseCase {

fun getFlow(): Flow<Int>
}
20 changes: 20 additions & 0 deletions mvi/src/commonTest/kotlin/app/meetacy/vm/mvi/SomeViewModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package app.meetacy.vm.mvi

import app.meetacy.vm.extension.launchIn

class SomeViewModel: StateHostedViewModel<SomeViewModel.State, SomeViewModel.Effect>() {

override val holder: StateHolder<State, Effect> = holder(State())
data class State(val isLoading: Boolean = true)

sealed interface Effect

companion object : IntentHost<State, Effect> {

fun subscription(useCase: SomeUseCase) = intent {
useCase.getFlow().launchIn(scope) { value ->
reduce { copy(isLoading = value % 3 == 0) }
}
}
}
}
Loading

0 comments on commit a0eb755

Please sign in to comment.