From 22514845aea78399864e976f03815933b8f48cfc Mon Sep 17 00:00:00 2001 From: sebaslogen Date: Thu, 22 Aug 2024 14:59:23 +0200 Subject: [PATCH 1/3] CHANGE to ignore kotlin generated caches --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c104e97..4100bd2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.iml .gradle +.kotlin /local.properties /.idea/caches /.idea/libraries From 2042b01dd243df7f7a752f9afebd55c5fe7f1061 Mon Sep 17 00:00:00 2001 From: sebaslogen Date: Thu, 22 Aug 2024 15:00:33 +0200 Subject: [PATCH 2/3] CHANGE to use type-safe view state to avoid illegal states --- .../data/user/local/UserLocalDataSource.kt | 7 +++--- .../home/main/presentation/HomeViewModel.kt | 17 ++++++-------- .../home/main/presentation/HomeViewState.kt | 10 ++++---- .../q42/template/home/main/ui/HomeContent.kt | 23 +++++++++++-------- 4 files changed, 29 insertions(+), 28 deletions(-) diff --git a/data/user/src/main/kotlin/nl/q42/template/data/user/local/UserLocalDataSource.kt b/data/user/src/main/kotlin/nl/q42/template/data/user/local/UserLocalDataSource.kt index 8d070da..4de4baa 100644 --- a/data/user/src/main/kotlin/nl/q42/template/data/user/local/UserLocalDataSource.kt +++ b/data/user/src/main/kotlin/nl/q42/template/data/user/local/UserLocalDataSource.kt @@ -1,6 +1,7 @@ package nl.q42.template.data.user.local import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import nl.q42.template.data.user.local.model.UserEntity @@ -8,13 +9,13 @@ import javax.inject.Inject internal class UserLocalDataSource @Inject constructor() { - private val userFlow = MutableStateFlow(null) // this is dummy code, replace it with your own local storage implementation. + private val userFlow = MutableSharedFlow() // this is dummy code, replace it with your own local storage implementation. - fun setUser(userEntity: UserEntity) { + suspend fun setUser(userEntity: UserEntity) { // usually you store in DataStore or DB here... - userFlow.update { userEntity } // this is dummy code, replace it with your own local storage implementation. + userFlow.emit(userEntity) // this is dummy code, replace it with your own local storage implementation. } fun getUserFlow(): Flow = userFlow diff --git a/feature/home/src/main/kotlin/nl/q42/template/home/main/presentation/HomeViewModel.kt b/feature/home/src/main/kotlin/nl/q42/template/home/main/presentation/HomeViewModel.kt index 06e083b..bdbcaec 100644 --- a/feature/home/src/main/kotlin/nl/q42/template/home/main/presentation/HomeViewModel.kt +++ b/feature/home/src/main/kotlin/nl/q42/template/home/main/presentation/HomeViewModel.kt @@ -10,7 +10,6 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import nl.q42.template.actionresult.data.handleAction import nl.q42.template.domain.user.usecase.FetchUserUseCase @@ -29,7 +28,7 @@ class HomeViewModel @Inject constructor( private val navigator: RouteNavigator, ) : ViewModel(), RouteNavigator by navigator { - private val _uiState = MutableStateFlow(HomeViewState()) + private val _uiState = MutableStateFlow(HomeViewState.Loading) val uiState: StateFlow = _uiState.asStateFlow() init { @@ -58,23 +57,21 @@ class HomeViewModel @Inject constructor( fun fetchUser() { viewModelScope.launch { - _uiState.update { it.copy(showError = false, isLoading = true) } + _uiState.value = HomeViewState.Loading handleAction( action = fetchUserUseCase(), - onError = { _uiState.update { it.copy(showError = true, isLoading = false) } }, - onSuccess = { _uiState.update { it.copy(isLoading = false) } }, + onError = { _uiState.value = HomeViewState.Error }, + onSuccess = {}, ) } } private fun startObservingUserChanges() { getUserFlowUseCase().filterNotNull().onEach { user -> - _uiState.update { - it.copy( - userEmailTitle = ViewStateString.Res(R.string.emailTitle, user.email.value) - ) - } + _uiState.value = HomeViewState.Content( + userEmailTitle = ViewStateString.Res(R.string.emailTitle, user.email.value) + ) }.launchIn(viewModelScope) } } diff --git a/feature/home/src/main/kotlin/nl/q42/template/home/main/presentation/HomeViewState.kt b/feature/home/src/main/kotlin/nl/q42/template/home/main/presentation/HomeViewState.kt index ff319a8..3be55a5 100644 --- a/feature/home/src/main/kotlin/nl/q42/template/home/main/presentation/HomeViewState.kt +++ b/feature/home/src/main/kotlin/nl/q42/template/home/main/presentation/HomeViewState.kt @@ -2,8 +2,8 @@ package nl.q42.template.home.main.presentation import nl.q42.template.ui.presentation.ViewStateString -data class HomeViewState( - val userEmailTitle: ViewStateString? = null, - val isLoading: Boolean = false, - val showError: Boolean = false, -) +sealed interface HomeViewState { + data class Content(val userEmailTitle: ViewStateString) : HomeViewState + data object Loading : HomeViewState + data object Error : HomeViewState +} diff --git a/feature/home/src/main/kotlin/nl/q42/template/home/main/ui/HomeContent.kt b/feature/home/src/main/kotlin/nl/q42/template/home/main/ui/HomeContent.kt index b113920..368bf8d 100644 --- a/feature/home/src/main/kotlin/nl/q42/template/home/main/ui/HomeContent.kt +++ b/feature/home/src/main/kotlin/nl/q42/template/home/main/ui/HomeContent.kt @@ -30,13 +30,16 @@ internal fun HomeContent( horizontalAlignment = CenterHorizontally, ) { - /** - * This is dummy. Use the strings file IRL. - */ - viewState.userEmailTitle?.get()?.let { Text(text = it) } - - if (viewState.isLoading) CircularProgressIndicator() - if (viewState.showError) Text(text = "Error") + when(viewState) { + is HomeViewState.Content -> { + /** + * This is dummy. Use the strings file IRL. + */ + Text(text = viewState.userEmailTitle.get()) + } + HomeViewState.Loading -> CircularProgressIndicator() + HomeViewState.Error -> Text(text = "Error") + } Button(onClick = onLoadClicked) { Text("Refresh") @@ -55,7 +58,7 @@ internal fun HomeContent( @Composable private fun HomeContentErrorPreview() { PreviewAppTheme { - HomeContent(HomeViewState(showError = true), {}, {}, {}) + HomeContent(HomeViewState.Error, {}, {}, {}) } } @@ -63,7 +66,7 @@ private fun HomeContentErrorPreview() { @Composable private fun HomeContentLoadingPreview() { PreviewAppTheme { - HomeContent(HomeViewState(isLoading = true), {}, {}, {}) + HomeContent(HomeViewState.Loading, {}, {}, {}) } } @@ -71,6 +74,6 @@ private fun HomeContentLoadingPreview() { @Composable private fun HomeContentEmptyPreview() { PreviewAppTheme { - HomeContent(HomeViewState(userEmailTitle = "preview@preview.com".toViewStateString()), {}, {}, {}) + HomeContent(HomeViewState.Content(userEmailTitle = "preview@preview.com".toViewStateString()), {}, {}, {}) } } From 9851a7e51b361f279671d313c8538b7abfe8e9fc Mon Sep 17 00:00:00 2001 From: sebaslogen Date: Thu, 22 Aug 2024 15:42:21 +0200 Subject: [PATCH 3/3] Fix unit tests --- .../q42/template/home/main/presentation/HomeViewModelTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/feature/home/src/test/kotlin/nl/q42/template/home/main/presentation/HomeViewModelTest.kt b/feature/home/src/test/kotlin/nl/q42/template/home/main/presentation/HomeViewModelTest.kt index 8c282f4..52e6600 100644 --- a/feature/home/src/test/kotlin/nl/q42/template/home/main/presentation/HomeViewModelTest.kt +++ b/feature/home/src/test/kotlin/nl/q42/template/home/main/presentation/HomeViewModelTest.kt @@ -43,7 +43,7 @@ class HomeViewModelTest { ) viewModel.uiState.test { - assertTrue(awaitItem().isLoading) + assertTrue(awaitItem() == HomeViewState.Loading) } } @@ -65,7 +65,7 @@ class HomeViewModelTest { ) viewModel.uiState.test { - assertTrue(awaitItem().showError) + assertTrue(awaitItem() == HomeViewState.Error) } } }