Skip to content

Commit e2d79da

Browse files
committed
wip store
1 parent 5d1e2c6 commit e2d79da

File tree

4 files changed

+142
-74
lines changed

4 files changed

+142
-74
lines changed

data/src/main/java/com/hoc/flowmvi/data/DataModule.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,21 @@ val dataModule =
6363
responseToDomain = get<UserResponseToUserDomainMapper>(),
6464
domainToBody = get<UserDomainToUserBodyMapper>(),
6565
errorMapper = get<UserErrorMapper>(),
66+
userRepositoryStore = get(),
67+
)
68+
}
69+
70+
single<UserRepositoryStore> {
71+
DefaultUserRepositoryStore(
72+
errorMapper = get<UserErrorMapper>(),
73+
fetcher = get(),
74+
)
75+
}
76+
77+
factory<UserRepositoryStore.Fetcher> {
78+
UserFetcherWithExponentialBackoffRetry(
79+
userApiService = get(),
80+
responseToDomain = get<UserResponseToUserDomainMapper>(),
6681
)
6782
}
6883
}

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

Lines changed: 6 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
package com.hoc.flowmvi.data
22

33
import arrow.core.Either.Companion.catch as catchEither
4-
import arrow.core.getOrElse
5-
import arrow.core.left
64
import arrow.core.raise.either
7-
import arrow.core.right
85
import com.hoc.flowmvi.core.EitherNes
96
import com.hoc.flowmvi.core.Mapper
107
import com.hoc.flowmvi.core.dispatchers.AppCoroutineDispatchers
@@ -15,93 +12,28 @@ import com.hoc.flowmvi.domain.model.User
1512
import com.hoc.flowmvi.domain.model.UserError
1613
import com.hoc.flowmvi.domain.model.UserValidationError
1714
import com.hoc.flowmvi.domain.repository.UserRepository
18-
import com.hoc081098.flowext.FlowExtPreview
19-
import com.hoc081098.flowext.catchAndReturn
20-
import com.hoc081098.flowext.flowFromSuspend
21-
import com.hoc081098.flowext.retryWithExponentialBackoff
22-
import com.hoc081098.flowext.scanWith
23-
import java.io.IOException
24-
import kotlin.time.Duration.Companion.milliseconds
2515
import kotlin.time.ExperimentalTime
2616
import kotlinx.coroutines.ExperimentalCoroutinesApi
2717
import kotlinx.coroutines.FlowPreview
28-
import kotlinx.coroutines.flow.MutableSharedFlow
29-
import kotlinx.coroutines.flow.first
30-
import kotlinx.coroutines.flow.map
31-
import kotlinx.coroutines.flow.onEach
3218
import kotlinx.coroutines.withContext
3319
import timber.log.Timber
3420

3521
@FlowPreview
3622
@ExperimentalTime
3723
@ExperimentalCoroutinesApi
38-
internal class UserRepositoryImpl(
24+
internal class UserRepositoryImpl constructor(
3925
private val userApiService: UserApiService,
4026
private val dispatchers: AppCoroutineDispatchers,
4127
private val responseToDomain: Mapper<UserResponse, EitherNes<UserValidationError, User>>,
4228
private val domainToBody: Mapper<User, UserBody>,
4329
private val errorMapper: Mapper<Throwable, UserError>,
30+
private val userRepositoryStore: UserRepositoryStore,
4431
) : UserRepository {
45-
private sealed interface Change {
46-
class Removed(
47-
val removed: User,
48-
) : Change
49-
50-
class Refreshed(
51-
val user: List<User>,
52-
) : Change
53-
54-
class Added(
55-
val user: User,
56-
) : Change
57-
}
58-
59-
private val changesFlow = MutableSharedFlow<Change>(extraBufferCapacity = 64)
60-
61-
private suspend inline fun sendChange(change: Change) = changesFlow.emit(change)
62-
63-
private suspend fun getUsersFromRemoteWithRetry(): List<User> =
64-
flowFromSuspend {
65-
Timber.d("[USER_REPO] getUsersFromRemote ...")
66-
67-
userApiService
68-
.getUsers()
69-
.map { response ->
70-
responseToDomain(response)
71-
.mapLeft(UserError::ValidationFailed)
72-
.onLeft { logError(it, "Map $response to user") }
73-
.getOrElse { throw it }
74-
}
75-
}.retryWithExponentialBackoff(
76-
maxAttempt = 2,
77-
initialDelay = 500.milliseconds,
78-
factor = 2.0,
79-
) { it is IOException }
80-
.first()
81-
82-
@OptIn(FlowExtPreview::class)
8332
override fun getUsers() =
84-
changesFlow
85-
.onEach { Timber.d("[USER_REPO] Change=$it") }
86-
.scanWith(::getUsersFromRemoteWithRetry) { acc, change ->
87-
when (change) {
88-
is Change.Removed -> acc.filter { it.id != change.removed.id }
89-
is Change.Refreshed -> change.user
90-
is Change.Added -> acc + change.user
91-
}
92-
}.onEach { Timber.d("[USER_REPO] Emit users.size=${it.size} ") }
93-
.map { it.right() }
94-
.catchAndReturn {
95-
logError(it, "getUsers")
96-
errorMapper(it).left()
97-
}
33+
userRepositoryStore.observeUsersFlow()
9834

9935
override suspend fun refresh() =
100-
catchEither { getUsersFromRemoteWithRetry() }
101-
.onRight { sendChange(Change.Refreshed(it)) }
102-
.map {}
103-
.onLeft { logError(it, "refresh") }
104-
.mapLeft(errorMapper)
36+
userRepositoryStore.refresh()
10537

10638
override suspend fun remove(user: User) =
10739
either {
@@ -118,7 +50,7 @@ internal class UserRepositoryImpl(
11850
.onLeft { logError(it, "remove user=$user") }
11951
.bind()
12052

121-
sendChange(Change.Removed(deleted))
53+
userRepositoryStore.sendChange(UserRepositoryStore.Change.Removed(deleted))
12254
}
12355
}
12456

@@ -137,7 +69,7 @@ internal class UserRepositoryImpl(
13769
.onLeft { logError(it, "add user=$user") }
13870
.bind()
13971

140-
sendChange(Change.Added(added))
72+
userRepositoryStore.sendChange(UserRepositoryStore.Change.Added(added))
14173
}
14274
}
14375

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package com.hoc.flowmvi.data
2+
3+
import arrow.core.Either
4+
import arrow.core.getOrElse
5+
import arrow.core.left
6+
import arrow.core.right
7+
import com.hoc.flowmvi.core.EitherNes
8+
import com.hoc.flowmvi.core.Mapper
9+
import arrow.core.Either.Companion.catch as catchEither
10+
import com.hoc.flowmvi.data.UserRepositoryStore.Change
11+
import com.hoc.flowmvi.data.remote.UserApiService
12+
import com.hoc.flowmvi.data.remote.UserResponse
13+
import com.hoc.flowmvi.domain.model.User
14+
import com.hoc.flowmvi.domain.model.UserError
15+
import com.hoc.flowmvi.domain.model.UserValidationError
16+
import com.hoc081098.flowext.FlowExtPreview
17+
import com.hoc081098.flowext.catchAndReturn
18+
import com.hoc081098.flowext.flowFromSuspend
19+
import com.hoc081098.flowext.retryWithExponentialBackoff
20+
import com.hoc081098.flowext.scanWith
21+
import java.io.IOException
22+
import kotlin.time.Duration.Companion.milliseconds
23+
import kotlinx.coroutines.flow.Flow
24+
import kotlinx.coroutines.flow.MutableSharedFlow
25+
import kotlinx.coroutines.flow.first
26+
import kotlinx.coroutines.flow.map
27+
import kotlinx.coroutines.flow.onEach
28+
import timber.log.Timber
29+
30+
internal interface UserRepositoryStore {
31+
sealed interface Change {
32+
class Removed(val removed: User) : Change
33+
34+
class Refreshed(val user: List<User>) : Change
35+
36+
class Added(val user: User) : Change
37+
}
38+
39+
interface Fetcher {
40+
suspend fun fetch(): List<User>
41+
}
42+
43+
val errorMapper: Mapper<Throwable, UserError>
44+
val fetcher: Fetcher
45+
46+
suspend fun sendChange(change: Change)
47+
suspend fun refresh(): Either<UserError, Unit>
48+
fun observeUsersFlow(): Flow<Either<UserError, List<User>>>
49+
}
50+
51+
internal class DefaultUserRepositoryStore(
52+
override val errorMapper: Mapper<Throwable, UserError>,
53+
override val fetcher: UserRepositoryStore.Fetcher,
54+
) : UserRepositoryStore {
55+
private val changesFlow = MutableSharedFlow<Change>(extraBufferCapacity = 64)
56+
57+
override suspend fun sendChange(change: Change) =
58+
changesFlow.emit(change)
59+
60+
@OptIn(FlowExtPreview::class)
61+
override fun observeUsersFlow() =
62+
changesFlow
63+
.onEach { Timber.tag(TAG).d("Change=$it") }
64+
.scanWith(fetcher::fetch) { acc, change ->
65+
when (change) {
66+
is Change.Removed -> acc.filter { it.id != change.removed.id }
67+
is Change.Refreshed -> change.user
68+
is Change.Added -> acc + change.user
69+
}
70+
}
71+
.onEach { Timber.tag(TAG).d("Emit users.size=${it.size} ") }
72+
.map { it.right() }
73+
.catchAndReturn {
74+
Timber.tag(TAG).e(it, "getUsers")
75+
errorMapper(it).left()
76+
}
77+
78+
override suspend fun refresh() =
79+
catchEither { fetcher.fetch() }
80+
.onRight { sendChange(Change.Refreshed(it)) }
81+
.map {}
82+
.onLeft { Timber.tag(TAG).e(it, "refresh") }
83+
.mapLeft(errorMapper)
84+
85+
private companion object {
86+
private val TAG = DefaultUserRepositoryStore::class.java.simpleName
87+
}
88+
}
89+
90+
91+
internal class UserFetcherWithExponentialBackoffRetry(
92+
private val userApiService: UserApiService,
93+
private val responseToDomain: Mapper<UserResponse, EitherNes<UserValidationError, User>>,
94+
) : UserRepositoryStore.Fetcher {
95+
override suspend fun fetch(): List<User> =
96+
flowFromSuspend {
97+
Timber.d("[USER_REPO] getUsersFromRemote ...")
98+
99+
userApiService
100+
.getUsers()
101+
.map { response ->
102+
responseToDomain(response)
103+
.mapLeft(UserError::ValidationFailed)
104+
.onLeft { Timber.tag(TAG).d(it, "Map $response to user") }
105+
.getOrElse { throw it }
106+
}
107+
}.retryWithExponentialBackoff(
108+
maxAttempt = 2,
109+
initialDelay = 500.milliseconds,
110+
factor = 2.0,
111+
) { it is IOException }
112+
.first()
113+
114+
private companion object {
115+
private val TAG = UserFetcherWithExponentialBackoffRetry::class.java.simpleName
116+
}
117+
}

data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,13 +113,15 @@ class UserRepositoryImplTest {
113113
private lateinit var responseToDomain: Mapper<UserResponse, EitherNes<UserValidationError, User>>
114114
private lateinit var domainToBody: Mapper<User, UserBody>
115115
private lateinit var errorMapper: Mapper<Throwable, UserError>
116+
private lateinit var userRepositoryStore: UserRepositoryStore
116117

117118
@BeforeTest
118119
fun setup() {
119120
userApiService = mockk()
120121
responseToDomain = mockk()
121122
domainToBody = mockk()
122123
errorMapper = mockk()
124+
userRepositoryStore = mockk()
123125

124126
repo =
125127
UserRepositoryImpl(
@@ -128,6 +130,7 @@ class UserRepositoryImplTest {
128130
responseToDomain = responseToDomain,
129131
domainToBody = domainToBody,
130132
errorMapper = errorMapper,
133+
userRepositoryStore = userRepositoryStore,
131134
)
132135
}
133136

@@ -138,6 +141,7 @@ class UserRepositoryImplTest {
138141
responseToDomain,
139142
domainToBody,
140143
errorMapper,
144+
userRepositoryStore,
141145
)
142146
clearAllMocks()
143147
}

0 commit comments

Comments
 (0)