Skip to content

Commit 483a21a

Browse files
authored
Update deps, replace Validateds with Eithers (#188)
* update deps, use Either instead of Validated * fix tests * fix lint * simplify * rename * wip
1 parent f241a54 commit 483a21a

File tree

34 files changed

+275
-239
lines changed

34 files changed

+275
-239
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
# MVI-Coroutines-Flow
2+
## MVI-Coroutines-Flow-Clean-Architecture
3+
## MVI-Coroutines-Flow-Clean-Architecture-ArrowKt
4+
## MVI-Coroutines-Flow-Clean-Architecture-ArrowKt-KoinDI
25
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
36
[![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-)
47
<!-- ALL-CONTRIBUTORS-BADGE:END -->
@@ -9,7 +12,7 @@
912
[![Qodana](https://github.com/Kotlin-Android-Open-Source/MVI-Coroutines-Flow/actions/workflows/qodana.yml/badge.svg)](https://github.com/Kotlin-Android-Open-Source/MVI-Coroutines-Flow/actions/workflows/qodana.yml)
1013
[![Validate Gradle Wrapper](https://github.com/Kotlin-Android-Open-Source/MVI-Coroutines-Flow/actions/workflows/gradle-wrapper-validation.yml/badge.svg)](https://github.com/Kotlin-Android-Open-Source/MVI-Coroutines-Flow/actions/workflows/gradle-wrapper-validation.yml)
1114
[![API](https://img.shields.io/badge/API-21%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=21)
12-
[![Kotlin](https://img.shields.io/badge/kotlin-1.7.20-blue.svg?logo=kotlin)](http://kotlinlang.org)
15+
[![Kotlin](https://img.shields.io/badge/kotlin-1.8.10-blue.svg?logo=kotlin)](http://kotlinlang.org)
1316
[![Hits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2FKotlin-Android-Open-Source%2FMVI-Coroutines-Flow&count_bg=%2379C83D&title_bg=%23555555&icon=&icon_color=%23E7E7E7&title=hits&edge_flat=false)](https://hits.seeyoufarm.com)
1417
[![License: MIT](https://img.shields.io/badge/License-MIT-purple.svg)](https://opensource.org/licenses/MIT)
1518
[![Gitter](https://badges.gitter.im/Kotlin-Android-Open-Source/community.svg)](https://gitter.im/Kotlin-Android-Open-Source/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ buildscript {
1717
classpath("com.android.tools.build:gradle:7.4.1")
1818
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
1919
classpath("com.diffplug.spotless:spotless-plugin-gradle:6.15.0")
20-
classpath("dev.ahmedmourad.nocopy:nocopy-gradle-plugin:1.4.0")
20+
classpath("dev.drewhamilton.poko:poko-gradle-plugin:0.11.0")
2121
classpath("org.jacoco:org.jacoco.core:0.8.8")
2222
classpath("com.vanniktech:gradle-android-junit-jacoco-plugin:0.17.0-SNAPSHOT")
2323
classpath("com.github.ben-manes:gradle-versions-plugin:0.45.0")

buildSrc/src/main/kotlin/deps.kt

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@ import org.gradle.plugin.use.PluginDependenciesSpec
77
import org.gradle.plugin.use.PluginDependencySpec
88

99
const val ktlintVersion = "0.46.1"
10-
const val kotlinVersion = "1.7.20"
10+
const val kotlinVersion = "1.8.10"
1111

1212
object appConfig {
1313
const val applicationId = "com.hoc.flowmvi"
1414

15-
const val compileSdkVersion = 32
16-
const val buildToolsVersion = "32.0.0"
15+
const val compileSdkVersion = 33
16+
const val buildToolsVersion = "33.0.1"
1717

1818
const val minSdkVersion = 21
19-
const val targetSdkVersion = 32
19+
const val targetSdkVersion = 33
2020

2121
private const val MAJOR = 2
2222
private const val MINOR = 1
@@ -28,16 +28,16 @@ object appConfig {
2828
object deps {
2929
object androidx {
3030
const val appCompat = "androidx.appcompat:appcompat:1.4.2"
31-
const val coreKtx = "androidx.core:core-ktx:1.8.0"
31+
const val coreKtx = "androidx.core:core-ktx:1.9.0"
3232
const val constraintLayout = "androidx.constraintlayout:constraintlayout:2.1.4"
3333
const val recyclerView = "androidx.recyclerview:recyclerview:1.2.1"
3434
const val swipeRefreshLayout = "androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01"
35-
const val material = "com.google.android.material:material:1.6.1"
35+
const val material = "com.google.android.material:material:1.8.0"
3636
const val startup = "androidx.startup:startup-runtime:1.1.1"
3737
}
3838

3939
object lifecycle {
40-
private const val version = "2.5.0"
40+
private const val version = "2.5.1"
4141

4242
const val viewModelKtx = "androidx.lifecycle:lifecycle-viewmodel-ktx:$version" // viewModelScope
4343
const val runtimeKtx = "androidx.lifecycle:lifecycle-runtime-ktx:$version" // lifecycleScope
@@ -49,7 +49,7 @@ object deps {
4949
const val converterMoshi = "com.squareup.retrofit2:converter-moshi:2.9.0"
5050
const val loggingInterceptor = "com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.10"
5151
const val moshiKotlin = "com.squareup.moshi:moshi-kotlin:1.13.0"
52-
const val leakCanary = "com.squareup.leakcanary:leakcanary-android:2.9.1"
52+
const val leakCanary = "com.squareup.leakcanary:leakcanary-android:2.10"
5353
}
5454

5555
object coroutines {
@@ -61,7 +61,7 @@ object deps {
6161
}
6262

6363
object koin {
64-
private const val version = "3.2.0"
64+
private const val version = "3.3.3"
6565

6666
const val core = "io.insert-koin:koin-core:$version"
6767
const val android = "io.insert-koin:koin-android:$version"
@@ -75,7 +75,7 @@ object deps {
7575
const val timber = "com.jakewharton.timber:timber:5.0.1"
7676

7777
object arrow {
78-
private const val version = "1.1.3"
78+
private const val version = "1.1.6-alpha.28"
7979
const val core = "io.arrow-kt:arrow-core:$version"
8080
}
8181

@@ -91,7 +91,7 @@ object deps {
9191
}
9292
}
9393

94-
const val mockk = "io.mockk:mockk:1.12.4"
94+
const val mockk = "io.mockk:mockk:1.13.4"
9595
const val kotlinJUnit = "org.jetbrains.kotlin:kotlin-test-junit:$kotlinVersion"
9696
}
9797
}
@@ -105,7 +105,7 @@ inline val PDsS.kotlinAndroid: PDS get() = id("kotlin-android")
105105
inline val PDsS.kotlin: PDS get() = id("kotlin")
106106
inline val PDsS.kotlinKapt: PDS get() = id("kotlin-kapt")
107107
inline val PDsS.kotlinParcelize: PDS get() = id("kotlin-parcelize")
108-
inline val PDsS.nocopyPlugin: PDS get() = id("dev.ahmedmourad.nocopy.nocopy-gradle-plugin")
108+
inline val PDsS.pokoPlugin: PDS get() = id("dev.drewhamilton.poko")
109109

110110
inline val DependencyHandler.domain get() = project(":domain")
111111
inline val DependencyHandler.core get() = project(":core")
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.hoc.flowmvi.core
2+
3+
import arrow.core.Either
4+
import arrow.core.left
5+
import arrow.core.right
6+
import com.hoc.flowmvi.core.NonEmptySet.Companion.toNonEmptySetOrNull
7+
import kotlin.contracts.ExperimentalContracts
8+
import kotlin.contracts.InvocationKind
9+
import kotlin.contracts.contract
10+
11+
/**
12+
* A typealias for [Either] with [NonEmptySet] as the left side.
13+
*/
14+
typealias EitherNes<E, A> = Either<NonEmptySet<E>, A>
15+
16+
@Suppress("NOTHING_TO_INLINE")
17+
inline fun <A> A.rightNes(): EitherNes<Nothing, A> = this.right()
18+
19+
@Suppress("NOTHING_TO_INLINE")
20+
inline fun <E> E.leftNes(): EitherNes<E, Nothing> =
21+
NonEmptySet.of(this).left()
22+
23+
@OptIn(ExperimentalContracts::class)
24+
inline fun <E, A, B, C, Z> Either.Companion.zipOrAccumulateNonEmptySet(
25+
a: EitherNes<E, A>,
26+
b: EitherNes<E, B>,
27+
c: EitherNes<E, C>,
28+
transform: (A, B, C) -> Z,
29+
): EitherNes<E, Z> {
30+
contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) }
31+
32+
return if (
33+
a is Either.Right &&
34+
b is Either.Right &&
35+
c is Either.Right
36+
) {
37+
Either.Right(
38+
transform(
39+
a.value,
40+
b.value,
41+
c.value,
42+
)
43+
)
44+
} else {
45+
Either.Left(
46+
buildSet(capacity = a.count + b.count + c.count) {
47+
if (a is Either.Left) this.addAll(a.value)
48+
if (b is Either.Left) this.addAll(b.value)
49+
if (c is Either.Left) this.addAll(c.value)
50+
}.toNonEmptySetOrNull()!!
51+
)
52+
}
53+
}
54+
55+
@PublishedApi
56+
internal inline val <L, R> Either<L, R>.count: Int get() = if (isRight()) 1 else 0

core/src/main/java/com/hoc/flowmvi/core/ValidatedNes.kt

Lines changed: 0 additions & 22 deletions
This file was deleted.
Lines changed: 43 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
package com.hoc.flowmvi.data
22

33
import arrow.core.Either.Companion.catch as catchEither
4-
import arrow.core.continuations.either
4+
import arrow.core.getOrElse
55
import arrow.core.left
66
import arrow.core.leftWiden
7+
import arrow.core.raise.either
78
import arrow.core.right
8-
import arrow.core.valueOr
9+
import com.hoc.flowmvi.core.EitherNes
910
import com.hoc.flowmvi.core.Mapper
10-
import com.hoc.flowmvi.core.ValidatedNes
1111
import com.hoc.flowmvi.core.dispatchers.AppCoroutineDispatchers
1212
import com.hoc.flowmvi.data.remote.UserApiService
1313
import com.hoc.flowmvi.data.remote.UserBody
@@ -16,6 +16,7 @@ import com.hoc.flowmvi.domain.model.User
1616
import com.hoc.flowmvi.domain.model.UserError
1717
import com.hoc.flowmvi.domain.model.UserValidationError
1818
import com.hoc.flowmvi.domain.repository.UserRepository
19+
import com.hoc081098.flowext.flowFromSuspend
1920
import com.hoc081098.flowext.retryWithExponentialBackoff
2021
import java.io.IOException
2122
import kotlin.time.Duration.Companion.milliseconds
@@ -24,7 +25,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
2425
import kotlinx.coroutines.FlowPreview
2526
import kotlinx.coroutines.flow.Flow
2627
import kotlinx.coroutines.flow.MutableSharedFlow
27-
import kotlinx.coroutines.flow.asFlow
2828
import kotlinx.coroutines.flow.catch
2929
import kotlinx.coroutines.flow.first
3030
import kotlinx.coroutines.flow.flatMapConcat
@@ -40,7 +40,7 @@ import timber.log.Timber
4040
internal class UserRepositoryImpl(
4141
private val userApiService: UserApiService,
4242
private val dispatchers: AppCoroutineDispatchers,
43-
private val responseToDomain: Mapper<UserResponse, ValidatedNes<UserValidationError, User>>,
43+
private val responseToDomain: Mapper<UserResponse, EitherNes<UserValidationError, User>>,
4444
private val domainToBody: Mapper<User, UserBody>,
4545
private val errorMapper: Mapper<Throwable, UserError>,
4646
) : UserRepository {
@@ -51,29 +51,21 @@ internal class UserRepositoryImpl(
5151
class Added(val user: User) : Change()
5252
}
5353

54-
private val responseToDomainThrows: (UserResponse) -> User = { response ->
55-
responseToDomain(response).let { validated ->
56-
validated.valueOr {
57-
val t = UserError.ValidationFailed(it.toSet())
58-
logError(t, "Map $response to user")
59-
throw t
60-
}
61-
}
62-
}
63-
6454
private val changesFlow = MutableSharedFlow<Change>(extraBufferCapacity = 64)
65-
6655
private suspend inline fun sendChange(change: Change) = changesFlow.emit(change)
6756

68-
@Suppress("NOTHING_TO_INLINE")
69-
private inline fun logError(t: Throwable, message: String) = Timber.tag(TAG).e(t, message)
70-
71-
private fun getUsersFromRemote(): Flow<List<User>> = suspend {
57+
private fun getUsersFromRemote(): Flow<List<User>> = flowFromSuspend {
7258
Timber.d("[USER_REPO] getUsersFromRemote ...")
59+
7360
userApiService
7461
.getUsers()
75-
.map(responseToDomainThrows)
76-
}.asFlow()
62+
.map { response ->
63+
responseToDomain(response)
64+
.mapLeft(UserError::ValidationFailed)
65+
.onLeft { logError(it, "Map $response to user") }
66+
.getOrElse { throw it }
67+
}
68+
}
7769
.retryWithExponentialBackoff(
7870
maxAttempt = 2,
7971
initialDelay = 500.milliseconds,
@@ -100,50 +92,65 @@ internal class UserRepositoryImpl(
10092
}
10193

10294
override suspend fun refresh() = catchEither { getUsersFromRemote().first() }
103-
.tap { sendChange(Change.Refreshed(it)) }
95+
.onRight { sendChange(Change.Refreshed(it)) }
10496
.map { }
105-
.tapLeft { logError(it, "refresh") }
97+
.onLeft { logError(it, "refresh") }
10698
.mapLeft(errorMapper)
10799

108-
override suspend fun remove(user: User) = either<UserError, Unit> {
100+
override suspend fun remove(user: User) = either {
109101
withContext(dispatchers.io) {
110102
val response = catchEither { userApiService.remove(user.id) }
111-
.tapLeft { logError(it, "remove user=$user") }
103+
.onLeft { logError(it, "remove user=$user") }
112104
.mapLeft(errorMapper)
113105
.bind()
114106

115107
val deleted = responseToDomain(response)
116-
.mapLeft { UserError.ValidationFailed(it.toSet()) }
117-
.tapInvalid { logError(it, "remove user=$user") }
108+
.mapLeft { UserError.ValidationFailed(it) }
109+
.onLeft { logError(it, "remove user=$user") }
118110
.bind()
119111

120112
sendChange(Change.Removed(deleted))
121113
}
122114
}
123115

124-
override suspend fun add(user: User) = either<UserError, Unit> {
116+
override suspend fun add(user: User) = either {
125117
withContext(dispatchers.io) {
126118
val response = catchEither { userApiService.add(domainToBody(user)) }
127-
.tapLeft { logError(it, "add user=$user") }
119+
.onLeft { logError(it, "add user=$user") }
128120
.mapLeft(errorMapper)
129121
.bind()
130122

131123
val added = responseToDomain(response)
132-
.mapLeft { UserError.ValidationFailed(it.toSet()) }
133-
.tapInvalid { logError(it, "add user=$user") }
124+
.mapLeft { UserError.ValidationFailed(it) }
125+
.onLeft { logError(it, "add user=$user") }
134126
.bind()
135127

136128
sendChange(Change.Added(added))
137129
}
138130
}
139131

140-
override suspend fun search(query: String) = withContext(dispatchers.io) {
141-
catchEither { userApiService.search(query).map(responseToDomainThrows) }
142-
.tapLeft { logError(it, "search query=$query") }
143-
.mapLeft(errorMapper)
132+
override suspend fun search(query: String) = either {
133+
withContext(dispatchers.io) {
134+
val userResponses = catchEither { userApiService.search(query) }
135+
.onLeft { logError(it, "search query=$query") }
136+
.mapLeft(errorMapper)
137+
.bind()
138+
139+
val users = userResponses.map { userResponse ->
140+
responseToDomain(userResponse)
141+
.mapLeft(UserError::ValidationFailed)
142+
.onLeft { logError(it, "search query=$query") }
143+
.bind()
144+
}
145+
146+
users
147+
}
144148
}
145149

146150
private companion object {
151+
@Suppress("NOTHING_TO_INLINE")
152+
private inline fun logError(t: Throwable, message: String) = Timber.tag(TAG).e(t, message)
153+
147154
private val TAG = UserRepositoryImpl::class.java.simpleName
148155
}
149156
}

data/src/main/java/com/hoc/flowmvi/data/mapper/UserErrorMapper.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import arrow.core.nonFatalOrThrow
44
import com.hoc.flowmvi.core.Mapper
55
import com.hoc.flowmvi.data.remote.ErrorResponse
66
import com.hoc.flowmvi.domain.model.UserError
7+
import com.hoc.flowmvi.domain.model.UserValidationError
78
import com.squareup.moshi.JsonAdapter
89
import java.io.IOException
910
import java.net.SocketException
@@ -48,7 +49,9 @@ internal class UserErrorMapper(private val errorResponseJsonAdapter: JsonAdapter
4849
"internal-error" -> UserError.ServerError
4950
"invalid-id" -> UserError.InvalidId(id = errorResponse.data as String)
5051
"user-not-found" -> UserError.UserNotFound(id = errorResponse.data as String)
51-
"validation-failed" -> UserError.ValidationFailed(errors = emptySet())
52+
"validation-failed" -> UserError.ValidationFailed(
53+
errors = UserValidationError.VALUES_SET // TODO(hoc081098): Map validation errors from server response
54+
)
5255
else -> UserError.Unexpected
5356
}
5457
}

data/src/main/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapper.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
package com.hoc.flowmvi.data.mapper
22

3+
import com.hoc.flowmvi.core.EitherNes
34
import com.hoc.flowmvi.core.Mapper
4-
import com.hoc.flowmvi.core.ValidatedNes
55
import com.hoc.flowmvi.data.remote.UserResponse
66
import com.hoc.flowmvi.domain.model.User
77
import com.hoc.flowmvi.domain.model.UserValidationError
88

99
internal class UserResponseToUserDomainMapper :
10-
Mapper<UserResponse, ValidatedNes<UserValidationError, User>> {
11-
override fun invoke(response: UserResponse): ValidatedNes<UserValidationError, User> {
10+
Mapper<UserResponse, EitherNes<UserValidationError, User>> {
11+
override fun invoke(response: UserResponse): EitherNes<UserValidationError, User> {
1212
return User.create(
1313
id = response.id,
1414
avatar = response.avatar,

0 commit comments

Comments
 (0)