Skip to content

Commit ca58d92

Browse files
committed
Refactor: RoutineRepository를 Flow 기반으로 전환 및 Optimistic Update 책임 이동
1 parent 822a734 commit ca58d92

6 files changed

Lines changed: 96 additions & 73 deletions

File tree

Lines changed: 93 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,125 @@
11
package com.threegap.bitnagil.data.routine.repositoryImpl
22

3+
import com.threegap.bitnagil.data.di.IoDispatcher
4+
import com.threegap.bitnagil.data.routine.datasource.RoutineLocalDataSource
35
import com.threegap.bitnagil.data.routine.datasource.RoutineRemoteDataSource
46
import com.threegap.bitnagil.data.routine.model.request.toDto
57
import com.threegap.bitnagil.data.routine.model.response.toDomain
68
import com.threegap.bitnagil.domain.routine.model.Routine
9+
import com.threegap.bitnagil.domain.routine.model.RoutineCompletionInfo
710
import com.threegap.bitnagil.domain.routine.model.RoutineCompletionInfos
811
import com.threegap.bitnagil.domain.routine.model.RoutineEditInfo
912
import com.threegap.bitnagil.domain.routine.model.RoutineRegisterInfo
1013
import com.threegap.bitnagil.domain.routine.model.RoutineSchedule
11-
import com.threegap.bitnagil.domain.routine.model.WriteRoutineEvent
1214
import com.threegap.bitnagil.domain.routine.repository.RoutineRepository
15+
import kotlinx.coroutines.CoroutineDispatcher
16+
import kotlinx.coroutines.CoroutineScope
17+
import kotlinx.coroutines.FlowPreview
18+
import kotlinx.coroutines.SupervisorJob
19+
import kotlinx.coroutines.channels.BufferOverflow
1320
import kotlinx.coroutines.flow.Flow
1421
import kotlinx.coroutines.flow.MutableSharedFlow
15-
import kotlinx.coroutines.flow.asSharedFlow
22+
import kotlinx.coroutines.flow.debounce
23+
import kotlinx.coroutines.flow.emitAll
24+
import kotlinx.coroutines.flow.filterNotNull
25+
import kotlinx.coroutines.flow.flow
26+
import kotlinx.coroutines.flow.onEach
27+
import kotlinx.coroutines.launch
1628
import javax.inject.Inject
29+
import javax.inject.Singleton
1730

31+
@Singleton
1832
class RoutineRepositoryImpl @Inject constructor(
1933
private val routineRemoteDataSource: RoutineRemoteDataSource,
34+
private val routineLocalDataSource: RoutineLocalDataSource,
35+
@param:IoDispatcher private val dispatcher: CoroutineDispatcher,
2036
) : RoutineRepository {
21-
override suspend fun fetchWeeklyRoutines(startDate: String, endDate: String): Result<RoutineSchedule> =
37+
38+
private val repositoryScope = CoroutineScope(SupervisorJob() + dispatcher)
39+
private val pendingChangesByDate = mutableMapOf<String, MutableMap<String, RoutineCompletionInfo>>()
40+
private val originalStatesByDate = mutableMapOf<String, MutableMap<String, RoutineCompletionInfo>>()
41+
private val syncTrigger = MutableSharedFlow<String>(
42+
extraBufferCapacity = 1,
43+
onBufferOverflow = BufferOverflow.DROP_LATEST,
44+
)
45+
46+
init {
47+
@OptIn(FlowPreview::class)
48+
repositoryScope.launch {
49+
syncTrigger
50+
.debounce(500L)
51+
.collect { dateKey -> flushPendingChanges(dateKey) }
52+
}
53+
}
54+
55+
override fun observeWeeklyRoutines(startDate: String, endDate: String): Flow<RoutineSchedule> = flow {
56+
if (routineLocalDataSource.lastFetchRange != (startDate to endDate)) {
57+
routineLocalDataSource.clearCache()
58+
fetchAndSave(startDate, endDate)
59+
}
60+
emitAll(
61+
routineLocalDataSource.routineSchedule
62+
.onEach { if (it == null) fetchAndSave(startDate, endDate) }
63+
.filterNotNull(),
64+
)
65+
}
66+
67+
private suspend fun fetchAndSave(startDate: String, endDate: String) {
2268
routineRemoteDataSource.fetchWeeklyRoutines(startDate, endDate)
23-
.map { it.toDomain() }
69+
.onSuccess { routineLocalDataSource.saveSchedule(it.toDomain(), startDate, endDate) }
70+
.onFailure { throw it }
71+
}
72+
73+
override suspend fun applyRoutineToggle(dateKey: String, routineId: String, completionInfo: RoutineCompletionInfo) {
74+
if (originalStatesByDate[dateKey]?.containsKey(routineId) != true) {
75+
routineLocalDataSource.getCompletionInfo(dateKey, routineId)?.let {
76+
originalStatesByDate.getOrPut(dateKey) { mutableMapOf() }[routineId] = it
77+
}
78+
}
79+
routineLocalDataSource.applyOptimisticToggle(dateKey, routineId, completionInfo)
80+
pendingChangesByDate.getOrPut(dateKey) { mutableMapOf() }[routineId] = completionInfo
81+
syncTrigger.emit(dateKey)
82+
}
2483

25-
override suspend fun syncRoutineCompletion(routineCompletionInfos: RoutineCompletionInfos): Result<Unit> =
26-
routineRemoteDataSource.syncRoutineCompletion(routineCompletionInfos.toDto())
84+
private suspend fun flushPendingChanges(dateKey: String) {
85+
val snapshot = pendingChangesByDate.remove(dateKey)
86+
val originals = originalStatesByDate.remove(dateKey)
87+
val actualChanges = snapshot?.filter { (routineId, pending) -> originals?.get(routineId) != pending }
88+
if (actualChanges.isNullOrEmpty()) return
89+
90+
val syncRequest = RoutineCompletionInfos(routineCompletionInfos = actualChanges.values.toList())
91+
routineRemoteDataSource.syncRoutineCompletion(syncRequest.toDto())
92+
.onFailure {
93+
val range = routineLocalDataSource.lastFetchRange ?: return
94+
fetchAndSave(range.first, range.second)
95+
}
96+
}
97+
98+
private suspend fun refreshCache() {
99+
val range = routineLocalDataSource.lastFetchRange ?: return
100+
fetchAndSave(range.first, range.second)
101+
}
27102

28103
override suspend fun getRoutine(routineId: String): Result<Routine> =
29104
routineRemoteDataSource.getRoutine(routineId).map { it.toDomain() }
30105

31106
override suspend fun deleteRoutine(routineId: String): Result<Unit> =
32-
routineRemoteDataSource.deleteRoutine(routineId)
107+
routineRemoteDataSource.deleteRoutine(routineId).also {
108+
if (it.isSuccess) refreshCache()
109+
}
33110

34111
override suspend fun deleteRoutineForDay(routineId: String): Result<Unit> =
35-
routineRemoteDataSource.deleteRoutineForDay(routineId)
36-
37-
override suspend fun registerRoutine(routineRegisterInfo: RoutineRegisterInfo): Result<Unit> {
38-
val request = routineRegisterInfo.toDto()
39-
return routineRemoteDataSource.registerRoutine(request).also {
40-
if (it.isSuccess) {
41-
_writeRoutineEventFlow.emit(WriteRoutineEvent.AddRoutine)
42-
}
112+
routineRemoteDataSource.deleteRoutineForDay(routineId).also {
113+
if (it.isSuccess) refreshCache()
43114
}
44-
}
45115

46-
override suspend fun editRoutine(routineEditInfo: RoutineEditInfo): Result<Unit> {
47-
val request = routineEditInfo.toDto()
48-
return routineRemoteDataSource.editRoutine(request).also {
49-
if (it.isSuccess) {
50-
_writeRoutineEventFlow.emit(WriteRoutineEvent.EditRoutine(routineEditInfo.id))
51-
}
116+
override suspend fun registerRoutine(routineRegisterInfo: RoutineRegisterInfo): Result<Unit> =
117+
routineRemoteDataSource.registerRoutine(routineRegisterInfo.toDto()).also {
118+
if (it.isSuccess) refreshCache()
52119
}
53-
}
54120

55-
private val _writeRoutineEventFlow = MutableSharedFlow<WriteRoutineEvent>()
56-
override suspend fun getWriteRoutineEventFlow(): Flow<WriteRoutineEvent> = _writeRoutineEventFlow.asSharedFlow()
121+
override suspend fun editRoutine(routineEditInfo: RoutineEditInfo): Result<Unit> =
122+
routineRemoteDataSource.editRoutine(routineEditInfo.toDto()).also {
123+
if (it.isSuccess) refreshCache()
124+
}
57125
}

domain/src/main/java/com/threegap/bitnagil/domain/routine/model/WriteRoutineEvent.kt

Lines changed: 0 additions & 6 deletions
This file was deleted.
Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
11
package com.threegap.bitnagil.domain.routine.repository
22

33
import com.threegap.bitnagil.domain.routine.model.Routine
4-
import com.threegap.bitnagil.domain.routine.model.RoutineCompletionInfos
4+
import com.threegap.bitnagil.domain.routine.model.RoutineCompletionInfo
55
import com.threegap.bitnagil.domain.routine.model.RoutineEditInfo
66
import com.threegap.bitnagil.domain.routine.model.RoutineRegisterInfo
77
import com.threegap.bitnagil.domain.routine.model.RoutineSchedule
8-
import com.threegap.bitnagil.domain.routine.model.WriteRoutineEvent
98
import kotlinx.coroutines.flow.Flow
109

1110
interface RoutineRepository {
12-
suspend fun fetchWeeklyRoutines(startDate: String, endDate: String): Result<RoutineSchedule>
13-
suspend fun syncRoutineCompletion(routineCompletionInfos: RoutineCompletionInfos): Result<Unit>
11+
fun observeWeeklyRoutines(startDate: String, endDate: String): Flow<RoutineSchedule>
12+
suspend fun applyRoutineToggle(dateKey: String, routineId: String, completionInfo: RoutineCompletionInfo)
1413
suspend fun getRoutine(routineId: String): Result<Routine>
1514
suspend fun deleteRoutine(routineId: String): Result<Unit>
1615
suspend fun deleteRoutineForDay(routineId: String): Result<Unit>
1716
suspend fun registerRoutine(routineRegisterInfo: RoutineRegisterInfo): Result<Unit>
1817
suspend fun editRoutine(routineEditInfo: RoutineEditInfo): Result<Unit>
19-
suspend fun getWriteRoutineEventFlow(): Flow<WriteRoutineEvent>
2018
}

domain/src/main/java/com/threegap/bitnagil/domain/routine/usecase/FetchWeeklyRoutinesUseCase.kt

Lines changed: 0 additions & 13 deletions
This file was deleted.

domain/src/main/java/com/threegap/bitnagil/domain/routine/usecase/GetWriteRoutineEventFlowUseCase.kt

Lines changed: 0 additions & 12 deletions
This file was deleted.

domain/src/main/java/com/threegap/bitnagil/domain/routine/usecase/RoutineCompletionUseCase.kt

Lines changed: 0 additions & 12 deletions
This file was deleted.

0 commit comments

Comments
 (0)