diff --git a/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt b/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt index 6f59ec1a..61535f14 100644 --- a/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt +++ b/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt @@ -8,8 +8,10 @@ import com.threegap.bitnagil.data.auth.datasource.AuthLocalDataSource import com.threegap.bitnagil.data.auth.datasource.AuthRemoteDataSource import com.threegap.bitnagil.data.auth.datasourceimpl.AuthLocalDataSourceImpl import com.threegap.bitnagil.data.auth.datasourceimpl.AuthRemoteDataSourceImpl -import com.threegap.bitnagil.data.emotion.datasource.EmotionDataSource -import com.threegap.bitnagil.data.emotion.datasourceImpl.EmotionDataSourceImpl +import com.threegap.bitnagil.data.emotion.datasource.EmotionLocalDataSource +import com.threegap.bitnagil.data.emotion.datasource.EmotionRemoteDataSource +import com.threegap.bitnagil.data.emotion.datasourceImpl.EmotionLocalDataSourceImpl +import com.threegap.bitnagil.data.emotion.datasourceImpl.EmotionRemoteDataSourceImpl import com.threegap.bitnagil.data.file.datasource.FileDataSource import com.threegap.bitnagil.data.file.datasourceImpl.FileDataSourceImpl import com.threegap.bitnagil.data.onboarding.datasource.OnBoardingDataSource @@ -20,8 +22,10 @@ import com.threegap.bitnagil.data.report.datasource.ReportDataSource import com.threegap.bitnagil.data.report.datasourceImpl.ReportDataSourceImpl import com.threegap.bitnagil.data.routine.datasource.RoutineRemoteDataSource import com.threegap.bitnagil.data.routine.datasourceImpl.RoutineRemoteDataSourceImpl -import com.threegap.bitnagil.data.user.datasource.UserDataSource -import com.threegap.bitnagil.data.user.datasourceImpl.UserDataSourceImpl +import com.threegap.bitnagil.data.user.datasource.UserLocalDataSource +import com.threegap.bitnagil.data.user.datasource.UserRemoteDataSource +import com.threegap.bitnagil.data.user.datasourceImpl.UserLocalDataSourceImpl +import com.threegap.bitnagil.data.user.datasourceImpl.UserRemoteDataSourceImpl import com.threegap.bitnagil.data.version.datasource.VersionDataSource import com.threegap.bitnagil.data.version.datasourceImpl.VersionDataSourceImpl import dagger.Binds @@ -52,11 +56,19 @@ abstract class DataSourceModule { @Binds @Singleton - abstract fun bindEmotionDataSource(emotionDataSourceImpl: EmotionDataSourceImpl): EmotionDataSource + abstract fun bindEmotionLocalDataSource(emotionLocalDataSourceImpl: EmotionLocalDataSourceImpl): EmotionLocalDataSource @Binds @Singleton - abstract fun bindUserDataSource(userDataSourceImpl: UserDataSourceImpl): UserDataSource + abstract fun bindEmotionRemoteDataSource(emotionRemoteDataSourceImpl: EmotionRemoteDataSourceImpl): EmotionRemoteDataSource + + @Binds + @Singleton + abstract fun bindUserLocalDataSource(userLocalDataSourceImpl: UserLocalDataSourceImpl): UserLocalDataSource + + @Binds + @Singleton + abstract fun bindUserRemoteDataSource(userRemoteDataSourceImpl: UserRemoteDataSourceImpl): UserRemoteDataSource @Binds @Singleton diff --git a/data/build.gradle.kts b/data/build.gradle.kts index fcee75af..d167730e 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -20,4 +20,7 @@ dependencies { implementation(libs.bundles.retrofit) implementation(libs.play.services.location) implementation(libs.kotlinx.coroutines.play) + + testImplementation(libs.androidx.junit) + testImplementation(libs.kotlin.coroutines.test) } diff --git a/data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionLocalDataSource.kt b/data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionLocalDataSource.kt new file mode 100644 index 00000000..0a64f25c --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionLocalDataSource.kt @@ -0,0 +1,10 @@ +package com.threegap.bitnagil.data.emotion.datasource + +import com.threegap.bitnagil.domain.emotion.model.DailyEmotion +import kotlinx.coroutines.flow.StateFlow + +interface EmotionLocalDataSource { + val dailyEmotion: StateFlow + fun saveDailyEmotion(dailyEmotion: DailyEmotion) + fun clearCache() +} diff --git a/data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionDataSource.kt b/data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionRemoteDataSource.kt similarity index 93% rename from data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionDataSource.kt rename to data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionRemoteDataSource.kt index c3f81e30..d8c7e004 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionDataSource.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionRemoteDataSource.kt @@ -4,7 +4,7 @@ import com.threegap.bitnagil.data.emotion.model.dto.EmotionDto import com.threegap.bitnagil.data.emotion.model.response.DailyEmotionResponse import com.threegap.bitnagil.data.emotion.model.response.RegisterEmotionResponse -interface EmotionDataSource { +interface EmotionRemoteDataSource { suspend fun getEmotions(): Result> suspend fun registerEmotion(emotion: String): Result suspend fun fetchDailyEmotion(currentDate: String): Result diff --git a/data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionLocalDataSourceImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionLocalDataSourceImpl.kt new file mode 100644 index 00000000..e3f30f99 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionLocalDataSourceImpl.kt @@ -0,0 +1,22 @@ +package com.threegap.bitnagil.data.emotion.datasourceImpl + +import com.threegap.bitnagil.data.emotion.datasource.EmotionLocalDataSource +import com.threegap.bitnagil.domain.emotion.model.DailyEmotion +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +class EmotionLocalDataSourceImpl @Inject constructor() : EmotionLocalDataSource { + private val _dailyEmotion = MutableStateFlow(null) + override val dailyEmotion: StateFlow = _dailyEmotion.asStateFlow() + + override fun saveDailyEmotion(dailyEmotion: DailyEmotion) { + _dailyEmotion.update { dailyEmotion } + } + + override fun clearCache() { + _dailyEmotion.update { null } + } +} diff --git a/data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionDataSourceImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionRemoteDataSourceImpl.kt similarity index 88% rename from data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionDataSourceImpl.kt rename to data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionRemoteDataSourceImpl.kt index 20035b56..78effc19 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionDataSourceImpl.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionRemoteDataSourceImpl.kt @@ -1,7 +1,7 @@ package com.threegap.bitnagil.data.emotion.datasourceImpl import com.threegap.bitnagil.data.common.safeApiCall -import com.threegap.bitnagil.data.emotion.datasource.EmotionDataSource +import com.threegap.bitnagil.data.emotion.datasource.EmotionRemoteDataSource import com.threegap.bitnagil.data.emotion.model.dto.EmotionDto import com.threegap.bitnagil.data.emotion.model.request.RegisterEmotionRequest import com.threegap.bitnagil.data.emotion.model.response.DailyEmotionResponse @@ -9,9 +9,9 @@ import com.threegap.bitnagil.data.emotion.model.response.RegisterEmotionResponse import com.threegap.bitnagil.data.emotion.service.EmotionService import javax.inject.Inject -class EmotionDataSourceImpl @Inject constructor( +class EmotionRemoteDataSourceImpl @Inject constructor( private val emotionService: EmotionService, -) : EmotionDataSource { +) : EmotionRemoteDataSource { override suspend fun getEmotions(): Result> { return safeApiCall { emotionService.getEmotions() diff --git a/data/src/main/java/com/threegap/bitnagil/data/emotion/model/response/DailyEmotionResponse.kt b/data/src/main/java/com/threegap/bitnagil/data/emotion/model/response/DailyEmotionResponse.kt index 7dbebbc7..781f64e0 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/emotion/model/response/DailyEmotionResponse.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/emotion/model/response/DailyEmotionResponse.kt @@ -4,6 +4,7 @@ import com.threegap.bitnagil.domain.emotion.model.DailyEmotion import com.threegap.bitnagil.domain.emotion.model.EmotionMarbleType import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import java.time.LocalDate @Serializable data class DailyEmotionResponse( @@ -17,10 +18,11 @@ data class DailyEmotionResponse( val emotionMarbleHomeMessage: String?, ) -fun DailyEmotionResponse.toDomain(): DailyEmotion = +fun DailyEmotionResponse.toDomain(fetchedDate: LocalDate): DailyEmotion = DailyEmotion( type = emotionMarbleType, name = emotionMarbleName, imageUrl = imageUrl, homeMessage = emotionMarbleHomeMessage, + fetchedDate = fetchedDate, ) diff --git a/data/src/main/java/com/threegap/bitnagil/data/emotion/repositoryImpl/EmotionRepositoryImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/emotion/repositoryImpl/EmotionRepositoryImpl.kt index 811209dc..73ed05a1 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/emotion/repositoryImpl/EmotionRepositoryImpl.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/emotion/repositoryImpl/EmotionRepositoryImpl.kt @@ -1,42 +1,84 @@ package com.threegap.bitnagil.data.emotion.repositoryImpl -import com.threegap.bitnagil.data.emotion.datasource.EmotionDataSource +import com.threegap.bitnagil.data.emotion.datasource.EmotionLocalDataSource +import com.threegap.bitnagil.data.emotion.datasource.EmotionRemoteDataSource import com.threegap.bitnagil.data.emotion.model.response.toDomain import com.threegap.bitnagil.domain.emotion.model.DailyEmotion import com.threegap.bitnagil.domain.emotion.model.Emotion -import com.threegap.bitnagil.domain.emotion.model.EmotionChangeEvent import com.threegap.bitnagil.domain.emotion.model.EmotionRecommendRoutine import com.threegap.bitnagil.domain.emotion.repository.EmotionRepository import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import java.time.LocalDate import javax.inject.Inject class EmotionRepositoryImpl @Inject constructor( - private val emotionDataSource: EmotionDataSource, + private val emotionRemoteDataSource: EmotionRemoteDataSource, + private val emotionLocalDataSource: EmotionLocalDataSource, ) : EmotionRepository { + + private val fetchMutex = Mutex() + override suspend fun getEmotions(): Result> { - return emotionDataSource.getEmotions().map { response -> + return emotionRemoteDataSource.getEmotions().map { response -> response.map { it.toDomain() } } } override suspend fun registerEmotion(emotionMarbleType: String): Result> { - return emotionDataSource.registerEmotion(emotionMarbleType).map { - it.recommendedRoutines.map { - emotionRecommendedRoutineDto -> + return emotionRemoteDataSource.registerEmotion(emotionMarbleType).map { + it.recommendedRoutines.map { emotionRecommendedRoutineDto -> emotionRecommendedRoutineDto.toEmotionRecommendRoutine() } }.also { - if (it.isSuccess) { - _emotionChangeEventFlow.emit(EmotionChangeEvent.ChangeEmotion(emotionMarbleType)) - } + if (it.isSuccess) fetchAndSaveDailyEmotion(today = LocalDate.now(), forceRefresh = true) } } - override suspend fun fetchDailyEmotion(currentDate: String): Result = - emotionDataSource.fetchDailyEmotion(currentDate).map { it.toDomain() } + override fun observeDailyEmotion(): Flow> = flow { + fetchAndSaveDailyEmotion(LocalDate.now()) + .onFailure { + emit(Result.failure(it)) + return@flow + } + + emitAll( + emotionLocalDataSource.dailyEmotion + .filterNotNull() + .map { Result.success(it) }, + ) + } + + override fun clearCache() { + emotionLocalDataSource.clearCache() + } + + private suspend fun fetchAndSaveDailyEmotion( + today: LocalDate, + forceRefresh: Boolean = false, + ): Result { + if (!forceRefresh) { + val currentLocalData = emotionLocalDataSource.dailyEmotion.value + if (currentLocalData != null && !currentLocalData.isStale(today)) { + return Result.success(currentLocalData) + } + } - private val _emotionChangeEventFlow = MutableSharedFlow() - override suspend fun getEmotionChangeEventFlow(): Flow = _emotionChangeEventFlow.asSharedFlow() + return fetchMutex.withLock { + if (!forceRefresh) { + val doubleCheckData = emotionLocalDataSource.dailyEmotion.value + if (doubleCheckData != null && !doubleCheckData.isStale(today)) { + return@withLock Result.success(doubleCheckData) + } + } + emotionRemoteDataSource.fetchDailyEmotion(today.toString()) + .onSuccess { emotionLocalDataSource.saveDailyEmotion(it.toDomain(today)) } + .map { it.toDomain(today) } + } + } } diff --git a/data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserLocalDataSource.kt b/data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserLocalDataSource.kt new file mode 100644 index 00000000..e8856ecf --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserLocalDataSource.kt @@ -0,0 +1,10 @@ +package com.threegap.bitnagil.data.user.datasource + +import com.threegap.bitnagil.domain.user.model.UserProfile +import kotlinx.coroutines.flow.StateFlow + +interface UserLocalDataSource { + val userProfile: StateFlow + fun saveUserProfile(userProfile: UserProfile) + fun clearCache() +} diff --git a/data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserDataSource.kt b/data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserRemoteDataSource.kt similarity index 85% rename from data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserDataSource.kt rename to data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserRemoteDataSource.kt index c492b8e0..dc194abc 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserDataSource.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserRemoteDataSource.kt @@ -2,6 +2,6 @@ package com.threegap.bitnagil.data.user.datasource import com.threegap.bitnagil.data.user.model.response.UserProfileResponse -interface UserDataSource { +interface UserRemoteDataSource { suspend fun fetchUserProfile(): Result } diff --git a/data/src/main/java/com/threegap/bitnagil/data/user/datasourceImpl/UserLocalDataSourceImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/user/datasourceImpl/UserLocalDataSourceImpl.kt new file mode 100644 index 00000000..7c8bfc78 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/user/datasourceImpl/UserLocalDataSourceImpl.kt @@ -0,0 +1,24 @@ +package com.threegap.bitnagil.data.user.datasourceImpl + +import com.threegap.bitnagil.data.user.datasource.UserLocalDataSource +import com.threegap.bitnagil.domain.user.model.UserProfile +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class UserLocalDataSourceImpl @Inject constructor() : UserLocalDataSource { + private val _userProfile = MutableStateFlow(null) + override val userProfile: StateFlow = _userProfile.asStateFlow() + + override fun saveUserProfile(userProfile: UserProfile) { + _userProfile.update { userProfile } + } + + override fun clearCache() { + _userProfile.update { null } + } +} diff --git a/data/src/main/java/com/threegap/bitnagil/data/user/datasourceImpl/UserDataSourceImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/user/datasourceImpl/UserRemoteDataSourceImpl.kt similarity index 65% rename from data/src/main/java/com/threegap/bitnagil/data/user/datasourceImpl/UserDataSourceImpl.kt rename to data/src/main/java/com/threegap/bitnagil/data/user/datasourceImpl/UserRemoteDataSourceImpl.kt index 9b7f9190..550725c8 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/user/datasourceImpl/UserDataSourceImpl.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/user/datasourceImpl/UserRemoteDataSourceImpl.kt @@ -1,16 +1,14 @@ package com.threegap.bitnagil.data.user.datasourceImpl import com.threegap.bitnagil.data.common.safeApiCall -import com.threegap.bitnagil.data.user.datasource.UserDataSource +import com.threegap.bitnagil.data.user.datasource.UserRemoteDataSource import com.threegap.bitnagil.data.user.model.response.UserProfileResponse import com.threegap.bitnagil.data.user.service.UserService import javax.inject.Inject -class UserDataSourceImpl @Inject constructor( +class UserRemoteDataSourceImpl @Inject constructor( private val userService: UserService, -) : UserDataSource { +) : UserRemoteDataSource { override suspend fun fetchUserProfile(): Result = - safeApiCall { - userService.fetchUserProfile() - } + safeApiCall { userService.fetchUserProfile() } } diff --git a/data/src/main/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImpl.kt index e0506f24..19be4a5f 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImpl.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImpl.kt @@ -1,14 +1,59 @@ package com.threegap.bitnagil.data.user.repositoryImpl -import com.threegap.bitnagil.data.user.datasource.UserDataSource +import com.threegap.bitnagil.data.user.datasource.UserLocalDataSource +import com.threegap.bitnagil.data.user.datasource.UserRemoteDataSource import com.threegap.bitnagil.data.user.model.response.toDomain import com.threegap.bitnagil.domain.user.model.UserProfile import com.threegap.bitnagil.domain.user.repository.UserRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import javax.inject.Inject +import javax.inject.Singleton +@Singleton class UserRepositoryImpl @Inject constructor( - private val userDataSource: UserDataSource, + private val userLocalDataSource: UserLocalDataSource, + private val userRemoteDataSource: UserRemoteDataSource, ) : UserRepository { - override suspend fun fetchUserProfile(): Result = - userDataSource.fetchUserProfile().map { it.toDomain() } + private val fetchMutex = Mutex() + + override fun observeUserProfile(): Flow> = flow { + fetchAndCacheIfNeeded().onFailure { + emit(Result.failure(it)) + return@flow + } + + emitAll( + userLocalDataSource.userProfile + .filterNotNull() + .map { Result.success(it) }, + ) + } + + override suspend fun getUserProfile(): Result { + return fetchAndCacheIfNeeded() + } + + override fun clearCache() { + userLocalDataSource.clearCache() + } + + private suspend fun fetchAndCacheIfNeeded(): Result { + userLocalDataSource.userProfile.value?.let { return Result.success(it) } + + return fetchMutex.withLock { + userLocalDataSource.userProfile.value?.let { return@withLock Result.success(it) } + + userRemoteDataSource.fetchUserProfile() + .onSuccess { response -> + userLocalDataSource.saveUserProfile(response.toDomain()) + } + .map { it.toDomain() } + } + } } diff --git a/data/src/test/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImplTest.kt b/data/src/test/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImplTest.kt new file mode 100644 index 00000000..a8cca845 --- /dev/null +++ b/data/src/test/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImplTest.kt @@ -0,0 +1,126 @@ +package com.threegap.bitnagil.data.user.repositoryImpl + +import com.threegap.bitnagil.data.user.datasource.UserLocalDataSource +import com.threegap.bitnagil.data.user.datasource.UserRemoteDataSource +import com.threegap.bitnagil.data.user.model.response.UserProfileResponse +import com.threegap.bitnagil.domain.user.model.UserProfile +import com.threegap.bitnagil.domain.user.repository.UserRepository +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import java.util.concurrent.atomic.AtomicInteger + +class UserRepositoryImplTest { + + private lateinit var localDataSource: FakeUserLocalDataSource + private lateinit var remoteDataSource: FakeUserRemoteDataSource + private lateinit var userRepository: UserRepository + + @Before + fun setup() { + localDataSource = FakeUserLocalDataSource() + remoteDataSource = FakeUserRemoteDataSource() + userRepository = UserRepositoryImpl(localDataSource, remoteDataSource) + } + + @Test + fun `캐시가 비어있을 때 observeUserProfile을 구독하면 Remote에서 데이터를 가져와 캐시를 업데이트해야 한다`() = + runTest { + // given + val expectedProfile = UserProfile(nickname = "TestUser") + remoteDataSource.profileResponse = UserProfileResponse(nickname = "TestUser") + + // when + // 구독(first)이 시작되는 순간 Fetch가 발생함 + val result = userRepository.observeUserProfile().first() + + // then + assertEquals(expectedProfile, result.getOrNull()) + assertEquals(1, remoteDataSource.fetchCount.get()) + assertEquals(expectedProfile, localDataSource.userProfile.value) + } + + @Test + fun `캐시가 이미 존재할 때 observeUserProfile을 구독하면 Remote를 호출하지 않고 캐시를 반환해야 한다`() = + runTest { + // given + val cachedProfile = UserProfile(nickname = "CachedUser") + localDataSource.saveUserProfile(cachedProfile) + + // when + val result = userRepository.observeUserProfile().first() + + // then + assertEquals(cachedProfile, result.getOrNull()) + assertEquals(0, remoteDataSource.fetchCount.get()) + } + + @Test + fun `여러 코루틴이 동시에 observeUserProfile을 구독해도 Remote API는 1회만 호출되어야 한다 (Race Condition 방지)`() = + runTest { + // given + remoteDataSource.profileResponse = UserProfileResponse(nickname = "RaceUser") + remoteDataSource.delayMillis = 100L // 네트워크 지연 시뮬레이션 + + // when + // 10개의 코루틴이 동시에 구독 시작 + val jobs = List(10) { + async { userRepository.observeUserProfile().first() } + } + jobs.awaitAll() + + // then + assertEquals(1, remoteDataSource.fetchCount.get()) + assertEquals("RaceUser", localDataSource.userProfile.value?.nickname) + } + + @Test + fun `clearCache를 호출하면 로컬 캐시가 초기화되어야 한다`() = + runTest { + // given + localDataSource.saveUserProfile(UserProfile(nickname = "ToDelete")) + + // when + userRepository.clearCache() + + // then + assertEquals(null, localDataSource.userProfile.value) + } + + // --- Fake Objects --- + + private class FakeUserLocalDataSource : UserLocalDataSource { + private val _userProfile = MutableStateFlow(null) + override val userProfile: StateFlow = _userProfile.asStateFlow() + + override fun saveUserProfile(userProfile: UserProfile) { + _userProfile.update { userProfile } + } + + override fun clearCache() { + _userProfile.update { null } + } + } + + private class FakeUserRemoteDataSource : UserRemoteDataSource { + var profileResponse: UserProfileResponse? = null + val fetchCount = AtomicInteger(0) + var delayMillis = 0L + + override suspend fun fetchUserProfile(): Result { + if (delayMillis > 0) delay(delayMillis) + fetchCount.incrementAndGet() + return profileResponse?.let { Result.success(it) } + ?: Result.failure(Exception("No profile set in fake")) + } + } +} diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/LogoutUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/LogoutUseCase.kt index f8893314..d5b5ddac 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/LogoutUseCase.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/LogoutUseCase.kt @@ -1,10 +1,18 @@ package com.threegap.bitnagil.domain.auth.usecase import com.threegap.bitnagil.domain.auth.repository.AuthRepository +import com.threegap.bitnagil.domain.emotion.repository.EmotionRepository +import com.threegap.bitnagil.domain.user.repository.UserRepository import javax.inject.Inject class LogoutUseCase @Inject constructor( private val authRepository: AuthRepository, + private val userRepository: UserRepository, + private val emotionRepository: EmotionRepository, ) { - suspend operator fun invoke(): Result = authRepository.logout() + suspend operator fun invoke(): Result = + authRepository.logout().onSuccess { + userRepository.clearCache() + emotionRepository.clearCache() + } } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/WithdrawalUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/WithdrawalUseCase.kt index 956a80ed..880b8e3a 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/WithdrawalUseCase.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/WithdrawalUseCase.kt @@ -1,10 +1,18 @@ package com.threegap.bitnagil.domain.auth.usecase import com.threegap.bitnagil.domain.auth.repository.AuthRepository +import com.threegap.bitnagil.domain.emotion.repository.EmotionRepository +import com.threegap.bitnagil.domain.user.repository.UserRepository import javax.inject.Inject class WithdrawalUseCase @Inject constructor( private val authRepository: AuthRepository, + private val userRepository: UserRepository, + private val emotionRepository: EmotionRepository, ) { - suspend operator fun invoke(reason: String): Result = authRepository.withdrawal(reason) + suspend operator fun invoke(reason: String): Result = + authRepository.withdrawal(reason).onSuccess { + userRepository.clearCache() + emotionRepository.clearCache() + } } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/DailyEmotion.kt b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/DailyEmotion.kt index ab27dd52..e582467e 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/DailyEmotion.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/DailyEmotion.kt @@ -1,8 +1,23 @@ package com.threegap.bitnagil.domain.emotion.model +import java.time.LocalDate + data class DailyEmotion( val type: EmotionMarbleType?, val name: String?, val imageUrl: String?, val homeMessage: String?, -) + val fetchedDate: LocalDate = LocalDate.MIN, +) { + fun isStale(today: LocalDate): Boolean = fetchedDate != today + + companion object { + val INIT = DailyEmotion( + type = null, + name = null, + imageUrl = null, + homeMessage = null, + fetchedDate = LocalDate.MIN, + ) + } +} diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/EmotionChangeEvent.kt b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/EmotionChangeEvent.kt deleted file mode 100644 index f848a647..00000000 --- a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/EmotionChangeEvent.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.threegap.bitnagil.domain.emotion.model - -sealed interface EmotionChangeEvent { - data class ChangeEmotion(val emotionType: String) : EmotionChangeEvent -} diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/repository/EmotionRepository.kt b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/repository/EmotionRepository.kt index d2a1df80..a95af407 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/repository/EmotionRepository.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/repository/EmotionRepository.kt @@ -2,13 +2,12 @@ package com.threegap.bitnagil.domain.emotion.repository import com.threegap.bitnagil.domain.emotion.model.DailyEmotion import com.threegap.bitnagil.domain.emotion.model.Emotion -import com.threegap.bitnagil.domain.emotion.model.EmotionChangeEvent import com.threegap.bitnagil.domain.emotion.model.EmotionRecommendRoutine import kotlinx.coroutines.flow.Flow interface EmotionRepository { suspend fun getEmotions(): Result> suspend fun registerEmotion(emotionMarbleType: String): Result> - suspend fun fetchDailyEmotion(currentDate: String): Result - suspend fun getEmotionChangeEventFlow(): Flow + fun observeDailyEmotion(): Flow> + fun clearCache() } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/GetEmotionChangeEventFlowUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/GetEmotionChangeEventFlowUseCase.kt deleted file mode 100644 index d30147c4..00000000 --- a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/GetEmotionChangeEventFlowUseCase.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.threegap.bitnagil.domain.emotion.usecase - -import com.threegap.bitnagil.domain.emotion.model.EmotionChangeEvent -import com.threegap.bitnagil.domain.emotion.repository.EmotionRepository -import kotlinx.coroutines.flow.Flow -import javax.inject.Inject - -class GetEmotionChangeEventFlowUseCase @Inject constructor( - private val repository: EmotionRepository, -) { - suspend operator fun invoke(): Flow = repository.getEmotionChangeEventFlow() -} diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/FetchDailyEmotionUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/ObserveDailyEmotionUseCase.kt similarity index 51% rename from domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/FetchDailyEmotionUseCase.kt rename to domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/ObserveDailyEmotionUseCase.kt index 0b5d5bdb..9892334b 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/FetchDailyEmotionUseCase.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/ObserveDailyEmotionUseCase.kt @@ -2,14 +2,11 @@ package com.threegap.bitnagil.domain.emotion.usecase import com.threegap.bitnagil.domain.emotion.model.DailyEmotion import com.threegap.bitnagil.domain.emotion.repository.EmotionRepository -import java.time.LocalDate +import kotlinx.coroutines.flow.Flow import javax.inject.Inject -class FetchDailyEmotionUseCase @Inject constructor( +class ObserveDailyEmotionUseCase @Inject constructor( private val emotionRepository: EmotionRepository, ) { - suspend operator fun invoke(): Result { - val currentDate = LocalDate.now().toString() - return emotionRepository.fetchDailyEmotion(currentDate) - } + operator fun invoke(): Flow> = emotionRepository.observeDailyEmotion() } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/user/repository/UserRepository.kt b/domain/src/main/java/com/threegap/bitnagil/domain/user/repository/UserRepository.kt index de352843..3bc25503 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/user/repository/UserRepository.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/user/repository/UserRepository.kt @@ -1,7 +1,10 @@ package com.threegap.bitnagil.domain.user.repository import com.threegap.bitnagil.domain.user.model.UserProfile +import kotlinx.coroutines.flow.Flow interface UserRepository { - suspend fun fetchUserProfile(): Result + fun observeUserProfile(): Flow> + suspend fun getUserProfile(): Result + fun clearCache() } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/user/usecase/FetchUserProfileUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/user/usecase/GetUserProfileUseCase.kt similarity index 63% rename from domain/src/main/java/com/threegap/bitnagil/domain/user/usecase/FetchUserProfileUseCase.kt rename to domain/src/main/java/com/threegap/bitnagil/domain/user/usecase/GetUserProfileUseCase.kt index baee4a20..b07b6822 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/user/usecase/FetchUserProfileUseCase.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/user/usecase/GetUserProfileUseCase.kt @@ -4,9 +4,8 @@ import com.threegap.bitnagil.domain.user.model.UserProfile import com.threegap.bitnagil.domain.user.repository.UserRepository import javax.inject.Inject -class FetchUserProfileUseCase @Inject constructor( +class GetUserProfileUseCase @Inject constructor( private val userRepository: UserRepository, ) { - suspend operator fun invoke(): Result = - userRepository.fetchUserProfile() + suspend operator fun invoke(): Result = userRepository.getUserProfile() } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/user/usecase/ObserveUserProfileUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/user/usecase/ObserveUserProfileUseCase.kt new file mode 100644 index 00000000..ed371c28 --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/user/usecase/ObserveUserProfileUseCase.kt @@ -0,0 +1,12 @@ +package com.threegap.bitnagil.domain.user.usecase + +import com.threegap.bitnagil.domain.user.model.UserProfile +import com.threegap.bitnagil.domain.user.repository.UserRepository +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class ObserveUserProfileUseCase @Inject constructor( + private val userRepository: UserRepository, +) { + operator fun invoke(): Flow> = userRepository.observeUserProfile() +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/home/HomeViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/home/HomeViewModel.kt index 810c9a1f..639da290 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/home/HomeViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/home/HomeViewModel.kt @@ -2,8 +2,7 @@ package com.threegap.bitnagil.presentation.screen.home import android.util.Log import androidx.lifecycle.ViewModel -import com.threegap.bitnagil.domain.emotion.usecase.FetchDailyEmotionUseCase -import com.threegap.bitnagil.domain.emotion.usecase.GetEmotionChangeEventFlowUseCase +import com.threegap.bitnagil.domain.emotion.usecase.ObserveDailyEmotionUseCase import com.threegap.bitnagil.domain.onboarding.usecase.GetOnBoardingRecommendRoutineEventFlowUseCase import com.threegap.bitnagil.domain.routine.model.RoutineCompletionInfo import com.threegap.bitnagil.domain.routine.model.RoutineCompletionInfos @@ -11,7 +10,7 @@ import com.threegap.bitnagil.domain.routine.usecase.FetchWeeklyRoutinesUseCase import com.threegap.bitnagil.domain.routine.usecase.GetWriteRoutineEventFlowUseCase import com.threegap.bitnagil.domain.routine.usecase.RoutineCompletionUseCase import com.threegap.bitnagil.domain.routine.usecase.ToggleRoutineUseCase -import com.threegap.bitnagil.domain.user.usecase.FetchUserProfileUseCase +import com.threegap.bitnagil.domain.user.usecase.ObserveUserProfileUseCase import com.threegap.bitnagil.presentation.screen.home.contract.HomeSideEffect import com.threegap.bitnagil.presentation.screen.home.contract.HomeState import com.threegap.bitnagil.presentation.screen.home.model.ToggleStrategy @@ -35,22 +34,41 @@ import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor( private val fetchWeeklyRoutinesUseCase: FetchWeeklyRoutinesUseCase, - private val fetchUserProfileUseCase: FetchUserProfileUseCase, - private val fetchDailyEmotionUseCase: FetchDailyEmotionUseCase, + private val observeUserProfileUseCase: ObserveUserProfileUseCase, + private val observeDailyEmotionUseCase: ObserveDailyEmotionUseCase, private val routineCompletionUseCase: RoutineCompletionUseCase, private val getWriteRoutineEventFlowUseCase: GetWriteRoutineEventFlowUseCase, - private val getEmotionChangeEventFlowUseCase: GetEmotionChangeEventFlowUseCase, private val getOnBoardingRecommendRoutineEventFlowUseCase: GetOnBoardingRecommendRoutineEventFlowUseCase, private val toggleRoutineUseCase: ToggleRoutineUseCase, ) : ContainerHost, ViewModel() { - override val container: Container = container(initialState = HomeState.INIT) + override val container: Container = + container( + initialState = HomeState.INIT, + buildSettings = { repeatOnSubscribedStopTimeout = 5_000L }, + ) private val pendingChangesByDate = mutableMapOf>() private val routineSyncTrigger = MutableSharedFlow(extraBufferCapacity = 64) init { initialize() + observeDailyEmotion() + } + + private fun observeDailyEmotion() { + intent { + repeatOnSubscription { + observeDailyEmotionUseCase().collect { result -> + result.fold( + onSuccess = { dailyEmotion -> + reduce { state.copy(dailyEmotion = dailyEmotion.toUiModel()) } + }, + onFailure = {}, + ) + } + } + } } fun selectDate(data: LocalDate) { @@ -185,11 +203,9 @@ class HomeViewModel @Inject constructor( private fun initialize() { intent { coroutineScope { - launch { fetchUserProfile() } - launch { fetchDailyEmotion() } + launch { observeUserProfile() } launch { fetchWeeklyRoutines(state.currentWeeks) } launch { observeWriteRoutineEvent() } - launch { observeEmotionChangeEvent() } launch { observeRecommendRoutineEvent() } launch { observeWeekChanges() } launch { observeRoutineUpdates() } @@ -205,14 +221,6 @@ class HomeViewModel @Inject constructor( } } - private suspend fun observeEmotionChangeEvent() { - subIntent { - getEmotionChangeEventFlowUseCase().collect { - fetchDailyEmotion() - } - } - } - private suspend fun observeRecommendRoutineEvent() { subIntent { getOnBoardingRecommendRoutineEventFlowUseCase().collect { @@ -246,18 +254,22 @@ class HomeViewModel @Inject constructor( } } - private suspend fun fetchUserProfile() { - subIntent { - reduce { state.copy(loadingCount = state.loadingCount + 1) } - fetchUserProfileUseCase().fold( - onSuccess = { - reduce { state.copy(userNickname = it.nickname, loadingCount = state.loadingCount - 1) } - }, - onFailure = { - Log.e("HomeViewModel", "유저 정보 가져오기 실패: ${it.message}") - reduce { state.copy(loadingCount = state.loadingCount - 1) } - }, - ) + private fun observeUserProfile() { + intent { + repeatOnSubscription { + reduce { state.copy(loadingCount = state.loadingCount + 1) } + observeUserProfileUseCase().collect { result -> + result.fold( + onSuccess = { + reduce { state.copy(userNickname = it.nickname, loadingCount = state.loadingCount - 1) } + }, + onFailure = { + Log.e("HomeViewModel", "유저 정보 가져오기 실패: ${it.message}") + reduce { state.copy(loadingCount = state.loadingCount - 1) } + }, + ) + } + } } } @@ -278,21 +290,6 @@ class HomeViewModel @Inject constructor( } } - private suspend fun fetchDailyEmotion() { - subIntent { - reduce { state.copy(loadingCount = state.loadingCount + 1) } - fetchDailyEmotionUseCase().fold( - onSuccess = { - reduce { state.copy(dailyEmotion = it.toUiModel(), loadingCount = state.loadingCount - 1) } - }, - onFailure = { - Log.e("HomeViewModel", "나의 감정 실패: ${it.message}") - reduce { state.copy(loadingCount = state.loadingCount - 1) } - }, - ) - } - } - private fun syncRoutineChangesForDate(dateKey: String) { intent { val dateChanges = pendingChangesByDate.remove(dateKey) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/mypage/MyPageViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/mypage/MyPageViewModel.kt index 1e417234..5b40610d 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/mypage/MyPageViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/mypage/MyPageViewModel.kt @@ -1,7 +1,7 @@ package com.threegap.bitnagil.presentation.screen.mypage import androidx.lifecycle.ViewModel -import com.threegap.bitnagil.domain.user.usecase.FetchUserProfileUseCase +import com.threegap.bitnagil.domain.user.usecase.ObserveUserProfileUseCase import com.threegap.bitnagil.presentation.screen.mypage.contract.MyPageSideEffect import com.threegap.bitnagil.presentation.screen.mypage.contract.MyPageState import dagger.hilt.android.lifecycle.HiltViewModel @@ -12,26 +12,24 @@ import javax.inject.Inject @HiltViewModel class MyPageViewModel @Inject constructor( - private val fetchUserProfileUseCase: FetchUserProfileUseCase, + private val observeUserProfileUseCase: ObserveUserProfileUseCase, ) : ContainerHost, ViewModel() { - override val container: Container = container(initialState = MyPageState.INIT) - init { - loadMyPageInfo() - } - - private fun loadMyPageInfo() = intent { - fetchUserProfileUseCase().fold( - onSuccess = { - reduce { - state.copy( - name = it.nickname, - profileUrl = "profileUrl", - ) + override val container: Container = + container( + initialState = MyPageState.INIT, + buildSettings = { repeatOnSubscribedStopTimeout = 5_000L }, + onCreate = { + repeatOnSubscription { + observeUserProfileUseCase().collect { result -> + result.fold( + onSuccess = { + reduce { state.copy(name = it.nickname, profileUrl = "profileUrl") } + }, + onFailure = {}, + ) + } } }, - onFailure = { - }, ) - } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/onboarding/OnBoardingViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/onboarding/OnBoardingViewModel.kt index c9e6a6ca..03f54a07 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/onboarding/OnBoardingViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/onboarding/OnBoardingViewModel.kt @@ -8,7 +8,7 @@ import com.threegap.bitnagil.domain.onboarding.usecase.GetOnBoardingsUseCase import com.threegap.bitnagil.domain.onboarding.usecase.GetRecommendOnBoardingRoutinesUseCase import com.threegap.bitnagil.domain.onboarding.usecase.GetUserOnBoardingUseCase import com.threegap.bitnagil.domain.onboarding.usecase.RegisterRecommendOnBoardingRoutinesUseCase -import com.threegap.bitnagil.domain.user.usecase.FetchUserProfileUseCase +import com.threegap.bitnagil.domain.user.usecase.GetUserProfileUseCase import com.threegap.bitnagil.presentation.screen.onboarding.contract.OnBoardingSideEffect import com.threegap.bitnagil.presentation.screen.onboarding.contract.OnBoardingState import com.threegap.bitnagil.presentation.screen.onboarding.model.OnBoardingItemUiModel @@ -34,7 +34,7 @@ class OnBoardingViewModel @AssistedInject constructor( private val getRecommendOnBoardingRoutinesUseCase: GetRecommendOnBoardingRoutinesUseCase, private val getOnBoardingAbstractUseCase: GetOnBoardingAbstractUseCase, private val registerRecommendOnBoardingRoutinesUseCase: RegisterRecommendOnBoardingRoutinesUseCase, - private val fetchUserProfileUseCase: FetchUserProfileUseCase, + private val getUserProfileUseCase: GetUserProfileUseCase, private val getUserOnBoardingUseCase: GetUserOnBoardingUseCase, @Assisted private val onBoardingArg: OnBoardingScreenArg, ) : ContainerHost, ViewModel() { @@ -71,7 +71,7 @@ class OnBoardingViewModel @AssistedInject constructor( } private fun loadIntro() = intent { - val userName = fetchUserProfileUseCase().getOrNull()?.nickname ?: "-" + val userName = getUserProfileUseCase().getOrNull()?.nickname ?: "-" reduce { OnBoardingState.Idle( @@ -86,7 +86,7 @@ class OnBoardingViewModel @AssistedInject constructor( } private fun loadUserOnBoarding() = intent { - val userName = fetchUserProfileUseCase().getOrNull()?.nickname ?: "-" + val userName = getUserProfileUseCase().getOrNull()?.nickname ?: "-" val userOnBoarding = getUserOnBoardingUseCase().fold( onSuccess = { it }, onFailure = { diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/recommendroutine/RecommendRoutineViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/recommendroutine/RecommendRoutineViewModel.kt index c9381687..83e0f741 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/recommendroutine/RecommendRoutineViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/recommendroutine/RecommendRoutineViewModel.kt @@ -1,8 +1,7 @@ package com.threegap.bitnagil.presentation.screen.recommendroutine import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.threegap.bitnagil.domain.emotion.usecase.GetEmotionChangeEventFlowUseCase +import com.threegap.bitnagil.domain.emotion.usecase.ObserveDailyEmotionUseCase import com.threegap.bitnagil.domain.recommendroutine.model.RecommendCategory import com.threegap.bitnagil.domain.recommendroutine.model.RecommendLevel import com.threegap.bitnagil.domain.recommendroutine.usecase.FetchRecommendRoutinesUseCase @@ -12,7 +11,8 @@ import com.threegap.bitnagil.presentation.screen.recommendroutine.model.Recommen import com.threegap.bitnagil.presentation.screen.recommendroutine.model.RecommendRoutinesUiModel import com.threegap.bitnagil.presentation.screen.recommendroutine.model.toUiModel import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.viewmodel.container @@ -20,17 +20,23 @@ import javax.inject.Inject @HiltViewModel class RecommendRoutineViewModel @Inject constructor( + private val observeDailyEmotionUseCase: ObserveDailyEmotionUseCase, private val fetchRecommendRoutinesUseCase: FetchRecommendRoutinesUseCase, - private val getEmotionChangeEventFlowUseCase: GetEmotionChangeEventFlowUseCase, ) : ContainerHost, ViewModel() { override val container: Container = - container(initialState = RecommendRoutineState.INIT) - - init { - loadRecommendRoutines() - observeEmotionChangeEvent() - } + container( + initialState = RecommendRoutineState.INIT, + buildSettings = { repeatOnSubscribedStopTimeout = 5_000L }, + onCreate = { + repeatOnSubscription { + observeDailyEmotionUseCase() + .map { result -> result.getOrNull()?.type } + .distinctUntilChanged() + .collect { loadRecommendRoutines() } + } + }, + ) private var recommendRoutines: RecommendRoutinesUiModel = RecommendRoutinesUiModel.INIT @@ -80,14 +86,6 @@ class RecommendRoutineViewModel @Inject constructor( } } - private fun observeEmotionChangeEvent() { - viewModelScope.launch { - getEmotionChangeEventFlowUseCase().collect { - loadRecommendRoutines() - } - } - } - private fun loadRecommendRoutines() { intent { reduce { state.copy(isLoading = true) }