Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[POC] refactor: context receivers #164

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .idea/codeStyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ allprojects {
kotlinOptions {
val version = JavaVersion.VERSION_11.toString()
jvmTarget = version
freeCompilerArgs = freeCompilerArgs + "-Xcontext-receivers"
}
}

Expand Down
70 changes: 43 additions & 27 deletions data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

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
Expand All @@ -16,6 +17,7 @@
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
Expand All @@ -24,7 +26,6 @@
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
Expand All @@ -51,34 +52,28 @@
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<Change>(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<List<User>> = suspend {
private fun getUsersFromRemote(): Flow<List<User>> = 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")
}

Check warning on line 69 in data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt

View check run for this annotation

Codecov / codecov/patch

data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt#L67-L69

Added lines #L67 - L69 were not covered by tests
}
}
}.retryWithExponentialBackoff(
maxAttempt = 2,
initialDelay = 500.milliseconds,
factor = 2.0,
) { it is IOException }

override fun getUsers() = getUsersFromRemote()
.flatMapConcat { initial ->
Expand All @@ -99,13 +94,16 @@
emit(errorMapper(it).left())
}

context(EffectScope<UserError>)
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<UserError, Unit> {
context(EffectScope<UserError>)
override suspend fun remove(user: User) {
withContext(dispatchers.io) {
val response = catchEither { userApiService.remove(user.id) }
.tapLeft { logError(it, "remove user=$user") }
Expand All @@ -121,7 +119,8 @@
}
}

override suspend fun add(user: User) = either<UserError, Unit> {
context(EffectScope<UserError>)
override suspend fun add(user: User) {
withContext(dispatchers.io) {
val response = catchEither { userApiService.add(domainToBody(user)) }
.tapLeft { logError(it, "add user=$user") }
Expand All @@ -137,13 +136,30 @@
}
}

context(EffectScope<UserError>)
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(

Check warning on line 149 in data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt

View check run for this annotation

Codecov / codecov/patch

data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt#L149

Added line #L149 was not covered by tests
it,
"search query=$query, failed to map $userResponses to List<User>"

Check warning on line 151 in data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt

View check run for this annotation

Codecov / codecov/patch

data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt#L151

Added line #L151 was not covered by tests
)
}

Check warning on line 153 in data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt

View check run for this annotation

Codecov / codecov/patch

data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt#L153

Added line #L153 was not covered by tests
.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)

Check warning on line 163 in data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt

View check run for this annotation

Codecov / codecov/patch

data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt#L163

Added line #L163 was not covered by tests
}
}
23 changes: 13 additions & 10 deletions data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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())
Expand All @@ -163,7 +164,7 @@ class UserRepositoryImplTest {
coEvery { userApiService.getUsers() } throws ioException
every { errorMapper(ofType<IOException>()) } returns UserError.NetworkError

val result = repo.refresh()
val result = either { repo.refresh() }

assertTrue(result.isLeft())
assertEquals(UserError.NetworkError, result.leftOrThrow)
Expand All @@ -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())
Expand All @@ -194,7 +195,7 @@ class UserRepositoryImplTest {
coEvery { userApiService.remove(user.id) } throws IOException()
every { errorMapper(ofType<IOException>()) } returns UserError.NetworkError

val result = repo.remove(user)
val result = either { repo.remove(user) }

assertTrue(result.isLeft())
assertEquals(UserError.NetworkError, result.leftOrThrow)
Expand All @@ -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())
Expand All @@ -228,7 +229,7 @@ class UserRepositoryImplTest {
every { domainToBody(user) } returns USER_BODY
every { errorMapper(ofType<IOException>()) } returns UserError.NetworkError

val result = repo.add(user)
val result = either { repo.add(user) }

assertTrue(result.isLeft())
assertEquals(UserError.NetworkError, result.leftOrThrow)
Expand All @@ -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())
Expand All @@ -264,7 +265,7 @@ class UserRepositoryImplTest {
coEvery { userApiService.search(q) } throws IOException()
every { errorMapper(ofType<IOException>()) } returns UserError.NetworkError

val result = repo.search(q)
val result = either { repo.search(q) }

assertTrue(result.isLeft())
assertEquals(UserError.NetworkError, result.leftOrThrow)
Expand Down Expand Up @@ -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()

Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
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

interface UserRepository {
fun getUsers(): Flow<Either<UserError, List<User>>>

suspend fun refresh(): Either<UserError, Unit>
context(EffectScope<UserError>)
suspend fun refresh()

suspend fun remove(user: User): Either<UserError, Unit>
context(EffectScope<UserError>)
suspend fun remove(user: User)

suspend fun add(user: User): Either<UserError, Unit>
context(EffectScope<UserError>)
suspend fun add(user: User)

suspend fun search(query: String): Either<UserError, List<User>>
context(EffectScope<UserError>)
suspend fun search(query: String): List<User>
}
Original file line number Diff line number Diff line change
@@ -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<UserError, Unit> = userRepository.add(user)
context(EffectScope<UserError>)
suspend operator fun invoke(user: User): Unit = userRepository.add(user)
}
Original file line number Diff line number Diff line change
@@ -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<UserError, Unit> = userRepository.refresh()
context(EffectScope<UserError>)
suspend operator fun invoke(): Unit = userRepository.refresh()
}
Original file line number Diff line number Diff line change
@@ -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<UserError, Unit> = userRepository.remove(user)
context(EffectScope<UserError>)
suspend operator fun invoke(user: User): Unit = userRepository.remove(user)
}
Original file line number Diff line number Diff line change
@@ -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<UserError, List<User>> =
context(EffectScope<UserError>)
suspend operator fun invoke(query: String): List<User> =
userRepository.search(query)
}
Loading