diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 17eb6a80..f125d491 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -12,6 +12,9 @@
+
+
+
diff --git a/build.gradle.kts b/build.gradle.kts
index c27fb309..c4b589df 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -157,6 +157,7 @@ allprojects {
kotlinOptions {
val version = JavaVersion.VERSION_11.toString()
jvmTarget = version
+ freeCompilerArgs = freeCompilerArgs + "-Xcontext-receivers"
}
}
diff --git a/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt b/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt
index 9297e6c1..cf424dcd 100644
--- a/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt
+++ b/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt
@@ -2,10 +2,11 @@ package com.hoc.flowmvi.data
import arrow.core.Either.Companion.catch as catchEither
import arrow.core.ValidatedNel
-import arrow.core.continuations.either
+import arrow.core.continuations.EffectScope
import arrow.core.left
import arrow.core.leftWiden
import arrow.core.right
+import arrow.core.traverse
import arrow.core.valueOr
import com.hoc.flowmvi.core.Mapper
import com.hoc.flowmvi.core.dispatchers.AppCoroutineDispatchers
@@ -16,6 +17,7 @@ import com.hoc.flowmvi.domain.model.User
import com.hoc.flowmvi.domain.model.UserError
import com.hoc.flowmvi.domain.model.UserValidationError
import com.hoc.flowmvi.domain.repository.UserRepository
+import com.hoc081098.flowext.flowFromSuspend
import com.hoc081098.flowext.retryWithExponentialBackoff
import java.io.IOException
import kotlin.time.Duration.Companion.milliseconds
@@ -24,7 +26,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapConcat
@@ -51,34 +52,28 @@ internal class UserRepositoryImpl(
class Added(val user: User) : Change()
}
- private val responseToDomainThrows: (UserResponse) -> User = { response ->
- responseToDomain(response).let { validated ->
- validated.valueOr {
- val t = UserError.ValidationFailed(it.toSet())
- logError(t, "Map $response to user")
- throw t
- }
- }
- }
-
private val changesFlow = MutableSharedFlow(extraBufferCapacity = 64)
-
private suspend inline fun sendChange(change: Change) = changesFlow.emit(change)
- @Suppress("NOTHING_TO_INLINE")
- private inline fun logError(t: Throwable, message: String) = Timber.tag(TAG).e(t, message)
-
- private fun getUsersFromRemote(): Flow> = suspend {
+ private fun getUsersFromRemote(): Flow> = flowFromSuspend {
Timber.d("[USER_REPO] getUsersFromRemote ...")
+
userApiService
.getUsers()
- .map(responseToDomainThrows)
- }.asFlow()
- .retryWithExponentialBackoff(
- maxAttempt = 2,
- initialDelay = 500.milliseconds,
- factor = 2.0,
- ) { it is IOException }
+ .let { response ->
+ response
+ .traverse(responseToDomain)
+ .valueOr { validationErrors ->
+ throw UserError.ValidationFailed(validationErrors.toSet()).also {
+ logError(it, "Failed to map $response to user")
+ }
+ }
+ }
+ }.retryWithExponentialBackoff(
+ maxAttempt = 2,
+ initialDelay = 500.milliseconds,
+ factor = 2.0,
+ ) { it is IOException }
override fun getUsers() = getUsersFromRemote()
.flatMapConcat { initial ->
@@ -99,13 +94,16 @@ internal class UserRepositoryImpl(
emit(errorMapper(it).left())
}
+ context(EffectScope)
override suspend fun refresh() = catchEither { getUsersFromRemote().first() }
.tap { sendChange(Change.Refreshed(it)) }
.map { }
.tapLeft { logError(it, "refresh") }
.mapLeft(errorMapper)
+ .bind()
- override suspend fun remove(user: User) = either {
+ context(EffectScope)
+ override suspend fun remove(user: User) {
withContext(dispatchers.io) {
val response = catchEither { userApiService.remove(user.id) }
.tapLeft { logError(it, "remove user=$user") }
@@ -121,7 +119,8 @@ internal class UserRepositoryImpl(
}
}
- override suspend fun add(user: User) = either {
+ context(EffectScope)
+ override suspend fun add(user: User) {
withContext(dispatchers.io) {
val response = catchEither { userApiService.add(domainToBody(user)) }
.tapLeft { logError(it, "add user=$user") }
@@ -137,13 +136,30 @@ internal class UserRepositoryImpl(
}
}
+ context(EffectScope)
override suspend fun search(query: String) = withContext(dispatchers.io) {
- catchEither { userApiService.search(query).map(responseToDomainThrows) }
+ val userResponses = catchEither { userApiService.search(query) }
.tapLeft { logError(it, "search query=$query") }
.mapLeft(errorMapper)
+ .bind()
+
+ val users = userResponses.traverse(responseToDomain)
+ .mapLeft { UserError.ValidationFailed(it.toSet()) }
+ .tapInvalid {
+ logError(
+ it,
+ "search query=$query, failed to map $userResponses to List"
+ )
+ }
+ .bind()
+
+ users
}
private companion object {
private val TAG = UserRepositoryImpl::class.java.simpleName
+
+ @Suppress("NOTHING_TO_INLINE")
+ private inline fun logError(t: Throwable, message: String) = Timber.tag(TAG).e(t, message)
}
}
diff --git a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt
index cd5fcf78..7ea5164c 100644
--- a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt
+++ b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt
@@ -2,6 +2,7 @@ package com.hoc.flowmvi.data
import arrow.core.Either
import arrow.core.ValidatedNel
+import arrow.core.continuations.either
import arrow.core.validNel
import com.hoc.flowmvi.core.Mapper
import com.hoc.flowmvi.data.remote.UserApiService
@@ -144,7 +145,7 @@ class UserRepositoryImplTest {
coEvery { userApiService.getUsers() } returns USER_RESPONSES
every { responseToDomain(any()) } returnsMany VALID_NEL_USERS
- val result = repo.refresh()
+ val result = either { repo.refresh() }
assertTrue(result.isRight())
assertNotNull(result.orNull())
@@ -163,7 +164,7 @@ class UserRepositoryImplTest {
coEvery { userApiService.getUsers() } throws ioException
every { errorMapper(ofType()) } returns UserError.NetworkError
- val result = repo.refresh()
+ val result = either { repo.refresh() }
assertTrue(result.isLeft())
assertEquals(UserError.NetworkError, result.leftOrThrow)
@@ -179,7 +180,7 @@ class UserRepositoryImplTest {
coEvery { userApiService.remove(user.id) } returns userResponse
every { responseToDomain(userResponse) } returns user.validNel()
- val result = repo.remove(user)
+ val result = either { repo.remove(user) }
assertTrue(result.isRight())
assertNotNull(result.orNull())
@@ -194,7 +195,7 @@ class UserRepositoryImplTest {
coEvery { userApiService.remove(user.id) } throws IOException()
every { errorMapper(ofType()) } returns UserError.NetworkError
- val result = repo.remove(user)
+ val result = either { repo.remove(user) }
assertTrue(result.isLeft())
assertEquals(UserError.NetworkError, result.leftOrThrow)
@@ -211,7 +212,7 @@ class UserRepositoryImplTest {
every { domainToBody(user) } returns USER_BODY
every { responseToDomain(userResponse) } returns user.validNel()
- val result = repo.add(user)
+ val result = either { repo.add(user) }
assertTrue(result.isRight())
assertNotNull(result.orNull())
@@ -228,7 +229,7 @@ class UserRepositoryImplTest {
every { domainToBody(user) } returns USER_BODY
every { errorMapper(ofType()) } returns UserError.NetworkError
- val result = repo.add(user)
+ val result = either { repo.add(user) }
assertTrue(result.isLeft())
assertEquals(UserError.NetworkError, result.leftOrThrow)
@@ -244,7 +245,7 @@ class UserRepositoryImplTest {
coEvery { userApiService.search(q) } returns USER_RESPONSES
every { responseToDomain(any()) } returnsMany VALID_NEL_USERS
- val result = repo.search(q)
+ val result = either { repo.search(q) }
assertTrue(result.isRight())
assertNotNull(result.orNull())
@@ -264,7 +265,7 @@ class UserRepositoryImplTest {
coEvery { userApiService.search(q) } throws IOException()
every { errorMapper(ofType()) } returns UserError.NetworkError
- val result = repo.search(q)
+ val result = either { repo.search(q) }
assertTrue(result.isLeft())
assertEquals(UserError.NetworkError, result.leftOrThrow)
@@ -337,8 +338,10 @@ class UserRepositoryImplTest {
val job = launch(start = CoroutineStart.UNDISPATCHED) {
repo.getUsers().toList(events)
}
- repo.add(user)
- repo.remove(user)
+ either {
+ repo.add(user)
+ repo.remove(user)
+ }.getOrThrow
delay(120_000)
job.cancel()
diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/repository/UserRepository.kt b/domain/src/main/java/com/hoc/flowmvi/domain/repository/UserRepository.kt
index acb9843e..e60bc747 100644
--- a/domain/src/main/java/com/hoc/flowmvi/domain/repository/UserRepository.kt
+++ b/domain/src/main/java/com/hoc/flowmvi/domain/repository/UserRepository.kt
@@ -1,6 +1,7 @@
package com.hoc.flowmvi.domain.repository
import arrow.core.Either
+import arrow.core.continuations.EffectScope
import com.hoc.flowmvi.domain.model.User
import com.hoc.flowmvi.domain.model.UserError
import kotlinx.coroutines.flow.Flow
@@ -8,11 +9,15 @@ import kotlinx.coroutines.flow.Flow
interface UserRepository {
fun getUsers(): Flow>>
- suspend fun refresh(): Either
+ context(EffectScope)
+ suspend fun refresh()
- suspend fun remove(user: User): Either
+ context(EffectScope)
+ suspend fun remove(user: User)
- suspend fun add(user: User): Either
+ context(EffectScope)
+ suspend fun add(user: User)
- suspend fun search(query: String): Either>
+ context(EffectScope)
+ suspend fun search(query: String): List
}
diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/usecase/AddUserUseCase.kt b/domain/src/main/java/com/hoc/flowmvi/domain/usecase/AddUserUseCase.kt
index 59eeb0e6..851b5fda 100644
--- a/domain/src/main/java/com/hoc/flowmvi/domain/usecase/AddUserUseCase.kt
+++ b/domain/src/main/java/com/hoc/flowmvi/domain/usecase/AddUserUseCase.kt
@@ -1,10 +1,11 @@
package com.hoc.flowmvi.domain.usecase
-import arrow.core.Either
+import arrow.core.continuations.EffectScope
import com.hoc.flowmvi.domain.model.User
import com.hoc.flowmvi.domain.model.UserError
import com.hoc.flowmvi.domain.repository.UserRepository
class AddUserUseCase(private val userRepository: UserRepository) {
- suspend operator fun invoke(user: User): Either = userRepository.add(user)
+ context(EffectScope)
+ suspend operator fun invoke(user: User): Unit = userRepository.add(user)
}
diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/usecase/RefreshGetUsersUseCase.kt b/domain/src/main/java/com/hoc/flowmvi/domain/usecase/RefreshGetUsersUseCase.kt
index ae6e17a5..70f42e86 100644
--- a/domain/src/main/java/com/hoc/flowmvi/domain/usecase/RefreshGetUsersUseCase.kt
+++ b/domain/src/main/java/com/hoc/flowmvi/domain/usecase/RefreshGetUsersUseCase.kt
@@ -1,9 +1,10 @@
package com.hoc.flowmvi.domain.usecase
-import arrow.core.Either
+import arrow.core.continuations.EffectScope
import com.hoc.flowmvi.domain.model.UserError
import com.hoc.flowmvi.domain.repository.UserRepository
class RefreshGetUsersUseCase(private val userRepository: UserRepository) {
- suspend operator fun invoke(): Either = userRepository.refresh()
+ context(EffectScope)
+ suspend operator fun invoke(): Unit = userRepository.refresh()
}
diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/usecase/RemoveUserUseCase.kt b/domain/src/main/java/com/hoc/flowmvi/domain/usecase/RemoveUserUseCase.kt
index fce13af0..8a2c2917 100644
--- a/domain/src/main/java/com/hoc/flowmvi/domain/usecase/RemoveUserUseCase.kt
+++ b/domain/src/main/java/com/hoc/flowmvi/domain/usecase/RemoveUserUseCase.kt
@@ -1,10 +1,11 @@
package com.hoc.flowmvi.domain.usecase
-import arrow.core.Either
+import arrow.core.continuations.EffectScope
import com.hoc.flowmvi.domain.model.User
import com.hoc.flowmvi.domain.model.UserError
import com.hoc.flowmvi.domain.repository.UserRepository
class RemoveUserUseCase(private val userRepository: UserRepository) {
- suspend operator fun invoke(user: User): Either = userRepository.remove(user)
+ context(EffectScope)
+ suspend operator fun invoke(user: User): Unit = userRepository.remove(user)
}
diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/usecase/SearchUsersUseCase.kt b/domain/src/main/java/com/hoc/flowmvi/domain/usecase/SearchUsersUseCase.kt
index fd6713c0..b1c22afd 100644
--- a/domain/src/main/java/com/hoc/flowmvi/domain/usecase/SearchUsersUseCase.kt
+++ b/domain/src/main/java/com/hoc/flowmvi/domain/usecase/SearchUsersUseCase.kt
@@ -1,11 +1,12 @@
package com.hoc.flowmvi.domain.usecase
-import arrow.core.Either
+import arrow.core.continuations.EffectScope
import com.hoc.flowmvi.domain.model.User
import com.hoc.flowmvi.domain.model.UserError
import com.hoc.flowmvi.domain.repository.UserRepository
class SearchUsersUseCase(private val userRepository: UserRepository) {
- suspend operator fun invoke(query: String): Either> =
+ context(EffectScope)
+ suspend operator fun invoke(query: String): List =
userRepository.search(query)
}
diff --git a/domain/src/test/java/com/hoc/flowmvi/domain/UseCaseTest.kt b/domain/src/test/java/com/hoc/flowmvi/domain/UseCaseTest.kt
index a0bbb45e..885dc99c 100644
--- a/domain/src/test/java/com/hoc/flowmvi/domain/UseCaseTest.kt
+++ b/domain/src/test/java/com/hoc/flowmvi/domain/UseCaseTest.kt
@@ -1,5 +1,6 @@
package com.hoc.flowmvi.domain
+import arrow.core.continuations.either
import arrow.core.left
import arrow.core.right
import com.hoc.flowmvi.domain.model.User
@@ -11,12 +12,16 @@ import com.hoc.flowmvi.domain.usecase.RefreshGetUsersUseCase
import com.hoc.flowmvi.domain.usecase.RemoveUserUseCase
import com.hoc.flowmvi.domain.usecase.SearchUsersUseCase
import com.hoc.flowmvi.test_utils.TestCoroutineDispatcherRule
+import com.hoc.flowmvi.test_utils.justShift
import com.hoc.flowmvi.test_utils.valueOrThrow
+import com.hoc.flowmvi.test_utils.withAnyEffectScope
+import io.mockk.Runs
import io.mockk.clearAllMocks
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.confirmVerified
import io.mockk.every
+import io.mockk.just
import io.mockk.mockk
import io.mockk.verify
import kotlin.test.AfterTest
@@ -65,7 +70,8 @@ class UseCaseTest {
private lateinit var addUserUseCase: AddUserUseCase
private lateinit var searchUsersUseCase: SearchUsersUseCase
- private val errorLeft = UserError.NetworkError.left()
+ private val networkError = UserError.NetworkError
+ private val errorLeft = networkError.left()
@BeforeTest
fun setup() {
@@ -107,83 +113,83 @@ class UseCaseTest {
@Test
fun test_refreshUseCase_whenSuccess_returnsUnit() = runTest {
- coEvery { userRepository.refresh() } returns Unit.right()
+ coEvery { withAnyEffectScope { userRepository.refresh() } } just Runs
- val result = refreshUseCase()
+ val result = either { refreshUseCase() }
- coVerify { userRepository.refresh() }
+ coVerify { withAnyEffectScope { userRepository.refresh() } }
assertEquals(Unit.right(), result)
}
@Test
fun test_refreshUseCase_whenError_throwsError() = runTest {
- coEvery { userRepository.refresh() } returns errorLeft
+ coEvery { withAnyEffectScope { userRepository.refresh() } } justShift networkError
- val result = refreshUseCase()
+ val result = either { refreshUseCase() }
- coVerify { userRepository.refresh() }
+ coVerify { withAnyEffectScope { userRepository.refresh() } }
assertEquals(errorLeft, result)
}
@Test
fun test_removeUserUseCase_whenSuccess_returnsUnit() = runTest {
- coEvery { userRepository.remove(any()) } returns Unit.right()
+ coEvery { withAnyEffectScope { userRepository.remove(any()) } } just Runs
- val result = removeUserUseCase(USERS[0])
+ val result = either { removeUserUseCase(USERS[0]) }
- coVerify { userRepository.remove(USERS[0]) }
+ coVerify { withAnyEffectScope { userRepository.remove(USERS[0]) } }
assertEquals(Unit.right(), result)
}
@Test
fun test_removeUserUseCase_whenError_throwsError() = runTest {
- coEvery { userRepository.remove(any()) } returns errorLeft
+ coEvery { withAnyEffectScope { userRepository.remove(any()) } } justShift networkError
- val result = removeUserUseCase(USERS[0])
+ val result = either { removeUserUseCase(USERS[0]) }
- coVerify { userRepository.remove(USERS[0]) }
+ coVerify { withAnyEffectScope { userRepository.remove(USERS[0]) } }
assertEquals(errorLeft, result)
}
@Test
fun test_addUserUseCase_whenSuccess_returnsUnit() = runTest {
- coEvery { userRepository.add(any()) } returns Unit.right()
+ coEvery { withAnyEffectScope { userRepository.add(any()) } } just Runs
- val result = addUserUseCase(USERS[0])
+ val result = either { addUserUseCase(USERS[0]) }
- coVerify { userRepository.add(USERS[0]) }
+ coVerify { withAnyEffectScope { userRepository.add(USERS[0]) } }
assertEquals(Unit.right(), result)
}
@Test
fun test_addUserUseCase_whenError_throwsError() = runTest {
- coEvery { userRepository.add(any()) } returns errorLeft
+ coEvery { withAnyEffectScope { userRepository.add(any()) } } justShift networkError
- val result = addUserUseCase(USERS[0])
+ val result = either { addUserUseCase(USERS[0]) }
- coVerify { userRepository.add(USERS[0]) }
+ coVerify { withAnyEffectScope { userRepository.add(USERS[0]) } }
assertEquals(errorLeft, result)
}
@Test
fun test_searchUsersUseCase_whenSuccess_returnsUsers() = runTest {
- coEvery { userRepository.search(any()) } returns USERS.right()
+ coEvery { withAnyEffectScope { userRepository.search(any()) } } returns USERS
val query = "hoc081098"
- val result = searchUsersUseCase(query)
+ val result = either { searchUsersUseCase(query) }
- coVerify { userRepository.search(query) }
+ coVerify { withAnyEffectScope { userRepository.search(query) } }
assertEquals(USERS.right(), result)
}
@Test
fun test_searchUsersUseCase_whenError_throwsError() = runTest {
- coEvery { userRepository.search(any()) } returns errorLeft
+ coEvery { withAnyEffectScope { userRepository.search(any()) } } justShift networkError
val query = "hoc081098"
- val result = searchUsersUseCase(query)
+ val result = either { searchUsersUseCase(query) }
- coVerify { userRepository.search(query) }
+ coVerify { withAnyEffectScope { userRepository.search(query) } }
assertEquals(errorLeft, result)
}
}
diff --git a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddVM.kt b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddVM.kt
index 04c43e36..ebfc61db 100644
--- a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddVM.kt
+++ b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddVM.kt
@@ -2,13 +2,13 @@ package com.hoc.flowmvi.ui.add
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
+import arrow.core.continuations.effect
import arrow.core.orNull
import com.hoc.flowmvi.core.dispatchers.AppCoroutineDispatchers
import com.hoc.flowmvi.domain.model.User
import com.hoc.flowmvi.domain.usecase.AddUserUseCase
import com.hoc.flowmvi.mvi_base.AbstractMviViewModel
import com.hoc081098.flowext.flatMapFirst
-import com.hoc081098.flowext.flowFromSuspend
import com.hoc081098.flowext.mapTo
import com.hoc081098.flowext.startWith
import com.hoc081098.flowext.withLatestFrom
@@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.merge
@@ -112,11 +113,15 @@ class AddVM(
.withLatestFrom(userFormFlow) { _, userForm -> userForm }
.mapNotNull { it?.orNull() }
.flatMapFirst { user ->
- flowFromSuspend { addUser(user) }
+ flowOf(
+ effect {
+ addUser(user)
+ }
+ )
.map { result ->
result.fold(
- ifLeft = { PartialStateChange.AddUser.AddUserFailure(user, it) },
- ifRight = { PartialStateChange.AddUser.AddUserSuccess(user) }
+ recover = { PartialStateChange.AddUser.AddUserFailure(user, it) },
+ transform = { PartialStateChange.AddUser.AddUserSuccess(user) }
)
}
.startWith(PartialStateChange.AddUser.Loading)
diff --git a/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt b/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt
index 0f03a327..cecc0ab2 100644
--- a/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt
+++ b/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt
@@ -1,8 +1,6 @@
package com.hoc.flowmvi.ui.add
import androidx.lifecycle.SavedStateHandle
-import arrow.core.left
-import arrow.core.right
import com.hoc.flowmvi.domain.model.User
import com.hoc.flowmvi.domain.model.UserError
import com.hoc.flowmvi.domain.model.UserValidationError
@@ -10,10 +8,12 @@ import com.hoc.flowmvi.domain.model.UserValidationError.TOO_SHORT_FIRST_NAME
import com.hoc.flowmvi.domain.model.UserValidationError.TOO_SHORT_LAST_NAME
import com.hoc.flowmvi.domain.usecase.AddUserUseCase
import com.hoc.flowmvi.mvi_testing.BaseMviViewModelTest
+import com.hoc.flowmvi.mvi_testing.justShiftWithDelay
import com.hoc.flowmvi.mvi_testing.mapRight
import com.hoc.flowmvi.mvi_testing.returnsWithDelay
import com.hoc.flowmvi.test_utils.TestAppCoroutineDispatchers
import com.hoc.flowmvi.test_utils.valueOrThrow
+import com.hoc.flowmvi.test_utils.withAnyEffectScope
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.confirmVerified
@@ -279,7 +279,7 @@ class AddVMTest : BaseMviViewModelTest
result.fold(
- ifLeft = { PartialChange.Refresh.Failure(it) },
- ifRight = { PartialChange.Refresh.Success }
+ recover = { PartialChange.Refresh.Failure(it) },
+ transform = { PartialChange.Refresh.Success }
)
}
.startWith(PartialChange.Refresh.Loading)
@@ -123,15 +121,16 @@ class MainVM(
.log("Intent")
.map { it.user }
.flatMapMerge { userItem ->
- flowFromSuspend {
- userItem
- .toDomain()
- .flatMap { removeUser(it) }
- }
+ flowOf(
+ effect {
+ val user = userItem.toDomain().bind()
+ removeUser(user)
+ }
+ )
.map { result ->
result.fold(
- ifLeft = { PartialChange.RemoveUser.Failure(userItem, it) },
- ifRight = { PartialChange.RemoveUser.Success(userItem) },
+ recover = { PartialChange.RemoveUser.Failure(userItem, it) },
+ transform = { PartialChange.RemoveUser.Success(userItem) },
)
}
}
diff --git a/feature-main/src/test/java/com/hoc/flowmvi/ui/main/MainVMTest.kt b/feature-main/src/test/java/com/hoc/flowmvi/ui/main/MainVMTest.kt
index c0a295e8..11c2ef7c 100644
--- a/feature-main/src/test/java/com/hoc/flowmvi/ui/main/MainVMTest.kt
+++ b/feature-main/src/test/java/com/hoc/flowmvi/ui/main/MainVMTest.kt
@@ -9,9 +9,12 @@ import com.hoc.flowmvi.domain.usecase.RefreshGetUsersUseCase
import com.hoc.flowmvi.domain.usecase.RemoveUserUseCase
import com.hoc.flowmvi.mvi_testing.BaseMviViewModelTest
import com.hoc.flowmvi.mvi_testing.delayEach
+import com.hoc.flowmvi.mvi_testing.justShiftWithDelay
import com.hoc.flowmvi.mvi_testing.mapRight
import com.hoc.flowmvi.mvi_testing.returnsWithDelay
import com.hoc.flowmvi.test_utils.TestAppCoroutineDispatchers
+import com.hoc.flowmvi.test_utils.justShift
+import com.hoc.flowmvi.test_utils.withAnyEffectScope
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.coVerifySequence
@@ -120,7 +123,7 @@ class MainVMTest : BaseMviViewModelTest
either.map { users ->
- users.filter { it.id != firstArg().id }
+ users.filter { it.id != secondArg().id }
}
}
Unit.right()
@@ -383,8 +386,8 @@ class MainVMTest : BaseMviViewModelTest.toPartialStateChangesFlow(): Flow {
val executeSearch: suspend (String) -> Flow = { query: String ->
- flowFromSuspend { searchUsersUseCase(query) }
+ flowOf(effect { searchUsersUseCase(query) })
.map { result ->
result.fold(
- ifLeft = { PartialStateChange.Failure(it, query) },
- ifRight = {
+ recover = { PartialStateChange.Failure(it, query) },
+ transform = {
PartialStateChange.Success(
it.map(UserItem::from),
query
diff --git a/feature-search/src/test/java/com/hoc/flowmvi/ui/search/SearchVMTest.kt b/feature-search/src/test/java/com/hoc/flowmvi/ui/search/SearchVMTest.kt
index 9e29b66b..51aeb77a 100644
--- a/feature-search/src/test/java/com/hoc/flowmvi/ui/search/SearchVMTest.kt
+++ b/feature-search/src/test/java/com/hoc/flowmvi/ui/search/SearchVMTest.kt
@@ -1,15 +1,15 @@
package com.hoc.flowmvi.ui.search
import androidx.lifecycle.SavedStateHandle
-import arrow.core.left
-import arrow.core.right
import com.hoc.flowmvi.domain.model.UserError
import com.hoc.flowmvi.domain.usecase.SearchUsersUseCase
import com.hoc.flowmvi.mvi_testing.BaseMviViewModelTest
+import com.hoc.flowmvi.mvi_testing.justShiftWithDelay
import com.hoc.flowmvi.mvi_testing.mapRight
-import com.hoc.flowmvi.mvi_testing.returnsManyWithDelay
import com.hoc.flowmvi.mvi_testing.returnsWithDelay
import com.hoc.flowmvi.test_utils.TestAppCoroutineDispatchers
+import com.hoc.flowmvi.test_utils.shift
+import com.hoc.flowmvi.test_utils.withAnyEffectScope
import com.hoc.flowmvi.ui.search.SearchVM.Companion.SEARCH_DEBOUNCE_DURATION
import com.hoc081098.flowext.concatWith
import com.hoc081098.flowext.timer
@@ -60,7 +60,7 @@ class SearchVMTest : BaseMviViewModelTest cancelled by (2)
- USERS.right()
+ USERS
}
- coEvery { searchUsersUseCase(query2) } returns USERS.right()
+ coEvery { withAnyEffectScope { searchUsersUseCase(query2) } } returns USERS
runVMTest(
vmProducer = { vm },
@@ -305,8 +305,8 @@ class SearchVMTest : BaseMviViewModelTest shift(networkError)
+ 1 -> {
+ delay(1)
+ USERS
+ }
+ else -> error("Should not reach here!")
+ }
+ }
runVMTest(
vmProducer = { vm },
@@ -449,7 +456,7 @@ class SearchVMTest : BaseMviViewModelTest networkError.left()
+ 0 -> shift(networkError)
1 -> {
repeat(3) { timeout() } // (1) very long ... -> cancelled by (2)
- USERS.right()
+ USERS
}
else -> error("Should not reach here!")
}
}
- coEvery { searchUsersUseCase(query2) } returns USERS.right()
+ coEvery { withAnyEffectScope { searchUsersUseCase(query2) } } returns USERS
runVMTest(
vmProducer = { vm },
@@ -562,9 +569,9 @@ class SearchVMTest : BaseMviViewModelTest MockKStubScope.returnsManyWithDelay(values: List) {
}
}
+/**
+ * Workaround for [Kotlin/kotlinx.coroutines/issues/3120](https://github.com/Kotlin/kotlinx.coroutines/issues/3120).
+ * TODO(coroutines): https://github.com/Kotlin/kotlinx.coroutines/issues/3120
+ */
+inline infix fun MockKStubScope<*, *>.justShiftWithDelay(r: R) {
+ coAnswers {
+ delay(1)
+ shift(r)
+ }
+}
+
@ExperimentalTime
@ExperimentalCoroutinesApi
abstract class BaseMviViewModelTest<
diff --git a/test-utils/src/main/java/com/hoc/flowmvi/test_utils/utils.kt b/test-utils/src/main/java/com/hoc/flowmvi/test_utils/utils.kt
index 1f09c584..1ee512bc 100644
--- a/test-utils/src/main/java/com/hoc/flowmvi/test_utils/utils.kt
+++ b/test-utils/src/main/java/com/hoc/flowmvi/test_utils/utils.kt
@@ -2,9 +2,14 @@ package com.hoc.flowmvi.test_utils
import arrow.core.Either
import arrow.core.Validated
+import arrow.core.continuations.EffectScope
import arrow.core.getOrHandle
import arrow.core.identity
import arrow.core.valueOr
+import com.hoc.flowmvi.core.unit
+import io.mockk.MockKAnswerScope
+import io.mockk.MockKMatcherScope
+import io.mockk.MockKStubScope
inline val Validated.valueOrThrow: A
get() = valueOr(this::throws)
@@ -22,3 +27,15 @@ inline val Either.getOrThrow: R
internal fun Any.throws(it: E): Nothing =
if (it is Throwable) throw it
else error("$this - $it - Should not reach here!")
+
+context(MockKMatcherScope)
+inline fun withAnyEffectScope(block: EffectScope.() -> RR): RR =
+ with(any>()) {
+ block()
+ }
+
+inline infix fun MockKStubScope<*, *>.justShift(r: R): Unit =
+ coAnswers { shift(r) }.unit
+
+suspend inline infix fun MockKAnswerScope<*, *>.shift(r: R): Nothing =
+ arg>(0).shift(r)