Skip to content

Commit 5baef7b

Browse files
committed
update and test :feature-add
1 parent e1ae938 commit 5baef7b

File tree

7 files changed

+274
-70
lines changed

7 files changed

+274
-70
lines changed

buildSrc/src/main/kotlin/deps.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ inline val PDsS.androidLib: PDS get() = id("com.android.library")
9595
inline val PDsS.kotlinAndroid: PDS get() = id("kotlin-android")
9696
inline val PDsS.kotlin: PDS get() = id("kotlin")
9797
inline val PDsS.kotlinKapt: PDS get() = id("kotlin-kapt")
98+
inline val PDsS.kotlinParcelize: PDS get() = id("kotlin-parcelize")
9899

99100
inline val DependencyHandler.domain get() = project(":domain")
100101
inline val DependencyHandler.core get() = project(":core")

feature-add/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
plugins {
22
androidLib
33
kotlinAndroid
4+
kotlinParcelize
45
}
56

67
android {

feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddContract.kt

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package com.hoc.flowmvi.ui.add
22

3+
import android.os.Parcelable
34
import arrow.core.ValidatedNel
45
import arrow.core.invalidNel
56
import com.hoc.flowmvi.domain.entity.User
67
import com.hoc.flowmvi.domain.repository.UserError
78
import com.hoc.flowmvi.mvi_base.MviIntent
89
import com.hoc.flowmvi.mvi_base.MviSingleEvent
910
import com.hoc.flowmvi.mvi_base.MviViewState
11+
import kotlinx.parcelize.Parcelize
1012

1113
enum class ValidationError {
1214
INVALID_EMAIL_ADDRESS,
@@ -16,32 +18,29 @@ enum class ValidationError {
1618
val asInvalidNel: ValidatedNel<ValidationError, Nothing> = invalidNel()
1719
}
1820

21+
@Parcelize
1922
data class ViewState(
2023
val errors: Set<ValidationError>,
2124
val isLoading: Boolean,
22-
//
25+
// show error or not
2326
val emailChanged: Boolean,
2427
val firstNameChanged: Boolean,
2528
val lastNameChanged: Boolean,
26-
//
29+
// form values
2730
val email: String?,
2831
val firstName: String?,
2932
val lastName: String?,
30-
) : MviViewState {
33+
) : MviViewState, Parcelable {
3134
companion object {
32-
fun initial(
33-
email: String?,
34-
firstName: String?,
35-
lastName: String?,
36-
) = ViewState(
35+
fun initial() = ViewState(
3736
errors = emptySet(),
3837
isLoading = false,
3938
emailChanged = false,
4039
firstNameChanged = false,
4140
lastNameChanged = false,
42-
email = email,
43-
firstName = firstName,
44-
lastName = lastName,
41+
email = null,
42+
firstName = null,
43+
lastName = null,
4544
)
4645
}
4746
}

feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddVM.kt

Lines changed: 7 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
package com.hoc.flowmvi.ui.add
22

3-
import androidx.core.util.PatternsCompat
43
import androidx.lifecycle.SavedStateHandle
54
import androidx.lifecycle.viewModelScope
6-
import arrow.core.ValidatedNel
75
import arrow.core.orNull
8-
import arrow.core.validNel
96
import arrow.core.zip
107
import com.hoc.flowmvi.domain.entity.User
118
import com.hoc.flowmvi.domain.usecase.AddUserUseCase
@@ -36,23 +33,20 @@ import timber.log.Timber
3633
@ExperimentalCoroutinesApi
3734
class AddVM(
3835
private val addUser: AddUserUseCase,
39-
private val savedStateHandle: SavedStateHandle,
36+
savedStateHandle: SavedStateHandle,
4037
) : AbstractMviViewModel<ViewIntent, ViewState, SingleEvent>() {
4138

4239
override val viewState: StateFlow<ViewState>
4340

4441
init {
45-
val initialVS = ViewState.initial(
46-
email = savedStateHandle.get<String?>(EMAIL_KEY),
47-
firstName = savedStateHandle.get<String?>(FIRST_NAME_KEY),
48-
lastName = savedStateHandle.get<String?>(LAST_NAME_KEY),
49-
)
42+
val initialVS = savedStateHandle.get<ViewState?>(VIEW_STATE) ?: ViewState.initial()
5043
Timber.tag(logTag).d("[ADD_VM] initialVS: $initialVS")
5144

5245
viewState = intentFlow
5346
.toPartialStateChangesFlow()
5447
.sendSingleEvent()
5548
.scan(initialVS) { state, change -> change.reduce(state) }
49+
.onEach { savedStateHandle[VIEW_STATE] = it }
5650
.catch { Timber.tag(logTag).e(it, "[ADD_VM] Throwable: $it") }
5751
.stateIn(viewModelScope, SharingStarted.Eagerly, initialVS)
5852
}
@@ -70,18 +64,9 @@ class AddVM(
7064
PartialStateChange.FirstChange.EmailChangedFirstTime -> return@onEach
7165
PartialStateChange.FirstChange.FirstNameChangedFirstTime -> return@onEach
7266
PartialStateChange.FirstChange.LastNameChangedFirstTime -> return@onEach
73-
is PartialStateChange.FormValueChange.EmailChanged -> {
74-
savedStateHandle[EMAIL_KEY] = change.email
75-
return@onEach
76-
}
77-
is PartialStateChange.FormValueChange.FirstNameChanged -> {
78-
savedStateHandle[FIRST_NAME_KEY] = change.firstName
79-
return@onEach
80-
}
81-
is PartialStateChange.FormValueChange.LastNameChanged -> {
82-
savedStateHandle[LAST_NAME_KEY] = change.lastName
83-
return@onEach
84-
}
67+
is PartialStateChange.FormValueChange.EmailChanged -> return@onEach
68+
is PartialStateChange.FormValueChange.FirstNameChanged -> return@onEach
69+
is PartialStateChange.FormValueChange.LastNameChanged -> return@onEach
8570
}
8671
sendEvent(event)
8772
}
@@ -176,35 +161,6 @@ class AddVM(
176161
}
177162

178163
private companion object {
179-
const val EMAIL_KEY = "com.hoc.flowmvi.ui.add.email"
180-
const val FIRST_NAME_KEY = "com.hoc.flowmvi.ui.add.first_name"
181-
const val LAST_NAME_KEY = "com.hoc.flowmvi.ui.add.last_name"
182-
183-
const val MIN_LENGTH_FIRST_NAME = 3
184-
const val MIN_LENGTH_LAST_NAME = 3
185-
186-
fun validateFirstName(firstName: String?): ValidatedNel<ValidationError, String> {
187-
if (firstName == null || firstName.length < MIN_LENGTH_FIRST_NAME) {
188-
return ValidationError.TOO_SHORT_FIRST_NAME.asInvalidNel
189-
}
190-
// more validations here
191-
return firstName.validNel()
192-
}
193-
194-
fun validateLastName(lastName: String?): ValidatedNel<ValidationError, String> {
195-
if (lastName == null || lastName.length < MIN_LENGTH_LAST_NAME) {
196-
return ValidationError.TOO_SHORT_LAST_NAME.asInvalidNel
197-
}
198-
// more validations here
199-
return lastName.validNel()
200-
}
201-
202-
fun validateEmail(email: String?): ValidatedNel<ValidationError, String> {
203-
if (email == null || !PatternsCompat.EMAIL_ADDRESS.matcher(email).matches()) {
204-
return ValidationError.INVALID_EMAIL_ADDRESS.asInvalidNel
205-
}
206-
// more validations here
207-
return email.validNel()
208-
}
164+
private const val VIEW_STATE = "com.hoc.flowmvi.ui.add.view_state"
209165
}
210166
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.hoc.flowmvi.ui.add
2+
3+
import androidx.core.util.PatternsCompat
4+
import arrow.core.ValidatedNel
5+
import arrow.core.validNel
6+
7+
private const val MIN_LENGTH_FIRST_NAME = 3
8+
private const val MIN_LENGTH_LAST_NAME = 3
9+
10+
internal fun validateFirstName(firstName: String?): ValidatedNel<ValidationError, String> {
11+
if (firstName == null || firstName.length < MIN_LENGTH_FIRST_NAME) {
12+
return ValidationError.TOO_SHORT_FIRST_NAME.asInvalidNel
13+
}
14+
// more validations here
15+
return firstName.validNel()
16+
}
17+
18+
internal fun validateLastName(lastName: String?): ValidatedNel<ValidationError, String> {
19+
if (lastName == null || lastName.length < MIN_LENGTH_LAST_NAME) {
20+
return ValidationError.TOO_SHORT_LAST_NAME.asInvalidNel
21+
}
22+
// more validations here
23+
return lastName.validNel()
24+
}
25+
26+
internal fun validateEmail(email: String?): ValidatedNel<ValidationError, String> {
27+
if (email == null || !PatternsCompat.EMAIL_ADDRESS.matcher(email).matches()) {
28+
return ValidationError.INVALID_EMAIL_ADDRESS.asInvalidNel
29+
}
30+
// more validations here
31+
return email.validNel()
32+
}

feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt

Lines changed: 130 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@ import androidx.lifecycle.SavedStateHandle
44
import com.flowmvi.mvi_testing.BaseMviViewModelTest
55
import com.flowmvi.mvi_testing.mapRight
66
import com.hoc.flowmvi.domain.usecase.AddUserUseCase
7+
import com.hoc.flowmvi.ui.add.ValidationError.TOO_SHORT_FIRST_NAME
8+
import com.hoc.flowmvi.ui.add.ValidationError.TOO_SHORT_LAST_NAME
9+
import com.hoc.flowmvi.ui.add.ValidationError.values
710
import io.mockk.confirmVerified
811
import io.mockk.mockk
912
import kotlinx.coroutines.ExperimentalCoroutinesApi
1013
import kotlinx.coroutines.flow.flowOf
1114
import kotlin.test.Test
1215
import kotlin.time.ExperimentalTime
1316

14-
private val ALL_ERRORS = ValidationError.values().toSet()
17+
private val ALL_ERRORS = values().toSet()
1518

1619
@ExperimentalCoroutinesApi
1720
@ExperimentalTime
@@ -41,7 +44,7 @@ class AddVMTest : BaseMviViewModelTest<ViewIntent, ViewState, SingleEvent, AddVM
4144
}
4245

4346
@Test
44-
fun test_withFormValueIntents_returnsStateWithChangedValuesWithErrors() {
47+
fun test_withFormValueIntents_returnsStateWithChangedValuesAndErrors() {
4548
test(
4649
vmProducer = { vm },
4750
intents = flowOf(
@@ -54,11 +57,7 @@ class AddVMTest : BaseMviViewModelTest<ViewIntent, ViewState, SingleEvent, AddVM
5457
ViewIntent.LastNameChanged("c"),
5558
),
5659
expectedStates = listOf(
57-
ViewState.initial(
58-
email = null,
59-
firstName = null,
60-
lastName = null,
61-
),
60+
ViewState.initial(),
6261
ViewState(
6362
errors = emptySet(),
6463
isLoading = false,
@@ -120,6 +119,7 @@ class AddVMTest : BaseMviViewModelTest<ViewIntent, ViewState, SingleEvent, AddVM
120119
firstName = "b",
121120
lastName = ""
122121
),
122+
// invalid state
123123
ViewState(
124124
errors = ALL_ERRORS,
125125
isLoading = false,
@@ -131,7 +131,129 @@ class AddVMTest : BaseMviViewModelTest<ViewIntent, ViewState, SingleEvent, AddVM
131131
lastName = "c"
132132
)
133133
).mapRight(),
134-
expectedEvents = listOf(),
134+
expectedEvents = emptyList(),
135+
)
136+
}
137+
138+
@Test
139+
fun test_withFormValueIntents_returnsStateWithChangedValuesAndNoErrors() {
140+
test(
141+
vmProducer = { vm },
142+
intents = flowOf(
143+
ViewIntent.EmailChanged(""),
144+
ViewIntent.FirstNameChanged(""),
145+
ViewIntent.LastNameChanged(""),
146+
// all fields changed
147+
ViewIntent.EmailChanged("hoc081098@gmail.com"),
148+
ViewIntent.FirstNameChanged("hoc081098"),
149+
ViewIntent.LastNameChanged("hoc081098"),
150+
),
151+
expectedStates = listOf(
152+
ViewState.initial(),
153+
ViewState(
154+
errors = emptySet(),
155+
isLoading = false,
156+
emailChanged = false,
157+
firstNameChanged = false,
158+
lastNameChanged = false,
159+
email = "",
160+
firstName = null,
161+
lastName = null
162+
),
163+
ViewState(
164+
errors = emptySet(),
165+
isLoading = false,
166+
emailChanged = false,
167+
firstNameChanged = false,
168+
lastNameChanged = false,
169+
email = "",
170+
firstName = "",
171+
lastName = null
172+
),
173+
ViewState(
174+
errors = emptySet(),
175+
isLoading = false,
176+
emailChanged = false,
177+
firstNameChanged = false,
178+
lastNameChanged = false,
179+
email = "",
180+
firstName = "",
181+
lastName = ""
182+
),
183+
ViewState(
184+
errors = ALL_ERRORS,
185+
isLoading = false,
186+
emailChanged = false,
187+
firstNameChanged = false,
188+
lastNameChanged = false,
189+
email = "",
190+
firstName = "",
191+
lastName = ""
192+
),
193+
// all fields changed.
194+
ViewState(
195+
errors = ALL_ERRORS,
196+
isLoading = false,
197+
emailChanged = false,
198+
firstNameChanged = false,
199+
lastNameChanged = false,
200+
email = "hoc081098@gmail.com",
201+
firstName = "",
202+
lastName = ""
203+
),
204+
ViewState(
205+
errors = setOf(TOO_SHORT_FIRST_NAME, TOO_SHORT_LAST_NAME),
206+
isLoading = false,
207+
emailChanged = false,
208+
firstNameChanged = false,
209+
lastNameChanged = false,
210+
email = "hoc081098@gmail.com",
211+
firstName = "",
212+
lastName = ""
213+
),
214+
ViewState(
215+
errors = setOf(TOO_SHORT_FIRST_NAME, TOO_SHORT_LAST_NAME),
216+
isLoading = false,
217+
emailChanged = false,
218+
firstNameChanged = false,
219+
lastNameChanged = false,
220+
email = "hoc081098@gmail.com",
221+
firstName = "hoc081098",
222+
lastName = ""
223+
),
224+
ViewState(
225+
errors = setOf(TOO_SHORT_LAST_NAME),
226+
isLoading = false,
227+
emailChanged = false,
228+
firstNameChanged = false,
229+
lastNameChanged = false,
230+
email = "hoc081098@gmail.com",
231+
firstName = "hoc081098",
232+
lastName = ""
233+
),
234+
ViewState(
235+
errors = setOf(TOO_SHORT_LAST_NAME),
236+
isLoading = false,
237+
emailChanged = false,
238+
firstNameChanged = false,
239+
lastNameChanged = false,
240+
email = "hoc081098@gmail.com",
241+
firstName = "hoc081098",
242+
lastName = "hoc081098"
243+
),
244+
// valid state
245+
ViewState(
246+
errors = emptySet(),
247+
isLoading = false,
248+
emailChanged = false,
249+
firstNameChanged = false,
250+
lastNameChanged = false,
251+
email = "hoc081098@gmail.com",
252+
firstName = "hoc081098",
253+
lastName = "hoc081098"
254+
),
255+
).mapRight(),
256+
expectedEvents = emptyList(),
135257
)
136258
}
137259
}

0 commit comments

Comments
 (0)