From 279254f89007997d078067a909f415c024dc89c8 Mon Sep 17 00:00:00 2001 From: marcopla99 Date: Tue, 11 Apr 2023 22:09:12 +0200 Subject: [PATCH 01/14] Remove viewModel from HomeScreen composable parameters and pass the state instead --- .../flashcards/presentation/home/HomeScreenRobot.kt | 10 +++------- .../flashcards/presentation/home/HomeScreenTest.kt | 7 ++++--- .../flashcards/presentation/navigation/AppNavHost.kt | 11 ++++++++++- .../flashcards/presentation/screen/home/HomeScreen.kt | 8 ++------ .../presentation/screen/result/ResultItem.kt | 2 +- 5 files changed, 20 insertions(+), 18 deletions(-) diff --git a/app/src/androidTest/java/com/marcopla/flashcards/presentation/home/HomeScreenRobot.kt b/app/src/androidTest/java/com/marcopla/flashcards/presentation/home/HomeScreenRobot.kt index 08160e9..d08bd2a 100644 --- a/app/src/androidTest/java/com/marcopla/flashcards/presentation/home/HomeScreenRobot.kt +++ b/app/src/androidTest/java/com/marcopla/flashcards/presentation/home/HomeScreenRobot.kt @@ -8,24 +8,20 @@ import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.rules.ActivityScenarioRule import com.marcopla.flashcards.R import com.marcopla.flashcards.data.model.FlashCard -import com.marcopla.flashcards.data.repository.FlashCardRepositoryImpl -import com.marcopla.flashcards.domain.usecase.LoadUseCase import com.marcopla.flashcards.presentation.screen.home.HomeScreen -import com.marcopla.flashcards.presentation.screen.home.HomeViewModel -import com.marcopla.testing_shared.FakeFlashCardDao +import com.marcopla.flashcards.presentation.screen.home.HomeScreenState typealias ComponentActivityTestRule = AndroidComposeTestRule, ComponentActivity> suspend fun launchHomeScreen( rule: ComponentActivityTestRule, - flashCards: List = emptyList(), + screenState: HomeScreenState, block: suspend HomeScreenRobot.() -> Unit ): HomeScreenRobot { - val repository = FlashCardRepositoryImpl(FakeFlashCardDao(flashCards)) rule.setContent { HomeScreen( - viewModel = HomeViewModel(LoadUseCase(repository)), + screenState = screenState, onNavigateToAddScreen = {}, onItemClicked = {}, onNavigateToCarouselScreen = {} diff --git a/app/src/androidTest/java/com/marcopla/flashcards/presentation/home/HomeScreenTest.kt b/app/src/androidTest/java/com/marcopla/flashcards/presentation/home/HomeScreenTest.kt index 1bb05ca..a79a031 100644 --- a/app/src/androidTest/java/com/marcopla/flashcards/presentation/home/HomeScreenTest.kt +++ b/app/src/androidTest/java/com/marcopla/flashcards/presentation/home/HomeScreenTest.kt @@ -3,6 +3,7 @@ package com.marcopla.flashcards.presentation.home import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.createAndroidComposeRule import com.marcopla.flashcards.data.model.FlashCard +import com.marcopla.flashcards.presentation.screen.home.HomeScreenState import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -16,7 +17,7 @@ class HomeScreenTest { @Test fun homeScreen_whenGettingEmptyState_thenShowEmptyMessage() = runTest { - launchHomeScreen(composeRule, emptyList()) { + launchHomeScreen(composeRule, HomeScreenState.Empty) { // Do nothing } verify { emptyMessageIsDisplayed() @@ -31,7 +32,7 @@ class HomeScreenTest { FlashCard(frontText = "front3", backText = "back3") ) - launchHomeScreen(composeRule, flashCards) { + launchHomeScreen(composeRule, HomeScreenState.Cards(flashCards)) { // Do nothing } verify { listOfFlashCardsIsDisplayed(flashCards) @@ -40,7 +41,7 @@ class HomeScreenTest { @Test fun homeScreen_whenNoFlashCards_thenDoNotShowCarouselButton() = runTest { - launchHomeScreen(composeRule, emptyList()) { + launchHomeScreen(composeRule, HomeScreenState.Empty) { // Empty } verify { carouselButtonIsNotDisplayed() diff --git a/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt b/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt index c0f086f..28ff467 100644 --- a/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt +++ b/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt @@ -1,6 +1,9 @@ package com.marcopla.flashcards.presentation.navigation import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController import androidx.navigation.NavType import androidx.navigation.compose.NavHost @@ -10,18 +13,24 @@ import com.marcopla.flashcards.presentation.screen.add.AddScreen import com.marcopla.flashcards.presentation.screen.carousel.CarouselScreen import com.marcopla.flashcards.presentation.screen.edit.EditScreen import com.marcopla.flashcards.presentation.screen.home.HomeScreen +import com.marcopla.flashcards.presentation.screen.home.HomeScreenState +import com.marcopla.flashcards.presentation.screen.home.HomeViewModel import com.marcopla.flashcards.presentation.screen.result.ResultsScreen @Composable fun AppNavHost( - navController: NavHostController + navController: NavHostController, + viewModel: HomeViewModel = hiltViewModel() ) { NavHost( navController = navController, startDestination = Routes.HOME_SCREEN ) { composable(Routes.HOME_SCREEN) { + val screenState: HomeScreenState + by viewModel.homeScreenState.collectAsStateWithLifecycle() HomeScreen( + screenState = screenState, onNavigateToAddScreen = { navController.navigate(Routes.ADD_SCREEN) }, diff --git a/app/src/main/java/com/marcopla/flashcards/presentation/screen/home/HomeScreen.kt b/app/src/main/java/com/marcopla/flashcards/presentation/screen/home/HomeScreen.kt index 75917eb..fe760db 100644 --- a/app/src/main/java/com/marcopla/flashcards/presentation/screen/home/HomeScreen.kt +++ b/app/src/main/java/com/marcopla/flashcards/presentation/screen/home/HomeScreen.kt @@ -8,7 +8,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -17,8 +16,6 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.marcopla.flashcards.R import com.marcopla.flashcards.data.model.FlashCard import kotlinx.collections.immutable.ImmutableList @@ -26,13 +23,12 @@ import kotlinx.collections.immutable.toImmutableList @Composable fun HomeScreen( + screenState: HomeScreenState, onNavigateToAddScreen: () -> Unit, onItemClicked: (Int) -> Unit, onNavigateToCarouselScreen: () -> Unit, - modifier: Modifier = Modifier, - viewModel: HomeViewModel = hiltViewModel() + modifier: Modifier = Modifier ) { - val screenState: HomeScreenState by viewModel.homeScreenState.collectAsStateWithLifecycle() Scaffold( topBar = { TopAppBar( diff --git a/app/src/main/java/com/marcopla/flashcards/presentation/screen/result/ResultItem.kt b/app/src/main/java/com/marcopla/flashcards/presentation/screen/result/ResultItem.kt index 81905a0..0e8d02f 100644 --- a/app/src/main/java/com/marcopla/flashcards/presentation/screen/result/ResultItem.kt +++ b/app/src/main/java/com/marcopla/flashcards/presentation/screen/result/ResultItem.kt @@ -16,7 +16,7 @@ import com.marcopla.flashcards.data.model.QuizResult @Composable fun ResultItem( quizResult: QuizResult, - modifier: Modifier = Modifier, + modifier: Modifier = Modifier ) { Card { Row( From f31ea4f880e1e77576585cc3021a26bea1b65d7c Mon Sep 17 00:00:00 2001 From: marcopla99 Date: Tue, 11 Apr 2023 22:23:18 +0200 Subject: [PATCH 02/14] Extract HomeRoute to separate composable --- .../presentation/navigation/AppNavHost.kt | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt b/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt index 28ff467..ac996e3 100644 --- a/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt +++ b/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt @@ -19,18 +19,14 @@ import com.marcopla.flashcards.presentation.screen.result.ResultsScreen @Composable fun AppNavHost( - navController: NavHostController, - viewModel: HomeViewModel = hiltViewModel() + navController: NavHostController ) { NavHost( navController = navController, startDestination = Routes.HOME_SCREEN ) { composable(Routes.HOME_SCREEN) { - val screenState: HomeScreenState - by viewModel.homeScreenState.collectAsStateWithLifecycle() - HomeScreen( - screenState = screenState, + HomeRoute( onNavigateToAddScreen = { navController.navigate(Routes.ADD_SCREEN) }, @@ -79,6 +75,22 @@ fun AppNavHost( } } +@Composable +private fun HomeRoute( + onNavigateToAddScreen: () -> Unit, + onItemClicked: (Int) -> Unit, + onNavigateToCarouselScreen: () -> Unit, + viewModel: HomeViewModel = hiltViewModel() +) { + val screenState: HomeScreenState by viewModel.homeScreenState.collectAsStateWithLifecycle() + HomeScreen( + screenState = screenState, + onNavigateToAddScreen = onNavigateToAddScreen, + onItemClicked = onItemClicked, + onNavigateToCarouselScreen = onNavigateToCarouselScreen + ) +} + object Routes { const val CAROUSEL_SCREEN = "CAROUSEL_SCREEN" const val HOME_SCREEN = "HOME_SCREEN" From b034a2dd6844f8beef4c7271fac3ccb8ded01441 Mon Sep 17 00:00:00 2001 From: marcopla99 Date: Mon, 24 Apr 2023 10:51:56 +0200 Subject: [PATCH 03/14] Remove viewModel from AddScreen composable parameters and pass the state instead --- .idea/androidTestResultsUserPreferences.xml | 39 +++++++++++++++++++ .idea/misc.xml | 1 - .../presentation/add/AddScreenRobot.kt | 23 ++++++++++- .../presentation/navigation/AppNavHost.kt | 21 ++++++++-- .../presentation/screen/add/AddScreen.kt | 36 ++++++++--------- 5 files changed, 95 insertions(+), 25 deletions(-) diff --git a/.idea/androidTestResultsUserPreferences.xml b/.idea/androidTestResultsUserPreferences.xml index 0ad17ea..058a49f 100644 --- a/.idea/androidTestResultsUserPreferences.xml +++ b/.idea/androidTestResultsUserPreferences.xml @@ -543,6 +543,19 @@ + + + + + + + @@ -595,6 +608,19 @@ + + + + + + + @@ -636,6 +662,19 @@ + + + + + + + diff --git a/.idea/misc.xml b/.idea/misc.xml index 0ad17cb..8978d23 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/app/src/androidTest/java/com/marcopla/flashcards/presentation/add/AddScreenRobot.kt b/app/src/androidTest/java/com/marcopla/flashcards/presentation/add/AddScreenRobot.kt index 0c63246..6841954 100644 --- a/app/src/androidTest/java/com/marcopla/flashcards/presentation/add/AddScreenRobot.kt +++ b/app/src/androidTest/java/com/marcopla/flashcards/presentation/add/AddScreenRobot.kt @@ -1,8 +1,13 @@ package com.marcopla.flashcards.presentation.add import androidx.activity.ComponentActivity -import androidx.compose.ui.test.* +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsFocused import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput import androidx.test.ext.junit.rules.ActivityScenarioRule import com.marcopla.flashcards.R import com.marcopla.flashcards.data.repository.FlashCardRepository @@ -19,8 +24,22 @@ fun launchAddScreen( repository: FlashCardRepository = TestFlashCardRepository(), block: AddScreenRobot.() -> Unit ): AddScreenRobot { + val viewModel = AddViewModel(AddUseCase(repository)) composeRule.setContent { - AddScreen(viewModel = AddViewModel(AddUseCase(repository))) + AddScreen( + infoTextState = viewModel.infoTextState.value, + frontTextState = viewModel.frontTextState.value, + backTextState = viewModel.backTextState.value, + addScreenState = viewModel.addScreenState.value, + onSubmitClick = { + viewModel.attemptSubmit( + frontText = viewModel.frontTextState.value.text, + backText = viewModel.backTextState.value.text + ) + }, + onFrontTextValueChange = { frontInput -> viewModel.updateFrontText(frontInput) }, + onBackTextValueChange = { backInput -> viewModel.updateBackText(backInput) } + ) } return AddScreenRobot(composeRule).apply(block) } diff --git a/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt b/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt index ac996e3..a8b971d 100644 --- a/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt +++ b/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt @@ -10,6 +10,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.navArgument import com.marcopla.flashcards.presentation.screen.add.AddScreen +import com.marcopla.flashcards.presentation.screen.add.AddViewModel import com.marcopla.flashcards.presentation.screen.carousel.CarouselScreen import com.marcopla.flashcards.presentation.screen.edit.EditScreen import com.marcopla.flashcards.presentation.screen.home.HomeScreen @@ -19,7 +20,8 @@ import com.marcopla.flashcards.presentation.screen.result.ResultsScreen @Composable fun AppNavHost( - navController: NavHostController + navController: NavHostController, + addViewModel: AddViewModel = hiltViewModel() ) { NavHost( navController = navController, @@ -39,10 +41,23 @@ fun AppNavHost( ) } composable(Routes.ADD_SCREEN) { - AddScreen() + AddScreen( + infoTextState = addViewModel.infoTextState.value, + frontTextState = addViewModel.frontTextState.value, + backTextState = addViewModel.backTextState.value, + addScreenState = addViewModel.addScreenState.value, + onSubmitClick = { + addViewModel.attemptSubmit( + frontText = addViewModel.frontTextState.value.text, + backText = addViewModel.backTextState.value.text + ) + }, + onFrontTextValueChange = { frontInput -> addViewModel.updateFrontText(frontInput) }, + onBackTextValueChange = { backInput -> addViewModel.updateBackText(backInput) } + ) } composable( - "${Routes.EDIT_SCREEN}/{$FLASH_CARD_ID_ARG_KEY}", + route = "${Routes.EDIT_SCREEN}/{$FLASH_CARD_ID_ARG_KEY}", arguments = listOf( navArgument(FLASH_CARD_ID_ARG_KEY) { type = NavType.IntType diff --git a/app/src/main/java/com/marcopla/flashcards/presentation/screen/add/AddScreen.kt b/app/src/main/java/com/marcopla/flashcards/presentation/screen/add/AddScreen.kt index 6bc4664..76bea20 100644 --- a/app/src/main/java/com/marcopla/flashcards/presentation/screen/add/AddScreen.kt +++ b/app/src/main/java/com/marcopla/flashcards/presentation/screen/add/AddScreen.kt @@ -14,18 +14,23 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import com.marcopla.flashcards.R @OptIn(ExperimentalLayoutApi::class) @Composable fun AddScreen( - modifier: Modifier = Modifier, - viewModel: AddViewModel = hiltViewModel() + infoTextState: InfoTextState, + frontTextState: FrontTextState, + backTextState: BackTextState, + addScreenState: AddScreenState, + onSubmitClick: () -> Unit, + onFrontTextValueChange: (String) -> Unit, + onBackTextValueChange: (String) -> Unit, + modifier: Modifier = Modifier ) { val scaffoldState = rememberScaffoldState() HandleInfoTextEffect( - viewModel.infoTextState.value.messageStringRes, + infoTextState.messageStringRes, scaffoldState.snackbarHostState ) @@ -35,14 +40,7 @@ fun AddScreen( TopAppBar(title = { Text(stringResource(R.string.addScreenTitle)) }) }, floatingActionButton = { - FloatingActionButton( - onClick = { - viewModel.attemptSubmit( - frontText = viewModel.frontTextState.value.text, - backText = viewModel.backTextState.value.text - ) - } - ) { + FloatingActionButton(onClick = onSubmitClick) { Icon( imageVector = Icons.Default.Add, contentDescription = stringResource(R.string.addCardButton) @@ -52,16 +50,16 @@ fun AddScreen( ) { paddingValues: PaddingValues -> Column(modifier = modifier.padding(4.dp).consumeWindowInsets(paddingValues)) { FontTextField( - value = viewModel.frontTextState.value.text, - isError = viewModel.frontTextState.value.showError, - isFocused = viewModel.addScreenState.value == AddScreenState.SUCCESSFUL_SAVE, - onValueChange = { frontInput -> viewModel.updateFrontText(frontInput) } + value = frontTextState.text, + isError = frontTextState.showError, + isFocused = addScreenState == AddScreenState.SUCCESSFUL_SAVE, + onValueChange = onFrontTextValueChange ) Spacer(modifier = Modifier.height(8.dp)) BackTextField( - value = viewModel.backTextState.value.text, - isError = viewModel.backTextState.value.showError, - onValueChange = { backInput -> viewModel.updateBackText(backInput) } + value = backTextState.text, + isError = backTextState.showError, + onValueChange = onBackTextValueChange ) } } From 526b9c95b8d1269cba2109eef30d00762e73aa2e Mon Sep 17 00:00:00 2001 From: marcopla99 Date: Mon, 24 Apr 2023 11:03:24 +0200 Subject: [PATCH 04/14] Extract AddRoute to separate composable --- .../presentation/add/AddScreenRobot.kt | 19 ++-------- .../presentation/navigation/AppNavHost.kt | 36 ++++++++++--------- 2 files changed, 22 insertions(+), 33 deletions(-) diff --git a/app/src/androidTest/java/com/marcopla/flashcards/presentation/add/AddScreenRobot.kt b/app/src/androidTest/java/com/marcopla/flashcards/presentation/add/AddScreenRobot.kt index 6841954..a93519d 100644 --- a/app/src/androidTest/java/com/marcopla/flashcards/presentation/add/AddScreenRobot.kt +++ b/app/src/androidTest/java/com/marcopla/flashcards/presentation/add/AddScreenRobot.kt @@ -12,7 +12,7 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule import com.marcopla.flashcards.R import com.marcopla.flashcards.data.repository.FlashCardRepository import com.marcopla.flashcards.domain.usecase.AddUseCase -import com.marcopla.flashcards.presentation.screen.add.AddScreen +import com.marcopla.flashcards.presentation.navigation.AddRoute import com.marcopla.flashcards.presentation.screen.add.AddViewModel import com.marcopla.testing_shared.TestFlashCardRepository @@ -25,22 +25,7 @@ fun launchAddScreen( block: AddScreenRobot.() -> Unit ): AddScreenRobot { val viewModel = AddViewModel(AddUseCase(repository)) - composeRule.setContent { - AddScreen( - infoTextState = viewModel.infoTextState.value, - frontTextState = viewModel.frontTextState.value, - backTextState = viewModel.backTextState.value, - addScreenState = viewModel.addScreenState.value, - onSubmitClick = { - viewModel.attemptSubmit( - frontText = viewModel.frontTextState.value.text, - backText = viewModel.backTextState.value.text - ) - }, - onFrontTextValueChange = { frontInput -> viewModel.updateFrontText(frontInput) }, - onBackTextValueChange = { backInput -> viewModel.updateBackText(backInput) } - ) - } + composeRule.setContent { AddRoute(viewModel = viewModel) } return AddScreenRobot(composeRule).apply(block) } diff --git a/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt b/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt index a8b971d..7a36454 100644 --- a/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt +++ b/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt @@ -20,8 +20,7 @@ import com.marcopla.flashcards.presentation.screen.result.ResultsScreen @Composable fun AppNavHost( - navController: NavHostController, - addViewModel: AddViewModel = hiltViewModel() + navController: NavHostController ) { NavHost( navController = navController, @@ -41,20 +40,7 @@ fun AppNavHost( ) } composable(Routes.ADD_SCREEN) { - AddScreen( - infoTextState = addViewModel.infoTextState.value, - frontTextState = addViewModel.frontTextState.value, - backTextState = addViewModel.backTextState.value, - addScreenState = addViewModel.addScreenState.value, - onSubmitClick = { - addViewModel.attemptSubmit( - frontText = addViewModel.frontTextState.value.text, - backText = addViewModel.backTextState.value.text - ) - }, - onFrontTextValueChange = { frontInput -> addViewModel.updateFrontText(frontInput) }, - onBackTextValueChange = { backInput -> addViewModel.updateBackText(backInput) } - ) + AddRoute() } composable( route = "${Routes.EDIT_SCREEN}/{$FLASH_CARD_ID_ARG_KEY}", @@ -106,6 +92,24 @@ private fun HomeRoute( ) } +@Composable +fun AddRoute(viewModel: AddViewModel = hiltViewModel()) { + AddScreen( + infoTextState = viewModel.infoTextState.value, + frontTextState = viewModel.frontTextState.value, + backTextState = viewModel.backTextState.value, + addScreenState = viewModel.addScreenState.value, + onSubmitClick = { + viewModel.attemptSubmit( + frontText = viewModel.frontTextState.value.text, + backText = viewModel.backTextState.value.text + ) + }, + onFrontTextValueChange = { frontInput -> viewModel.updateFrontText(frontInput) }, + onBackTextValueChange = { backInput -> viewModel.updateBackText(backInput) } + ) +} + object Routes { const val CAROUSEL_SCREEN = "CAROUSEL_SCREEN" const val HOME_SCREEN = "HOME_SCREEN" From 920d828c51c99bb3eddf26a3950d2254ae7221fc Mon Sep 17 00:00:00 2001 From: marcopla99 Date: Mon, 24 Apr 2023 12:03:24 +0200 Subject: [PATCH 05/14] Extract EditRoute to separate composable --- .../presentation/edit/EditScreenRobot.kt | 29 ++++----- .../presentation/navigation/AppNavHost.kt | 47 +++++++++++--- .../presentation/screen/edit/EditScreen.kt | 62 +++++++++---------- 3 files changed, 85 insertions(+), 53 deletions(-) diff --git a/app/src/androidTest/java/com/marcopla/flashcards/presentation/edit/EditScreenRobot.kt b/app/src/androidTest/java/com/marcopla/flashcards/presentation/edit/EditScreenRobot.kt index e582798..288aa60 100644 --- a/app/src/androidTest/java/com/marcopla/flashcards/presentation/edit/EditScreenRobot.kt +++ b/app/src/androidTest/java/com/marcopla/flashcards/presentation/edit/EditScreenRobot.kt @@ -1,8 +1,12 @@ package com.marcopla.flashcards.presentation.edit import androidx.activity.ComponentActivity -import androidx.compose.ui.test.* +import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput import androidx.lifecycle.SavedStateHandle import androidx.test.ext.junit.rules.ActivityScenarioRule import com.marcopla.flashcards.R @@ -12,8 +16,8 @@ import com.marcopla.flashcards.data.repository.FlashCardRepositoryImpl import com.marcopla.flashcards.domain.usecase.DeleteUseCase import com.marcopla.flashcards.domain.usecase.EditUseCase import com.marcopla.flashcards.domain.usecase.LoadUseCase +import com.marcopla.flashcards.presentation.navigation.EditRoute import com.marcopla.flashcards.presentation.navigation.FLASH_CARD_ID_ARG_KEY -import com.marcopla.flashcards.presentation.screen.edit.EditScreen import com.marcopla.flashcards.presentation.screen.edit.EditViewModel import com.marcopla.testing_shared.FakeFlashCardDao import com.marcopla.testing_shared.TestFlashCardRepository @@ -27,19 +31,16 @@ fun launchEditScreenFor( flashCardRepository: FlashCardRepository = TestFlashCardRepository(), block: EditScreenRobot.() -> Unit ): EditScreenRobot { + val viewModel = EditViewModel( + SavedStateHandle(mapOf(FLASH_CARD_ID_ARG_KEY to selectedFlashCard.id)), + EditUseCase(flashCardRepository), + LoadUseCase( + FlashCardRepositoryImpl(FakeFlashCardDao(listOf(selectedFlashCard))) + ), + DeleteUseCase(TestFlashCardRepository()) + ) composeRule.setContent { - EditScreen( - viewModel = EditViewModel( - SavedStateHandle(mapOf(FLASH_CARD_ID_ARG_KEY to selectedFlashCard.id)), - EditUseCase(flashCardRepository), - LoadUseCase( - FlashCardRepositoryImpl(FakeFlashCardDao(listOf(selectedFlashCard))) - ), - DeleteUseCase(TestFlashCardRepository()) - ), - onFlashCardEdited = {}, - onFlashCardDeleted = {} - ) + EditRoute(viewModel) {} } return EditScreenRobot(composeRule).apply(block) } diff --git a/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt b/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt index 7a36454..7690b97 100644 --- a/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt +++ b/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt @@ -13,6 +13,7 @@ import com.marcopla.flashcards.presentation.screen.add.AddScreen import com.marcopla.flashcards.presentation.screen.add.AddViewModel import com.marcopla.flashcards.presentation.screen.carousel.CarouselScreen import com.marcopla.flashcards.presentation.screen.edit.EditScreen +import com.marcopla.flashcards.presentation.screen.edit.EditViewModel import com.marcopla.flashcards.presentation.screen.home.HomeScreen import com.marcopla.flashcards.presentation.screen.home.HomeScreenState import com.marcopla.flashcards.presentation.screen.home.HomeViewModel @@ -20,7 +21,7 @@ import com.marcopla.flashcards.presentation.screen.result.ResultsScreen @Composable fun AppNavHost( - navController: NavHostController + navController: NavHostController, ) { NavHost( navController = navController, @@ -50,11 +51,8 @@ fun AppNavHost( } ) ) { - EditScreen( - onFlashCardEdited = { - navController.popBackStack(Routes.HOME_SCREEN, false) - }, - onFlashCardDeleted = { + EditRoute( + onPopBackStack = { navController.popBackStack(Routes.HOME_SCREEN, false) } ) @@ -81,7 +79,7 @@ private fun HomeRoute( onNavigateToAddScreen: () -> Unit, onItemClicked: (Int) -> Unit, onNavigateToCarouselScreen: () -> Unit, - viewModel: HomeViewModel = hiltViewModel() + viewModel: HomeViewModel = hiltViewModel(), ) { val screenState: HomeScreenState by viewModel.homeScreenState.collectAsStateWithLifecycle() HomeScreen( @@ -110,6 +108,41 @@ fun AddRoute(viewModel: AddViewModel = hiltViewModel()) { ) } +@Composable +fun EditRoute( + viewModel: EditViewModel = hiltViewModel(), + onPopBackStack: () -> Unit, +) { + EditScreen( + onFlashCardEdited = onPopBackStack, + onFlashCardDeleted = { + viewModel.hideDeleteConfirmationDialog() + onPopBackStack() + }, + editScreenState = viewModel.screenState.value, + shouldShowDeleteConfirmationDialog = viewModel.shouldShowDeleteConfirmation.value, + onConfirmationClick = { viewModel.delete() }, + onDismissClick = { viewModel.hideDeleteConfirmationDialog() }, + onReset = viewModel::reset, + onShowDeleteConfirmationDialog = { + viewModel.showDeleteConfirmationDialog() + }, + onSubmit = { + viewModel.attemptSubmit( + viewModel.frontTextState.value.text, + viewModel.backTextState.value.text + ) + }, + onInitState = { + viewModel.initState() + }, + frontTextState = viewModel.frontTextState.value, + onFrontTextValueChange = viewModel::updateFrontText, + backTextState = viewModel.backTextState.value, + onBackTextValueChange = viewModel::updateBackText + ) +} + object Routes { const val CAROUSEL_SCREEN = "CAROUSEL_SCREEN" const val HOME_SCREEN = "HOME_SCREEN" diff --git a/app/src/main/java/com/marcopla/flashcards/presentation/screen/edit/EditScreen.kt b/app/src/main/java/com/marcopla/flashcards/presentation/screen/edit/EditScreen.kt index 191e71d..2e21ab6 100644 --- a/app/src/main/java/com/marcopla/flashcards/presentation/screen/edit/EditScreen.kt +++ b/app/src/main/java/com/marcopla/flashcards/presentation/screen/edit/EditScreen.kt @@ -13,7 +13,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import com.marcopla.flashcards.R @OptIn(ExperimentalLayoutApi::class) @@ -21,25 +20,33 @@ import com.marcopla.flashcards.R fun EditScreen( onFlashCardEdited: () -> Unit, onFlashCardDeleted: () -> Unit, - modifier: Modifier = Modifier, - viewModel: EditViewModel = hiltViewModel() + editScreenState: EditScreenState, + shouldShowDeleteConfirmationDialog: Boolean, + onConfirmationClick: () -> Unit, + onDismissClick: () -> Unit, + onReset: () -> Unit, + onShowDeleteConfirmationDialog: () -> Unit, + onSubmit: () -> Unit, + onInitState: () -> Unit, + frontTextState: EditFrontTextState, + onFrontTextValueChange: (String) -> Unit, + backTextState: EditBackTextState, + onBackTextValueChange: (String) -> Unit, + modifier: Modifier = Modifier ) { val scaffoldState = rememberScaffoldState() HandleScreenState( - viewModel.screenState.value, + editScreenState, scaffoldState, onFlashCardEdited, - onFlashCardDeleted = { - viewModel.hideDeleteConfirmationDialog() - onFlashCardDeleted() - } + onFlashCardDeleted ) - if (viewModel.shouldShowDeleteConfirmation.value) { + if (shouldShowDeleteConfirmationDialog) { DeleteConfirmationDialog( - onConfirmationClick = { viewModel.delete() }, - onCancelClick = { viewModel.hideDeleteConfirmationDialog() }, - onDismissRequest = { viewModel.hideDeleteConfirmationDialog() } + onConfirmationClick = onConfirmationClick, + onCancelClick = onDismissClick, + onDismissRequest = onDismissClick ) } @@ -51,17 +58,15 @@ fun EditScreen( Text(text = stringResource(R.string.editScreenTitle)) }, actions = { - if (viewModel.screenState.value == EditScreenState.Editing) { + if (editScreenState == EditScreenState.Editing) { Icon( - modifier = Modifier.clickable(onClick = viewModel::reset), + modifier = Modifier.clickable(onClick = onReset), imageVector = Icons.Default.Refresh, contentDescription = stringResource(id = R.string.resetButton) ) } else { Icon( - modifier = Modifier.clickable { - viewModel.showDeleteConfirmationDialog() - }, + modifier = Modifier.clickable(onClick = onShowDeleteConfirmationDialog), imageVector = Icons.Default.Delete, contentDescription = stringResource(R.string.deleteButton) ) @@ -70,12 +75,7 @@ fun EditScreen( ) }, floatingActionButton = { - FloatingActionButton(onClick = { - viewModel.attemptSubmit( - viewModel.frontTextState.value.text, - viewModel.backTextState.value.text - ) - }) { + FloatingActionButton(onClick = onSubmit) { Icon( imageVector = Icons.Default.Edit, contentDescription = stringResource(R.string.editButton) @@ -84,9 +84,7 @@ fun EditScreen( } ) { // TODO: Consider using Flows and call collectAsStateWithLifecycle here. - LaunchedEffect(key1 = Unit) { - viewModel.initState() - } + LaunchedEffect(key1 = Unit, block = { onInitState() }) Column( modifier = modifier @@ -100,10 +98,10 @@ fun EditScreen( .semantics { contentDescription = frontTextContentDescription }, - value = viewModel.frontTextState.value.text, + value = frontTextState.text, label = { Text(stringResource(R.string.frontTextFieldLabel)) }, - isError = viewModel.frontTextState.value.showError, - onValueChange = viewModel::updateFrontText + isError = frontTextState.showError, + onValueChange = onFrontTextValueChange ) Spacer(modifier = Modifier.height(8.dp)) @@ -115,10 +113,10 @@ fun EditScreen( .semantics { contentDescription = backTextContentDescription }, - value = viewModel.backTextState.value.text, + value = backTextState.text, label = { Text(stringResource(R.string.backTextFieldLabel)) }, - isError = viewModel.backTextState.value.showError, - onValueChange = viewModel::updateBackText + isError = backTextState.showError, + onValueChange = onBackTextValueChange ) } } From c56a60bc4e4ee1a3e8792b3e449628caac1dbb5d Mon Sep 17 00:00:00 2001 From: marcopla99 Date: Mon, 24 Apr 2023 12:21:06 +0200 Subject: [PATCH 06/14] Move DeleteConfirmationDialog from EditScreen to EditRoute --- .../presentation/navigation/AppNavHost.kt | 26 ++++++++++--------- .../presentation/screen/edit/EditScreen.kt | 13 +--------- 2 files changed, 15 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt b/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt index 7690b97..7f88db7 100644 --- a/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt +++ b/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt @@ -12,6 +12,7 @@ import androidx.navigation.navArgument import com.marcopla.flashcards.presentation.screen.add.AddScreen import com.marcopla.flashcards.presentation.screen.add.AddViewModel import com.marcopla.flashcards.presentation.screen.carousel.CarouselScreen +import com.marcopla.flashcards.presentation.screen.edit.DeleteConfirmationDialog import com.marcopla.flashcards.presentation.screen.edit.EditScreen import com.marcopla.flashcards.presentation.screen.edit.EditViewModel import com.marcopla.flashcards.presentation.screen.home.HomeScreen @@ -21,7 +22,7 @@ import com.marcopla.flashcards.presentation.screen.result.ResultsScreen @Composable fun AppNavHost( - navController: NavHostController, + navController: NavHostController ) { NavHost( navController = navController, @@ -79,7 +80,7 @@ private fun HomeRoute( onNavigateToAddScreen: () -> Unit, onItemClicked: (Int) -> Unit, onNavigateToCarouselScreen: () -> Unit, - viewModel: HomeViewModel = hiltViewModel(), + viewModel: HomeViewModel = hiltViewModel() ) { val screenState: HomeScreenState by viewModel.homeScreenState.collectAsStateWithLifecycle() HomeScreen( @@ -111,8 +112,16 @@ fun AddRoute(viewModel: AddViewModel = hiltViewModel()) { @Composable fun EditRoute( viewModel: EditViewModel = hiltViewModel(), - onPopBackStack: () -> Unit, + onPopBackStack: () -> Unit ) { + if (viewModel.shouldShowDeleteConfirmation.value) { + DeleteConfirmationDialog( + onConfirmationClick = viewModel::delete, + onCancelClick = viewModel::hideDeleteConfirmationDialog, + onDismissRequest = viewModel::hideDeleteConfirmationDialog + ) + } + EditScreen( onFlashCardEdited = onPopBackStack, onFlashCardDeleted = { @@ -120,22 +129,15 @@ fun EditRoute( onPopBackStack() }, editScreenState = viewModel.screenState.value, - shouldShowDeleteConfirmationDialog = viewModel.shouldShowDeleteConfirmation.value, - onConfirmationClick = { viewModel.delete() }, - onDismissClick = { viewModel.hideDeleteConfirmationDialog() }, onReset = viewModel::reset, - onShowDeleteConfirmationDialog = { - viewModel.showDeleteConfirmationDialog() - }, + onShowDeleteConfirmationDialog = viewModel::showDeleteConfirmationDialog, onSubmit = { viewModel.attemptSubmit( viewModel.frontTextState.value.text, viewModel.backTextState.value.text ) }, - onInitState = { - viewModel.initState() - }, + onInitState = viewModel::initState, frontTextState = viewModel.frontTextState.value, onFrontTextValueChange = viewModel::updateFrontText, backTextState = viewModel.backTextState.value, diff --git a/app/src/main/java/com/marcopla/flashcards/presentation/screen/edit/EditScreen.kt b/app/src/main/java/com/marcopla/flashcards/presentation/screen/edit/EditScreen.kt index 2e21ab6..b10adfc 100644 --- a/app/src/main/java/com/marcopla/flashcards/presentation/screen/edit/EditScreen.kt +++ b/app/src/main/java/com/marcopla/flashcards/presentation/screen/edit/EditScreen.kt @@ -21,9 +21,6 @@ fun EditScreen( onFlashCardEdited: () -> Unit, onFlashCardDeleted: () -> Unit, editScreenState: EditScreenState, - shouldShowDeleteConfirmationDialog: Boolean, - onConfirmationClick: () -> Unit, - onDismissClick: () -> Unit, onReset: () -> Unit, onShowDeleteConfirmationDialog: () -> Unit, onSubmit: () -> Unit, @@ -42,14 +39,6 @@ fun EditScreen( onFlashCardDeleted ) - if (shouldShowDeleteConfirmationDialog) { - DeleteConfirmationDialog( - onConfirmationClick = onConfirmationClick, - onCancelClick = onDismissClick, - onDismissRequest = onDismissClick - ) - } - Scaffold( scaffoldState = scaffoldState, topBar = { @@ -149,7 +138,7 @@ private fun HandleScreenState( } @Composable -private fun DeleteConfirmationDialog( +fun DeleteConfirmationDialog( onConfirmationClick: () -> Unit, onCancelClick: () -> Unit, onDismissRequest: () -> Unit, From cfa4fa37e95eec63ada9679645eac313ea40bd11 Mon Sep 17 00:00:00 2001 From: marcopla99 Date: Mon, 24 Apr 2023 16:57:41 +0200 Subject: [PATCH 07/14] Move call to HandleInfoTextEffect from AddScreen to AddRoute --- .../presentation/navigation/AppNavHost.kt | 13 +++++++++++-- .../flashcards/presentation/screen/add/AddScreen.kt | 10 ++-------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt b/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt index 7f88db7..1f32e0a 100644 --- a/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt +++ b/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt @@ -1,5 +1,6 @@ package com.marcopla.flashcards.presentation.navigation +import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.hilt.navigation.compose.hiltViewModel @@ -11,6 +12,7 @@ import androidx.navigation.compose.composable import androidx.navigation.navArgument import com.marcopla.flashcards.presentation.screen.add.AddScreen import com.marcopla.flashcards.presentation.screen.add.AddViewModel +import com.marcopla.flashcards.presentation.screen.add.HandleInfoTextEffect import com.marcopla.flashcards.presentation.screen.carousel.CarouselScreen import com.marcopla.flashcards.presentation.screen.edit.DeleteConfirmationDialog import com.marcopla.flashcards.presentation.screen.edit.EditScreen @@ -93,8 +95,14 @@ private fun HomeRoute( @Composable fun AddRoute(viewModel: AddViewModel = hiltViewModel()) { + val infoTextState = viewModel.infoTextState.value + val scaffoldState = rememberScaffoldState() + HandleInfoTextEffect( + infoTextState.messageStringRes, + scaffoldState.snackbarHostState + ) + AddScreen( - infoTextState = viewModel.infoTextState.value, frontTextState = viewModel.frontTextState.value, backTextState = viewModel.backTextState.value, addScreenState = viewModel.addScreenState.value, @@ -105,7 +113,8 @@ fun AddRoute(viewModel: AddViewModel = hiltViewModel()) { ) }, onFrontTextValueChange = { frontInput -> viewModel.updateFrontText(frontInput) }, - onBackTextValueChange = { backInput -> viewModel.updateBackText(backInput) } + onBackTextValueChange = { backInput -> viewModel.updateBackText(backInput) }, + scaffoldState = scaffoldState ) } diff --git a/app/src/main/java/com/marcopla/flashcards/presentation/screen/add/AddScreen.kt b/app/src/main/java/com/marcopla/flashcards/presentation/screen/add/AddScreen.kt index 76bea20..f15cf9b 100644 --- a/app/src/main/java/com/marcopla/flashcards/presentation/screen/add/AddScreen.kt +++ b/app/src/main/java/com/marcopla/flashcards/presentation/screen/add/AddScreen.kt @@ -19,21 +19,15 @@ import com.marcopla.flashcards.R @OptIn(ExperimentalLayoutApi::class) @Composable fun AddScreen( - infoTextState: InfoTextState, frontTextState: FrontTextState, backTextState: BackTextState, addScreenState: AddScreenState, onSubmitClick: () -> Unit, onFrontTextValueChange: (String) -> Unit, onBackTextValueChange: (String) -> Unit, + scaffoldState: ScaffoldState, modifier: Modifier = Modifier ) { - val scaffoldState = rememberScaffoldState() - HandleInfoTextEffect( - infoTextState.messageStringRes, - scaffoldState.snackbarHostState - ) - Scaffold( scaffoldState = scaffoldState, topBar = { @@ -112,7 +106,7 @@ private fun BackTextField( } @Composable -private fun HandleInfoTextEffect( +fun HandleInfoTextEffect( infoTextStringRes: Int?, snackbarHostState: SnackbarHostState ) { From 8eae995469dba41c46ad2905782fb53756bf7da0 Mon Sep 17 00:00:00 2001 From: marcopla99 Date: Mon, 24 Apr 2023 17:34:38 +0200 Subject: [PATCH 08/14] Move routes to the same screen file --- .idea/kotlinc.xml | 2 +- .idea/misc.xml | 2 +- .../presentation/add/AddScreenRobot.kt | 2 +- .../presentation/edit/EditScreenRobot.kt | 2 +- .../presentation/navigation/AppNavHost.kt | 93 +------------------ .../presentation/screen/add/AddScreen.kt | 28 +++++- .../presentation/screen/edit/EditScreen.kt | 37 ++++++++ .../presentation/screen/home/HomeScreen.kt | 21 ++++- 8 files changed, 91 insertions(+), 96 deletions(-) diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 0fc3113..69e8615 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 8978d23..773fe0f 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,6 @@ - + diff --git a/app/src/androidTest/java/com/marcopla/flashcards/presentation/add/AddScreenRobot.kt b/app/src/androidTest/java/com/marcopla/flashcards/presentation/add/AddScreenRobot.kt index a93519d..7a729ce 100644 --- a/app/src/androidTest/java/com/marcopla/flashcards/presentation/add/AddScreenRobot.kt +++ b/app/src/androidTest/java/com/marcopla/flashcards/presentation/add/AddScreenRobot.kt @@ -12,7 +12,7 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule import com.marcopla.flashcards.R import com.marcopla.flashcards.data.repository.FlashCardRepository import com.marcopla.flashcards.domain.usecase.AddUseCase -import com.marcopla.flashcards.presentation.navigation.AddRoute +import com.marcopla.flashcards.presentation.screen.add.AddRoute import com.marcopla.flashcards.presentation.screen.add.AddViewModel import com.marcopla.testing_shared.TestFlashCardRepository diff --git a/app/src/androidTest/java/com/marcopla/flashcards/presentation/edit/EditScreenRobot.kt b/app/src/androidTest/java/com/marcopla/flashcards/presentation/edit/EditScreenRobot.kt index 288aa60..ef5c5d6 100644 --- a/app/src/androidTest/java/com/marcopla/flashcards/presentation/edit/EditScreenRobot.kt +++ b/app/src/androidTest/java/com/marcopla/flashcards/presentation/edit/EditScreenRobot.kt @@ -16,8 +16,8 @@ import com.marcopla.flashcards.data.repository.FlashCardRepositoryImpl import com.marcopla.flashcards.domain.usecase.DeleteUseCase import com.marcopla.flashcards.domain.usecase.EditUseCase import com.marcopla.flashcards.domain.usecase.LoadUseCase -import com.marcopla.flashcards.presentation.navigation.EditRoute import com.marcopla.flashcards.presentation.navigation.FLASH_CARD_ID_ARG_KEY +import com.marcopla.flashcards.presentation.screen.edit.EditRoute import com.marcopla.flashcards.presentation.screen.edit.EditViewModel import com.marcopla.testing_shared.FakeFlashCardDao import com.marcopla.testing_shared.TestFlashCardRepository diff --git a/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt b/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt index 1f32e0a..b811b57 100644 --- a/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt +++ b/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt @@ -1,25 +1,15 @@ package com.marcopla.flashcards.presentation.navigation -import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.navArgument -import com.marcopla.flashcards.presentation.screen.add.AddScreen -import com.marcopla.flashcards.presentation.screen.add.AddViewModel -import com.marcopla.flashcards.presentation.screen.add.HandleInfoTextEffect +import com.marcopla.flashcards.presentation.screen.add.AddRoute import com.marcopla.flashcards.presentation.screen.carousel.CarouselScreen -import com.marcopla.flashcards.presentation.screen.edit.DeleteConfirmationDialog -import com.marcopla.flashcards.presentation.screen.edit.EditScreen -import com.marcopla.flashcards.presentation.screen.edit.EditViewModel -import com.marcopla.flashcards.presentation.screen.home.HomeScreen -import com.marcopla.flashcards.presentation.screen.home.HomeScreenState -import com.marcopla.flashcards.presentation.screen.home.HomeViewModel +import com.marcopla.flashcards.presentation.screen.edit.EditRoute +import com.marcopla.flashcards.presentation.screen.home.HomeRoute import com.marcopla.flashcards.presentation.screen.result.ResultsScreen @Composable @@ -77,83 +67,6 @@ fun AppNavHost( } } -@Composable -private fun HomeRoute( - onNavigateToAddScreen: () -> Unit, - onItemClicked: (Int) -> Unit, - onNavigateToCarouselScreen: () -> Unit, - viewModel: HomeViewModel = hiltViewModel() -) { - val screenState: HomeScreenState by viewModel.homeScreenState.collectAsStateWithLifecycle() - HomeScreen( - screenState = screenState, - onNavigateToAddScreen = onNavigateToAddScreen, - onItemClicked = onItemClicked, - onNavigateToCarouselScreen = onNavigateToCarouselScreen - ) -} - -@Composable -fun AddRoute(viewModel: AddViewModel = hiltViewModel()) { - val infoTextState = viewModel.infoTextState.value - val scaffoldState = rememberScaffoldState() - HandleInfoTextEffect( - infoTextState.messageStringRes, - scaffoldState.snackbarHostState - ) - - AddScreen( - frontTextState = viewModel.frontTextState.value, - backTextState = viewModel.backTextState.value, - addScreenState = viewModel.addScreenState.value, - onSubmitClick = { - viewModel.attemptSubmit( - frontText = viewModel.frontTextState.value.text, - backText = viewModel.backTextState.value.text - ) - }, - onFrontTextValueChange = { frontInput -> viewModel.updateFrontText(frontInput) }, - onBackTextValueChange = { backInput -> viewModel.updateBackText(backInput) }, - scaffoldState = scaffoldState - ) -} - -@Composable -fun EditRoute( - viewModel: EditViewModel = hiltViewModel(), - onPopBackStack: () -> Unit -) { - if (viewModel.shouldShowDeleteConfirmation.value) { - DeleteConfirmationDialog( - onConfirmationClick = viewModel::delete, - onCancelClick = viewModel::hideDeleteConfirmationDialog, - onDismissRequest = viewModel::hideDeleteConfirmationDialog - ) - } - - EditScreen( - onFlashCardEdited = onPopBackStack, - onFlashCardDeleted = { - viewModel.hideDeleteConfirmationDialog() - onPopBackStack() - }, - editScreenState = viewModel.screenState.value, - onReset = viewModel::reset, - onShowDeleteConfirmationDialog = viewModel::showDeleteConfirmationDialog, - onSubmit = { - viewModel.attemptSubmit( - viewModel.frontTextState.value.text, - viewModel.backTextState.value.text - ) - }, - onInitState = viewModel::initState, - frontTextState = viewModel.frontTextState.value, - onFrontTextValueChange = viewModel::updateFrontText, - backTextState = viewModel.backTextState.value, - onBackTextValueChange = viewModel::updateBackText - ) -} - object Routes { const val CAROUSEL_SCREEN = "CAROUSEL_SCREEN" const val HOME_SCREEN = "HOME_SCREEN" diff --git a/app/src/main/java/com/marcopla/flashcards/presentation/screen/add/AddScreen.kt b/app/src/main/java/com/marcopla/flashcards/presentation/screen/add/AddScreen.kt index f15cf9b..d25bd2d 100644 --- a/app/src/main/java/com/marcopla/flashcards/presentation/screen/add/AddScreen.kt +++ b/app/src/main/java/com/marcopla/flashcards/presentation/screen/add/AddScreen.kt @@ -14,8 +14,34 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import com.marcopla.flashcards.R +@Composable +fun AddRoute(viewModel: AddViewModel = hiltViewModel()) { + val infoTextState = viewModel.infoTextState.value + val scaffoldState = rememberScaffoldState() + HandleInfoTextEffect( + infoTextState.messageStringRes, + scaffoldState.snackbarHostState + ) + + AddScreen( + frontTextState = viewModel.frontTextState.value, + backTextState = viewModel.backTextState.value, + addScreenState = viewModel.addScreenState.value, + onSubmitClick = { + viewModel.attemptSubmit( + frontText = viewModel.frontTextState.value.text, + backText = viewModel.backTextState.value.text + ) + }, + onFrontTextValueChange = { frontInput -> viewModel.updateFrontText(frontInput) }, + onBackTextValueChange = { backInput -> viewModel.updateBackText(backInput) }, + scaffoldState = scaffoldState + ) +} + @OptIn(ExperimentalLayoutApi::class) @Composable fun AddScreen( @@ -115,4 +141,4 @@ fun HandleInfoTextEffect( LaunchedEffect(infoTextStringRes) { snackbarHostState.showSnackbar(infoText) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/marcopla/flashcards/presentation/screen/edit/EditScreen.kt b/app/src/main/java/com/marcopla/flashcards/presentation/screen/edit/EditScreen.kt index b10adfc..d213399 100644 --- a/app/src/main/java/com/marcopla/flashcards/presentation/screen/edit/EditScreen.kt +++ b/app/src/main/java/com/marcopla/flashcards/presentation/screen/edit/EditScreen.kt @@ -13,8 +13,45 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import com.marcopla.flashcards.R +@Composable +fun EditRoute( + viewModel: EditViewModel = hiltViewModel(), + onPopBackStack: () -> Unit +) { + if (viewModel.shouldShowDeleteConfirmation.value) { + DeleteConfirmationDialog( + onConfirmationClick = viewModel::delete, + onCancelClick = viewModel::hideDeleteConfirmationDialog, + onDismissRequest = viewModel::hideDeleteConfirmationDialog + ) + } + + EditScreen( + onFlashCardEdited = onPopBackStack, + onFlashCardDeleted = { + viewModel.hideDeleteConfirmationDialog() + onPopBackStack() + }, + editScreenState = viewModel.screenState.value, + onReset = viewModel::reset, + onShowDeleteConfirmationDialog = viewModel::showDeleteConfirmationDialog, + onSubmit = { + viewModel.attemptSubmit( + viewModel.frontTextState.value.text, + viewModel.backTextState.value.text + ) + }, + onInitState = viewModel::initState, + frontTextState = viewModel.frontTextState.value, + onFrontTextValueChange = viewModel::updateFrontText, + backTextState = viewModel.backTextState.value, + onBackTextValueChange = viewModel::updateBackText + ) +} + @OptIn(ExperimentalLayoutApi::class) @Composable fun EditScreen( diff --git a/app/src/main/java/com/marcopla/flashcards/presentation/screen/home/HomeScreen.kt b/app/src/main/java/com/marcopla/flashcards/presentation/screen/home/HomeScreen.kt index fe760db..5bff620 100644 --- a/app/src/main/java/com/marcopla/flashcards/presentation/screen/home/HomeScreen.kt +++ b/app/src/main/java/com/marcopla/flashcards/presentation/screen/home/HomeScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -16,11 +17,29 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.marcopla.flashcards.R import com.marcopla.flashcards.data.model.FlashCard import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList +@Composable +fun HomeRoute( + onNavigateToAddScreen: () -> Unit, + onItemClicked: (Int) -> Unit, + onNavigateToCarouselScreen: () -> Unit, + viewModel: HomeViewModel = hiltViewModel() +) { + val screenState: HomeScreenState by viewModel.homeScreenState.collectAsStateWithLifecycle() + HomeScreen( + screenState = screenState, + onNavigateToAddScreen = onNavigateToAddScreen, + onItemClicked = onItemClicked, + onNavigateToCarouselScreen = onNavigateToCarouselScreen + ) +} + @Composable fun HomeScreen( screenState: HomeScreenState, @@ -153,4 +172,4 @@ private fun CardsList( } } } -} \ No newline at end of file +} From e264fb61f4c74d001d5f85b6c5a07f209d53b481 Mon Sep 17 00:00:00 2001 From: marcopla99 Date: Mon, 24 Apr 2023 17:58:19 +0200 Subject: [PATCH 09/14] Create CarouselRoute --- .../presentation/carousel/CarouselRobot.kt | 23 +++++------ .../presentation/navigation/AppNavHost.kt | 4 +- .../screen/carousel/CarouselScreen.kt | 39 +++++++++++++------ 3 files changed, 41 insertions(+), 25 deletions(-) diff --git a/app/src/androidTest/java/com/marcopla/flashcards/presentation/carousel/CarouselRobot.kt b/app/src/androidTest/java/com/marcopla/flashcards/presentation/carousel/CarouselRobot.kt index e3c6a42..e086c37 100644 --- a/app/src/androidTest/java/com/marcopla/flashcards/presentation/carousel/CarouselRobot.kt +++ b/app/src/androidTest/java/com/marcopla/flashcards/presentation/carousel/CarouselRobot.kt @@ -1,15 +1,19 @@ package com.marcopla.flashcards.presentation.carousel import androidx.activity.ComponentActivity -import androidx.compose.ui.test.* +import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput import androidx.test.ext.junit.rules.ActivityScenarioRule import com.marcopla.flashcards.R import com.marcopla.flashcards.data.model.FlashCard import com.marcopla.flashcards.data.repository.FlashCardRepositoryImpl import com.marcopla.flashcards.domain.usecase.LoadUseCase import com.marcopla.flashcards.domain.usecase.SubmitQuizUseCase -import com.marcopla.flashcards.presentation.screen.carousel.CarouselScreen +import com.marcopla.flashcards.presentation.screen.carousel.CarouselRoute import com.marcopla.flashcards.presentation.screen.carousel.CarouselViewModel import com.marcopla.testing_shared.FakeFlashCardDao @@ -21,15 +25,12 @@ fun launchCarouselScreen( flashCards: List, block: CarouselScreenRobot.() -> Unit ): CarouselScreenRobot { - composeRule.setContent { - val repository = FlashCardRepositoryImpl(FakeFlashCardDao(flashCards)) - CarouselScreen( - viewModel = CarouselViewModel( - LoadUseCase(repository), - SubmitQuizUseCase(repository) - ) - ) {} - } + val repository = FlashCardRepositoryImpl(FakeFlashCardDao(flashCards)) + val viewModel = CarouselViewModel( + LoadUseCase(repository), + SubmitQuizUseCase(repository) + ) + composeRule.setContent { CarouselRoute(viewModel) {} } return CarouselScreenRobot(composeRule).apply(block) } diff --git a/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt b/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt index b811b57..4c6b3d0 100644 --- a/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt +++ b/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt @@ -7,7 +7,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.navArgument import com.marcopla.flashcards.presentation.screen.add.AddRoute -import com.marcopla.flashcards.presentation.screen.carousel.CarouselScreen +import com.marcopla.flashcards.presentation.screen.carousel.CarouselRoute import com.marcopla.flashcards.presentation.screen.edit.EditRoute import com.marcopla.flashcards.presentation.screen.home.HomeRoute import com.marcopla.flashcards.presentation.screen.result.ResultsScreen @@ -51,7 +51,7 @@ fun AppNavHost( ) } composable(Routes.CAROUSEL_SCREEN) { - CarouselScreen( + CarouselRoute( onLastFlashCardPlayed = { navController.navigate(Routes.RESULT_SCREEN) } diff --git a/app/src/main/java/com/marcopla/flashcards/presentation/screen/carousel/CarouselScreen.kt b/app/src/main/java/com/marcopla/flashcards/presentation/screen/carousel/CarouselScreen.kt index a5c10f5..8200be7 100644 --- a/app/src/main/java/com/marcopla/flashcards/presentation/screen/carousel/CarouselScreen.kt +++ b/app/src/main/java/com/marcopla/flashcards/presentation/screen/carousel/CarouselScreen.kt @@ -13,10 +13,8 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.marcopla.flashcards.R -@OptIn(ExperimentalLayoutApi::class) @Composable -fun CarouselScreen( - modifier: Modifier = Modifier, +fun CarouselRoute( viewModel: CarouselViewModel = hiltViewModel(), onLastFlashCardPlayed: () -> Unit ) { @@ -24,13 +22,32 @@ fun CarouselScreen( viewModel.loadFlashCards() } - val screenState = viewModel.screenState - if (screenState.value == CarouselScreenState.Finished) { + val screenState = viewModel.screenState.value + if (screenState == CarouselScreenState.Finished) { LaunchedEffect(key1 = Unit) { onLastFlashCardPlayed() } } + CarouselScreen( + screenState = screenState, + onSubmitClicked = { + viewModel.submit(viewModel.guessInput.value) + }, + guessInputState = viewModel.guessInput.value, + onGuessInputChange = viewModel::updateGuessInput + ) +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun CarouselScreen( + screenState: CarouselScreenState, + onSubmitClicked: () -> Unit, + guessInputState: String, + onGuessInputChange: (String) -> Unit, + modifier: Modifier = Modifier +) { Scaffold( topBar = { TopAppBar( @@ -40,9 +57,7 @@ fun CarouselScreen( ) }, floatingActionButton = { - FloatingActionButton(onClick = { - viewModel.submit(viewModel.guessInput.value) - }) { + FloatingActionButton(onClick = onSubmitClicked) { Icon(imageVector = Icons.Default.Done, stringResource(id = R.string.nextButton)) } } @@ -52,10 +67,10 @@ fun CarouselScreen( .padding(4.dp) .consumeWindowInsets(it) ) { - Prompt(screenState.value) + Prompt(screenState) Guess( - value = viewModel.guessInput.value, - onValueChange = viewModel::updateGuessInput + value = guessInputState, + onValueChange = onGuessInputChange ) } } @@ -94,4 +109,4 @@ private fun Prompt(screenState: CarouselScreenState) { } } Text(text = promptText) -} \ No newline at end of file +} From 8808ac898cc99b1ceec2b9673811f70fd5ba6450 Mon Sep 17 00:00:00 2001 From: marcopla99 Date: Mon, 24 Apr 2023 18:10:33 +0200 Subject: [PATCH 10/14] Create ResultsRoute --- .../presentation/navigation/AppNavHost.kt | 4 +-- .../screen/result/ResultsScreen.kt | 30 ++++++++++++++----- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt b/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt index 4c6b3d0..bc59832 100644 --- a/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt +++ b/app/src/main/java/com/marcopla/flashcards/presentation/navigation/AppNavHost.kt @@ -10,7 +10,7 @@ import com.marcopla.flashcards.presentation.screen.add.AddRoute import com.marcopla.flashcards.presentation.screen.carousel.CarouselRoute import com.marcopla.flashcards.presentation.screen.edit.EditRoute import com.marcopla.flashcards.presentation.screen.home.HomeRoute -import com.marcopla.flashcards.presentation.screen.result.ResultsScreen +import com.marcopla.flashcards.presentation.screen.result.ResultsRoute @Composable fun AppNavHost( @@ -58,7 +58,7 @@ fun AppNavHost( ) } composable(Routes.RESULT_SCREEN) { - ResultsScreen( + ResultsRoute( onDoneClicked = { navController.popBackStack(Routes.HOME_SCREEN, false) } diff --git a/app/src/main/java/com/marcopla/flashcards/presentation/screen/result/ResultsScreen.kt b/app/src/main/java/com/marcopla/flashcards/presentation/screen/result/ResultsScreen.kt index dcbac0e..3ca491d 100644 --- a/app/src/main/java/com/marcopla/flashcards/presentation/screen/result/ResultsScreen.kt +++ b/app/src/main/java/com/marcopla/flashcards/presentation/screen/result/ResultsScreen.kt @@ -15,13 +15,29 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.marcopla.flashcards.R +import com.marcopla.flashcards.data.model.QuizResult +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +@Composable +fun ResultsRoute( + onDoneClicked: () -> Unit, + viewModel: ResultViewModel = hiltViewModel() +) { + ResultsScreen( + onDoneClicked = onDoneClicked, + results = viewModel.results.value.toImmutableList(), + clearResults = { viewModel.clearResults() } + ) +} @OptIn(ExperimentalLayoutApi::class) @Composable fun ResultsScreen( - modifier: Modifier = Modifier, - viewModel: ResultViewModel = hiltViewModel(), - onDoneClicked: () -> Unit + onDoneClicked: () -> Unit, + results: ImmutableList, + clearResults: () -> Unit, + modifier: Modifier = Modifier ) { Scaffold(topBar = { TopAppBar(title = { Text(stringResource(R.string.results)) }) @@ -34,9 +50,9 @@ fun ResultsScreen( horizontalAlignment = Alignment.CenterHorizontally ) { LazyColumn { - items(viewModel.results.value.size) { index -> + items(results.size) { index -> ResultItem( - quizResult = viewModel.results.value[index] + quizResult = results[index] ) } } @@ -45,11 +61,11 @@ fun ResultsScreen( modifier = Modifier.semantics { contentDescription = buttonText }, onClick = { onDoneClicked() - viewModel.clearResults() + clearResults() } ) { Text(buttonText) } } } -} \ No newline at end of file +} From 453ddb54f1680107ae8c6598fe573b20e111120e Mon Sep 17 00:00:00 2001 From: marcopla99 Date: Mon, 24 Apr 2023 18:15:33 +0200 Subject: [PATCH 11/14] Update gitignore by including autogenerated file when UI testing --- app/.gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/.gitignore b/app/.gitignore index 42afabf..c9bc8fa 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1 +1,2 @@ -/build \ No newline at end of file +/build +../.idea/androidTestResultsUserPreferences.xml \ No newline at end of file From e16ee880f49d8a226efd40a3e19fd9fafa3ed934 Mon Sep 17 00:00:00 2001 From: marcopla99 Date: Tue, 25 Apr 2023 11:41:56 +0200 Subject: [PATCH 12/14] Load carousel initial flash cards when viewModel is created --- .../presentation/screen/carousel/CarouselScreen.kt | 4 ---- .../presentation/screen/carousel/CarouselViewModel.kt | 6 +++++- .../presentation/carousel/CarouselViewModelTest.kt | 8 -------- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/marcopla/flashcards/presentation/screen/carousel/CarouselScreen.kt b/app/src/main/java/com/marcopla/flashcards/presentation/screen/carousel/CarouselScreen.kt index 8200be7..ef25506 100644 --- a/app/src/main/java/com/marcopla/flashcards/presentation/screen/carousel/CarouselScreen.kt +++ b/app/src/main/java/com/marcopla/flashcards/presentation/screen/carousel/CarouselScreen.kt @@ -18,10 +18,6 @@ fun CarouselRoute( viewModel: CarouselViewModel = hiltViewModel(), onLastFlashCardPlayed: () -> Unit ) { - LaunchedEffect(key1 = Unit) { - viewModel.loadFlashCards() - } - val screenState = viewModel.screenState.value if (screenState == CarouselScreenState.Finished) { LaunchedEffect(key1 = Unit) { diff --git a/app/src/main/java/com/marcopla/flashcards/presentation/screen/carousel/CarouselViewModel.kt b/app/src/main/java/com/marcopla/flashcards/presentation/screen/carousel/CarouselViewModel.kt index 62acca1..bdc2cea 100644 --- a/app/src/main/java/com/marcopla/flashcards/presentation/screen/carousel/CarouselViewModel.kt +++ b/app/src/main/java/com/marcopla/flashcards/presentation/screen/carousel/CarouselViewModel.kt @@ -30,7 +30,11 @@ class CarouselViewModel @Inject constructor( private val isLastFlashCard: Boolean get() = currentFlashCardIndex >= flashCards.size - 1 - fun loadFlashCards() { + init { + loadFlashCards() + } + + private fun loadFlashCards() { viewModelScope.launch { flashCards = loadUseCase.loadAll().first() _screenState.value = CarouselScreenState.Initial(flashCards[0]) diff --git a/app/src/test/java/com/marcopla/flashcards/presentation/carousel/CarouselViewModelTest.kt b/app/src/test/java/com/marcopla/flashcards/presentation/carousel/CarouselViewModelTest.kt index e7cf3e2..65374bd 100644 --- a/app/src/test/java/com/marcopla/flashcards/presentation/carousel/CarouselViewModelTest.kt +++ b/app/src/test/java/com/marcopla/flashcards/presentation/carousel/CarouselViewModelTest.kt @@ -34,7 +34,6 @@ class CarouselViewModelTest { LoadUseCase(repository), SubmitQuizUseCase(repository) ) - viewModel.loadFlashCards() viewModel.submit(emptyUserGuess) @@ -59,8 +58,6 @@ class CarouselViewModelTest { SubmitQuizUseCase(repository) ) - viewModel.loadFlashCards() - assertEquals( CarouselScreenState.Initial( listOf( @@ -86,7 +83,6 @@ class CarouselViewModelTest { LoadUseCase(repository), SubmitQuizUseCase(repository) ) - viewModel.loadFlashCards() viewModel.submit("wrong guess") @@ -110,7 +106,6 @@ class CarouselViewModelTest { LoadUseCase(repository), SubmitQuizUseCase(repository) ) - viewModel.loadFlashCards() viewModel.submit("English") @@ -134,7 +129,6 @@ class CarouselViewModelTest { LoadUseCase(repository), SubmitQuizUseCase(repository) ) - viewModel.loadFlashCards() viewModel.submit("English") viewModel.submit("Dutch") @@ -156,7 +150,6 @@ class CarouselViewModelTest { LoadUseCase(repository), SubmitQuizUseCase(repository) ) - viewModel.loadFlashCards() viewModel.updateGuessInput(":guess:") viewModel.submit(":guess:") @@ -176,7 +169,6 @@ class CarouselViewModelTest { LoadUseCase(repository), SubmitQuizUseCase(repository) ) - viewModel.loadFlashCards() repeat(storedFlashCards.size) { viewModel.submit(":guess:") From ecd0306b4bb3c27ac6de9b204e347dc8eae4a1ec Mon Sep 17 00:00:00 2001 From: marcopla99 Date: Tue, 25 Apr 2023 11:47:20 +0200 Subject: [PATCH 13/14] Load initial flash card for EditScreen when viewModel is created --- .../flashcards/presentation/screen/edit/EditScreen.kt | 5 ----- .../flashcards/presentation/screen/edit/EditViewModel.kt | 8 ++++++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/marcopla/flashcards/presentation/screen/edit/EditScreen.kt b/app/src/main/java/com/marcopla/flashcards/presentation/screen/edit/EditScreen.kt index d213399..8213a3b 100644 --- a/app/src/main/java/com/marcopla/flashcards/presentation/screen/edit/EditScreen.kt +++ b/app/src/main/java/com/marcopla/flashcards/presentation/screen/edit/EditScreen.kt @@ -44,7 +44,6 @@ fun EditRoute( viewModel.backTextState.value.text ) }, - onInitState = viewModel::initState, frontTextState = viewModel.frontTextState.value, onFrontTextValueChange = viewModel::updateFrontText, backTextState = viewModel.backTextState.value, @@ -61,7 +60,6 @@ fun EditScreen( onReset: () -> Unit, onShowDeleteConfirmationDialog: () -> Unit, onSubmit: () -> Unit, - onInitState: () -> Unit, frontTextState: EditFrontTextState, onFrontTextValueChange: (String) -> Unit, backTextState: EditBackTextState, @@ -109,9 +107,6 @@ fun EditScreen( } } ) { - // TODO: Consider using Flows and call collectAsStateWithLifecycle here. - LaunchedEffect(key1 = Unit, block = { onInitState() }) - Column( modifier = modifier .padding(4.dp) diff --git a/app/src/main/java/com/marcopla/flashcards/presentation/screen/edit/EditViewModel.kt b/app/src/main/java/com/marcopla/flashcards/presentation/screen/edit/EditViewModel.kt index 77da895..8b720ec 100644 --- a/app/src/main/java/com/marcopla/flashcards/presentation/screen/edit/EditViewModel.kt +++ b/app/src/main/java/com/marcopla/flashcards/presentation/screen/edit/EditViewModel.kt @@ -15,8 +15,8 @@ import com.marcopla.flashcards.domain.usecase.exceptions.InvalidBackTextExceptio import com.marcopla.flashcards.domain.usecase.exceptions.InvalidFrontTextException import com.marcopla.flashcards.presentation.navigation.FLASH_CARD_ID_ARG_KEY import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.launch +import javax.inject.Inject @HiltViewModel class EditViewModel @Inject constructor( @@ -40,7 +40,11 @@ class EditViewModel @Inject constructor( private val flashCardId: Int by lazy { checkNotNull(savedStateHandle[FLASH_CARD_ID_ARG_KEY]) } - fun initState() { + init { + initializeFlashCard() + } + + private fun initializeFlashCard() { viewModelScope.launch { val flashCard = loadFlashCardUseCase.loadById(flashCardId) _frontTextState.value = _frontTextState.value.copy(text = flashCard.frontText) From 414752c75606f49540d4d65936469164194b84ce Mon Sep 17 00:00:00 2001 From: marcopla99 Date: Wed, 26 Apr 2023 13:03:47 +0200 Subject: [PATCH 14/14] Load results when viewModel is created --- .../data/FlashCardRepositoryTest.kt | 4 +-- .../data/repository/FlashCardRepository.kt | 2 +- .../repository/FlashCardRepositoryImpl.kt | 5 +-- .../domain/usecase/LoadResultsUseCase.kt | 3 +- .../presentation/screen/edit/EditViewModel.kt | 2 +- .../screen/result/ResultViewModel.kt | 13 ++++--- .../screen/result/ResultsScreen.kt | 5 ++- .../domain/usecase/SubmitQuizUseCaseTest.kt | 35 +++++++++++-------- .../carousel/CarouselViewModelTest.kt | 6 ++-- .../result/ResultViewModelTest.kt | 12 ++++++- .../DuplicateFlashCardRepository.kt | 2 +- .../testing_shared/TestFlashCardRepository.kt | 4 +-- 12 files changed, 60 insertions(+), 33 deletions(-) diff --git a/app/src/androidTest/java/com/marcopla/flashcards/data/FlashCardRepositoryTest.kt b/app/src/androidTest/java/com/marcopla/flashcards/data/FlashCardRepositoryTest.kt index fd48328..a53bd13 100644 --- a/app/src/androidTest/java/com/marcopla/flashcards/data/FlashCardRepositoryTest.kt +++ b/app/src/androidTest/java/com/marcopla/flashcards/data/FlashCardRepositoryTest.kt @@ -121,7 +121,7 @@ class FlashCardRepositoryTest { } @Test - fun whenAddingResult_thenIsSaved() { + fun whenAddingResult_thenIsSaved() = runTest { val quizResult = QuizResult( FlashCard("Engels", "English"), "Dutch", @@ -130,7 +130,7 @@ class FlashCardRepositoryTest { repository.addResult(quizResult) - val currentResults = repository.getCurrentResults() + val currentResults = repository.getCurrentResults().first() assertEquals(listOf(quizResult), currentResults) } } diff --git a/app/src/main/java/com/marcopla/flashcards/data/repository/FlashCardRepository.kt b/app/src/main/java/com/marcopla/flashcards/data/repository/FlashCardRepository.kt index 63eef51..69a7989 100644 --- a/app/src/main/java/com/marcopla/flashcards/data/repository/FlashCardRepository.kt +++ b/app/src/main/java/com/marcopla/flashcards/data/repository/FlashCardRepository.kt @@ -19,7 +19,7 @@ interface FlashCardRepository { fun addResult(quizResult: QuizResult) - fun getCurrentResults(): List + fun getCurrentResults(): Flow> fun clearResults() } \ No newline at end of file diff --git a/app/src/main/java/com/marcopla/flashcards/data/repository/FlashCardRepositoryImpl.kt b/app/src/main/java/com/marcopla/flashcards/data/repository/FlashCardRepositoryImpl.kt index 6b28c06..6daaaba 100644 --- a/app/src/main/java/com/marcopla/flashcards/data/repository/FlashCardRepositoryImpl.kt +++ b/app/src/main/java/com/marcopla/flashcards/data/repository/FlashCardRepositoryImpl.kt @@ -5,6 +5,7 @@ import com.marcopla.flashcards.data.datasource.FlashCardDao import com.marcopla.flashcards.data.model.FlashCard import com.marcopla.flashcards.data.model.QuizResult import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow class FlashCardRepositoryImpl( private val flashCardDao: FlashCardDao @@ -49,8 +50,8 @@ class FlashCardRepositoryImpl( * Get the stored results for this session. * Note: results are currently just saved in memory. */ - override fun getCurrentResults(): List { - return currentResults + override fun getCurrentResults(): Flow> { + return flow { emit(currentResults) } } override fun clearResults() { diff --git a/app/src/main/java/com/marcopla/flashcards/domain/usecase/LoadResultsUseCase.kt b/app/src/main/java/com/marcopla/flashcards/domain/usecase/LoadResultsUseCase.kt index 52ca374..11bc0f1 100644 --- a/app/src/main/java/com/marcopla/flashcards/domain/usecase/LoadResultsUseCase.kt +++ b/app/src/main/java/com/marcopla/flashcards/domain/usecase/LoadResultsUseCase.kt @@ -3,11 +3,12 @@ package com.marcopla.flashcards.domain.usecase import com.marcopla.flashcards.data.model.QuizResult import com.marcopla.flashcards.data.repository.FlashCardRepository import javax.inject.Inject +import kotlinx.coroutines.flow.Flow // TODO: Currently the results are stored only in memory. Would be better to store them on disk. class LoadResultsUseCase @Inject constructor(private val repository: FlashCardRepository) { - operator fun invoke(): List { + operator fun invoke(): Flow> { return repository.getCurrentResults() } diff --git a/app/src/main/java/com/marcopla/flashcards/presentation/screen/edit/EditViewModel.kt b/app/src/main/java/com/marcopla/flashcards/presentation/screen/edit/EditViewModel.kt index 8b720ec..8e9139b 100644 --- a/app/src/main/java/com/marcopla/flashcards/presentation/screen/edit/EditViewModel.kt +++ b/app/src/main/java/com/marcopla/flashcards/presentation/screen/edit/EditViewModel.kt @@ -15,8 +15,8 @@ import com.marcopla.flashcards.domain.usecase.exceptions.InvalidBackTextExceptio import com.marcopla.flashcards.domain.usecase.exceptions.InvalidFrontTextException import com.marcopla.flashcards.presentation.navigation.FLASH_CARD_ID_ARG_KEY import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch import javax.inject.Inject +import kotlinx.coroutines.launch @HiltViewModel class EditViewModel @Inject constructor( diff --git a/app/src/main/java/com/marcopla/flashcards/presentation/screen/result/ResultViewModel.kt b/app/src/main/java/com/marcopla/flashcards/presentation/screen/result/ResultViewModel.kt index 31f4e2c..63c8e6e 100644 --- a/app/src/main/java/com/marcopla/flashcards/presentation/screen/result/ResultViewModel.kt +++ b/app/src/main/java/com/marcopla/flashcards/presentation/screen/result/ResultViewModel.kt @@ -1,19 +1,24 @@ package com.marcopla.flashcards.presentation.screen.result -import androidx.compose.runtime.State -import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.marcopla.flashcards.data.model.QuizResult import com.marcopla.flashcards.domain.usecase.LoadResultsUseCase import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn @HiltViewModel class ResultViewModel @Inject constructor( private val loadResults: LoadResultsUseCase ) : ViewModel() { - private val _results = mutableStateOf(loadResults()) - val results: State> = _results + val results: StateFlow> = loadResults().stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = emptyList() + ) fun clearResults() { loadResults.clearAll() diff --git a/app/src/main/java/com/marcopla/flashcards/presentation/screen/result/ResultsScreen.kt b/app/src/main/java/com/marcopla/flashcards/presentation/screen/result/ResultsScreen.kt index 3ca491d..c6d44c2 100644 --- a/app/src/main/java/com/marcopla/flashcards/presentation/screen/result/ResultsScreen.kt +++ b/app/src/main/java/com/marcopla/flashcards/presentation/screen/result/ResultsScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.material.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -14,6 +15,7 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.marcopla.flashcards.R import com.marcopla.flashcards.data.model.QuizResult import kotlinx.collections.immutable.ImmutableList @@ -24,9 +26,10 @@ fun ResultsRoute( onDoneClicked: () -> Unit, viewModel: ResultViewModel = hiltViewModel() ) { + val results: List by viewModel.results.collectAsStateWithLifecycle() ResultsScreen( onDoneClicked = onDoneClicked, - results = viewModel.results.value.toImmutableList(), + results = results.toImmutableList(), clearResults = { viewModel.clearResults() } ) } diff --git a/app/src/test/java/com/marcopla/flashcards/domain/usecase/SubmitQuizUseCaseTest.kt b/app/src/test/java/com/marcopla/flashcards/domain/usecase/SubmitQuizUseCaseTest.kt index b46e145..6c6214e 100644 --- a/app/src/test/java/com/marcopla/flashcards/domain/usecase/SubmitQuizUseCaseTest.kt +++ b/app/src/test/java/com/marcopla/flashcards/domain/usecase/SubmitQuizUseCaseTest.kt @@ -2,31 +2,36 @@ package com.marcopla.flashcards.domain.usecase import com.marcopla.flashcards.data.model.FlashCard import com.marcopla.testing_shared.TestFlashCardRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource +@OptIn(ExperimentalCoroutinesApi::class) class SubmitQuizUseCaseTest { @ParameterizedTest @ValueSource(strings = ["", " ", " "]) - fun whenSubmittingBlankGuess_thenAddWrongResult_andReturnWrongValidation(blankGuess: String) { - val repository = TestFlashCardRepository() - val submitQuizUseCase = SubmitQuizUseCase(repository) + fun whenSubmittingBlankGuess_thenAddWrongResult_andReturnWrongValidation(blankGuess: String) = + runTest { + val repository = TestFlashCardRepository() + val submitQuizUseCase = SubmitQuizUseCase(repository) - val isValidationCorrect = submitQuizUseCase.invoke( - FlashCard("Engels", "English"), - blankGuess - ) + val isValidationCorrect = submitQuizUseCase.invoke( + FlashCard("Engels", "English"), + blankGuess + ) - val quizResult = repository.getCurrentResults().first() - assertFalse(quizResult.isCorrect) - assertFalse(isValidationCorrect) - } + val quizResult = repository.getCurrentResults().first().first() + assertFalse(quizResult.isCorrect) + assertFalse(isValidationCorrect) + } @Test - fun whenSubmittingWrongGuess_thenAddWrongResult_andReturnWrongValidation() { + fun whenSubmittingWrongGuess_thenAddWrongResult_andReturnWrongValidation() = runTest { val repository = TestFlashCardRepository() val submitQuizUseCase = SubmitQuizUseCase(repository) @@ -35,13 +40,13 @@ class SubmitQuizUseCaseTest { ":wrongGuess:" ) - val quizResult = repository.getCurrentResults().first() + val quizResult = repository.getCurrentResults().first().first() assertFalse(quizResult.isCorrect) assertFalse(isValidationCorrect) } @Test - fun whenSubmittingCorrectGuess_thenAddCorrectResult_andReturnCorrectValidation() { + fun whenSubmittingCorrectGuess_thenAddCorrectResult_andReturnCorrectValidation() = runTest { val repository = TestFlashCardRepository() val submitQuizUseCase = SubmitQuizUseCase(repository) @@ -50,7 +55,7 @@ class SubmitQuizUseCaseTest { "English" ) - val quizResult = repository.getCurrentResults().first() + val quizResult = repository.getCurrentResults().first().first() assertTrue(quizResult.isCorrect) assertTrue(isValidationCorrect) } diff --git a/app/src/test/java/com/marcopla/flashcards/presentation/carousel/CarouselViewModelTest.kt b/app/src/test/java/com/marcopla/flashcards/presentation/carousel/CarouselViewModelTest.kt index 65374bd..0910895 100644 --- a/app/src/test/java/com/marcopla/flashcards/presentation/carousel/CarouselViewModelTest.kt +++ b/app/src/test/java/com/marcopla/flashcards/presentation/carousel/CarouselViewModelTest.kt @@ -9,6 +9,8 @@ import com.marcopla.flashcards.presentation.screen.carousel.CarouselScreenState import com.marcopla.flashcards.presentation.screen.carousel.CarouselViewModel import com.marcopla.testing_shared.FakeFlashCardDao import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -158,7 +160,7 @@ class CarouselViewModelTest { } @Test - fun whenFinished_allFlashCardsWereProcessed() { + fun whenFinished_allFlashCardsWereProcessed() = runTest { val storedFlashCards = listOf( FlashCard("Engels", "English"), FlashCard("Nederlands", "Dutch"), @@ -174,6 +176,6 @@ class CarouselViewModelTest { viewModel.submit(":guess:") } - assertEquals(storedFlashCards.size, repository.getCurrentResults().size) + assertEquals(storedFlashCards.size, repository.getCurrentResults().first().size) } } \ No newline at end of file diff --git a/app/src/test/java/com/marcopla/flashcards/presentation/result/ResultViewModelTest.kt b/app/src/test/java/com/marcopla/flashcards/presentation/result/ResultViewModelTest.kt index b43f4b9..1f9aea1 100644 --- a/app/src/test/java/com/marcopla/flashcards/presentation/result/ResultViewModelTest.kt +++ b/app/src/test/java/com/marcopla/flashcards/presentation/result/ResultViewModelTest.kt @@ -1,17 +1,25 @@ package com.marcopla.flashcards.presentation.result +import com.marcopla.flashcards.MainDispatcherExtension import com.marcopla.flashcards.data.model.FlashCard import com.marcopla.flashcards.data.model.QuizResult import com.marcopla.flashcards.data.repository.FlashCardRepositoryImpl import com.marcopla.flashcards.domain.usecase.LoadResultsUseCase import com.marcopla.flashcards.presentation.screen.result.ResultViewModel import com.marcopla.testing_shared.FakeFlashCardDao +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +@OptIn(ExperimentalCoroutinesApi::class) +@ExtendWith(MainDispatcherExtension::class) class ResultViewModelTest { @Test - fun whenResultsAreLoaded_thenEmitResults() { + fun whenResultsAreLoaded_thenEmitResults() = runTest { val storedResults = listOf( QuizResult(FlashCard("Engels", "English"), "English", true), QuizResult(FlashCard("Nederlands", "Dutch"), "", false), @@ -21,7 +29,9 @@ class ResultViewModelTest { storedResults.forEach { addResult(it) } } val viewModel = ResultViewModel(LoadResultsUseCase(repository)) + val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.results.collect {} } assertEquals(storedResults, viewModel.results.value) + collectJob.cancel() } } \ No newline at end of file diff --git a/app/testing/src/main/java/com/marcopla/testing_shared/DuplicateFlashCardRepository.kt b/app/testing/src/main/java/com/marcopla/testing_shared/DuplicateFlashCardRepository.kt index 2d4b545..ea9122b 100644 --- a/app/testing/src/main/java/com/marcopla/testing_shared/DuplicateFlashCardRepository.kt +++ b/app/testing/src/main/java/com/marcopla/testing_shared/DuplicateFlashCardRepository.kt @@ -31,7 +31,7 @@ class DuplicateFlashCardRepository : FlashCardRepository { TODO("Not yet implemented") } - override fun getCurrentResults(): List { + override fun getCurrentResults(): Flow> { TODO("Not yet implemented") } diff --git a/app/testing/src/main/java/com/marcopla/testing_shared/TestFlashCardRepository.kt b/app/testing/src/main/java/com/marcopla/testing_shared/TestFlashCardRepository.kt index 9b21c56..20c7241 100644 --- a/app/testing/src/main/java/com/marcopla/testing_shared/TestFlashCardRepository.kt +++ b/app/testing/src/main/java/com/marcopla/testing_shared/TestFlashCardRepository.kt @@ -41,8 +41,8 @@ class TestFlashCardRepository : FlashCardRepository { currentResults.add(quizResult) } - override fun getCurrentResults(): List { - return currentResults + override fun getCurrentResults(): Flow> { + return flow { emit(currentResults) } } override fun clearResults() {