Skip to content

Commit 08fc652

Browse files
authored
Merge pull request #6 from YAPP-Github/feature/#2-base_view_model
[Feature/#3] BaseViewModel 구현
2 parents 564017c + d28ae74 commit 08fc652

File tree

8 files changed

+150
-0
lines changed

8 files changed

+150
-0
lines changed

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ plugins {
66
alias(libs.plugins.kotlin.android) apply false
77
alias(libs.plugins.kotlin.jvm) apply false
88
alias(libs.plugins.kotlin.serialization) apply false
9+
alias(libs.plugins.kotlin.parcelize) apply false
910
alias(libs.plugins.ksp) apply false
1011
alias(libs.plugins.hilt) apply false
1112
alias(libs.plugins.ktlint) apply false

gradle/libs.versions.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ javax-inject = { group = "javax.inject", name = "javax.inject", version.ref = "j
9595
orbit-core = { group = "org.orbit-mvi", name = "orbit-core", version.ref = "orbit" }
9696
orbit-compose = { group = "org.orbit-mvi", name = "orbit-compose", version.ref = "orbit" }
9797
orbit-viewmodel = { group = "org.orbit-mvi", name = "orbit-viewmodel", version.ref = "orbit" }
98+
orbit-test = { group = "org.orbit-mvi", name = "orbit-test", version.ref = "orbit" }
9899

99100
## retrofit
100101
retrofit-bom = { group = "com.squareup.retrofit2", name = "retrofit-bom", version.ref = "retrofit" }
@@ -110,6 +111,7 @@ okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-i
110111
junit = { group = "junit", name = "junit", version.ref = "junit" }
111112
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
112113
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
114+
kotlin-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" }
113115

114116
## Other
115117
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
@@ -171,6 +173,7 @@ android-library = { id = "com.android.library", version.ref = "androidGradlePlug
171173
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
172174
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
173175
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
176+
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin"}
174177
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
175178
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
176179
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }

presentation/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
plugins {
22
alias(libs.plugins.bitnagil.android.library)
33
alias(libs.plugins.bitnagil.android.compose.library)
4+
alias(libs.plugins.kotlin.parcelize)
45
}
56

67
android {
@@ -12,4 +13,8 @@ dependencies {
1213

1314
implementation(libs.bundles.androidx.core)
1415
implementation(libs.bundles.orbit)
16+
17+
testImplementation(libs.junit)
18+
testImplementation(libs.kotlin.coroutines.test)
19+
testImplementation(libs.orbit.test)
1520
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package com.threegap.bitnagil.presentation.common.mviviewmodel
2+
3+
interface MviIntent
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package com.threegap.bitnagil.presentation.common.mviviewmodel
2+
3+
interface MviSideEffect
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.threegap.bitnagil.presentation.common.mviviewmodel
2+
3+
import android.os.Parcelable
4+
5+
interface MviState : Parcelable
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.threegap.bitnagil.presentation.common.mviviewmodel
2+
3+
import androidx.lifecycle.SavedStateHandle
4+
import androidx.lifecycle.ViewModel
5+
import kotlinx.coroutines.flow.Flow
6+
import kotlinx.coroutines.flow.StateFlow
7+
import org.orbitmvi.orbit.ContainerHost
8+
import org.orbitmvi.orbit.syntax.simple.SimpleSyntax
9+
import org.orbitmvi.orbit.syntax.simple.intent
10+
import org.orbitmvi.orbit.syntax.simple.postSideEffect
11+
import org.orbitmvi.orbit.syntax.simple.reduce
12+
import org.orbitmvi.orbit.viewmodel.container
13+
14+
abstract class MviViewModel<STATE : MviState, SIDE_EFFECT : MviSideEffect, INTENT : MviIntent>(
15+
initState: STATE,
16+
savedStateHandle: SavedStateHandle,
17+
) : ContainerHost<STATE, SIDE_EFFECT>, ViewModel() {
18+
override val container = container<STATE, SIDE_EFFECT>(initialState = initState, savedStateHandle = savedStateHandle)
19+
20+
val stateFlow: StateFlow<STATE> get() = container.stateFlow
21+
val sideEffectFlow: Flow<SIDE_EFFECT> get() = container.sideEffectFlow
22+
23+
protected suspend fun SimpleSyntax<STATE, SIDE_EFFECT>.sendSideEffect(sideEffect: SIDE_EFFECT) = postSideEffect(sideEffect)
24+
25+
protected abstract suspend fun SimpleSyntax<STATE, SIDE_EFFECT>.reduceState(
26+
intent: INTENT,
27+
state: STATE,
28+
): STATE?
29+
30+
fun sendIntent(intent: INTENT) =
31+
intent {
32+
val newState = reduceState(intent, state)
33+
34+
newState?.let {
35+
reduce { newState }
36+
}
37+
}
38+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package com.threegap.bitnagil.presentation.common.mviviewmodel
2+
3+
import androidx.lifecycle.SavedStateHandle
4+
import kotlinx.coroutines.ExperimentalCoroutinesApi
5+
import kotlinx.coroutines.test.runTest
6+
import kotlinx.parcelize.Parcelize
7+
import org.junit.Before
8+
import org.junit.Test
9+
import org.orbitmvi.orbit.syntax.simple.SimpleSyntax
10+
import org.orbitmvi.orbit.test.test
11+
12+
@ExperimentalCoroutinesApi
13+
class MviViewModelTest {
14+
private lateinit var sampleMviViewModel: SampleMviViewModel
15+
16+
@Before
17+
fun setUp() {
18+
sampleMviViewModel = SampleMviViewModel(initState = SampleState(), savedStateHandle = SavedStateHandle())
19+
}
20+
21+
@Test
22+
fun `state는 호출 순서대로 갱신되어야 한다`() =
23+
runTest {
24+
sampleMviViewModel.test(testScope = this) {
25+
containerHost.sendIntent(SampleIntent.Increase(number = 1))
26+
containerHost.sendIntent(SampleIntent.Decrease(number = 2))
27+
containerHost.sendIntent(SampleIntent.Increase(number = 3))
28+
29+
expectState { SampleState() }
30+
expectState { SampleState(count = 1) }
31+
expectState { SampleState(count = -1) }
32+
expectState { SampleState(count = 2) }
33+
}
34+
}
35+
36+
@Test
37+
fun `state와 sideEffect는 호출 순서대로 갱신되어야 한다`() =
38+
runTest {
39+
sampleMviViewModel.test(testScope = this) {
40+
containerHost.sendIntent(SampleIntent.Increase(number = 1))
41+
containerHost.sendIntent(SampleIntent.Clear)
42+
containerHost.sendIntent(SampleIntent.Decrease(number = 2))
43+
containerHost.sendIntent(SampleIntent.Increase(number = 3))
44+
45+
expectState { SampleState() }
46+
expectState { SampleState(count = 1) }
47+
expectSideEffect(SampleSideEffect.ShowToast("Clear"))
48+
expectState { SampleState() }
49+
expectState { SampleState(count = -2) }
50+
expectState { SampleState(count = 1) }
51+
}
52+
}
53+
54+
// only for test
55+
private class SampleMviViewModel(
56+
initState: SampleState,
57+
savedStateHandle: SavedStateHandle,
58+
) : MviViewModel<SampleState, SampleSideEffect, SampleIntent>(initState, savedStateHandle) {
59+
override suspend fun SimpleSyntax<SampleState, SampleSideEffect>.reduceState(
60+
intent: SampleIntent,
61+
state: SampleState,
62+
): SampleState? {
63+
val newState =
64+
when (intent) {
65+
is SampleIntent.Decrease -> state.copy(count = state.count - intent.number)
66+
is SampleIntent.Increase -> state.copy(count = state.count + intent.number)
67+
SampleIntent.Clear -> {
68+
sendSideEffect(sideEffect = SampleSideEffect.ShowToast("Clear"))
69+
state.copy(count = 0)
70+
}
71+
}
72+
return newState
73+
}
74+
}
75+
76+
@Parcelize
77+
private data class SampleState(
78+
val count: Int = 0,
79+
) : MviState
80+
81+
private sealed class SampleSideEffect : MviSideEffect {
82+
data class ShowToast(val message: String) : SampleSideEffect()
83+
}
84+
85+
private sealed class SampleIntent : MviIntent {
86+
data class Increase(val number: Int) : SampleIntent()
87+
88+
data class Decrease(val number: Int) : SampleIntent()
89+
90+
object Clear : SampleIntent()
91+
}
92+
}

0 commit comments

Comments
 (0)