From 8f62c50f03b3bda65660dc683dd38ad4512020d4 Mon Sep 17 00:00:00 2001 From: "Wei.He" Date: Mon, 22 Jan 2024 15:53:42 +0800 Subject: [PATCH] Kotlinify codebase (#90) - Remove unnecessary nullable types - Replace no-op method bodies with Unit - Convert to expression body - Replace if with when - Remove braces from 'when' entries - Remove braces from if statement - Convert to single line lambda - oneline if/returns - Replace 'contains' call with 'in' operator Following this refactor, it could be great to envision a more "strict" code formatter like ktlint 1.0.1 (we are currently stuck at 0.48.1) --- .editorconfig | 1 + .../wei/amazingtalker/ui/AtAppStateTest.kt | 368 ++++++++++-------- .../wei/amazingtalker/ui/NavigationRobot.kt | 15 +- .../wei/amazingtalker/ui/NavigationTest.kt | 10 +- .../wei/amazingtalker/ui/NavigationUiRobot.kt | 18 +- .../wei/amazingtalker/ui/NavigationUiTest.kt | 1 - .../ui/robot/HomeEndToEndRobot.kt | 14 +- .../ui/robot/LoginEndToEndRobot.kt | 15 +- .../ui/robot/ScheduleEndToEndRobot.kt | 5 +- .../ui/robot/WelcomeEndToEndRobot.kt | 5 +- .../utilities/FoldingDeviceUtil.kt | 12 +- .../com/wei/amazingtalker/AtApplication.kt | 1 - .../com/wei/amazingtalker/MainActivity.kt | 27 +- .../amazingtalker/MainActivityViewModel.kt | 21 +- .../wei/amazingtalker/navigation/AtNavHost.kt | 6 +- .../java/com/wei/amazingtalker/ui/AtApp.kt | 45 ++- .../com/wei/amazingtalker/ui/AtAppState.kt | 192 ++++----- ...droidApplicationComposeConventionPlugin.kt | 1 - .../AndroidApplicationConventionPlugin.kt | 5 +- ...droidApplicationFlavorsConventionPlugin.kt | 2 +- .../kotlin/AndroidHiltConventionPlugin.kt | 2 - .../AndroidLibraryComposeConventionPlugin.kt | 1 - .../kotlin/AndroidTestConventionPlugin.kt | 1 - .../com/wei/amazingtalker/AndroidCompose.kt | 11 +- .../amazingtalker/AndroidInstrumentedTests.kt | 11 +- .../com/wei/amazingtalker/AtBuildType.kt | 2 +- .../kotlin/com/wei/amazingtalker/AtFlavor.kt | 6 +- .../wei/amazingtalker/GradleManagedDevices.kt | 17 +- .../com/wei/amazingtalker/KotlinAndroid.kt | 17 +- .../com/wei/amazingtalker/PrintTestApks.kt | 34 +- .../amazingtalker/core/utils/UiTextTest.kt | 45 ++- .../core/authentication/TokenManager.kt | 6 +- .../amazingtalker/core/base/BaseViewModel.kt | 5 +- .../amazingtalker/core/decoder/UriDecoder.kt | 4 +- .../core/extensions/state/LiveDataEvents.kt | 7 +- .../state/LiveDataStateExtensions.kt | 5 +- .../state/SharedFlowEventsExtensions.kt | 8 +- .../state/StateFlowStateExtensions.kt | 5 +- .../core/manager/SnackbarManager.kt | 23 +- .../core/network/di/DispatchersModule.kt | 1 - .../core/result/DataSourceResult.kt | 4 +- .../wei/amazingtalker/core/utils/UiText.kt | 3 - .../core/manager/SnackbarManagerTest.kt | 53 +-- .../core/result/DataSourceResultKtTest.kt | 50 +-- .../data/test/AlwaysOnlineNetworkMonitor.kt | 4 +- .../core/data/test/TestDataModule.kt | 17 +- .../amazingtalker/core/data/di/DataModule.kt | 17 +- .../core/data/model/TeacherSchedule.kt | 18 +- .../core/data/model/UserProfile.kt | 47 +-- .../DefaultTeacherScheduleRepository.kt | 14 +- .../repository/DefaultUserDataRepository.kt | 6 +- .../core/data/repository/ProfileRepository.kt | 1 - .../repository/TeacherScheduleRepository.kt | 6 +- .../data/repository/UserDataRepository.kt | 1 - .../repository/fake/FakeProfileRepository.kt | 48 +-- .../ConnectivityManagerNetworkMonitor.kt | 91 +++-- .../datastore/test/TestDataStoreModule.kt | 1 - .../core/datastore/AtPreferencesDataSource.kt | 52 +-- .../datastore/UserPreferencesSerializer.kt | 9 +- .../core/datastore/di/DataStoreModule.kt | 1 - .../core/designsystem/component/Background.kt | 2 +- .../component/BaseLineHeightModifier.kt | 4 +- .../designsystem/component/ImageLoaders.kt | 23 +- .../core/designsystem/component/Navigation.kt | 31 +- .../core/designsystem/component/TopAppBar.kt | 6 - .../states/topappbar/FixedScrollFlagState.kt | 1 - .../states/topappbar/ScrollFlagState.kt | 7 +- .../scrollflags/EnterAlwaysCollapsedState.kt | 72 ++-- .../topappbar/scrollflags/EnterAlwaysState.kt | 56 +-- .../scrollflags/ExitUntilCollapsedState.kt | 54 +-- .../topappbar/scrollflags/ScrollState.kt | 58 +-- .../core/designsystem/theme/Color.kt | 1 + .../core/designsystem/theme/Shapes.kt | 15 +- .../core/designsystem/theme/Theme.kt | 153 ++++---- .../core/designsystem/theme/Type.kt | 100 +++-- .../core/designsystem/ui/WindowStateUtils.kt | 10 +- .../designsystem/BackgroundScreenshotTests.kt | 1 - .../designsystem/NavigationScreenshotTests.kt | 1 - .../core/domain/IntervalizeScheduleUseCase.kt | 4 +- .../domain/IntervalizeScheduleUseCaseTest.kt | 64 +-- .../core/model/data/DarkThemeConfig.kt | 4 +- .../core/model/data/LanguageConfig.kt | 4 +- .../core/model/data/ScheduleState.kt | 3 +- .../core/model/data/ThemeBrand.kt | 3 +- .../core/network/AtNetworkDataSource.kt | 6 +- .../core/network/di/FlavoredNetworkModule.kt | 1 - .../core/network/di/NetworkModule.kt | 29 +- .../network/retrofit/RetrofitAtNetwork.kt | 24 +- .../core/testing/AtTestRunner.kt | 6 +- .../core/testing/data/ScheduleTestData.kt | 110 +++--- .../TestTeacherScheduleRepository.kt | 5 +- .../core/testing/util/ScreenshotHelper.kt | 47 ++- .../core/testing/util/TestNetworkMonitor.kt | 1 - .../contactme/ContactMeScreenRobot.kt | 25 +- .../contactme/ContactMeScreenTest.kt | 1 - .../contactme/contactme/ContactMeScreen.kt | 76 ++-- .../contactme/contactme/ContactMeViewModel.kt | 5 +- .../contactme/contactme/ContactMeViewState.kt | 2 +- .../navigation/ContactMeNavigation.kt | 6 +- .../contactme/ui/DecorativeBackgroundText.kt | 33 +- .../contactme/contactme/ui/ProfileProperty.kt | 23 +- .../feature/home/home/HomeScreenRobot.kt | 51 +-- .../feature/home/home/HomeScreenTest.kt | 41 +- .../feature/home/home/HomeScreen.kt | 21 +- .../feature/home/home/HomeViewModel.kt | 35 +- .../feature/home/home/HomeViewState.kt | 6 +- .../feature/home/home/MyCoursesContenState.kt | 4 +- .../feature/home/home/MyCoursesContent.kt | 40 +- .../home/home/navigation/HomeNavigation.kt | 6 +- .../feature/home/home/ui/ContactListCard.kt | 85 ++-- .../feature/home/home/ui/HomeTabRow.kt | 20 +- .../feature/home/home/ui/HomeTopBar.kt | 19 +- .../feature/home/home/ui/SkillProgressCard.kt | 30 +- .../feature/login/login/LoginScreenRobot.kt | 30 +- .../feature/login/login/LoginScreenTest.kt | 1 - .../login/welcome/WelcomeScreenRobot.kt | 5 +- .../login/welcome/WelcomeScreenTest.kt | 1 - .../feature/login/login/LoginScreen.kt | 40 +- .../feature/login/login/LoginViewModel.kt | 10 +- .../login/login/navigation/LoginNavigation.kt | 10 +- .../feature/login/welcome/WelcomeScreen.kt | 73 ++-- .../feature/login/welcome/WelcomeViewModel.kt | 5 +- .../feature/login/welcome/WelcomeViewState.kt | 2 +- .../welcome/navigation/WelcomeNavigation.kt | 6 +- .../schedule/ScheduleScreenRobot.kt | 22 +- .../schedule/ScheduleScreenTest.kt | 36 +- .../ScheduleDetailScreenRobot.kt | 24 +- .../ScheduleDetailScreenTest.kt | 1 - .../domain/GetTeacherScheduleUseCase.kt | 5 +- .../schedule/ScheduleScreen.kt | 178 +++++---- .../schedule/ScheduleViewModel.kt | 67 ++-- .../schedule/ScheduleViewState.kt | 12 +- .../schedule/navigation/ScheduleNavigation.kt | 6 +- .../schedule/ui/DateTabLayout.kt | 6 +- .../ScheduleListPreviewParameterProvider.kt | 66 ++-- .../teacherschedule/schedule/ui/TimeList.kt | 45 ++- .../scheduledetail/ScheduleDetailScreen.kt | 18 +- .../scheduledetail/ScheduleDetailViewModel.kt | 14 +- .../navigation/ScheduleDetailNavigation.kt | 18 +- .../utilities/WeekDataHelper.kt | 14 +- .../domain/GetTeacherScheduleUseCaseTest.kt | 150 +++---- .../schedule/ScheduleViewModelTest.kt | 223 ++++++----- .../utilities/WeekDataHelperTest.kt | 27 +- gradle/init.gradle.kts | 10 +- 144 files changed, 2180 insertions(+), 1798 deletions(-) delete mode 100644 core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/component/TopAppBar.kt diff --git a/.editorconfig b/.editorconfig index 83be1d5a..7be3f878 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,3 +4,4 @@ [*.{kt,kts}] ij_kotlin_allow_trailing_comma=true ij_kotlin_allow_trailing_comma_on_call_site=true +ktlint_function_naming_ignore_when_annotated_with=Composable, Test diff --git a/app/src/androidTest/java/com/wei/amazingtalker/ui/AtAppStateTest.kt b/app/src/androidTest/java/com/wei/amazingtalker/ui/AtAppStateTest.kt index 5e61d9e5..165a4c17 100644 --- a/app/src/androidTest/java/com/wei/amazingtalker/ui/AtAppStateTest.kt +++ b/app/src/androidTest/java/com/wei/amazingtalker/ui/AtAppStateTest.kt @@ -40,7 +40,6 @@ import org.junit.Test */ @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) class AtAppStateTest { - @get:Rule val composeTestRule = createComposeRule() @@ -51,190 +50,215 @@ class AtAppStateTest { private lateinit var state: AtAppState @Test - fun verifyCurrentDestinationIsSet_whenNavigatedToDestination() = runTest { - var currentDestination: String? = null + fun verifyCurrentDestinationIsSet_whenNavigatedToDestination() = + runTest { + var currentDestination: String? = null - composeTestRule.setContent { - val navController = rememberTestNavController() - state = remember(navController) { - AtAppState( - navController = navController, - coroutineScope = backgroundScope, - windowSizeClass = getCompactWindowClass(), - networkMonitor = networkMonitor, - displayFeatures = emptyList(), - ) - } + composeTestRule.setContent { + val navController = rememberTestNavController() + state = + remember(navController) { + AtAppState( + navController = navController, + coroutineScope = backgroundScope, + windowSizeClass = getCompactWindowClass(), + networkMonitor = networkMonitor, + displayFeatures = emptyList(), + ) + } - // Update currentDestination whenever it changes - currentDestination = state.currentDestination?.route + // Update currentDestination whenever it changes + currentDestination = state.currentDestination?.route - // Navigate to destination b once - LaunchedEffect(Unit) { - navController.setCurrentDestination("b") + // Navigate to destination b once + LaunchedEffect(Unit) { + navController.setCurrentDestination("b") + } } - } - assertThat("b").isEqualTo(currentDestination) - } + assertThat("b").isEqualTo(currentDestination) + } @Test - fun verifyTopLevelDestinationsContainExpectedNames() = runTest { - composeTestRule.setContent { - state = rememberAtAppState( - windowSizeClass = getCompactWindowClass(), - networkMonitor = networkMonitor, - displayFeatures = emptyList(), - ) - } + fun verifyTopLevelDestinationsContainExpectedNames() = + runTest { + composeTestRule.setContent { + state = + rememberAtAppState( + windowSizeClass = getCompactWindowClass(), + networkMonitor = networkMonitor, + displayFeatures = emptyList(), + ) + } - assertThat(state.topLevelDestinations).hasSize(3) - assertThat(state.topLevelDestinations[0].name).ignoringCase().contains("home") - assertThat(state.topLevelDestinations[1].name).ignoringCase().contains("schedule") - assertThat(state.topLevelDestinations[2].name).ignoringCase().contains("contact_me") - } + assertThat(state.topLevelDestinations).hasSize(3) + assertThat(state.topLevelDestinations[0].name).ignoringCase().contains("home") + assertThat(state.topLevelDestinations[1].name).ignoringCase().contains("schedule") + assertThat(state.topLevelDestinations[2].name).ignoringCase().contains("contact_me") + } @Test - fun verifyBottomNavigationDisplayed_whenWindowSizeIsCompact() = runTest { - composeTestRule.setContent { - state = AtAppState( - navController = NavHostController(LocalContext.current), - coroutineScope = backgroundScope, - windowSizeClass = getCompactWindowClass(), - networkMonitor = networkMonitor, - displayFeatures = emptyList(), - ) - assertThat(state.navigationType).isEqualTo(AtNavigationType.BOTTOM_NAVIGATION) + fun verifyBottomNavigationDisplayed_whenWindowSizeIsCompact() = + runTest { + composeTestRule.setContent { + state = + AtAppState( + navController = NavHostController(LocalContext.current), + coroutineScope = backgroundScope, + windowSizeClass = getCompactWindowClass(), + networkMonitor = networkMonitor, + displayFeatures = emptyList(), + ) + assertThat(state.navigationType).isEqualTo(AtNavigationType.BOTTOM_NAVIGATION) + } } - } @Test - fun verifyNavigationRailDisplayed_whenWindowSizeIsMedium() = runTest { - composeTestRule.setContent { - state = AtAppState( - navController = NavHostController(LocalContext.current), - coroutineScope = backgroundScope, - windowSizeClass = WindowSizeClass.calculateFromSize( - DpSize( - MEDIUM_WIDTH, - MEDIUM_HEIGHT, - ), - ), - networkMonitor = networkMonitor, - displayFeatures = emptyList(), - ) - assertThat(state.navigationType).isEqualTo(AtNavigationType.NAVIGATION_RAIL) + fun verifyNavigationRailDisplayed_whenWindowSizeIsMedium() = + runTest { + composeTestRule.setContent { + state = + AtAppState( + navController = NavHostController(LocalContext.current), + coroutineScope = backgroundScope, + windowSizeClass = + WindowSizeClass.calculateFromSize( + DpSize( + MEDIUM_WIDTH, + MEDIUM_HEIGHT, + ), + ), + networkMonitor = networkMonitor, + displayFeatures = emptyList(), + ) + assertThat(state.navigationType).isEqualTo(AtNavigationType.NAVIGATION_RAIL) + } } - } @Test - fun verifyPermanentNavDrawerDisplayed_whenWindowSizeIsExpanded() = runTest { - composeTestRule.setContent { - state = AtAppState( - navController = NavHostController(LocalContext.current), - coroutineScope = backgroundScope, - windowSizeClass = WindowSizeClass.calculateFromSize( - DpSize( - EXPANDED_WIDTH, - EXPANDED_HEIGHT, - ), - ), - networkMonitor = networkMonitor, - displayFeatures = emptyList(), - ) - assertThat(state.navigationType).isEqualTo(AtNavigationType.PERMANENT_NAVIGATION_DRAWER) + fun verifyPermanentNavDrawerDisplayed_whenWindowSizeIsExpanded() = + runTest { + composeTestRule.setContent { + state = + AtAppState( + navController = NavHostController(LocalContext.current), + coroutineScope = backgroundScope, + windowSizeClass = + WindowSizeClass.calculateFromSize( + DpSize( + EXPANDED_WIDTH, + EXPANDED_HEIGHT, + ), + ), + networkMonitor = networkMonitor, + displayFeatures = emptyList(), + ) + assertThat(state.navigationType).isEqualTo(AtNavigationType.PERMANENT_NAVIGATION_DRAWER) + } } - } @Test - fun verifyContentTypeIsSINGLE_PANE_whenWindowSizeIsCompact() = runTest { - composeTestRule.setContent { - state = AtAppState( - navController = NavHostController(LocalContext.current), - coroutineScope = backgroundScope, - windowSizeClass = getCompactWindowClass(), - networkMonitor = networkMonitor, - displayFeatures = emptyList(), - ) - assertThat(state.contentType).isEqualTo(AtContentType.SINGLE_PANE) + fun verifyContentTypeIsSINGLE_PANE_whenWindowSizeIsCompact() = + runTest { + composeTestRule.setContent { + state = + AtAppState( + navController = NavHostController(LocalContext.current), + coroutineScope = backgroundScope, + windowSizeClass = getCompactWindowClass(), + networkMonitor = networkMonitor, + displayFeatures = emptyList(), + ) + assertThat(state.contentType).isEqualTo(AtContentType.SINGLE_PANE) + } } - } @Test - fun verifyContentTypeIsSINGLE_PANE_whenWindowSizeIsMedium_withNormalPosture() = runTest { - composeTestRule.setContent { - state = AtAppState( - navController = NavHostController(LocalContext.current), - coroutineScope = backgroundScope, - windowSizeClass = WindowSizeClass.calculateFromSize( - DpSize( - MEDIUM_WIDTH, - MEDIUM_HEIGHT, - ), - ), - networkMonitor = networkMonitor, - displayFeatures = emptyList(), - ) - assertThat(state.contentType).isEqualTo(AtContentType.SINGLE_PANE) + fun verifyContentTypeIsSINGLE_PANE_whenWindowSizeIsMedium_withNormalPosture() = + runTest { + composeTestRule.setContent { + state = + AtAppState( + navController = NavHostController(LocalContext.current), + coroutineScope = backgroundScope, + windowSizeClass = + WindowSizeClass.calculateFromSize( + DpSize( + MEDIUM_WIDTH, + MEDIUM_HEIGHT, + ), + ), + networkMonitor = networkMonitor, + displayFeatures = emptyList(), + ) + assertThat(state.contentType).isEqualTo(AtContentType.SINGLE_PANE) + } } - } @Test - fun verifyContentTypeIsDUAL_PANE_whenWindowSizeIsMedium_withBookPosture() = runTest { - composeTestRule.setContent { - val dpSize = DpSize(MEDIUM_WIDTH, MEDIUM_HEIGHT) - val foldBounds = FoldingDeviceUtil.getFoldBounds(dpSize) - state = AtAppState( - navController = NavHostController(LocalContext.current), - coroutineScope = backgroundScope, - windowSizeClass = WindowSizeClass.calculateFromSize(dpSize), - networkMonitor = networkMonitor, - displayFeatures = listOf( - FoldingDeviceUtil.getFoldingFeature( - foldBounds, - FoldingFeature.State.HALF_OPENED, - ), - ), - ) - assertThat(state.contentType).isEqualTo(AtContentType.DUAL_PANE) + fun verifyContentTypeIsDUAL_PANE_whenWindowSizeIsMedium_withBookPosture() = + runTest { + composeTestRule.setContent { + val dpSize = DpSize(MEDIUM_WIDTH, MEDIUM_HEIGHT) + val foldBounds = FoldingDeviceUtil.getFoldBounds(dpSize) + state = + AtAppState( + navController = NavHostController(LocalContext.current), + coroutineScope = backgroundScope, + windowSizeClass = WindowSizeClass.calculateFromSize(dpSize), + networkMonitor = networkMonitor, + displayFeatures = + listOf( + FoldingDeviceUtil.getFoldingFeature( + foldBounds, + FoldingFeature.State.HALF_OPENED, + ), + ), + ) + assertThat(state.contentType).isEqualTo(AtContentType.DUAL_PANE) + } } - } @Test - fun verifyContentTypeIsDUAL_PANE_whenWindowSizeIsMedium_withSeparating() = runTest { - composeTestRule.setContent { - val dpSize = DpSize(MEDIUM_WIDTH, MEDIUM_HEIGHT) - val foldBounds = FoldingDeviceUtil.getFoldBounds(dpSize) - state = AtAppState( - navController = NavHostController(LocalContext.current), - coroutineScope = backgroundScope, - windowSizeClass = WindowSizeClass.calculateFromSize(dpSize), - networkMonitor = networkMonitor, - displayFeatures = listOf( - FoldingDeviceUtil.getFoldingFeature( - foldBounds, - FoldingFeature.State.FLAT, - ), - ), - ) - assertThat(state.contentType).isEqualTo(AtContentType.DUAL_PANE) + fun verifyContentTypeIsDUAL_PANE_whenWindowSizeIsMedium_withSeparating() = + runTest { + composeTestRule.setContent { + val dpSize = DpSize(MEDIUM_WIDTH, MEDIUM_HEIGHT) + val foldBounds = FoldingDeviceUtil.getFoldBounds(dpSize) + state = + AtAppState( + navController = NavHostController(LocalContext.current), + coroutineScope = backgroundScope, + windowSizeClass = WindowSizeClass.calculateFromSize(dpSize), + networkMonitor = networkMonitor, + displayFeatures = + listOf( + FoldingDeviceUtil.getFoldingFeature( + foldBounds, + FoldingFeature.State.FLAT, + ), + ), + ) + assertThat(state.contentType).isEqualTo(AtContentType.DUAL_PANE) + } } - } @Test - fun verifyContentTypeIsDUAL_PANE_whenWindowSizeIsExpanded() = runTest { - composeTestRule.setContent { - val dpSize = DpSize(EXPANDED_WIDTH, EXPANDED_HEIGHT) - state = AtAppState( - navController = NavHostController(LocalContext.current), - coroutineScope = backgroundScope, - windowSizeClass = WindowSizeClass.calculateFromSize(dpSize), - networkMonitor = networkMonitor, - displayFeatures = emptyList(), - ) - assertThat(state.contentType).isEqualTo(AtContentType.DUAL_PANE) + fun verifyContentTypeIsDUAL_PANE_whenWindowSizeIsExpanded() = + runTest { + composeTestRule.setContent { + val dpSize = DpSize(EXPANDED_WIDTH, EXPANDED_HEIGHT) + state = + AtAppState( + navController = NavHostController(LocalContext.current), + coroutineScope = backgroundScope, + windowSizeClass = WindowSizeClass.calculateFromSize(dpSize), + networkMonitor = networkMonitor, + displayFeatures = emptyList(), + ) + assertThat(state.contentType).isEqualTo(AtContentType.DUAL_PANE) + } } - } @Test fun verifyStateIsOffline_whenNetworkMonitorReportsDisconnection() = @@ -242,18 +266,20 @@ class AtAppStateTest { val results = mutableListOf() composeTestRule.setContent { - state = AtAppState( - navController = NavHostController(LocalContext.current), - coroutineScope = backgroundScope, - windowSizeClass = WindowSizeClass.calculateFromSize( - DpSize( - EXPANDED_WIDTH, - EXPANDED_HEIGHT, + state = + AtAppState( + navController = NavHostController(LocalContext.current), + coroutineScope = backgroundScope, + windowSizeClass = + WindowSizeClass.calculateFromSize( + DpSize( + EXPANDED_WIDTH, + EXPANDED_HEIGHT, + ), ), - ), - networkMonitor = networkMonitor, - displayFeatures = emptyList(), - ) + networkMonitor = networkMonitor, + displayFeatures = emptyList(), + ) } backgroundScope.launch { @@ -266,8 +292,7 @@ class AtAppStateTest { assertTrue(results.contains(true)) } - private fun getCompactWindowClass() = - WindowSizeClass.calculateFromSize(DpSize(COMPACT_WIDTH, COMPACT_HEIGHT)) + private fun getCompactWindowClass() = WindowSizeClass.calculateFromSize(DpSize(COMPACT_WIDTH, COMPACT_HEIGHT)) } @Composable @@ -276,11 +301,12 @@ private fun rememberTestNavController(): TestNavHostController { return remember { TestNavHostController(context).apply { navigatorProvider.addNavigator(ComposeNavigator()) - graph = createGraph(startDestination = "a") { - composable("a") { } - composable("b") { } - composable("c") { } - } + graph = + createGraph(startDestination = "a") { + composable("a") { } + composable("b") { } + composable("c") { } + } } } } diff --git a/app/src/androidTest/java/com/wei/amazingtalker/ui/NavigationRobot.kt b/app/src/androidTest/java/com/wei/amazingtalker/ui/NavigationRobot.kt index 6c4c48b6..12ebccea 100644 --- a/app/src/androidTest/java/com/wei/amazingtalker/ui/NavigationRobot.kt +++ b/app/src/androidTest/java/com/wei/amazingtalker/ui/NavigationRobot.kt @@ -4,7 +4,9 @@ import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.performClick import androidx.test.ext.junit.rules.ActivityScenarioRule import com.wei.amazingtalker.MainActivity +import com.wei.amazingtalker.R import kotlin.properties.ReadOnlyProperty +import com.wei.amazingtalker.feature.teacherschedule.R as FeatureTeacherscheduleR /** * Robot for [NavigationTest]. @@ -23,14 +25,15 @@ internal fun navigationRobot( internal open class NavigationRobot( private val composeTestRule: AndroidComposeTestRule, MainActivity>, ) { - private fun AndroidComposeTestRule<*, *>.stringResource(@StringRes resId: Int) = - ReadOnlyProperty { _, _ -> activity.getString(resId) } + private fun AndroidComposeTestRule<*, *>.stringResource( + @StringRes resId: Int, + ) = ReadOnlyProperty { _, _ -> activity.getString(resId) } // The strings used for matching in these tests - private val schedule by composeTestRule.stringResource(com.wei.amazingtalker.R.string.schedule) - private val home by composeTestRule.stringResource(com.wei.amazingtalker.R.string.home) - private val contactMe by composeTestRule.stringResource(com.wei.amazingtalker.R.string.contact_me) - private val backDescription by composeTestRule.stringResource(com.wei.amazingtalker.feature.teacherschedule.R.string.content_description_back) + private val schedule by composeTestRule.stringResource(R.string.schedule) + private val home by composeTestRule.stringResource(R.string.home) + private val contactMe by composeTestRule.stringResource(R.string.contact_me) + private val backDescription by composeTestRule.stringResource(FeatureTeacherscheduleR.string.content_description_back) private val back by lazy { composeTestRule.onNodeWithContentDescription( diff --git a/app/src/androidTest/java/com/wei/amazingtalker/ui/NavigationTest.kt b/app/src/androidTest/java/com/wei/amazingtalker/ui/NavigationTest.kt index 9f5f37cf..4938e07f 100644 --- a/app/src/androidTest/java/com/wei/amazingtalker/ui/NavigationTest.kt +++ b/app/src/androidTest/java/com/wei/amazingtalker/ui/NavigationTest.kt @@ -77,9 +77,9 @@ class NavigationTest { } /* - * When pressing back from any top level destination except "Home", the app navigates back - * to the "Home" destination, no matter which destinations you visited in between. - */ + * When pressing back from any top level destination except "Home", the app navigates back + * to the "Home" destination, no matter which destinations you visited in between. + */ @Test fun navigationBar_backFromAnyDestination_returnsToHome() { welcomeEndToEndRobot(composeTestRule) { @@ -96,8 +96,8 @@ class NavigationTest { } /* - * There should always be at most one instance of a top-level destination at the same time. - */ + * There should always be at most one instance of a top-level destination at the same time. + */ @Test(expected = NoActivityResumedException::class) fun topDestination_back_quitsApp() { welcomeEndToEndRobot(composeTestRule) { diff --git a/app/src/androidTest/java/com/wei/amazingtalker/ui/NavigationUiRobot.kt b/app/src/androidTest/java/com/wei/amazingtalker/ui/NavigationUiRobot.kt index f410992e..e4c61994 100644 --- a/app/src/androidTest/java/com/wei/amazingtalker/ui/NavigationUiRobot.kt +++ b/app/src/androidTest/java/com/wei/amazingtalker/ui/NavigationUiRobot.kt @@ -36,8 +36,9 @@ internal fun navigationUiRobot( internal open class NavigationUiRobot( private val composeTestRule: AndroidComposeTestRule, HiltComponentActivity>, ) { - private fun AndroidComposeTestRule<*, *>.stringResource(@StringRes resId: Int) = - ReadOnlyProperty { _, _ -> activity.getString(resId) } + private fun AndroidComposeTestRule<*, *>.stringResource( + @StringRes resId: Int, + ) = ReadOnlyProperty { _, _ -> activity.getString(resId) } // The strings used for matching in these tests private val atBottomBarTag by composeTestRule.stringResource(R.string.tag_at_bottom_bar) @@ -73,12 +74,13 @@ internal open class NavigationUiRobot( composeTestRule.setContent { TestHarness(dpSize) { BoxWithConstraints { - val displayFeatures = if (foldingState != null) { - val foldBounds = FoldingDeviceUtil.getFoldBounds(dpSize) - listOf(FoldingDeviceUtil.getFoldingFeature(foldBounds, foldingState)) - } else { - emptyList() - } + val displayFeatures = + if (foldingState != null) { + val foldBounds = FoldingDeviceUtil.getFoldBounds(dpSize) + listOf(FoldingDeviceUtil.getFoldingFeature(foldBounds, foldingState)) + } else { + emptyList() + } AtApp( windowSizeClass = WindowSizeClass.calculateFromSize(dpSize), diff --git a/app/src/androidTest/java/com/wei/amazingtalker/ui/NavigationUiTest.kt b/app/src/androidTest/java/com/wei/amazingtalker/ui/NavigationUiTest.kt index 7eabba5d..58d030ef 100644 --- a/app/src/androidTest/java/com/wei/amazingtalker/ui/NavigationUiTest.kt +++ b/app/src/androidTest/java/com/wei/amazingtalker/ui/NavigationUiTest.kt @@ -26,7 +26,6 @@ import javax.inject.Inject */ @HiltAndroidTest class NavigationUiTest { - /** * Manages the components' state and is used to perform injection on your test */ diff --git a/app/src/androidTest/java/com/wei/amazingtalker/ui/robot/HomeEndToEndRobot.kt b/app/src/androidTest/java/com/wei/amazingtalker/ui/robot/HomeEndToEndRobot.kt index 6c7d8437..b1a0740c 100644 --- a/app/src/androidTest/java/com/wei/amazingtalker/ui/robot/HomeEndToEndRobot.kt +++ b/app/src/androidTest/java/com/wei/amazingtalker/ui/robot/HomeEndToEndRobot.kt @@ -30,13 +30,15 @@ internal fun homeEndToEndRobot( internal open class HomeEndToEndRobot( private val composeTestRule: AndroidComposeTestRule, MainActivity>, ) { - private fun AndroidComposeTestRule<*, *>.stringResource(@StringRes resId: Int) = - ReadOnlyProperty { _, _ -> activity.getString(resId) } + private fun AndroidComposeTestRule<*, *>.stringResource( + @StringRes resId: Int, + ) = ReadOnlyProperty { _, _ -> activity.getString(resId) } - private fun withRole(role: Role) = SemanticsMatcher("${SemanticsProperties.Role.name} contains '$role'") { - val roleProperty = it.config.getOrNull(SemanticsProperties.Role) ?: false - roleProperty == role - } + private fun withRole(role: Role) = + SemanticsMatcher("${SemanticsProperties.Role.name} contains '$role'") { + val roleProperty = it.config.getOrNull(SemanticsProperties.Role) ?: false + roleProperty == role + } // The strings used for matching in these tests private val menuDescription by composeTestRule.stringResource(FeatureHomeR.string.menu) diff --git a/app/src/androidTest/java/com/wei/amazingtalker/ui/robot/LoginEndToEndRobot.kt b/app/src/androidTest/java/com/wei/amazingtalker/ui/robot/LoginEndToEndRobot.kt index 2859fdf3..118f5351 100644 --- a/app/src/androidTest/java/com/wei/amazingtalker/ui/robot/LoginEndToEndRobot.kt +++ b/app/src/androidTest/java/com/wei/amazingtalker/ui/robot/LoginEndToEndRobot.kt @@ -31,14 +31,15 @@ internal fun loginEndToEndRobot( internal open class LoginEndToEndRobotRobot( private val composeTestRule: AndroidComposeTestRule, MainActivity>, ) { + private fun AndroidComposeTestRule<*, *>.stringResource( + @StringRes resId: Int, + ) = ReadOnlyProperty { _, _ -> activity.getString(resId) } - private fun AndroidComposeTestRule<*, *>.stringResource(@StringRes resId: Int) = - ReadOnlyProperty { _, _ -> activity.getString(resId) } - - private fun withRole(role: Role) = SemanticsMatcher("${SemanticsProperties.Role.name} contains '$role'") { - val roleProperty = it.config.getOrNull(SemanticsProperties.Role) ?: false - roleProperty == role - } + private fun withRole(role: Role) = + SemanticsMatcher("${SemanticsProperties.Role.name} contains '$role'") { + val roleProperty = it.config.getOrNull(SemanticsProperties.Role) ?: false + roleProperty == role + } // The strings used for matching in these tests private val loginDescription by composeTestRule.stringResource(FeatureLoginR.string.content_description_login) diff --git a/app/src/androidTest/java/com/wei/amazingtalker/ui/robot/ScheduleEndToEndRobot.kt b/app/src/androidTest/java/com/wei/amazingtalker/ui/robot/ScheduleEndToEndRobot.kt index 96acf89c..da4568ea 100644 --- a/app/src/androidTest/java/com/wei/amazingtalker/ui/robot/ScheduleEndToEndRobot.kt +++ b/app/src/androidTest/java/com/wei/amazingtalker/ui/robot/ScheduleEndToEndRobot.kt @@ -26,8 +26,9 @@ internal fun scheduleEndToEndRobot( internal open class ScheduleEndToEndRobot( private val composeTestRule: AndroidComposeTestRule, MainActivity>, ) { - private fun AndroidComposeTestRule<*, *>.stringResource(@StringRes resId: Int) = - ReadOnlyProperty { _, _ -> activity.getString(resId) } + private fun AndroidComposeTestRule<*, *>.stringResource( + @StringRes resId: Int, + ) = ReadOnlyProperty { _, _ -> activity.getString(resId) } // The strings used for matching in these tests private val scheduleTopAppBarTag by composeTestRule.stringResource(FeatureTeacherScheduleR.string.tag_schedule_top_app_bar) diff --git a/app/src/androidTest/java/com/wei/amazingtalker/ui/robot/WelcomeEndToEndRobot.kt b/app/src/androidTest/java/com/wei/amazingtalker/ui/robot/WelcomeEndToEndRobot.kt index 8519b387..bf3f7635 100644 --- a/app/src/androidTest/java/com/wei/amazingtalker/ui/robot/WelcomeEndToEndRobot.kt +++ b/app/src/androidTest/java/com/wei/amazingtalker/ui/robot/WelcomeEndToEndRobot.kt @@ -27,8 +27,9 @@ internal fun welcomeEndToEndRobot( internal open class WelcomeEndToEndRobot( private val composeTestRule: AndroidComposeTestRule, MainActivity>, ) { - private fun AndroidComposeTestRule<*, *>.stringResource(@StringRes resId: Int) = - ReadOnlyProperty { _, _ -> activity.getString(resId) } + private fun AndroidComposeTestRule<*, *>.stringResource( + @StringRes resId: Int, + ) = ReadOnlyProperty { _, _ -> activity.getString(resId) } // The strings used for matching in these tests private val welcomeTitleString by composeTestRule.stringResource(FeatureLoginR.string.welcome_title) diff --git a/app/src/androidTest/java/com/wei/amazingtalker/utilities/FoldingDeviceUtil.kt b/app/src/androidTest/java/com/wei/amazingtalker/utilities/FoldingDeviceUtil.kt index c33637d3..ad281f47 100644 --- a/app/src/androidTest/java/com/wei/amazingtalker/utilities/FoldingDeviceUtil.kt +++ b/app/src/androidTest/java/com/wei/amazingtalker/utilities/FoldingDeviceUtil.kt @@ -5,7 +5,6 @@ import androidx.compose.ui.unit.DpSize import androidx.window.layout.FoldingFeature object FoldingDeviceUtil { - fun getFoldBounds(dpSize: DpSize): Rect { val middleWidth = (dpSize.width / 2f).value.toInt() return Rect( @@ -16,12 +15,17 @@ object FoldingDeviceUtil { ) } - fun getFoldingFeature(foldBounds: Rect, state: FoldingFeature.State): FoldingFeature { + fun getFoldingFeature( + foldBounds: Rect, + state: FoldingFeature.State, + ): FoldingFeature { return object : FoldingFeature { override val bounds: Rect = foldBounds override val isSeparating: Boolean = true - override val occlusionType: FoldingFeature.OcclusionType = FoldingFeature.OcclusionType.NONE - override val orientation: FoldingFeature.Orientation = FoldingFeature.Orientation.VERTICAL + override val occlusionType: FoldingFeature.OcclusionType = + FoldingFeature.OcclusionType.NONE + override val orientation: FoldingFeature.Orientation = + FoldingFeature.Orientation.VERTICAL override val state: FoldingFeature.State = state } } diff --git a/app/src/main/java/com/wei/amazingtalker/AtApplication.kt b/app/src/main/java/com/wei/amazingtalker/AtApplication.kt index 920722c4..0e9e13c3 100644 --- a/app/src/main/java/com/wei/amazingtalker/AtApplication.kt +++ b/app/src/main/java/com/wei/amazingtalker/AtApplication.kt @@ -7,7 +7,6 @@ import timber.log.Timber @HiltAndroidApp class AtApplication : Application() { - override fun onCreate() { super.onCreate() diff --git a/app/src/main/java/com/wei/amazingtalker/MainActivity.kt b/app/src/main/java/com/wei/amazingtalker/MainActivity.kt index ef6cae15..3f814031 100644 --- a/app/src/main/java/com/wei/amazingtalker/MainActivity.kt +++ b/app/src/main/java/com/wei/amazingtalker/MainActivity.kt @@ -37,7 +37,6 @@ private const val TAG = "MainActivity" @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) @AndroidEntryPoint class MainActivity : ComponentActivity() { - @Inject lateinit var snackbarManager: SnackbarManager @@ -95,11 +94,13 @@ class MainActivity : ComponentActivity() { // than the configuration's dark theme value based on the user preference. DisposableEffect(darkTheme) { enableEdgeToEdge( - statusBarStyle = SystemBarStyle.auto( + statusBarStyle = + SystemBarStyle.auto( android.graphics.Color.TRANSPARENT, android.graphics.Color.TRANSPARENT, ) { darkTheme }, - navigationBarStyle = SystemBarStyle.auto( + navigationBarStyle = + SystemBarStyle.auto( lightScrim, darkScrim, ) { darkTheme }, @@ -107,7 +108,7 @@ class MainActivity : ComponentActivity() { onDispose {} } - CompositionLocalProvider() { + CompositionLocalProvider { AtTheme(darkTheme = darkTheme) { AtApp( networkMonitor = networkMonitor, @@ -131,16 +132,16 @@ class MainActivity : ComponentActivity() { * current system context. */ @Composable -private fun shouldUseDarkTheme( - uiState: MainActivityUiState, -): Boolean = when (uiState) { - Loading -> isSystemInDarkTheme() - is Success -> when (uiState.userData.darkThemeConfig) { - DarkThemeConfig.FOLLOW_SYSTEM -> isSystemInDarkTheme() - DarkThemeConfig.LIGHT -> false - DarkThemeConfig.DARK -> true +private fun shouldUseDarkTheme(uiState: MainActivityUiState): Boolean = + when (uiState) { + Loading -> isSystemInDarkTheme() + is Success -> + when (uiState.userData.darkThemeConfig) { + DarkThemeConfig.FOLLOW_SYSTEM -> isSystemInDarkTheme() + DarkThemeConfig.LIGHT -> false + DarkThemeConfig.DARK -> true + } } -} /** * The default light scrim, as defined by androidx and the platform: diff --git a/app/src/main/java/com/wei/amazingtalker/MainActivityViewModel.kt b/app/src/main/java/com/wei/amazingtalker/MainActivityViewModel.kt index 6993d03f..0d36582e 100644 --- a/app/src/main/java/com/wei/amazingtalker/MainActivityViewModel.kt +++ b/app/src/main/java/com/wei/amazingtalker/MainActivityViewModel.kt @@ -14,20 +14,23 @@ import kotlinx.coroutines.flow.stateIn import javax.inject.Inject @HiltViewModel -class MainActivityViewModel @Inject constructor( +class MainActivityViewModel +@Inject +constructor( userDataRepository: UserDataRepository, ) : ViewModel() { - - val uiState: StateFlow = userDataRepository.userData.map { - Success(it) - }.stateIn( - scope = viewModelScope, - initialValue = Loading, - started = SharingStarted.WhileSubscribed(5_000), - ) + val uiState: StateFlow = + userDataRepository.userData.map { + Success(it) + }.stateIn( + scope = viewModelScope, + initialValue = Loading, + started = SharingStarted.WhileSubscribed(5_000), + ) } sealed interface MainActivityUiState { object Loading : MainActivityUiState + data class Success(val userData: UserData) : MainActivityUiState } diff --git a/app/src/main/java/com/wei/amazingtalker/navigation/AtNavHost.kt b/app/src/main/java/com/wei/amazingtalker/navigation/AtNavHost.kt index d095d58f..09480b61 100644 --- a/app/src/main/java/com/wei/amazingtalker/navigation/AtNavHost.kt +++ b/app/src/main/java/com/wei/amazingtalker/navigation/AtNavHost.kt @@ -6,11 +6,11 @@ import androidx.navigation.compose.NavHost import androidx.window.layout.DisplayFeature import com.wei.amazingtalker.core.designsystem.ui.DeviceOrientation import com.wei.amazingtalker.feature.contactme.contactme.navigation.contactMeGraph +import com.wei.amazingtalker.feature.home.home.navigation.HOME_ROUTE import com.wei.amazingtalker.feature.home.home.navigation.homeGraph -import com.wei.amazingtalker.feature.home.home.navigation.homeRoute import com.wei.amazingtalker.feature.login.login.navigation.loginScreen +import com.wei.amazingtalker.feature.login.welcome.navigation.WELCOME_ROUTE import com.wei.amazingtalker.feature.login.welcome.navigation.welcomeGraph -import com.wei.amazingtalker.feature.login.welcome.navigation.welcomeRoute import com.wei.amazingtalker.feature.teacherschedule.schedule.navigation.scheduleGraph import com.wei.amazingtalker.feature.teacherschedule.scheduledetail.navigation.scheduleDetailScreen import com.wei.amazingtalker.ui.AtAppState @@ -28,7 +28,7 @@ fun AtNavHost( appState: AtAppState, isTokenValid: Boolean, displayFeatures: List, - startDestination: String = if (isTokenValid) homeRoute else welcomeRoute, + startDestination: String = if (isTokenValid) HOME_ROUTE else WELCOME_ROUTE, ) { val navController = appState.navController val navigationType = appState.navigationType diff --git a/app/src/main/java/com/wei/amazingtalker/ui/AtApp.kt b/app/src/main/java/com/wei/amazingtalker/ui/AtApp.kt index e6333bdb..326f24c7 100644 --- a/app/src/main/java/com/wei/amazingtalker/ui/AtApp.kt +++ b/app/src/main/java/com/wei/amazingtalker/ui/AtApp.kt @@ -51,7 +51,7 @@ import com.wei.amazingtalker.core.designsystem.component.AtNavigationRailItem import com.wei.amazingtalker.core.designsystem.component.FunctionalityNotAvailablePopup import com.wei.amazingtalker.core.designsystem.theme.SPACING_LARGE import com.wei.amazingtalker.core.designsystem.ui.AtNavigationType -import com.wei.amazingtalker.core.manager.ErrorTextPrefix +import com.wei.amazingtalker.core.manager.ERROR_TEXT_PREFIX import com.wei.amazingtalker.core.manager.Message import com.wei.amazingtalker.core.manager.SnackbarManager import com.wei.amazingtalker.core.manager.SnackbarState @@ -70,11 +70,12 @@ fun AtApp( windowSizeClass: WindowSizeClass, displayFeatures: List, isTokenValid: Boolean = true, - appState: AtAppState = rememberAtAppState( - networkMonitor = networkMonitor, - windowSizeClass = windowSizeClass, - displayFeatures = displayFeatures, - ), + appState: AtAppState = + rememberAtAppState( + networkMonitor = networkMonitor, + windowSizeClass = windowSizeClass, + displayFeatures = displayFeatures, + ), snackbarManager: SnackbarManager, ) { val snackbarHostState = remember { SnackbarHostState() } @@ -110,7 +111,8 @@ fun AtApp( } AtBackground { Scaffold( - modifier = Modifier.semantics { + modifier = + Modifier.semantics { testTagsAsResourceId = true }, containerColor = Color.Transparent, @@ -122,7 +124,7 @@ fun AtApp( snackbar = { snackbarData -> if (!appState.isFullScreenCurrentDestination) { // TODO [Revert]: Temporary removal of Spacer in Snackbar to prevent extra space on phone devices. See issue #38. - val isError = snackbarData.visuals.message.startsWith(ErrorTextPrefix) + val isError = snackbarData.visuals.message.startsWith(ERROR_TEXT_PREFIX) AtAppSnackbar(snackbarData, isError) } }, @@ -159,7 +161,8 @@ fun AtApp( destinations = appState.topLevelDestinations, onNavigateToDestination = appState::navigateToTopLevelDestination, currentDestination = appState.currentDestination, - modifier = Modifier + modifier = + Modifier .testTag(atNavDrawer) .padding(SPACING_LARGE.dp) .safeDrawingPadding(), @@ -173,14 +176,16 @@ fun AtApp( destinations = appState.topLevelDestinations, onNavigateToDestination = appState::navigateToTopLevelDestination, currentDestination = appState.currentDestination, - modifier = Modifier + modifier = + Modifier .testTag(atNavRail) .safeDrawingPadding(), ) } Column( - modifier = Modifier + modifier = + Modifier .fillMaxSize(), ) { AtNavHost( @@ -312,7 +317,7 @@ suspend fun collectAndShowSnackbar( if (message.state == SnackbarState.Error) { snackbarHostState.showSnackbar( - message = ErrorTextPrefix + text, + message = ERROR_TEXT_PREFIX + text, ) } else { snackbarHostState.showSnackbar(message = text) @@ -322,13 +327,17 @@ suspend fun collectAndShowSnackbar( } } -fun getMessageText(message: Message, context: Context): String { +fun getMessageText( + message: Message, + context: Context, +): String { return when (message.uiText) { is UiText.DynamicString -> (message.uiText as UiText.DynamicString).value - is UiText.StringResource -> context.getString( - (message.uiText as UiText.StringResource).resId, - *(message.uiText as UiText.StringResource).args.map { it.toString(context) } - .toTypedArray(), - ) + is UiText.StringResource -> + context.getString( + (message.uiText as UiText.StringResource).resId, + *(message.uiText as UiText.StringResource).args.map { it.toString(context) } + .toTypedArray(), + ) } } diff --git a/app/src/main/java/com/wei/amazingtalker/ui/AtAppState.kt b/app/src/main/java/com/wei/amazingtalker/ui/AtAppState.kt index 18e40407..6dd4bf56 100644 --- a/app/src/main/java/com/wei/amazingtalker/ui/AtAppState.kt +++ b/app/src/main/java/com/wei/amazingtalker/ui/AtAppState.kt @@ -26,15 +26,15 @@ import com.wei.amazingtalker.core.designsystem.ui.DevicePosture import com.wei.amazingtalker.core.designsystem.ui.currentDeviceOrientation import com.wei.amazingtalker.core.designsystem.ui.isBookPosture import com.wei.amazingtalker.core.designsystem.ui.isSeparating -import com.wei.amazingtalker.feature.contactme.contactme.navigation.contactMeRoute +import com.wei.amazingtalker.feature.contactme.contactme.navigation.CONTACT_ME_ROUTE import com.wei.amazingtalker.feature.contactme.contactme.navigation.navigateToContactMe -import com.wei.amazingtalker.feature.home.home.navigation.homeRoute +import com.wei.amazingtalker.feature.home.home.navigation.HOME_ROUTE import com.wei.amazingtalker.feature.home.home.navigation.navigateToHome -import com.wei.amazingtalker.feature.login.login.navigation.loginRoute +import com.wei.amazingtalker.feature.login.login.navigation.LOGIN_ROUTE +import com.wei.amazingtalker.feature.login.welcome.navigation.WELCOME_ROUTE import com.wei.amazingtalker.feature.login.welcome.navigation.navigateToWelcome -import com.wei.amazingtalker.feature.login.welcome.navigation.welcomeRoute +import com.wei.amazingtalker.feature.teacherschedule.schedule.navigation.SCHEDULE_ROUTE import com.wei.amazingtalker.feature.teacherschedule.schedule.navigation.navigateToSchedule -import com.wei.amazingtalker.feature.teacherschedule.schedule.navigation.scheduleRoute import com.wei.amazingtalker.navigation.TopLevelDestination import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted @@ -85,93 +85,100 @@ class AtAppState( */ val foldingFeature = displayFeatures.filterIsInstance().firstOrNull() - val foldingDevicePosture = when { - isBookPosture(foldingFeature) -> - DevicePosture.BookPosture(foldingFeature.bounds) + val foldingDevicePosture = + when { + isBookPosture(foldingFeature) -> + DevicePosture.BookPosture(foldingFeature.bounds) - isSeparating(foldingFeature) -> - DevicePosture.Separating(foldingFeature.bounds, foldingFeature.orientation) + isSeparating(foldingFeature) -> + DevicePosture.Separating(foldingFeature.bounds, foldingFeature.orientation) - else -> DevicePosture.NormalPosture - } + else -> DevicePosture.NormalPosture + } /** * This will help us select type of navigation and content type depending on window size and * fold state of the device. */ val navigationType: AtNavigationType - @Composable get() = when (windowSizeClass.widthSizeClass) { - WindowWidthSizeClass.Compact -> { - AtNavigationType.BOTTOM_NAVIGATION - } - - WindowWidthSizeClass.Medium -> { - AtNavigationType.NAVIGATION_RAIL - } + @Composable get() = + when (windowSizeClass.widthSizeClass) { + WindowWidthSizeClass.Compact -> { + AtNavigationType.BOTTOM_NAVIGATION + } - WindowWidthSizeClass.Expanded -> { - if (foldingDevicePosture is DevicePosture.BookPosture) { + WindowWidthSizeClass.Medium -> { AtNavigationType.NAVIGATION_RAIL - } else { - AtNavigationType.PERMANENT_NAVIGATION_DRAWER } - } - else -> { - AtNavigationType.BOTTOM_NAVIGATION - } - } + WindowWidthSizeClass.Expanded -> { + if (foldingDevicePosture is DevicePosture.BookPosture) { + AtNavigationType.NAVIGATION_RAIL + } else { + AtNavigationType.PERMANENT_NAVIGATION_DRAWER + } + } - val contentType: AtContentType - @Composable get() = when (windowSizeClass.widthSizeClass) { - WindowWidthSizeClass.Compact -> { - AtContentType.SINGLE_PANE + else -> { + AtNavigationType.BOTTOM_NAVIGATION + } } - WindowWidthSizeClass.Medium -> { - if (foldingDevicePosture != DevicePosture.NormalPosture) { - AtContentType.DUAL_PANE - } else { + val contentType: AtContentType + @Composable get() = + when (windowSizeClass.widthSizeClass) { + WindowWidthSizeClass.Compact -> { AtContentType.SINGLE_PANE } - } - WindowWidthSizeClass.Expanded -> { - AtContentType.DUAL_PANE - } + WindowWidthSizeClass.Medium -> { + if (foldingDevicePosture != DevicePosture.NormalPosture) { + AtContentType.DUAL_PANE + } else { + AtContentType.SINGLE_PANE + } + } - else -> { - AtContentType.SINGLE_PANE + WindowWidthSizeClass.Expanded -> { + AtContentType.DUAL_PANE + } + + else -> { + AtContentType.SINGLE_PANE + } } - } val currentDestination: NavDestination? - @Composable get() = navController - .currentBackStackEntryAsState().value?.destination + @Composable get() = + navController + .currentBackStackEntryAsState().value?.destination val isFullScreenCurrentDestination: Boolean - @Composable get() = when (currentDestination?.route) { - null -> true - welcomeRoute -> true - loginRoute -> true - else -> false - } + @Composable get() = + when (currentDestination?.route) { + null -> true + WELCOME_ROUTE -> true + LOGIN_ROUTE -> true + else -> false + } val currentTopLevelDestination: TopLevelDestination? - @Composable get() = when (currentDestination?.route) { - homeRoute -> TopLevelDestination.HOME - scheduleRoute -> TopLevelDestination.SCHEDULE - contactMeRoute -> TopLevelDestination.CONTACT_ME - else -> null - } + @Composable get() = + when (currentDestination?.route) { + HOME_ROUTE -> TopLevelDestination.HOME + SCHEDULE_ROUTE -> TopLevelDestination.SCHEDULE + CONTACT_ME_ROUTE -> TopLevelDestination.CONTACT_ME + else -> null + } - val isOffline = networkMonitor.isOnline - .map(Boolean::not) - .stateIn( - scope = coroutineScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = false, - ) + val isOffline = + networkMonitor.isOnline + .map(Boolean::not) + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = false, + ) val showFunctionalityNotAvailablePopup: MutableState = mutableStateOf(false) @@ -190,32 +197,36 @@ class AtAppState( */ fun navigateToTopLevelDestination(topLevelDestination: TopLevelDestination) { trace("Navigation: ${topLevelDestination.name}") { - val topLevelNavOptions = navOptions { - // Pop up to the start destination of the graph to - // avoid building up a large stack of destinations - // on the back stack as users select items - popUpTo(navController.graph.findStartDestination().id) { - saveState = true + val topLevelNavOptions = + navOptions { + // Pop up to the start destination of the graph to + // avoid building up a large stack of destinations + // on the back stack as users select items + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + // Avoid multiple copies of the same destination when + // reselecting the same item + launchSingleTop = true + // Restore state when reselecting a previously selected item + restoreState = true } - // Avoid multiple copies of the same destination when - // reselecting the same item - launchSingleTop = true - // Restore state when reselecting a previously selected item - restoreState = true - } when (topLevelDestination) { - TopLevelDestination.HOME -> navController.navigateToHome( - topLevelNavOptions, - ) + TopLevelDestination.HOME -> + navController.navigateToHome( + topLevelNavOptions, + ) - TopLevelDestination.SCHEDULE -> navController.navigateToSchedule( - topLevelNavOptions, - ) + TopLevelDestination.SCHEDULE -> + navController.navigateToSchedule( + topLevelNavOptions, + ) - TopLevelDestination.CONTACT_ME -> navController.navigateToContactMe( - topLevelNavOptions, - ) + TopLevelDestination.CONTACT_ME -> + navController.navigateToContactMe( + topLevelNavOptions, + ) else -> showFunctionalityNotAvailablePopup.value = true } @@ -229,10 +240,11 @@ class AtAppState( fun tokenInvalidNavigate() { Timber.d("tokenInvalidNavigate()") - val navOptions = NavOptions.Builder() - .setPopUpTo(navController.graph.startDestinationId, true) // This will pop all screens - .setLaunchSingleTop(true) - .build() + val navOptions = + NavOptions.Builder() + .setPopUpTo(navController.graph.startDestinationId, true) // This will pop all screens + .setLaunchSingleTop(true) + .build() navController.navigateToWelcome(navOptions) } diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt index 53df5e13..69ef474b 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt @@ -15,5 +15,4 @@ class AndroidApplicationComposeConventionPlugin : Plugin { configureAndroidCompose(extension) } } - } diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt index 9512a7c8..d4caa9d2 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt @@ -1,6 +1,6 @@ import com.android.build.api.dsl.ApplicationExtension -import com.wei.amazingtalker.configureGradleManagedDevices import com.android.build.api.variant.ApplicationAndroidComponentsExtension +import com.wei.amazingtalker.configureGradleManagedDevices import com.wei.amazingtalker.configureKotlinAndroid import com.wei.amazingtalker.configurePrintApksTask import org.gradle.api.Plugin @@ -25,5 +25,4 @@ class AndroidApplicationConventionPlugin : Plugin { } } } - -} \ No newline at end of file +} diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationFlavorsConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationFlavorsConventionPlugin.kt index 4bf70023..2697e6f1 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationFlavorsConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationFlavorsConventionPlugin.kt @@ -12,4 +12,4 @@ class AndroidApplicationFlavorsConventionPlugin : Plugin { } } } -} \ No newline at end of file +} diff --git a/build-logic/convention/src/main/kotlin/AndroidHiltConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidHiltConventionPlugin.kt index e89da199..24f65bf6 100644 --- a/build-logic/convention/src/main/kotlin/AndroidHiltConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidHiltConventionPlugin.kt @@ -17,8 +17,6 @@ class AndroidHiltConventionPlugin : Plugin { "kspAndroidTest"(libs.findLibrary("hilt.compiler").get()) "kspTest"(libs.findLibrary("hilt.compiler").get()) } - } } - } diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt index bcc007e2..31b36f64 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt @@ -15,5 +15,4 @@ class AndroidLibraryComposeConventionPlugin : Plugin { configureAndroidCompose(extension) } } - } diff --git a/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt index f75dd011..1942bf4e 100644 --- a/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt @@ -20,5 +20,4 @@ class AndroidTestConventionPlugin : Plugin { } } } - } diff --git a/build-logic/convention/src/main/kotlin/com/wei/amazingtalker/AndroidCompose.kt b/build-logic/convention/src/main/kotlin/com/wei/amazingtalker/AndroidCompose.kt index b4723cf4..4667ad3c 100644 --- a/build-logic/convention/src/main/kotlin/com/wei/amazingtalker/AndroidCompose.kt +++ b/build-logic/convention/src/main/kotlin/com/wei/amazingtalker/AndroidCompose.kt @@ -9,16 +9,15 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile /** * Configure Compose-specific options */ -internal fun Project.configureAndroidCompose( - commonExtension: CommonExtension<*, *, *, *, *>, -) { +internal fun Project.configureAndroidCompose(commonExtension: CommonExtension<*, *, *, *, *>) { commonExtension.apply { buildFeatures { compose = true } composeOptions { - kotlinCompilerExtensionVersion = libs.findVersion("androidxComposeCompiler").get().toString() + kotlinCompilerExtensionVersion = + libs.findVersion("androidxComposeCompiler").get().toString() } dependencies { @@ -57,7 +56,7 @@ private fun Project.buildComposeMetricsParameters(): List { val metricsFolder = rootProject.buildDir.resolve("compose-metrics").resolve(relativePath) metricParameters.add("-P") metricParameters.add( - "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + metricsFolder.absolutePath + "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + metricsFolder.absolutePath, ) } @@ -67,7 +66,7 @@ private fun Project.buildComposeMetricsParameters(): List { val reportsFolder = rootProject.buildDir.resolve("compose-reports").resolve(relativePath) metricParameters.add("-P") metricParameters.add( - "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + reportsFolder.absolutePath + "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + reportsFolder.absolutePath, ) } return metricParameters.toList() diff --git a/build-logic/convention/src/main/kotlin/com/wei/amazingtalker/AndroidInstrumentedTests.kt b/build-logic/convention/src/main/kotlin/com/wei/amazingtalker/AndroidInstrumentedTests.kt index 4f58f5cf..7c247bd8 100644 --- a/build-logic/convention/src/main/kotlin/com/wei/amazingtalker/AndroidInstrumentedTests.kt +++ b/build-logic/convention/src/main/kotlin/com/wei/amazingtalker/AndroidInstrumentedTests.kt @@ -11,9 +11,8 @@ import org.gradle.api.Project * * Note: this could be improved by checking other potential sourceSets based on buildTypes and flavors. */ -internal fun LibraryAndroidComponentsExtension.disableUnnecessaryAndroidTests( - project: Project, -) = beforeVariants { - it.enableAndroidTest = it.enableAndroidTest - && project.projectDir.resolve("src/androidTest").exists() -} +internal fun LibraryAndroidComponentsExtension.disableUnnecessaryAndroidTests(project: Project) = + beforeVariants { + it.enableAndroidTest = it.enableAndroidTest && + project.projectDir.resolve("src/androidTest").exists() + } diff --git a/build-logic/convention/src/main/kotlin/com/wei/amazingtalker/AtBuildType.kt b/build-logic/convention/src/main/kotlin/com/wei/amazingtalker/AtBuildType.kt index d7eebe33..59039453 100644 --- a/build-logic/convention/src/main/kotlin/com/wei/amazingtalker/AtBuildType.kt +++ b/build-logic/convention/src/main/kotlin/com/wei/amazingtalker/AtBuildType.kt @@ -5,5 +5,5 @@ package com.wei.amazingtalker */ enum class AtBuildType(val applicationIdSuffix: String? = null) { DEBUG(".debug"), - RELEASE + RELEASE, } diff --git a/build-logic/convention/src/main/kotlin/com/wei/amazingtalker/AtFlavor.kt b/build-logic/convention/src/main/kotlin/com/wei/amazingtalker/AtFlavor.kt index 7d534f52..0d6a44a1 100644 --- a/build-logic/convention/src/main/kotlin/com/wei/amazingtalker/AtFlavor.kt +++ b/build-logic/convention/src/main/kotlin/com/wei/amazingtalker/AtFlavor.kt @@ -7,7 +7,7 @@ import com.android.build.api.dsl.ProductFlavor @Suppress("EnumEntryName") enum class FlavorDimension { - contentType + contentType, } // The content for the app can either come from local static data which is useful for demo @@ -16,12 +16,12 @@ enum class FlavorDimension { @Suppress("EnumEntryName") enum class AtFlavor(val dimension: FlavorDimension, val applicationIdSuffix: String? = null) { demo(FlavorDimension.contentType, applicationIdSuffix = ".demo"), - prod(FlavorDimension.contentType) + prod(FlavorDimension.contentType), } fun configureFlavors( commonExtension: CommonExtension<*, *, *, *, *>, - flavorConfigurationBlock: ProductFlavor.(flavor: AtFlavor) -> Unit = {} + flavorConfigurationBlock: ProductFlavor.(flavor: AtFlavor) -> Unit = {}, ) { commonExtension.apply { flavorDimensions += FlavorDimension.contentType.name diff --git a/build-logic/convention/src/main/kotlin/com/wei/amazingtalker/GradleManagedDevices.kt b/build-logic/convention/src/main/kotlin/com/wei/amazingtalker/GradleManagedDevices.kt index fae7e996..0a34941c 100644 --- a/build-logic/convention/src/main/kotlin/com/wei/amazingtalker/GradleManagedDevices.kt +++ b/build-logic/convention/src/main/kotlin/com/wei/amazingtalker/GradleManagedDevices.kt @@ -8,9 +8,7 @@ import org.gradle.kotlin.dsl.invoke /** * Configure project for Gradle managed devices */ -internal fun configureGradleManagedDevices( - commonExtension: CommonExtension<*, *, *, *, *>, -) { +internal fun configureGradleManagedDevices(commonExtension: CommonExtension<*, *, *, *, *>) { val pixel4 = DeviceConfig("Pixel 4", 30, "aosp-atd") val pixel6 = DeviceConfig("Pixel 6", 31, "aosp") val pixelC = DeviceConfig("Pixel C", 30, "aosp-atd") @@ -45,10 +43,11 @@ private data class DeviceConfig( val apiLevel: Int, val systemImageSource: String, ) { - val taskName = buildString { - append(device.lowercase().replace(" ", "")) - append("api") - append(apiLevel.toString()) - append(systemImageSource.replace("-", "")) - } + val taskName = + buildString { + append(device.lowercase().replace(" ", "")) + append("api") + append(apiLevel.toString()) + append(systemImageSource.replace("-", "")) + } } diff --git a/build-logic/convention/src/main/kotlin/com/wei/amazingtalker/KotlinAndroid.kt b/build-logic/convention/src/main/kotlin/com/wei/amazingtalker/KotlinAndroid.kt index 1f3a957f..c4bd7865 100644 --- a/build-logic/convention/src/main/kotlin/com/wei/amazingtalker/KotlinAndroid.kt +++ b/build-logic/convention/src/main/kotlin/com/wei/amazingtalker/KotlinAndroid.kt @@ -13,9 +13,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile /** * Configure base Kotlin with Android options */ -internal fun Project.configureKotlinAndroid( - commonExtension: CommonExtension<*, *, *, *, *>, -) { +internal fun Project.configureKotlinAndroid(commonExtension: CommonExtension<*, *, *, *, *>) { commonExtension.apply { compileSdk = 34 @@ -62,12 +60,13 @@ private fun Project.configureKotlin() { // Override by setting warningsAsErrors=true in your ~/.gradle/gradle.properties val warningsAsErrors: String? by project allWarningsAsErrors = warningsAsErrors.toBoolean() - freeCompilerArgs = freeCompilerArgs + listOf( - "-opt-in=kotlin.RequiresOptIn", - // Enable experimental coroutines APIs, including Flow - "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", - "-opt-in=kotlinx.coroutines.FlowPreview", - ) + freeCompilerArgs = freeCompilerArgs + + listOf( + "-opt-in=kotlin.RequiresOptIn", + // Enable experimental coroutines APIs, including Flow + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", + "-opt-in=kotlinx.coroutines.FlowPreview", + ) } } } diff --git a/build-logic/convention/src/main/kotlin/com/wei/amazingtalker/PrintTestApks.kt b/build-logic/convention/src/main/kotlin/com/wei/amazingtalker/PrintTestApks.kt index b1feb44d..afe988d0 100644 --- a/build-logic/convention/src/main/kotlin/com/wei/amazingtalker/PrintTestApks.kt +++ b/build-logic/convention/src/main/kotlin/com/wei/amazingtalker/PrintTestApks.kt @@ -25,16 +25,19 @@ internal fun Project.configurePrintApksTask(extension: AndroidComponentsExtensio val javaSources = variant.androidTest?.sources?.java?.all val kotlinSources = variant.androidTest?.sources?.kotlin?.all - val testSources = if (javaSources != null && kotlinSources != null) { - javaSources.zip(kotlinSources) { javaDirs, kotlinDirs -> - javaDirs + kotlinDirs + val testSources = + if (javaSources != null && kotlinSources != null) { + javaSources.zip(kotlinSources) { javaDirs, kotlinDirs -> + javaDirs + kotlinDirs + } + } else { + javaSources ?: kotlinSources } - } else javaSources ?: kotlinSources if (artifact != null && testSources != null) { tasks.register( "${variant.name}PrintTestApk", - PrintApkLocationTask::class.java + PrintApkLocationTask::class.java, ) { apkFolder.set(artifact) builtArtifactsLoader.set(loader) @@ -61,22 +64,25 @@ internal abstract class PrintApkLocationTask : DefaultTask() { @TaskAction fun taskAction() { - val hasFiles = sources.orNull?.any { directory -> - directory.asFileTree.files.any { - it.isFile && it.parentFile.path.contains("build${File.separator}generated").not() - } - } ?: throw RuntimeException("Cannot check androidTest sources") + val hasFiles = + sources.orNull?.any { directory -> + directory.asFileTree.files.any { + it.isFile && it.parentFile.path.contains("build${File.separator}generated").not() + } + } ?: throw RuntimeException("Cannot check androidTest sources") // Don't print APK location if there are no androidTest source files if (!hasFiles) { return } - val builtArtifacts = builtArtifactsLoader.get().load(apkFolder.get()) - ?: throw RuntimeException("Cannot load APKs") - if (builtArtifacts.elements.size != 1) + val builtArtifacts = + builtArtifactsLoader.get().load(apkFolder.get()) + ?: throw RuntimeException("Cannot load APKs") + if (builtArtifacts.elements.size != 1) { throw RuntimeException("Expected one APK !") + } val apk = File(builtArtifacts.elements.single().outputFile).toPath() println(apk) } -} \ No newline at end of file +} diff --git a/core/common/src/androidTest/java/com/wei/amazingtalker/core/utils/UiTextTest.kt b/core/common/src/androidTest/java/com/wei/amazingtalker/core/utils/UiTextTest.kt index bcddbcfb..37a79dfe 100644 --- a/core/common/src/androidTest/java/com/wei/amazingtalker/core/utils/UiTextTest.kt +++ b/core/common/src/androidTest/java/com/wei/amazingtalker/core/utils/UiTextTest.kt @@ -21,7 +21,6 @@ import kotlin.properties.ReadOnlyProperty * https://developer.android.com/jetpack/compose/testing-cheatsheet */ class UiTextTest { - /** * 通常我們使用 createComposeRule(),作為 composeTestRule * @@ -31,8 +30,9 @@ class UiTextTest { @get:Rule val composeTestRule = createAndroidComposeRule() - private fun AndroidComposeTestRule<*, *>.stringResource(@StringRes resId: Int) = - ReadOnlyProperty { _, _ -> activity.getString(resId) } + private fun AndroidComposeTestRule<*, *>.stringResource( + @StringRes resId: Int, + ) = ReadOnlyProperty { _, _ -> activity.getString(resId) } // The strings used for matching in these tests private val testString by composeTestRule.stringResource(R.string.generic_hello) @@ -79,10 +79,11 @@ class UiTextTest { @Test fun stringResource_returnsExpectedValue_withSingleArg() { val argName = "Alice" - val uiText = UiText.StringResource( - R.string.greeting_with_name, - listOf(UiText.StringResource.Args.DynamicString(argName)), - ) + val uiText = + UiText.StringResource( + R.string.greeting_with_name, + listOf(UiText.StringResource.Args.DynamicString(argName)), + ) composeTestRule.setContent { TestUiTextContent(uiText) @@ -102,13 +103,14 @@ class UiTextTest { fun stringResource_returnsExpectedValue_withMultipleArgs() { val argName = "Alice" val argWeather = "sunny" - val uiText = UiText.StringResource( - R.string.greeting_with_name_and_weather, - listOf( - UiText.StringResource.Args.DynamicString(argName), - UiText.StringResource.Args.DynamicString(argWeather), - ), - ) + val uiText = + UiText.StringResource( + R.string.greeting_with_name_and_weather, + listOf( + UiText.StringResource.Args.DynamicString(argName), + UiText.StringResource.Args.DynamicString(argWeather), + ), + ) composeTestRule.setContent { TestUiTextContent(uiText) @@ -129,13 +131,14 @@ class UiTextTest { fun stringResource_returnsExpectedValue_withNestedUiTextArg() { val argName = UiText.DynamicString("Alice") val argWeather = "sunny" - val uiText = UiText.StringResource( - R.string.greeting_with_name_and_weather, - listOf( - UiText.StringResource.Args.UiTextArg(argName), - UiText.StringResource.Args.DynamicString(argWeather), - ), - ) + val uiText = + UiText.StringResource( + R.string.greeting_with_name_and_weather, + listOf( + UiText.StringResource.Args.UiTextArg(argName), + UiText.StringResource.Args.DynamicString(argWeather), + ), + ) composeTestRule.setContent { TestUiTextContent(uiText) diff --git a/core/common/src/main/java/com/wei/amazingtalker/core/authentication/TokenManager.kt b/core/common/src/main/java/com/wei/amazingtalker/core/authentication/TokenManager.kt index 0ca9c70d..5c26dd2d 100644 --- a/core/common/src/main/java/com/wei/amazingtalker/core/authentication/TokenManager.kt +++ b/core/common/src/main/java/com/wei/amazingtalker/core/authentication/TokenManager.kt @@ -29,6 +29,8 @@ object TokenManager { sealed interface TokenState { data class Valid(val token: String) : TokenState - object Invalid : TokenState - object Loading : TokenState + + data object Invalid : TokenState + + data object Loading : TokenState } diff --git a/core/common/src/main/java/com/wei/amazingtalker/core/base/BaseViewModel.kt b/core/common/src/main/java/com/wei/amazingtalker/core/base/BaseViewModel.kt index 988689f7..9183e4f2 100644 --- a/core/common/src/main/java/com/wei/amazingtalker/core/base/BaseViewModel.kt +++ b/core/common/src/main/java/com/wei/amazingtalker/core/base/BaseViewModel.kt @@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow /** + * * * UI 事件決策樹 * 下圖顯示了一個決策樹,用於查找處理特定事件用例的最佳方法。 @@ -34,9 +35,7 @@ import kotlinx.coroutines.flow.asStateFlow * └─────────────────────────────────┘ └──────────────────────────────────────┘ * * - */ - -/** + * * BaseViewModel 是一個抽象類別,封裝了 ViewModel 的共享邏輯。 * 該類使用了 MVI 架構,states 它表示了 UI 的狀態和 UI 事件。狀態是指 UI 在任何給定時間點的展示狀態,事件是指一次性的、非持久性的用戶界面操作,例如顯示一個 Snackbar。 * 若為 UI 事件。你應該考慮接收 UI 事件後的狀態之變化,而不是直接傳遞 Event。 diff --git a/core/common/src/main/java/com/wei/amazingtalker/core/decoder/UriDecoder.kt b/core/common/src/main/java/com/wei/amazingtalker/core/decoder/UriDecoder.kt index 70edd24b..e610b8ac 100644 --- a/core/common/src/main/java/com/wei/amazingtalker/core/decoder/UriDecoder.kt +++ b/core/common/src/main/java/com/wei/amazingtalker/core/decoder/UriDecoder.kt @@ -6,7 +6,9 @@ import javax.inject.Inject /** * UriDecoder 是一個實現了 StringDecoder 接口的類別,它用來對 URI 進行解碼。 */ -class UriDecoder @Inject constructor() : StringDecoder { +class UriDecoder +@Inject +constructor() : StringDecoder { /** * 使用 Uri.decode 方法對傳入的已編碼字符串進行解碼。 * diff --git a/core/common/src/main/java/com/wei/amazingtalker/core/extensions/state/LiveDataEvents.kt b/core/common/src/main/java/com/wei/amazingtalker/core/extensions/state/LiveDataEvents.kt index 4dd8aa0b..50a46b19 100644 --- a/core/common/src/main/java/com/wei/amazingtalker/core/extensions/state/LiveDataEvents.kt +++ b/core/common/src/main/java/com/wei/amazingtalker/core/extensions/state/LiveDataEvents.kt @@ -12,7 +12,6 @@ import java.util.concurrent.atomic.AtomicBoolean * 這種一次性事件特別適用於 UI 事件,如顯示 Snackbar,導航到另一個頁面等。 */ class LiveDataEvents : MutableLiveData>() { - /** * 保存所有 ObserverWrapper 的集合。 */ @@ -23,7 +22,10 @@ class LiveDataEvents : MutableLiveData>() { * 為每個觀察者創建一個 ObserverWrapper,並將它加入到觀察者集合中。 */ @MainThread - override fun observe(owner: LifecycleOwner, observer: Observer>) { + override fun observe( + owner: LifecycleOwner, + observer: Observer>, + ) { // 為每個觀察者創建一個觀察者包裝器,並將它加入到集合中 val wrapper = ObserverWrapper(observer) observers.add(wrapper) @@ -49,7 +51,6 @@ class LiveDataEvents : MutableLiveData>() { * 當事件被消費後,會將 pending 設為 false,並清空待處理事件列表。 */ private class ObserverWrapper(val observer: Observer>) : Observer> { - /** * 一個標記,用於指示是否有新的待處理事件。 */ diff --git a/core/common/src/main/java/com/wei/amazingtalker/core/extensions/state/LiveDataStateExtensions.kt b/core/common/src/main/java/com/wei/amazingtalker/core/extensions/state/LiveDataStateExtensions.kt index 3db11023..2d322e95 100644 --- a/core/common/src/main/java/com/wei/amazingtalker/core/extensions/state/LiveDataStateExtensions.kt +++ b/core/common/src/main/java/com/wei/amazingtalker/core/extensions/state/LiveDataStateExtensions.kt @@ -7,12 +7,11 @@ import androidx.lifecycle.map import kotlin.reflect.KProperty1 /** + * * State 是一種特殊的 LiveData,用於封裝可重複使用的狀態。 * 當配置變化(如旋轉屏幕)時,它可以持續地提供相同的狀態值。 * State 專用於 UI 狀態。 - */ - -/** + * * LiveData.observeState 是一個擴展函式,用於觀察 State 對象的狀態。 * 它接收一個 LifecycleOwner 和一個 KProperty 對象,用於指定要觀察的狀態屬性。 * 當狀態屬性發生變化時,會調用指定的 action 函式進行處理。 diff --git a/core/common/src/main/java/com/wei/amazingtalker/core/extensions/state/SharedFlowEventsExtensions.kt b/core/common/src/main/java/com/wei/amazingtalker/core/extensions/state/SharedFlowEventsExtensions.kt index 9b1c30e1..e25805b0 100644 --- a/core/common/src/main/java/com/wei/amazingtalker/core/extensions/state/SharedFlowEventsExtensions.kt +++ b/core/common/src/main/java/com/wei/amazingtalker/core/extensions/state/SharedFlowEventsExtensions.kt @@ -9,15 +9,15 @@ private const val DEPRECATED_MESSAGE = "Google 官方明確表示不適合使用 Channels, SharedFlow 或其他回應式串流向 UI 公開 ViewModel 事件。\n" + "這可能會導致開發人員在後續發生問題,而由於這樣會造成應用程式處於不一致的狀態,可能導致發生錯誤,或是使用者可能會錯失重要資訊,因此對大部分的應用程式而言,這也是不合格的使用者體驗。\n\n" + "如果您有發生這些情形,請重新考慮這個單次 ViewModel 事件對 UI 的真正意義。立即處理這些事件,並將其降為 UI 狀態。UI 狀態更能代表特定時間點的 UI、可提供更多提交和處理的保證、測試較為容易,而且也可與其他應用程式的其餘部分整合。\n\n" + - "如要進一步瞭解部分程式碼範例不應使用上述 API 的原因,請閱讀「ViewModel:單次活動反模式」 (https://medium.com/androiddevelopers/viewmodel-one-off-event-antipatterns-16a1da869b95) 這篇網誌文章。" + "如要進一步瞭解部分程式碼範例不應使用上述 API 的原因,\n" + + "請閱讀「ViewModel:單次活動反模式」 (https://medium.com/androiddevelopers/viewmodel-one-off-event-antipatterns-16a1da869b95) 這篇網誌文章。" /** + * * SharedFlowEvents 是一種特殊的 MutableSharedFlow,它封裝了一種一次性事件。 * 這種一次性事件的特性在於它只會被消費一次,而不會在配置變化(如旋轉屏幕)時被重複消費。 * 這種一次性事件特別適用於 UI 事件,如顯示 Snackbar,導航到另一個頁面等。 - */ - -/** + * * 設置 SharedFlowEvents 的事件。 * @param values 這是一個變數參數列表,表示將要發射的值。 */ diff --git a/core/common/src/main/java/com/wei/amazingtalker/core/extensions/state/StateFlowStateExtensions.kt b/core/common/src/main/java/com/wei/amazingtalker/core/extensions/state/StateFlowStateExtensions.kt index dc8b180b..989aa35f 100644 --- a/core/common/src/main/java/com/wei/amazingtalker/core/extensions/state/StateFlowStateExtensions.kt +++ b/core/common/src/main/java/com/wei/amazingtalker/core/extensions/state/StateFlowStateExtensions.kt @@ -12,12 +12,11 @@ import kotlinx.coroutines.launch import kotlin.reflect.KProperty1 /** + * * StateFlow 是用於封裝可重複使用的狀態的 Kotlin Coroutines API。 * 當配置變化(如旋轉屏幕)時,它可以持續地提供相同的狀態值。 * StateFlow 主要用於持久化 UI 狀態。 - */ - -/** + * * StateFlow.observeState 是一個擴展函式,用於觀察 StateFlow 對象的狀態。 * 它接收一個 LifecycleOwner 和一個 KProperty 對象,用於指定要觀察的狀態屬性。 * 當狀態屬性發生變化時,會調用指定的 action 函式進行處理。 diff --git a/core/common/src/main/java/com/wei/amazingtalker/core/manager/SnackbarManager.kt b/core/common/src/main/java/com/wei/amazingtalker/core/manager/SnackbarManager.kt index 7549dc18..93faa647 100644 --- a/core/common/src/main/java/com/wei/amazingtalker/core/manager/SnackbarManager.kt +++ b/core/common/src/main/java/com/wei/amazingtalker/core/manager/SnackbarManager.kt @@ -9,7 +9,7 @@ import java.util.UUID import javax.inject.Inject import javax.inject.Singleton -const val ErrorTextPrefix = "Error:" +const val ERROR_TEXT_PREFIX = "Error:" enum class SnackbarState { Default, @@ -22,18 +22,23 @@ data class Message(val id: Long, val state: SnackbarState, val uiText: UiText) * Class responsible for managing Snackbar messages to show on the screen */ @Singleton -class SnackbarManager @Inject constructor() { - +class SnackbarManager +@Inject +constructor() { private val _messages: MutableStateFlow> = MutableStateFlow(emptyList()) val messages: StateFlow> get() = _messages.asStateFlow() - fun showMessage(state: SnackbarState, uiText: UiText) { + fun showMessage( + state: SnackbarState, + uiText: UiText, + ) { _messages.update { currentMessages -> - currentMessages + Message( - id = UUID.randomUUID().mostSignificantBits, - state = state, - uiText = uiText, - ) + currentMessages + + Message( + id = UUID.randomUUID().mostSignificantBits, + state = state, + uiText = uiText, + ) } } diff --git a/core/common/src/main/java/com/wei/amazingtalker/core/network/di/DispatchersModule.kt b/core/common/src/main/java/com/wei/amazingtalker/core/network/di/DispatchersModule.kt index 7b8cb1d1..ad7eccb2 100644 --- a/core/common/src/main/java/com/wei/amazingtalker/core/network/di/DispatchersModule.kt +++ b/core/common/src/main/java/com/wei/amazingtalker/core/network/di/DispatchersModule.kt @@ -12,7 +12,6 @@ import kotlinx.coroutines.Dispatchers @Module @InstallIn(SingletonComponent::class) object DispatchersModule { - /** * 提供 IO CoroutineDispatcher 的實例,可以在需要進行 IO 操作的協程中使用。 * diff --git a/core/common/src/main/java/com/wei/amazingtalker/core/result/DataSourceResult.kt b/core/common/src/main/java/com/wei/amazingtalker/core/result/DataSourceResult.kt index c81d7031..2661f74d 100644 --- a/core/common/src/main/java/com/wei/amazingtalker/core/result/DataSourceResult.kt +++ b/core/common/src/main/java/com/wei/amazingtalker/core/result/DataSourceResult.kt @@ -11,8 +11,10 @@ import kotlinx.coroutines.flow.onStart */ sealed interface DataSourceResult { data class Success(val data: T) : DataSourceResult + data class Error(val exception: Throwable? = null) : DataSourceResult - object Loading : DataSourceResult + + data object Loading : DataSourceResult } /** diff --git a/core/common/src/main/java/com/wei/amazingtalker/core/utils/UiText.kt b/core/common/src/main/java/com/wei/amazingtalker/core/utils/UiText.kt index 138e05a3..c02bff10 100644 --- a/core/common/src/main/java/com/wei/amazingtalker/core/utils/UiText.kt +++ b/core/common/src/main/java/com/wei/amazingtalker/core/utils/UiText.kt @@ -7,7 +7,6 @@ import androidx.annotation.StringRes * UiText 是一個密封接口,用來封裝不同形式的文字內容。 */ sealed class UiText { - /** * DynamicString 是 UiText 的一種形式,表示可以動態變化的文字。 * @param value 文字的內容。 @@ -23,12 +22,10 @@ sealed class UiText { @StringRes val resId: Int, val args: List = emptyList(), ) : UiText() { - /** * Args 是 StringResource 參數的密封接口,可以是動態字串或 UiText 類型。 */ sealed class Args { - /** * DynamicString 是 Args 的一種形式,表示可以動態變化的文字。 * @param value 文字的內容。 diff --git a/core/common/src/test/java/com/wei/amazingtalker/core/manager/SnackbarManagerTest.kt b/core/common/src/test/java/com/wei/amazingtalker/core/manager/SnackbarManagerTest.kt index 3f27600e..2073d145 100644 --- a/core/common/src/test/java/com/wei/amazingtalker/core/manager/SnackbarManagerTest.kt +++ b/core/common/src/test/java/com/wei/amazingtalker/core/manager/SnackbarManagerTest.kt @@ -14,7 +14,6 @@ import org.junit.Test * {Arrange}{Act}{Assert} */ class SnackbarManagerTest { - private lateinit var snackbarManager: SnackbarManager @Before @@ -23,32 +22,36 @@ class SnackbarManagerTest { } @Test - fun `test showMessage adds message to flow`() = runTest { - // Arrange - val testUiText = - UiText.StringResource(123, emptyList()) // Replace with an actual UiText instance - - // Act - snackbarManager.showMessage(SnackbarState.Error, testUiText) - - // Assert - val messages = snackbarManager.messages.first() - assertThat(messages.first().uiText).isEqualTo(testUiText) + fun `test showMessage adds message to flow`() { + runTest { + // Arrange + val testUiText = + UiText.StringResource(123, emptyList()) // Replace with an actual UiText instance + + // Act + snackbarManager.showMessage(SnackbarState.Error, testUiText) + + // Assert + val messages = snackbarManager.messages.first() + assertThat(messages.first().uiText).isEqualTo(testUiText) + } } @Test - fun `test setMessageShown removes message from flow`() = runTest { - // Arrange - val testUiText = - UiText.StringResource(123, emptyList()) // Replace with an actual UiText instance - - // Act - snackbarManager.showMessage(SnackbarState.Error, testUiText) - val messageId = snackbarManager.messages.first().first().id - snackbarManager.setMessageShown(messageId) - - // Assert - val messagesAfterRemoval = snackbarManager.messages.first() - assertThat(messagesAfterRemoval).isEmpty() + fun `test setMessageShown removes message from flow`() { + runTest { + // Arrange + val testUiText = + UiText.StringResource(123, emptyList()) // Replace with an actual UiText instance + + // Act + snackbarManager.showMessage(SnackbarState.Error, testUiText) + val messageId = snackbarManager.messages.first().first().id + snackbarManager.setMessageShown(messageId) + + // Assert + val messagesAfterRemoval = snackbarManager.messages.first() + assertThat(messagesAfterRemoval).isEmpty() + } } } diff --git a/core/common/src/test/java/com/wei/amazingtalker/core/result/DataSourceResultKtTest.kt b/core/common/src/test/java/com/wei/amazingtalker/core/result/DataSourceResultKtTest.kt index 6746aeb6..96c28273 100644 --- a/core/common/src/test/java/com/wei/amazingtalker/core/result/DataSourceResultKtTest.kt +++ b/core/common/src/test/java/com/wei/amazingtalker/core/result/DataSourceResultKtTest.kt @@ -19,31 +19,35 @@ import kotlin.test.assertEquals * 如果接收到的任何項目不符合預期,則會拋出異常,並使測試案例失敗。 */ class DataSourceResultKtTest { - @Test - fun `dataSourceResult catches errors`() = runTest { - flow { - emit(1) - throw Exception("Test Done") - } - .asDataSourceResult() - .test { - assertEquals(DataSourceResult.Loading, awaitItem()) - assertEquals(DataSourceResult.Success(1), awaitItem()) + fun `dataSourceResult catches errors`() { + runTest { + flow { + emit(1) + throw Exception("Test Done") + } + .asDataSourceResult() + .test { + assertEquals(DataSourceResult.Loading, awaitItem()) + assertEquals(DataSourceResult.Success(1), awaitItem()) - when (val errorResult = awaitItem()) { - is DataSourceResult.Error -> assertEquals( - "Test Done", - errorResult.exception?.message, - ) - DataSourceResult.Loading, - is DataSourceResult.Success, - -> throw IllegalStateException( - "The flow should have emitted an Error Result", - ) - } + when (val errorResult = awaitItem()) { + is DataSourceResult.Error -> { + assertEquals( + "Test Done", + errorResult.exception?.message, + ) + } - awaitComplete() - } + DataSourceResult.Loading, + is DataSourceResult.Success, + -> throw IllegalStateException( + "The flow should have emitted an Error Result", + ) + } + + awaitComplete() + } + } } } diff --git a/core/data-test/src/main/java/com/wei/amazingtalker/core/data/test/AlwaysOnlineNetworkMonitor.kt b/core/data-test/src/main/java/com/wei/amazingtalker/core/data/test/AlwaysOnlineNetworkMonitor.kt index d53c679c..2eb96174 100644 --- a/core/data-test/src/main/java/com/wei/amazingtalker/core/data/test/AlwaysOnlineNetworkMonitor.kt +++ b/core/data-test/src/main/java/com/wei/amazingtalker/core/data/test/AlwaysOnlineNetworkMonitor.kt @@ -5,6 +5,8 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import javax.inject.Inject -class AlwaysOnlineNetworkMonitor @Inject constructor() : NetworkMonitor { +class AlwaysOnlineNetworkMonitor +@Inject +constructor() : NetworkMonitor { override val isOnline: Flow = flowOf(true) } diff --git a/core/data-test/src/main/java/com/wei/amazingtalker/core/data/test/TestDataModule.kt b/core/data-test/src/main/java/com/wei/amazingtalker/core/data/test/TestDataModule.kt index 2da3775a..0f7f1671 100644 --- a/core/data-test/src/main/java/com/wei/amazingtalker/core/data/test/TestDataModule.kt +++ b/core/data-test/src/main/java/com/wei/amazingtalker/core/data/test/TestDataModule.kt @@ -19,24 +19,15 @@ import dagger.hilt.testing.TestInstallIn replaces = [DataModule::class], ) interface TestDataModule { - @Binds - fun bindsProfileRepository( - profileRepository: FakeProfileRepository, - ): ProfileRepository + fun bindsProfileRepository(profileRepository: FakeProfileRepository): ProfileRepository @Binds - fun bindsTeacherScheduleRepository( - teacherScheduleRepository: DefaultTeacherScheduleRepository, - ): TeacherScheduleRepository + fun bindsTeacherScheduleRepository(teacherScheduleRepository: DefaultTeacherScheduleRepository): TeacherScheduleRepository @Binds - fun bindsNetworkMonitor( - networkMonitor: AlwaysOnlineNetworkMonitor, - ): NetworkMonitor + fun bindsNetworkMonitor(networkMonitor: AlwaysOnlineNetworkMonitor): NetworkMonitor @Binds - fun bindsUserDataRepository( - userDataRepository: DefaultUserDataRepository, - ): UserDataRepository + fun bindsUserDataRepository(userDataRepository: DefaultUserDataRepository): UserDataRepository } diff --git a/core/data/src/main/java/com/wei/amazingtalker/core/data/di/DataModule.kt b/core/data/src/main/java/com/wei/amazingtalker/core/data/di/DataModule.kt index ed4b7e90..7e91a2d1 100644 --- a/core/data/src/main/java/com/wei/amazingtalker/core/data/di/DataModule.kt +++ b/core/data/src/main/java/com/wei/amazingtalker/core/data/di/DataModule.kt @@ -16,24 +16,15 @@ import dagger.hilt.components.SingletonComponent @Module @InstallIn(SingletonComponent::class) interface DataModule { - @Binds - fun bindsProfileRepository( - profileRepository: FakeProfileRepository, - ): ProfileRepository + fun bindsProfileRepository(profileRepository: FakeProfileRepository): ProfileRepository @Binds - fun bindsTeacherScheduleRepository( - teacherScheduleRepository: DefaultTeacherScheduleRepository, - ): TeacherScheduleRepository + fun bindsTeacherScheduleRepository(teacherScheduleRepository: DefaultTeacherScheduleRepository): TeacherScheduleRepository @Binds - fun bindsNetworkMonitor( - networkMonitor: ConnectivityManagerNetworkMonitor, - ): NetworkMonitor + fun bindsNetworkMonitor(networkMonitor: ConnectivityManagerNetworkMonitor): NetworkMonitor @Binds - fun bindsUserDataRepository( - userDataRepository: DefaultUserDataRepository, - ): UserDataRepository + fun bindsUserDataRepository(userDataRepository: DefaultUserDataRepository): UserDataRepository } diff --git a/core/data/src/main/java/com/wei/amazingtalker/core/data/model/TeacherSchedule.kt b/core/data/src/main/java/com/wei/amazingtalker/core/data/model/TeacherSchedule.kt index 43682787..6e5b4bf9 100644 --- a/core/data/src/main/java/com/wei/amazingtalker/core/data/model/TeacherSchedule.kt +++ b/core/data/src/main/java/com/wei/amazingtalker/core/data/model/TeacherSchedule.kt @@ -5,12 +5,14 @@ import com.wei.amazingtalker.core.model.data.TimeSlots import com.wei.amazingtalker.core.network.model.NetworkTeacherSchedule import com.wei.amazingtalker.core.network.model.NetworkTimeSlots -fun NetworkTeacherSchedule.asExternalModel() = TeacherSchedule( - available = this.available.map { it.asExternalModel() }, - booked = this.booked.map { it.asExternalModel() }, -) +fun NetworkTeacherSchedule.asExternalModel() = + TeacherSchedule( + available = this.available.map { it.asExternalModel() }, + booked = this.booked.map { it.asExternalModel() }, + ) -fun NetworkTimeSlots.asExternalModel() = TimeSlots( - startUtc = this.startUtc, - endUtc = this.endUtc, -) +fun NetworkTimeSlots.asExternalModel() = + TimeSlots( + startUtc = this.startUtc, + endUtc = this.endUtc, + ) diff --git a/core/data/src/main/java/com/wei/amazingtalker/core/data/model/UserProfile.kt b/core/data/src/main/java/com/wei/amazingtalker/core/data/model/UserProfile.kt index 3f795d50..96344436 100644 --- a/core/data/src/main/java/com/wei/amazingtalker/core/data/model/UserProfile.kt +++ b/core/data/src/main/java/com/wei/amazingtalker/core/data/model/UserProfile.kt @@ -7,27 +7,30 @@ import com.wei.amazingtalker.core.network.model.NetworkCoursesContent import com.wei.amazingtalker.core.network.model.NetworkSkill import com.wei.amazingtalker.core.network.model.NetworkUserProfile -fun NetworkUserProfile.asExternalModel() = UserProfile( - userName = this.userName, - userDisplayName = this.userDisplayName, - chatCount = this.chatCount, - coursesContent = this.coursesContent.asExternalModel(), - skill = this.skill.asExternalModel(), -) +fun NetworkUserProfile.asExternalModel() = + UserProfile( + userName = this.userName, + userDisplayName = this.userDisplayName, + chatCount = this.chatCount, + coursesContent = this.coursesContent.asExternalModel(), + skill = this.skill.asExternalModel(), + ) -fun NetworkCoursesContent.asExternalModel() = CoursesContent( - courseProgress = this.courseProgress, - courseCount = this.courseCount, - pupilRating = this.pupilRating, - tutorName = this.tutorName, - className = this.className, - lessonsCountDisplay = this.lessonsCountDisplay, - ratingCount = this.ratingCount, - startedDate = this.startedDate, -) +fun NetworkCoursesContent.asExternalModel() = + CoursesContent( + courseProgress = this.courseProgress, + courseCount = this.courseCount, + pupilRating = this.pupilRating, + tutorName = this.tutorName, + className = this.className, + lessonsCountDisplay = this.lessonsCountDisplay, + ratingCount = this.ratingCount, + startedDate = this.startedDate, + ) -fun NetworkSkill.asExternalModel() = Skill( - skillName = this.skillName, - skillLevel = this.skillLevel, - skillLevelProgress = this.skillLevelProgress, -) +fun NetworkSkill.asExternalModel() = + Skill( + skillName = this.skillName, + skillLevel = this.skillLevel, + skillLevelProgress = this.skillLevelProgress, + ) diff --git a/core/data/src/main/java/com/wei/amazingtalker/core/data/repository/DefaultTeacherScheduleRepository.kt b/core/data/src/main/java/com/wei/amazingtalker/core/data/repository/DefaultTeacherScheduleRepository.kt index 3bb04fc0..1fed9d95 100644 --- a/core/data/src/main/java/com/wei/amazingtalker/core/data/repository/DefaultTeacherScheduleRepository.kt +++ b/core/data/src/main/java/com/wei/amazingtalker/core/data/repository/DefaultTeacherScheduleRepository.kt @@ -17,11 +17,12 @@ import javax.inject.Inject * @param ioDispatcher 用於執行 IO 相關操作的 CoroutineDispatcher。 * @param network 數據源的網路接口。 */ -class DefaultTeacherScheduleRepository @Inject constructor( +class DefaultTeacherScheduleRepository +@Inject +constructor( @Dispatcher(AtDispatchers.IO) private val ioDispatcher: CoroutineDispatcher, private val network: AtNetworkDataSource, ) : TeacherScheduleRepository { - /** * 從數據源獲取教師的可用性。 * @param teacherName 教師名稱。 @@ -31,9 +32,10 @@ class DefaultTeacherScheduleRepository @Inject constructor( override suspend fun getTeacherAvailability( teacherName: String, startedAt: String, - ): Flow = withContext(ioDispatcher) { - flow { - emit(network.getTeacherAvailability(teacherName, startedAt).asExternalModel()) + ): Flow = + withContext(ioDispatcher) { + flow { + emit(network.getTeacherAvailability(teacherName, startedAt).asExternalModel()) + } } - } } diff --git a/core/data/src/main/java/com/wei/amazingtalker/core/data/repository/DefaultUserDataRepository.kt b/core/data/src/main/java/com/wei/amazingtalker/core/data/repository/DefaultUserDataRepository.kt index f4b1a767..8047b928 100644 --- a/core/data/src/main/java/com/wei/amazingtalker/core/data/repository/DefaultUserDataRepository.kt +++ b/core/data/src/main/java/com/wei/amazingtalker/core/data/repository/DefaultUserDataRepository.kt @@ -5,12 +5,14 @@ import com.wei.amazingtalker.core.model.data.UserData import kotlinx.coroutines.flow.Flow import javax.inject.Inject -class DefaultUserDataRepository @Inject constructor( +class DefaultUserDataRepository +@Inject +constructor( private val atPreferencesDataSource: AtPreferencesDataSource, ) : UserDataRepository { - override val userData: Flow = atPreferencesDataSource.userData + override suspend fun setTokenString(tokenString: String) { atPreferencesDataSource.setTokenString(tokenString) } diff --git a/core/data/src/main/java/com/wei/amazingtalker/core/data/repository/ProfileRepository.kt b/core/data/src/main/java/com/wei/amazingtalker/core/data/repository/ProfileRepository.kt index 02933654..55327ed4 100644 --- a/core/data/src/main/java/com/wei/amazingtalker/core/data/repository/ProfileRepository.kt +++ b/core/data/src/main/java/com/wei/amazingtalker/core/data/repository/ProfileRepository.kt @@ -4,6 +4,5 @@ import com.wei.amazingtalker.core.model.data.UserProfile import kotlinx.coroutines.flow.Flow interface ProfileRepository { - suspend fun getUserProfile(userId: String): Flow } diff --git a/core/data/src/main/java/com/wei/amazingtalker/core/data/repository/TeacherScheduleRepository.kt b/core/data/src/main/java/com/wei/amazingtalker/core/data/repository/TeacherScheduleRepository.kt index cd7b59cd..cb8d01df 100644 --- a/core/data/src/main/java/com/wei/amazingtalker/core/data/repository/TeacherScheduleRepository.kt +++ b/core/data/src/main/java/com/wei/amazingtalker/core/data/repository/TeacherScheduleRepository.kt @@ -4,6 +4,8 @@ import com.wei.amazingtalker.core.model.data.TeacherSchedule import kotlinx.coroutines.flow.Flow interface TeacherScheduleRepository { - - suspend fun getTeacherAvailability(teacherName: String, startedAt: String): Flow + suspend fun getTeacherAvailability( + teacherName: String, + startedAt: String, + ): Flow } diff --git a/core/data/src/main/java/com/wei/amazingtalker/core/data/repository/UserDataRepository.kt b/core/data/src/main/java/com/wei/amazingtalker/core/data/repository/UserDataRepository.kt index 14368ec9..490c2b0f 100644 --- a/core/data/src/main/java/com/wei/amazingtalker/core/data/repository/UserDataRepository.kt +++ b/core/data/src/main/java/com/wei/amazingtalker/core/data/repository/UserDataRepository.kt @@ -4,7 +4,6 @@ import com.wei.amazingtalker.core.model.data.UserData import kotlinx.coroutines.flow.Flow interface UserDataRepository { - /** * Stream of [UserData] */ diff --git a/core/data/src/main/java/com/wei/amazingtalker/core/data/repository/fake/FakeProfileRepository.kt b/core/data/src/main/java/com/wei/amazingtalker/core/data/repository/fake/FakeProfileRepository.kt index ce054505..e3831975 100644 --- a/core/data/src/main/java/com/wei/amazingtalker/core/data/repository/fake/FakeProfileRepository.kt +++ b/core/data/src/main/java/com/wei/amazingtalker/core/data/repository/fake/FakeProfileRepository.kt @@ -21,11 +21,12 @@ import javax.inject.Inject * @param ioDispatcher 用於執行 IO 相關操作的 CoroutineDispatcher。 * @param network 數據源的網路接口。 */ -class FakeProfileRepository @Inject constructor( +class FakeProfileRepository +@Inject +constructor( @Dispatcher(AtDispatchers.IO) private val ioDispatcher: CoroutineDispatcher, private val network: AtNetworkDataSource, ) : ProfileRepository { - /** * 模擬從資料來源獲取指定用戶的資料。 * @param userId 用戶的唯一標識符。 @@ -40,26 +41,29 @@ class FakeProfileRepository @Inject constructor( } val testNetworkUserProfile: NetworkUserProfile - get() = NetworkUserProfile( - userName = "He, Xuan-Wei", - userDisplayName = "Wei", - chatCount = 102, - coursesContent = NetworkCoursesContent( - courseProgress = 20, - courseCount = 14, - pupilRating = 9.9, - tutorName = TEST_TUTOR_NAME, - className = TEST_CLASS_NAME, - lessonsCountDisplay = "30+", - ratingCount = 4.9, - startedDate = "11.04", - ), - skill = NetworkSkill( - skillName = TEST_SKILL_NAME, - skillLevel = TEST_SKILL_LEVEL, - skillLevelProgress = TEST_SKILL_LEVEL_PROGRESS, - ), - ) + get() = + NetworkUserProfile( + userName = "He, Xuan-Wei", + userDisplayName = "Wei", + chatCount = 102, + coursesContent = + NetworkCoursesContent( + courseProgress = 20, + courseCount = 14, + pupilRating = 9.9, + tutorName = TEST_TUTOR_NAME, + className = TEST_CLASS_NAME, + lessonsCountDisplay = "30+", + ratingCount = 4.9, + startedDate = "11.04", + ), + skill = + NetworkSkill( + skillName = TEST_SKILL_NAME, + skillLevel = TEST_SKILL_LEVEL, + skillLevelProgress = TEST_SKILL_LEVEL_PROGRESS, + ), + ) const val TEST_TUTOR_NAME = "jamie-coleman" const val TEST_CLASS_NAME = "English Grammar" diff --git a/core/data/src/main/java/com/wei/amazingtalker/core/data/utils/ConnectivityManagerNetworkMonitor.kt b/core/data/src/main/java/com/wei/amazingtalker/core/data/utils/ConnectivityManagerNetworkMonitor.kt index 3909f441..2fafbd62 100644 --- a/core/data/src/main/java/com/wei/amazingtalker/core/data/utils/ConnectivityManagerNetworkMonitor.kt +++ b/core/data/src/main/java/com/wei/amazingtalker/core/data/utils/ConnectivityManagerNetworkMonitor.kt @@ -17,59 +17,64 @@ import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.conflate import javax.inject.Inject -class ConnectivityManagerNetworkMonitor @Inject constructor( +class ConnectivityManagerNetworkMonitor +@Inject +constructor( @ApplicationContext private val context: Context, ) : NetworkMonitor { - override val isOnline: Flow = callbackFlow { - val connectivityManager = context.getSystemService() - if (connectivityManager == null) { - channel.trySend(false) - channel.close() - return@callbackFlow - } + override val isOnline: Flow = + callbackFlow { + val connectivityManager = context.getSystemService() + if (connectivityManager == null) { + channel.trySend(false) + channel.close() + return@callbackFlow + } - /** - * The callback's methods are invoked on changes to *any* network matching the [NetworkRequest], - * not just the active network. So we can simply track the presence (or absence) of such [Network]. - */ - val callback = object : NetworkCallback() { + /** + * The callback's methods are invoked on changes to *any* network matching the [NetworkRequest], + * not just the active network. So we can simply track the presence (or absence) of such [Network]. + */ + val callback = + object : NetworkCallback() { + private val networks = mutableSetOf() - private val networks = mutableSetOf() + override fun onAvailable(network: Network) { + networks += network + channel.trySend(true) + } - override fun onAvailable(network: Network) { - networks += network - channel.trySend(true) - } + override fun onLost(network: Network) { + networks -= network + channel.trySend(networks.isNotEmpty()) + } + } - override fun onLost(network: Network) { - networks -= network - channel.trySend(networks.isNotEmpty()) - } - } - - val request = Builder() - .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - .build() - connectivityManager.registerNetworkCallback(request, callback) + val request = + Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build() + connectivityManager.registerNetworkCallback(request, callback) - /** - * Sends the latest connectivity status to the underlying channel. - */ - channel.trySend(connectivityManager.isCurrentlyConnected()) + /** + * Sends the latest connectivity status to the underlying channel. + */ + channel.trySend(connectivityManager.isCurrentlyConnected()) - awaitClose { - connectivityManager.unregisterNetworkCallback(callback) + awaitClose { + connectivityManager.unregisterNetworkCallback(callback) + } } - } - .conflate() + .conflate() @Suppress("DEPRECATION") - private fun ConnectivityManager.isCurrentlyConnected() = when { - VERSION.SDK_INT >= VERSION_CODES.M -> - activeNetwork - ?.let(::getNetworkCapabilities) - ?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + private fun ConnectivityManager.isCurrentlyConnected() = + when { + VERSION.SDK_INT >= VERSION_CODES.M -> + activeNetwork + ?.let(::getNetworkCapabilities) + ?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - else -> activeNetworkInfo?.isConnected - } ?: false + else -> activeNetworkInfo?.isConnected + } ?: false } diff --git a/core/datastore-test/src/main/java/com/wei/amazingtalker/core/datastore/test/TestDataStoreModule.kt b/core/datastore-test/src/main/java/com/wei/amazingtalker/core/datastore/test/TestDataStoreModule.kt index 36d8eba5..51c0144e 100644 --- a/core/datastore-test/src/main/java/com/wei/amazingtalker/core/datastore/test/TestDataStoreModule.kt +++ b/core/datastore-test/src/main/java/com/wei/amazingtalker/core/datastore/test/TestDataStoreModule.kt @@ -20,7 +20,6 @@ import javax.inject.Singleton replaces = [DataStoreModule::class], ) object TestDataStoreModule { - @Provides @Singleton fun providesUserPreferencesDataStore( diff --git a/core/datastore/src/main/java/com/wei/amazingtalker/core/datastore/AtPreferencesDataSource.kt b/core/datastore/src/main/java/com/wei/amazingtalker/core/datastore/AtPreferencesDataSource.kt index 45d0ff19..5dc080b8 100644 --- a/core/datastore/src/main/java/com/wei/amazingtalker/core/datastore/AtPreferencesDataSource.kt +++ b/core/datastore/src/main/java/com/wei/amazingtalker/core/datastore/AtPreferencesDataSource.kt @@ -10,31 +10,37 @@ import kotlinx.coroutines.flow.map import java.io.IOException import javax.inject.Inject -class AtPreferencesDataSource @Inject constructor( +class AtPreferencesDataSource +@Inject +constructor( private val userPreferences: DataStore, ) { - val userData = userPreferences.data.map { - UserData( - isFirstTimeUser = it.isFirstTimeUser, - tokenString = it.tokenString, - userName = it.userName, - useDynamicColor = it.useDynamicColor, - themeBrand = when (it.themeBrand) { - null, ThemeBrandProto.THEME_BRAND_UNSPECIFIED, ThemeBrandProto.UNRECOGNIZED, ThemeBrandProto.THEME_BRAND_DEFAULT -> ThemeBrand.DEFAULT - ThemeBrandProto.THEME_BRAND_ANDROID -> ThemeBrand.ANDROID - }, - darkThemeConfig = when (it.darkThemeConfig) { - null, DarkThemeConfigProto.DARK_THEME_CONFIG_UNSPECIFIED, DarkThemeConfigProto.UNRECOGNIZED, DarkThemeConfigProto.DARK_THEME_CONFIG_FOLLOW_SYSTEM -> DarkThemeConfig.FOLLOW_SYSTEM - DarkThemeConfigProto.DARK_THEME_CONFIG_LIGHT -> DarkThemeConfig.LIGHT - DarkThemeConfigProto.DARK_THEME_CONFIG_DARK -> DarkThemeConfig.DARK - }, - languageConfig = when (it.languageConfig) { - null, LanguageConfigProto.LANGUAGE_CONFIG_UNSPECIFIED, LanguageConfigProto.UNRECOGNIZED, LanguageConfigProto.LANGUAGE_CONFIG_FOLLOW_SYSTEM -> LanguageConfig.FOLLOW_SYSTEM - LanguageConfigProto.LANGUAGE_CONFIG_ENGLISH -> LanguageConfig.ENGLISH - LanguageConfigProto.LANGUAGE_CONFIG_CHINESE -> LanguageConfig.CHINESE - }, - ) - } + val userData = + userPreferences.data.map { + UserData( + isFirstTimeUser = it.isFirstTimeUser, + tokenString = it.tokenString, + userName = it.userName, + useDynamicColor = it.useDynamicColor, + themeBrand = + when (it.themeBrand) { + null, ThemeBrandProto.THEME_BRAND_UNSPECIFIED, ThemeBrandProto.UNRECOGNIZED, ThemeBrandProto.THEME_BRAND_DEFAULT -> ThemeBrand.DEFAULT + ThemeBrandProto.THEME_BRAND_ANDROID -> ThemeBrand.ANDROID + }, + darkThemeConfig = + when (it.darkThemeConfig) { + null, DarkThemeConfigProto.DARK_THEME_CONFIG_UNSPECIFIED, DarkThemeConfigProto.UNRECOGNIZED, DarkThemeConfigProto.DARK_THEME_CONFIG_FOLLOW_SYSTEM -> DarkThemeConfig.FOLLOW_SYSTEM + DarkThemeConfigProto.DARK_THEME_CONFIG_LIGHT -> DarkThemeConfig.LIGHT + DarkThemeConfigProto.DARK_THEME_CONFIG_DARK -> DarkThemeConfig.DARK + }, + languageConfig = + when (it.languageConfig) { + null, LanguageConfigProto.LANGUAGE_CONFIG_UNSPECIFIED, LanguageConfigProto.UNRECOGNIZED, LanguageConfigProto.LANGUAGE_CONFIG_FOLLOW_SYSTEM -> LanguageConfig.FOLLOW_SYSTEM + LanguageConfigProto.LANGUAGE_CONFIG_ENGLISH -> LanguageConfig.ENGLISH + LanguageConfigProto.LANGUAGE_CONFIG_CHINESE -> LanguageConfig.CHINESE + }, + ) + } suspend fun setTokenString(tokenString: String) { try { diff --git a/core/datastore/src/main/java/com/wei/amazingtalker/core/datastore/UserPreferencesSerializer.kt b/core/datastore/src/main/java/com/wei/amazingtalker/core/datastore/UserPreferencesSerializer.kt index d42d7cdf..f26009f4 100644 --- a/core/datastore/src/main/java/com/wei/amazingtalker/core/datastore/UserPreferencesSerializer.kt +++ b/core/datastore/src/main/java/com/wei/amazingtalker/core/datastore/UserPreferencesSerializer.kt @@ -10,7 +10,9 @@ import javax.inject.Inject /** * An [androidx.datastore.core.Serializer] for the [UserPreferences] proto. */ -class UserPreferencesSerializer @Inject constructor() : Serializer { +class UserPreferencesSerializer +@Inject +constructor() : Serializer { override val defaultValue: UserPreferences = UserPreferences.getDefaultInstance() override suspend fun readFrom(input: InputStream): UserPreferences = @@ -21,7 +23,10 @@ class UserPreferencesSerializer @Inject constructor() : Serializer= 0 && heightRange.last >= heightRange.first) { "The lowest height value must be >= 0 and the highest height value must be >= the lowest value." @@ -11,9 +10,9 @@ abstract class ScrollFlagState(heightRange: IntRange) : TopAppBarState { protected val minHeight = heightRange.first protected val maxHeight = heightRange.last protected val rangeDifference = maxHeight - minHeight - protected var _consumed: Float = 0f + protected var mConsumed: Float = 0f - protected abstract var _scrollOffset: Float + protected abstract var mScrollOffset: Float final override val height: Float get() = (maxHeight - scrollOffset).coerceIn(minHeight.toFloat(), maxHeight.toFloat()) @@ -22,7 +21,7 @@ abstract class ScrollFlagState(heightRange: IntRange) : TopAppBarState { get() = 1 - (maxHeight - height) / rangeDifference final override val consumed: Float - get() = _consumed + get() = mConsumed final override var scrollTopLimitReached: Boolean = true } diff --git a/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/management/states/topappbar/scrollflags/EnterAlwaysCollapsedState.kt b/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/management/states/topappbar/scrollflags/EnterAlwaysCollapsedState.kt index b1afdc58..98edfc37 100644 --- a/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/management/states/topappbar/scrollflags/EnterAlwaysCollapsedState.kt +++ b/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/management/states/topappbar/scrollflags/EnterAlwaysCollapsedState.kt @@ -11,53 +11,55 @@ class EnterAlwaysCollapsedState( heightRange: IntRange, scrollOffset: Float = 0f, ) : ScrollFlagState(heightRange) { - - override var _scrollOffset by mutableStateOf( + override var mScrollOffset by mutableStateOf( value = scrollOffset.coerceIn(0f, maxHeight.toFloat()), policy = structuralEqualityPolicy(), ) override val offset: Float - get() = if (scrollOffset > rangeDifference) { - -(scrollOffset - rangeDifference).coerceIn(0f, minHeight.toFloat()) - } else { - 0f - } + get() = + if (scrollOffset > rangeDifference) { + -(scrollOffset - rangeDifference).coerceIn(0f, minHeight.toFloat()) + } else { + 0f + } override var scrollOffset: Float - get() = _scrollOffset + get() = mScrollOffset set(value) { - val oldOffset = _scrollOffset - _scrollOffset = if (scrollTopLimitReached) { - value.coerceIn(0f, maxHeight.toFloat()) - } else { - value.coerceIn(rangeDifference.toFloat(), maxHeight.toFloat()) - } - _consumed = oldOffset - _scrollOffset + val oldOffset = mScrollOffset + mScrollOffset = + if (scrollTopLimitReached) { + value.coerceIn(0f, maxHeight.toFloat()) + } else { + value.coerceIn(rangeDifference.toFloat(), maxHeight.toFloat()) + } + mConsumed = oldOffset - mScrollOffset } companion object { - val Saver = run { + val Saver = + run { - val minHeightKey = "MinHeight" - val maxHeightKey = "MaxHeight" - val scrollOffsetKey = "ScrollOffset" + val minHeightKey = "MinHeight" + val maxHeightKey = "MaxHeight" + val scrollOffsetKey = "ScrollOffset" - mapSaver( - save = { - mapOf( - minHeightKey to it.minHeight, - maxHeightKey to it.maxHeight, - scrollOffsetKey to it.scrollOffset, - ) - }, - restore = { - EnterAlwaysCollapsedState( - heightRange = (it[minHeightKey] as Int)..(it[maxHeightKey] as Int), - scrollOffset = it[scrollOffsetKey] as Float, - ) - }, - ) - } + mapSaver( + save = { + mapOf( + minHeightKey to it.minHeight, + maxHeightKey to it.maxHeight, + scrollOffsetKey to it.scrollOffset, + ) + }, + restore = { + EnterAlwaysCollapsedState( + heightRange = (it[minHeightKey] as Int)..(it[maxHeightKey] as Int), + scrollOffset = it[scrollOffsetKey] as Float, + ) + }, + ) + } } } diff --git a/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/management/states/topappbar/scrollflags/EnterAlwaysState.kt b/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/management/states/topappbar/scrollflags/EnterAlwaysState.kt index 33f87e2e..cd4c3268 100644 --- a/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/management/states/topappbar/scrollflags/EnterAlwaysState.kt +++ b/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/management/states/topappbar/scrollflags/EnterAlwaysState.kt @@ -11,8 +11,7 @@ class EnterAlwaysState( heightRange: IntRange, scrollOffset: Float = 0f, ) : ScrollFlagState(heightRange) { - - override var _scrollOffset by mutableStateOf( + override var mScrollOffset by mutableStateOf( value = scrollOffset.coerceIn(0f, maxHeight.toFloat()), policy = structuralEqualityPolicy(), ) @@ -21,35 +20,36 @@ class EnterAlwaysState( get() = -(scrollOffset - rangeDifference).coerceIn(0f, minHeight.toFloat()) override var scrollOffset: Float - get() = _scrollOffset + get() = mScrollOffset set(value) { - val oldOffset = _scrollOffset - _scrollOffset = value.coerceIn(0f, maxHeight.toFloat()) - _consumed = oldOffset - _scrollOffset + val oldOffset = mScrollOffset + mScrollOffset = value.coerceIn(0f, maxHeight.toFloat()) + mConsumed = oldOffset - mScrollOffset } companion object { - val Saver = run { - - val minHeightKey = "MinHeight" - val maxHeightKey = "MaxHeight" - val scrollOffsetKey = "ScrollOffset" - - mapSaver( - save = { - mapOf( - minHeightKey to it.minHeight, - maxHeightKey to it.maxHeight, - scrollOffsetKey to it.scrollOffset, - ) - }, - restore = { - EnterAlwaysState( - heightRange = (it[minHeightKey] as Int)..(it[maxHeightKey] as Int), - scrollOffset = it[scrollOffsetKey] as Float, - ) - }, - ) - } + val Saver = + run { + + val minHeightKey = "MinHeight" + val maxHeightKey = "MaxHeight" + val scrollOffsetKey = "ScrollOffset" + + mapSaver( + save = { + mapOf( + minHeightKey to it.minHeight, + maxHeightKey to it.maxHeight, + scrollOffsetKey to it.scrollOffset, + ) + }, + restore = { + EnterAlwaysState( + heightRange = (it[minHeightKey] as Int)..(it[maxHeightKey] as Int), + scrollOffset = it[scrollOffsetKey] as Float, + ) + }, + ) + } } } diff --git a/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/management/states/topappbar/scrollflags/ExitUntilCollapsedState.kt b/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/management/states/topappbar/scrollflags/ExitUntilCollapsedState.kt index 7edd7962..7093c916 100644 --- a/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/management/states/topappbar/scrollflags/ExitUntilCollapsedState.kt +++ b/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/management/states/topappbar/scrollflags/ExitUntilCollapsedState.kt @@ -11,46 +11,46 @@ class ExitUntilCollapsedState( heightRange: IntRange, scrollOffset: Float = 0f, ) : FixedScrollFlagState(heightRange) { - - override var _scrollOffset by mutableStateOf( + override var mScrollOffset by mutableStateOf( value = scrollOffset.coerceIn(0f, rangeDifference.toFloat()), policy = structuralEqualityPolicy(), ) override var scrollOffset: Float - get() = _scrollOffset + get() = mScrollOffset set(value) { if (scrollTopLimitReached) { - val oldOffset = _scrollOffset - _scrollOffset = value.coerceIn(0f, rangeDifference.toFloat()) - _consumed = oldOffset - _scrollOffset + val oldOffset = mScrollOffset + mScrollOffset = value.coerceIn(0f, rangeDifference.toFloat()) + mConsumed = oldOffset - mScrollOffset } else { - _consumed = 0f + mConsumed = 0f } } companion object { - val Saver = run { + val Saver = + run { - val minHeightKey = "MinHeight" - val maxHeightKey = "MaxHeight" - val scrollOffsetKey = "ScrollOffset" + val minHeightKey = "MinHeight" + val maxHeightKey = "MaxHeight" + val scrollOffsetKey = "ScrollOffset" - mapSaver( - save = { - mapOf( - minHeightKey to it.minHeight, - maxHeightKey to it.maxHeight, - scrollOffsetKey to it.scrollOffset, - ) - }, - restore = { - ExitUntilCollapsedState( - heightRange = (it[minHeightKey] as Int)..(it[maxHeightKey] as Int), - scrollOffset = it[scrollOffsetKey] as Float, - ) - }, - ) - } + mapSaver( + save = { + mapOf( + minHeightKey to it.minHeight, + maxHeightKey to it.maxHeight, + scrollOffsetKey to it.scrollOffset, + ) + }, + restore = { + ExitUntilCollapsedState( + heightRange = (it[minHeightKey] as Int)..(it[maxHeightKey] as Int), + scrollOffset = it[scrollOffsetKey] as Float, + ) + }, + ) + } } } diff --git a/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/management/states/topappbar/scrollflags/ScrollState.kt b/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/management/states/topappbar/scrollflags/ScrollState.kt index 0c1b4a78..e82dc473 100644 --- a/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/management/states/topappbar/scrollflags/ScrollState.kt +++ b/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/management/states/topappbar/scrollflags/ScrollState.kt @@ -11,8 +11,7 @@ class ScrollState( heightRange: IntRange, scrollOffset: Float = 0f, ) : ScrollFlagState(heightRange) { - - override var _scrollOffset by mutableStateOf( + override var mScrollOffset by mutableStateOf( value = scrollOffset.coerceIn(0f, maxHeight.toFloat()), policy = structuralEqualityPolicy(), ) @@ -21,39 +20,40 @@ class ScrollState( get() = -(scrollOffset - rangeDifference).coerceIn(0f, minHeight.toFloat()) override var scrollOffset: Float - get() = _scrollOffset + get() = mScrollOffset set(value) { if (scrollTopLimitReached) { - val oldOffset = _scrollOffset - _scrollOffset = value.coerceIn(0f, maxHeight.toFloat()) - _consumed = oldOffset - _scrollOffset + val oldOffset = mScrollOffset + mScrollOffset = value.coerceIn(0f, maxHeight.toFloat()) + mConsumed = oldOffset - mScrollOffset } else { - _consumed = 0f + mConsumed = 0f } } companion object { - val Saver = run { - - val minHeightKey = "MinHeight" - val maxHeightKey = "MaxHeight" - val scrollOffsetKey = "ScrollOffset" - - mapSaver( - save = { - mapOf( - minHeightKey to it.minHeight, - maxHeightKey to it.maxHeight, - scrollOffsetKey to it.scrollOffset, - ) - }, - restore = { - ScrollState( - heightRange = (it[minHeightKey] as Int)..(it[maxHeightKey] as Int), - scrollOffset = it[scrollOffsetKey] as Float, - ) - }, - ) - } + val Saver = + run { + + val minHeightKey = "MinHeight" + val maxHeightKey = "MaxHeight" + val scrollOffsetKey = "ScrollOffset" + + mapSaver( + save = { + mapOf( + minHeightKey to it.minHeight, + maxHeightKey to it.maxHeight, + scrollOffsetKey to it.scrollOffset, + ) + }, + restore = { + ScrollState( + heightRange = (it[minHeightKey] as Int)..(it[maxHeightKey] as Int), + scrollOffset = it[scrollOffsetKey] as Float, + ) + }, + ) + } } } diff --git a/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/theme/Color.kt b/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/theme/Color.kt index dec3d737..a2b654df 100644 --- a/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/theme/Color.kt +++ b/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/theme/Color.kt @@ -1,4 +1,5 @@ package com.wei.amazingtalker.core.designsystem.theme + import androidx.compose.ui.graphics.Color val md_theme_light_primary = Color(0xFF006A66) diff --git a/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/theme/Shapes.kt b/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/theme/Shapes.kt index a0f5f889..a81e23d2 100644 --- a/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/theme/Shapes.kt +++ b/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/theme/Shapes.kt @@ -4,10 +4,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Shapes import androidx.compose.ui.unit.dp -val shapes = Shapes( - extraSmall = RoundedCornerShape(4.dp), - small = RoundedCornerShape(8.dp), - medium = RoundedCornerShape(12.dp), - large = RoundedCornerShape(16.dp), - extraLarge = RoundedCornerShape(24.dp), -) +val shapes = + Shapes( + extraSmall = RoundedCornerShape(4.dp), + small = RoundedCornerShape(8.dp), + medium = RoundedCornerShape(12.dp), + large = RoundedCornerShape(16.dp), + extraLarge = RoundedCornerShape(24.dp), + ) diff --git a/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/theme/Theme.kt b/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/theme/Theme.kt index 41da4e38..ce12a9af 100644 --- a/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/theme/Theme.kt +++ b/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/theme/Theme.kt @@ -8,69 +8,71 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.unit.dp -private val LightColors = lightColorScheme( - primary = md_theme_light_primary, - onPrimary = md_theme_light_onPrimary, - primaryContainer = md_theme_light_primaryContainer, - onPrimaryContainer = md_theme_light_onPrimaryContainer, - secondary = md_theme_light_secondary, - onSecondary = md_theme_light_onSecondary, - secondaryContainer = md_theme_light_secondaryContainer, - onSecondaryContainer = md_theme_light_onSecondaryContainer, - tertiary = md_theme_light_tertiary, - onTertiary = md_theme_light_onTertiary, - tertiaryContainer = md_theme_light_tertiaryContainer, - onTertiaryContainer = md_theme_light_onTertiaryContainer, - error = md_theme_light_error, - errorContainer = md_theme_light_errorContainer, - onError = md_theme_light_onError, - onErrorContainer = md_theme_light_onErrorContainer, - background = md_theme_light_background, - onBackground = md_theme_light_onBackground, - surface = md_theme_light_surface, - onSurface = md_theme_light_onSurface, - surfaceVariant = md_theme_light_surfaceVariant, - onSurfaceVariant = md_theme_light_onSurfaceVariant, - outline = md_theme_light_outline, - inverseOnSurface = md_theme_light_inverseOnSurface, - inverseSurface = md_theme_light_inverseSurface, - inversePrimary = md_theme_light_inversePrimary, - surfaceTint = md_theme_light_surfaceTint, - outlineVariant = md_theme_light_outlineVariant, - scrim = md_theme_light_scrim, -) +private val LightColors = + lightColorScheme( + primary = md_theme_light_primary, + onPrimary = md_theme_light_onPrimary, + primaryContainer = md_theme_light_primaryContainer, + onPrimaryContainer = md_theme_light_onPrimaryContainer, + secondary = md_theme_light_secondary, + onSecondary = md_theme_light_onSecondary, + secondaryContainer = md_theme_light_secondaryContainer, + onSecondaryContainer = md_theme_light_onSecondaryContainer, + tertiary = md_theme_light_tertiary, + onTertiary = md_theme_light_onTertiary, + tertiaryContainer = md_theme_light_tertiaryContainer, + onTertiaryContainer = md_theme_light_onTertiaryContainer, + error = md_theme_light_error, + errorContainer = md_theme_light_errorContainer, + onError = md_theme_light_onError, + onErrorContainer = md_theme_light_onErrorContainer, + background = md_theme_light_background, + onBackground = md_theme_light_onBackground, + surface = md_theme_light_surface, + onSurface = md_theme_light_onSurface, + surfaceVariant = md_theme_light_surfaceVariant, + onSurfaceVariant = md_theme_light_onSurfaceVariant, + outline = md_theme_light_outline, + inverseOnSurface = md_theme_light_inverseOnSurface, + inverseSurface = md_theme_light_inverseSurface, + inversePrimary = md_theme_light_inversePrimary, + surfaceTint = md_theme_light_surfaceTint, + outlineVariant = md_theme_light_outlineVariant, + scrim = md_theme_light_scrim, + ) -private val DarkColors = darkColorScheme( - primary = md_theme_dark_primary, - onPrimary = md_theme_dark_onPrimary, - primaryContainer = md_theme_dark_primaryContainer, - onPrimaryContainer = md_theme_dark_onPrimaryContainer, - secondary = md_theme_dark_secondary, - onSecondary = md_theme_dark_onSecondary, - secondaryContainer = md_theme_dark_secondaryContainer, - onSecondaryContainer = md_theme_dark_onSecondaryContainer, - tertiary = md_theme_dark_tertiary, - onTertiary = md_theme_dark_onTertiary, - tertiaryContainer = md_theme_dark_tertiaryContainer, - onTertiaryContainer = md_theme_dark_onTertiaryContainer, - error = md_theme_dark_error, - errorContainer = md_theme_dark_errorContainer, - onError = md_theme_dark_onError, - onErrorContainer = md_theme_dark_onErrorContainer, - background = md_theme_dark_background, - onBackground = md_theme_dark_onBackground, - surface = md_theme_dark_surface, - onSurface = md_theme_dark_onSurface, - surfaceVariant = md_theme_dark_surfaceVariant, - onSurfaceVariant = md_theme_dark_onSurfaceVariant, - outline = md_theme_dark_outline, - inverseOnSurface = md_theme_dark_inverseOnSurface, - inverseSurface = md_theme_dark_inverseSurface, - inversePrimary = md_theme_dark_inversePrimary, - surfaceTint = md_theme_dark_surfaceTint, - outlineVariant = md_theme_dark_outlineVariant, - scrim = md_theme_dark_scrim, -) +private val DarkColors = + darkColorScheme( + primary = md_theme_dark_primary, + onPrimary = md_theme_dark_onPrimary, + primaryContainer = md_theme_dark_primaryContainer, + onPrimaryContainer = md_theme_dark_onPrimaryContainer, + secondary = md_theme_dark_secondary, + onSecondary = md_theme_dark_onSecondary, + secondaryContainer = md_theme_dark_secondaryContainer, + onSecondaryContainer = md_theme_dark_onSecondaryContainer, + tertiary = md_theme_dark_tertiary, + onTertiary = md_theme_dark_onTertiary, + tertiaryContainer = md_theme_dark_tertiaryContainer, + onTertiaryContainer = md_theme_dark_onTertiaryContainer, + error = md_theme_dark_error, + errorContainer = md_theme_dark_errorContainer, + onError = md_theme_dark_onError, + onErrorContainer = md_theme_dark_onErrorContainer, + background = md_theme_dark_background, + onBackground = md_theme_dark_onBackground, + surface = md_theme_dark_surface, + onSurface = md_theme_dark_onSurface, + surfaceVariant = md_theme_dark_surfaceVariant, + onSurfaceVariant = md_theme_dark_onSurfaceVariant, + outline = md_theme_dark_outline, + inverseOnSurface = md_theme_dark_inverseOnSurface, + inverseSurface = md_theme_dark_inverseSurface, + inversePrimary = md_theme_dark_inversePrimary, + surfaceTint = md_theme_dark_surfaceTint, + outlineVariant = md_theme_dark_outlineVariant, + scrim = md_theme_dark_scrim, + ) /** * Light Android background theme @@ -91,20 +93,23 @@ fun AtTheme( val typography = getAppTypography() // Color scheme - val colorScheme = when { - androidTheme -> if (darkTheme) DarkColors else LightColors - else -> if (darkTheme) DarkColors else LightColors - } + val colorScheme = + when { + androidTheme -> if (darkTheme) DarkColors else LightColors + else -> if (darkTheme) DarkColors else LightColors + } // Background theme - val defaultBackgroundTheme = BackgroundTheme( - color = colorScheme.surface, - tonalElevation = 2.dp, - ) + val defaultBackgroundTheme = + BackgroundTheme( + color = colorScheme.surface, + tonalElevation = 2.dp, + ) - val backgroundTheme = when { - androidTheme -> if (darkTheme) DarkAndroidBackgroundTheme else LightAndroidBackgroundTheme - else -> defaultBackgroundTheme - } + val backgroundTheme = + when { + androidTheme -> if (darkTheme) DarkAndroidBackgroundTheme else LightAndroidBackgroundTheme + else -> defaultBackgroundTheme + } // Composition locals CompositionLocalProvider( diff --git a/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/theme/Type.kt b/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/theme/Type.kt index b4805600..020f8c32 100644 --- a/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/theme/Type.kt +++ b/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/theme/Type.kt @@ -10,137 +10,155 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp import com.wei.amazingtalker.core.designsystem.R -val atFontFamily = FontFamily( - Font(R.font.gilroy_black, FontWeight.Black), - Font(R.font.gilroy_black_italic, FontWeight.Black, FontStyle.Italic), - Font(R.font.gilroy_bold, FontWeight.Bold), - Font(R.font.gilroy_bold_italic, FontWeight.Bold, FontStyle.Italic), - Font(R.font.gilroy_extra_bold, FontWeight.ExtraBold), - Font(R.font.gilroy_extra_bold_italic, FontWeight.ExtraBold, FontStyle.Italic), - Font(R.font.gilroy_light, FontWeight.Light), - Font(R.font.gilroy_light_italic, FontWeight.Light, FontStyle.Italic), - Font(R.font.gilroy_medium, FontWeight.SemiBold), - Font(R.font.gilroy_medium_italic, FontWeight.SemiBold, FontStyle.Italic), - Font(R.font.gilroy_regular, FontWeight.Medium), - Font(R.font.gilroy_regular_italic, FontWeight.Normal, FontStyle.Italic), - Font(R.font.gilroy_semi_bold, FontWeight.SemiBold), - Font(R.font.gilroy_semi_bold_italic, FontWeight.SemiBold, FontStyle.Italic), - Font(R.font.gilroy_thin, FontWeight.Thin), - Font(R.font.gilroy_thin_italic, FontWeight.Thin, FontStyle.Italic), - Font(R.font.gilroy_ultra_light, FontWeight.ExtraLight), - Font(R.font.gilroy_ultra_light_italic, FontWeight.ExtraLight, FontStyle.Italic), -) +val atFontFamily = + FontFamily( + Font(R.font.gilroy_black, FontWeight.Black), + Font(R.font.gilroy_black_italic, FontWeight.Black, FontStyle.Italic), + Font(R.font.gilroy_bold, FontWeight.Bold), + Font(R.font.gilroy_bold_italic, FontWeight.Bold, FontStyle.Italic), + Font(R.font.gilroy_extra_bold, FontWeight.ExtraBold), + Font(R.font.gilroy_extra_bold_italic, FontWeight.ExtraBold, FontStyle.Italic), + Font(R.font.gilroy_light, FontWeight.Light), + Font(R.font.gilroy_light_italic, FontWeight.Light, FontStyle.Italic), + Font(R.font.gilroy_medium, FontWeight.SemiBold), + Font(R.font.gilroy_medium_italic, FontWeight.SemiBold, FontStyle.Italic), + Font(R.font.gilroy_regular, FontWeight.Medium), + Font(R.font.gilroy_regular_italic, FontWeight.Normal, FontStyle.Italic), + Font(R.font.gilroy_semi_bold, FontWeight.SemiBold), + Font(R.font.gilroy_semi_bold_italic, FontWeight.SemiBold, FontStyle.Italic), + Font(R.font.gilroy_thin, FontWeight.Thin), + Font(R.font.gilroy_thin_italic, FontWeight.Thin, FontStyle.Italic), + Font(R.font.gilroy_ultra_light, FontWeight.ExtraLight), + Font(R.font.gilroy_ultra_light_italic, FontWeight.ExtraLight, FontStyle.Italic), + ) -val notoSansTCFontFamily = FontFamily( - Font(R.font.noto_sans_tc_variablefont_wght), -) +val notoSansTCFontFamily = + FontFamily( + Font(R.font.noto_sans_tc_variablefont_wght), + ) -val abrilFatfaceFontFamily = FontFamily( - Font(R.font.abril_fatface), -) +val abrilFatfaceFontFamily = + FontFamily( + Font(R.font.abril_fatface), + ) @Composable fun getAppTypography(): Typography { return Typography( - displayLarge = TextStyle( + displayLarge = + TextStyle( fontFamily = atFontFamily, fontWeight = FontWeight.SemiBold, fontSize = 57.sp, lineHeight = 64.sp, letterSpacing = (-0.25).sp, ), - displayMedium = TextStyle( + displayMedium = + TextStyle( fontFamily = atFontFamily, fontWeight = FontWeight.SemiBold, fontSize = 45.sp, lineHeight = 52.sp, letterSpacing = 0.sp, ), - displaySmall = TextStyle( + displaySmall = + TextStyle( fontFamily = atFontFamily, fontWeight = FontWeight.SemiBold, fontSize = 36.sp, lineHeight = 44.sp, letterSpacing = 0.sp, ), - headlineLarge = TextStyle( + headlineLarge = + TextStyle( fontFamily = atFontFamily, fontWeight = FontWeight.SemiBold, fontSize = 32.sp, lineHeight = 40.sp, letterSpacing = 0.sp, ), - headlineMedium = TextStyle( + headlineMedium = + TextStyle( fontFamily = atFontFamily, fontWeight = FontWeight.SemiBold, fontSize = 28.sp, lineHeight = 36.sp, letterSpacing = 0.sp, ), - headlineSmall = TextStyle( + headlineSmall = + TextStyle( fontFamily = atFontFamily, fontWeight = FontWeight.SemiBold, fontSize = 24.sp, lineHeight = 32.sp, letterSpacing = 0.sp, ), - titleLarge = TextStyle( + titleLarge = + TextStyle( fontFamily = atFontFamily, fontWeight = FontWeight.SemiBold, fontSize = 22.sp, lineHeight = 28.sp, letterSpacing = 0.sp, ), - titleMedium = TextStyle( + titleMedium = + TextStyle( fontFamily = atFontFamily, fontWeight = FontWeight.SemiBold, fontSize = 18.sp, lineHeight = 24.sp, letterSpacing = 0.15.sp, ), - titleSmall = TextStyle( + titleSmall = + TextStyle( fontFamily = atFontFamily, fontWeight = FontWeight.SemiBold, fontSize = 14.sp, lineHeight = 20.sp, letterSpacing = 0.1.sp, ), - bodyLarge = TextStyle( + bodyLarge = + TextStyle( fontFamily = atFontFamily, fontWeight = FontWeight.SemiBold, fontSize = 16.sp, lineHeight = 24.sp, letterSpacing = 0.15.sp, ), - bodyMedium = TextStyle( + bodyMedium = + TextStyle( fontFamily = atFontFamily, fontWeight = FontWeight.SemiBold, fontSize = 14.sp, lineHeight = 20.sp, letterSpacing = 0.25.sp, ), - bodySmall = TextStyle( + bodySmall = + TextStyle( fontFamily = atFontFamily, fontWeight = FontWeight.SemiBold, fontSize = 12.sp, lineHeight = 16.sp, letterSpacing = 0.4.sp, ), - labelLarge = TextStyle( + labelLarge = + TextStyle( fontFamily = atFontFamily, fontWeight = FontWeight.SemiBold, fontSize = 14.sp, lineHeight = 20.sp, letterSpacing = 0.1.sp, ), - labelMedium = TextStyle( + labelMedium = + TextStyle( fontFamily = atFontFamily, fontWeight = FontWeight.SemiBold, fontSize = 12.sp, lineHeight = 16.sp, letterSpacing = 0.5.sp, ), - labelSmall = TextStyle( + labelSmall = + TextStyle( fontFamily = atFontFamily, fontWeight = FontWeight.SemiBold, fontSize = 10.sp, diff --git a/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/ui/WindowStateUtils.kt b/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/ui/WindowStateUtils.kt index 06c3a816..904c8782 100644 --- a/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/ui/WindowStateUtils.kt +++ b/core/designsystem/src/main/java/com/wei/amazingtalker/core/designsystem/ui/WindowStateUtils.kt @@ -49,19 +49,23 @@ fun isSeparating(foldFeature: FoldingFeature?): Boolean { * Different type of navigation supported by app depending on device size and state. */ enum class AtNavigationType { - BOTTOM_NAVIGATION, NAVIGATION_RAIL, PERMANENT_NAVIGATION_DRAWER + BOTTOM_NAVIGATION, + NAVIGATION_RAIL, + PERMANENT_NAVIGATION_DRAWER, } /** * Different position of navigation content inside Navigation Rail, Navigation Drawer depending on device size and state. */ enum class AtNavigationContentPosition { - TOP, CENTER + TOP, + CENTER, } /** * App Content shown depending on device size and state. */ enum class AtContentType { - SINGLE_PANE, DUAL_PANE + SINGLE_PANE, + DUAL_PANE, } diff --git a/core/designsystem/src/test/java/com/wei/amazingtalker/core/designsystem/BackgroundScreenshotTests.kt b/core/designsystem/src/test/java/com/wei/amazingtalker/core/designsystem/BackgroundScreenshotTests.kt index bfc7770d..a38a29ca 100644 --- a/core/designsystem/src/test/java/com/wei/amazingtalker/core/designsystem/BackgroundScreenshotTests.kt +++ b/core/designsystem/src/test/java/com/wei/amazingtalker/core/designsystem/BackgroundScreenshotTests.kt @@ -22,7 +22,6 @@ import org.robolectric.annotation.LooperMode @Config(application = HiltTestApplication::class, sdk = [33], qualifiers = "480dpi") @LooperMode(LooperMode.Mode.PAUSED) class BackgroundScreenshotTests { - @get:Rule val composeTestRule = createAndroidComposeRule() diff --git a/core/designsystem/src/test/java/com/wei/amazingtalker/core/designsystem/NavigationScreenshotTests.kt b/core/designsystem/src/test/java/com/wei/amazingtalker/core/designsystem/NavigationScreenshotTests.kt index e6b69ed0..f985e76f 100644 --- a/core/designsystem/src/test/java/com/wei/amazingtalker/core/designsystem/NavigationScreenshotTests.kt +++ b/core/designsystem/src/test/java/com/wei/amazingtalker/core/designsystem/NavigationScreenshotTests.kt @@ -35,7 +35,6 @@ import org.robolectric.annotation.LooperMode @Config(application = HiltTestApplication::class, sdk = [33], qualifiers = "480dpi") @LooperMode(LooperMode.Mode.PAUSED) class NavigationScreenshotTests() { - @get:Rule val composeTestRule = createAndroidComposeRule() diff --git a/core/domain/src/main/java/com/wei/amazingtalker/core/domain/IntervalizeScheduleUseCase.kt b/core/domain/src/main/java/com/wei/amazingtalker/core/domain/IntervalizeScheduleUseCase.kt index 8916710c..a9f5d918 100644 --- a/core/domain/src/main/java/com/wei/amazingtalker/core/domain/IntervalizeScheduleUseCase.kt +++ b/core/domain/src/main/java/com/wei/amazingtalker/core/domain/IntervalizeScheduleUseCase.kt @@ -23,7 +23,9 @@ enum class TimeInterval(val value: Long) { * @param scheduleState 時間段的狀態 * @return MutableList 切分後的時間段物件的列表 */ -class IntervalizeScheduleUseCase @Inject constructor() { +class IntervalizeScheduleUseCase +@Inject +constructor() { private val currentTimezone = ZoneId.systemDefault() operator fun invoke( diff --git a/core/domain/src/test/java/com/wei/amazingtalker/core/domain/IntervalizeScheduleUseCaseTest.kt b/core/domain/src/test/java/com/wei/amazingtalker/core/domain/IntervalizeScheduleUseCaseTest.kt index c0c708e6..816ed6aa 100644 --- a/core/domain/src/test/java/com/wei/amazingtalker/core/domain/IntervalizeScheduleUseCaseTest.kt +++ b/core/domain/src/test/java/com/wei/amazingtalker/core/domain/IntervalizeScheduleUseCaseTest.kt @@ -23,7 +23,6 @@ import java.time.OffsetDateTime class IntervalizeScheduleUseCaseTest( private val timeInterval: TimeInterval, ) { - companion object { @JvmStatic @Parameterized.Parameters(name = "{0}") @@ -85,35 +84,38 @@ class IntervalizeScheduleUseCaseTest( } } -private val testSchedules = TeacherSchedule( - available = listOf( - TimeSlots( - startUtc = "2023-07-31T04:30:00Z", - endUtc = "2023-07-31T09:30:00Z", - ), - TimeSlots( - startUtc = "2023-07-31T12:30:00Z", - endUtc = "2023-07-31T18:30:00Z", - ), - TimeSlots( - startUtc = "2023-07-31T19:30:00Z", - endUtc = "2023-07-31T20:30:00Z", - ), - // More Data... - ), - booked = listOf( - TimeSlots( - startUtc = "2023-07-31T09:30:00Z", - endUtc = "2023-07-31T10:30:00Z", - ), - TimeSlots( - startUtc = "2023-07-31T11:30:00Z", - endUtc = "2023-07-31T12:30:00Z", +private val testSchedules = + TeacherSchedule( + available = + listOf( + TimeSlots( + startUtc = "2023-07-31T04:30:00Z", + endUtc = "2023-07-31T09:30:00Z", + ), + TimeSlots( + startUtc = "2023-07-31T12:30:00Z", + endUtc = "2023-07-31T18:30:00Z", + ), + TimeSlots( + startUtc = "2023-07-31T19:30:00Z", + endUtc = "2023-07-31T20:30:00Z", + ), + // More Data... ), - TimeSlots( - startUtc = "2023-07-31T18:30:00Z", - endUtc = "2023-07-31T19:30:00Z", + booked = + listOf( + TimeSlots( + startUtc = "2023-07-31T09:30:00Z", + endUtc = "2023-07-31T10:30:00Z", + ), + TimeSlots( + startUtc = "2023-07-31T11:30:00Z", + endUtc = "2023-07-31T12:30:00Z", + ), + TimeSlots( + startUtc = "2023-07-31T18:30:00Z", + endUtc = "2023-07-31T19:30:00Z", + ), + // More Data... ), - // More Data... - ), -) + ) diff --git a/core/model/src/main/java/com/wei/amazingtalker/core/model/data/DarkThemeConfig.kt b/core/model/src/main/java/com/wei/amazingtalker/core/model/data/DarkThemeConfig.kt index db5435f7..0f8d23fc 100644 --- a/core/model/src/main/java/com/wei/amazingtalker/core/model/data/DarkThemeConfig.kt +++ b/core/model/src/main/java/com/wei/amazingtalker/core/model/data/DarkThemeConfig.kt @@ -1,5 +1,7 @@ package com.wei.amazingtalker.core.model.data enum class DarkThemeConfig { - FOLLOW_SYSTEM, LIGHT, DARK + FOLLOW_SYSTEM, + LIGHT, + DARK, } diff --git a/core/model/src/main/java/com/wei/amazingtalker/core/model/data/LanguageConfig.kt b/core/model/src/main/java/com/wei/amazingtalker/core/model/data/LanguageConfig.kt index 9c587664..5cee7f54 100644 --- a/core/model/src/main/java/com/wei/amazingtalker/core/model/data/LanguageConfig.kt +++ b/core/model/src/main/java/com/wei/amazingtalker/core/model/data/LanguageConfig.kt @@ -1,5 +1,7 @@ package com.wei.amazingtalker.core.model.data enum class LanguageConfig { - FOLLOW_SYSTEM, ENGLISH, CHINESE + FOLLOW_SYSTEM, + ENGLISH, + CHINESE, } diff --git a/core/model/src/main/java/com/wei/amazingtalker/core/model/data/ScheduleState.kt b/core/model/src/main/java/com/wei/amazingtalker/core/model/data/ScheduleState.kt index ce5eb36e..19af3e4c 100644 --- a/core/model/src/main/java/com/wei/amazingtalker/core/model/data/ScheduleState.kt +++ b/core/model/src/main/java/com/wei/amazingtalker/core/model/data/ScheduleState.kt @@ -5,5 +5,6 @@ package com.wei.amazingtalker.core.model.data * 包含 AVAILABLE(可用)和 BOOKED(已預定)兩種狀態。 */ enum class ScheduleState { - AVAILABLE, BOOKED + AVAILABLE, + BOOKED, } diff --git a/core/model/src/main/java/com/wei/amazingtalker/core/model/data/ThemeBrand.kt b/core/model/src/main/java/com/wei/amazingtalker/core/model/data/ThemeBrand.kt index 70ad4332..d8c3f403 100644 --- a/core/model/src/main/java/com/wei/amazingtalker/core/model/data/ThemeBrand.kt +++ b/core/model/src/main/java/com/wei/amazingtalker/core/model/data/ThemeBrand.kt @@ -1,5 +1,6 @@ package com.wei.amazingtalker.core.model.data enum class ThemeBrand { - DEFAULT, ANDROID + DEFAULT, + ANDROID, } diff --git a/core/network/src/main/java/com/wei/amazingtalker/core/network/AtNetworkDataSource.kt b/core/network/src/main/java/com/wei/amazingtalker/core/network/AtNetworkDataSource.kt index 031b04d8..47954ea7 100644 --- a/core/network/src/main/java/com/wei/amazingtalker/core/network/AtNetworkDataSource.kt +++ b/core/network/src/main/java/com/wei/amazingtalker/core/network/AtNetworkDataSource.kt @@ -6,6 +6,8 @@ import com.wei.amazingtalker.core.network.model.NetworkTeacherSchedule * Interface representing network calls to the Amazing Talker backend */ interface AtNetworkDataSource { - - suspend fun getTeacherAvailability(teacherName: String, startedAt: String? = null): NetworkTeacherSchedule + suspend fun getTeacherAvailability( + teacherName: String, + startedAt: String? = null, + ): NetworkTeacherSchedule } diff --git a/core/network/src/main/java/com/wei/amazingtalker/core/network/di/FlavoredNetworkModule.kt b/core/network/src/main/java/com/wei/amazingtalker/core/network/di/FlavoredNetworkModule.kt index 90e1a23f..197c0006 100644 --- a/core/network/src/main/java/com/wei/amazingtalker/core/network/di/FlavoredNetworkModule.kt +++ b/core/network/src/main/java/com/wei/amazingtalker/core/network/di/FlavoredNetworkModule.kt @@ -13,7 +13,6 @@ import dagger.hilt.components.SingletonComponent @Module @InstallIn(SingletonComponent::class) interface FlavoredNetworkModule { - @Binds fun binds(implementation: RetrofitAtNetwork): AtNetworkDataSource } diff --git a/core/network/src/main/java/com/wei/amazingtalker/core/network/di/NetworkModule.kt b/core/network/src/main/java/com/wei/amazingtalker/core/network/di/NetworkModule.kt index f8d71adc..8b7276c4 100644 --- a/core/network/src/main/java/com/wei/amazingtalker/core/network/di/NetworkModule.kt +++ b/core/network/src/main/java/com/wei/amazingtalker/core/network/di/NetworkModule.kt @@ -14,23 +14,24 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) object NetworkModule { - @Provides @Singleton - fun providesNetworkJson(): Json = Json { - ignoreUnknownKeys = true - } + fun providesNetworkJson(): Json = + Json { + ignoreUnknownKeys = true + } @Provides @Singleton - fun okHttpCallFactory(): Call.Factory = OkHttpClient.Builder() - .addInterceptor( - HttpLoggingInterceptor() - .apply { - if (BuildConfig.DEBUG) { - setLevel(HttpLoggingInterceptor.Level.BODY) - } - }, - ) - .build() + fun okHttpCallFactory(): Call.Factory = + OkHttpClient.Builder() + .addInterceptor( + HttpLoggingInterceptor() + .apply { + if (BuildConfig.DEBUG) { + setLevel(HttpLoggingInterceptor.Level.BODY) + } + }, + ) + .build() } diff --git a/core/network/src/main/java/com/wei/amazingtalker/core/network/retrofit/RetrofitAtNetwork.kt b/core/network/src/main/java/com/wei/amazingtalker/core/network/retrofit/RetrofitAtNetwork.kt index 02498372..11688688 100644 --- a/core/network/src/main/java/com/wei/amazingtalker/core/network/retrofit/RetrofitAtNetwork.kt +++ b/core/network/src/main/java/com/wei/amazingtalker/core/network/retrofit/RetrofitAtNetwork.kt @@ -28,25 +28,27 @@ interface RetrofitAtNetworkApi { ): NetworkTeacherSchedule } -private const val AtBaseUrl = BuildConfig.BACKEND_URL +private const val AT_BASE_URL = BuildConfig.BACKEND_URL /** * [Retrofit] backed [AtNetworkDataSource] */ @Singleton -class RetrofitAtNetwork @Inject constructor( +class RetrofitAtNetwork +@Inject +constructor( networkJson: Json, okhttpCallFactory: Call.Factory, ) : AtNetworkDataSource { - - private val networkApi = Retrofit.Builder() - .baseUrl(AtBaseUrl) - .callFactory(okhttpCallFactory) - .addConverterFactory( - networkJson.asConverterFactory("application/json".toMediaType()), - ) - .build() - .create(RetrofitAtNetworkApi::class.java) + private val networkApi = + Retrofit.Builder() + .baseUrl(AT_BASE_URL) + .callFactory(okhttpCallFactory) + .addConverterFactory( + networkJson.asConverterFactory("application/json".toMediaType()), + ) + .build() + .create(RetrofitAtNetworkApi::class.java) override suspend fun getTeacherAvailability( teacherName: String, diff --git a/core/testing/src/main/java/com/wei/amazingtalker/core/testing/AtTestRunner.kt b/core/testing/src/main/java/com/wei/amazingtalker/core/testing/AtTestRunner.kt index 147c4a08..acb92242 100644 --- a/core/testing/src/main/java/com/wei/amazingtalker/core/testing/AtTestRunner.kt +++ b/core/testing/src/main/java/com/wei/amazingtalker/core/testing/AtTestRunner.kt @@ -9,7 +9,11 @@ import dagger.hilt.android.testing.HiltTestApplication * A custom runner to set up the instrumented application class for tests. */ class AtTestRunner : AndroidJUnitRunner() { - override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application { + override fun newApplication( + cl: ClassLoader?, + name: String?, + context: Context?, + ): Application { return super.newApplication(cl, HiltTestApplication::class.java.name, context) } } diff --git a/core/testing/src/main/java/com/wei/amazingtalker/core/testing/data/ScheduleTestData.kt b/core/testing/src/main/java/com/wei/amazingtalker/core/testing/data/ScheduleTestData.kt index f5e778ae..e23f10d2 100644 --- a/core/testing/src/main/java/com/wei/amazingtalker/core/testing/data/ScheduleTestData.kt +++ b/core/testing/src/main/java/com/wei/amazingtalker/core/testing/data/ScheduleTestData.kt @@ -10,64 +10,70 @@ import java.time.ZoneId import java.time.ZoneOffset // mock currentTime -const val testCurrentTime = "2023-09-06T00:00:00Z" // 使用Z表示UTC時區 +const val TEST_CURRENT_TIME = "2023-09-06T00:00:00Z" // 使用Z表示UTC時區 -val fixedClock: Clock = Clock.fixed(Instant.parse(testCurrentTime), ZoneId.systemDefault()) -val fixedClockUtc: Clock = Clock.fixed(Instant.parse(testCurrentTime), ZoneOffset.UTC) +val fixedClock: Clock = Clock.fixed(Instant.parse(TEST_CURRENT_TIME), ZoneId.systemDefault()) +val fixedClockUtc: Clock = Clock.fixed(Instant.parse(TEST_CURRENT_TIME), ZoneOffset.UTC) -val testAvailableTimeSlot = IntervalScheduleTimeSlot( - OffsetDateTime.parse("2023-09-06T00:00+08:00"), - OffsetDateTime.parse("2023-09-06T00:30+08:00"), - ScheduleState.AVAILABLE, - DuringDayType.Morning, -) -val testUnavailableTimeSlot = IntervalScheduleTimeSlot( - OffsetDateTime.parse("2023-09-06T12:30+08:00"), - OffsetDateTime.parse("2023-09-06T13:00+08:00"), - ScheduleState.BOOKED, - DuringDayType.Afternoon, -) - -val morningTimeSlots = listOf( - testAvailableTimeSlot, - // ... add the other morning time slots similarly ... +val testAvailableTimeSlot = IntervalScheduleTimeSlot( - OffsetDateTime.parse("2023-09-06T04:00+08:00"), - OffsetDateTime.parse("2023-09-06T04:30+08:00"), + OffsetDateTime.parse("2023-09-06T00:00+08:00"), + OffsetDateTime.parse("2023-09-06T00:30+08:00"), ScheduleState.AVAILABLE, DuringDayType.Morning, - ), -) - -val afternoonTimeSlots = listOf( - testUnavailableTimeSlot, - // ... add the other afternoon time slots similarly ... + ) +val testUnavailableTimeSlot = IntervalScheduleTimeSlot( - OffsetDateTime.parse("2023-09-06T17:30+08:00"), - OffsetDateTime.parse("2023-09-06T18:00+08:00"), - ScheduleState.AVAILABLE, + OffsetDateTime.parse("2023-09-06T12:30+08:00"), + OffsetDateTime.parse("2023-09-06T13:00+08:00"), + ScheduleState.BOOKED, DuringDayType.Afternoon, - ), -) + ) -val eveningTimeSlots = listOf( - IntervalScheduleTimeSlot( - OffsetDateTime.parse("2023-09-06T18:00+08:00"), - OffsetDateTime.parse("2023-09-06T18:30+08:00"), - ScheduleState.AVAILABLE, - DuringDayType.Evening, - ), - // ... add the other evening time slots similarly ... - IntervalScheduleTimeSlot( - OffsetDateTime.parse("2023-09-06T23:30+08:00"), - OffsetDateTime.parse("2023-09-07T00:00+08:00"), - ScheduleState.AVAILABLE, - DuringDayType.Evening, - ), -) +val morningTimeSlots = + listOf( + testAvailableTimeSlot, + // ... add the other morning time slots similarly ... + IntervalScheduleTimeSlot( + OffsetDateTime.parse("2023-09-06T04:00+08:00"), + OffsetDateTime.parse("2023-09-06T04:30+08:00"), + ScheduleState.AVAILABLE, + DuringDayType.Morning, + ), + ) + +val afternoonTimeSlots = + listOf( + testUnavailableTimeSlot, + // ... add the other afternoon time slots similarly ... + IntervalScheduleTimeSlot( + OffsetDateTime.parse("2023-09-06T17:30+08:00"), + OffsetDateTime.parse("2023-09-06T18:00+08:00"), + ScheduleState.AVAILABLE, + DuringDayType.Afternoon, + ), + ) + +val eveningTimeSlots = + listOf( + IntervalScheduleTimeSlot( + OffsetDateTime.parse("2023-09-06T18:00+08:00"), + OffsetDateTime.parse("2023-09-06T18:30+08:00"), + ScheduleState.AVAILABLE, + DuringDayType.Evening, + ), + // ... add the other evening time slots similarly ... + IntervalScheduleTimeSlot( + OffsetDateTime.parse("2023-09-06T23:30+08:00"), + OffsetDateTime.parse("2023-09-07T00:00+08:00"), + ScheduleState.AVAILABLE, + DuringDayType.Evening, + ), + ) -val groupedTimeSlots = mapOf( - DuringDayType.Morning to morningTimeSlots, - DuringDayType.Afternoon to afternoonTimeSlots, - DuringDayType.Evening to eveningTimeSlots, -) +val groupedTimeSlots = + mapOf( + DuringDayType.Morning to morningTimeSlots, + DuringDayType.Afternoon to afternoonTimeSlots, + DuringDayType.Evening to eveningTimeSlots, + ) diff --git a/core/testing/src/main/java/com/wei/amazingtalker/core/testing/repository/TestTeacherScheduleRepository.kt b/core/testing/src/main/java/com/wei/amazingtalker/core/testing/repository/TestTeacherScheduleRepository.kt index bf433b6f..0df1138b 100644 --- a/core/testing/src/main/java/com/wei/amazingtalker/core/testing/repository/TestTeacherScheduleRepository.kt +++ b/core/testing/src/main/java/com/wei/amazingtalker/core/testing/repository/TestTeacherScheduleRepository.kt @@ -9,7 +9,6 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map class TestTeacherScheduleRepository : TeacherScheduleRepository { - private var errorException: Exception? = null /** @@ -47,10 +46,10 @@ class TestTeacherScheduleRepository : TeacherScheduleRepository { * A test-only API to cause `getTeacherAvailability()` to throw an exception. */ fun causeError() { - errorException = Exception(ErrorExceptionMessage) + errorException = Exception(ERROR_EXCEPTION_MESSAGE) } companion object { - const val ErrorExceptionMessage = "Test exception" + const val ERROR_EXCEPTION_MESSAGE = "Test exception" } } diff --git a/core/testing/src/main/java/com/wei/amazingtalker/core/testing/util/ScreenshotHelper.kt b/core/testing/src/main/java/com/wei/amazingtalker/core/testing/util/ScreenshotHelper.kt index 56da89be..373abb1d 100644 --- a/core/testing/src/main/java/com/wei/amazingtalker/core/testing/util/ScreenshotHelper.kt +++ b/core/testing/src/main/java/com/wei/amazingtalker/core/testing/util/ScreenshotHelper.kt @@ -22,8 +22,10 @@ import org.robolectric.RuntimeEnvironment val DefaultRoborazziOptions = RoborazziOptions( - compareOptions = CompareOptions(changeThreshold = 0f), // Pixel-perfect matching - recordOptions = RecordOptions(resizeScale = 0.5), // Reduce the size of the PNGs + // Pixel-perfect matching + compareOptions = CompareOptions(changeThreshold = 0f), + // Reduce the size of the PNGs + recordOptions = RecordOptions(resizeScale = 0.5), ) enum class DefaultTestDevices(val description: String, val spec: String) { @@ -97,12 +99,13 @@ fun AndroidComposeTestRule, A>.c ) { // Keying is necessary in some cases (e.g. animations) key(androidTheme, darkMode) { - val description = generateDescription( - shouldCompareDarkMode, - darkMode, - shouldCompareAndroidTheme, - androidTheme, - ) + val description = + generateDescription( + shouldCompareDarkMode, + darkMode, + shouldCompareAndroidTheme, + androidTheme, + ) content(description) } } @@ -140,17 +143,18 @@ private fun generateDescription( shouldCompareAndroidTheme: Boolean, androidTheme: Boolean, ): String { - val description = "" + - if (shouldCompareDarkMode) { - if (darkMode) "Dark" else "Light" - } else { - "" - } + - if (shouldCompareAndroidTheme) { - if (androidTheme) " Android" else " Default" - } else { - "" - } + val description = + "" + + if (shouldCompareDarkMode) { + if (darkMode) "Dark" else "Light" + } else { + "" + } + + if (shouldCompareAndroidTheme) { + if (androidTheme) " Android" else " Default" + } else { + "" + } return description.trim() } @@ -159,8 +163,9 @@ private fun generateDescription( * Extracts some properties from the spec string. Note that this function is not exhaustive. */ private fun extractSpecs(deviceSpec: String): TestDeviceSpecs { - val specs = deviceSpec.substringAfter("spec:") - .split(",").map { it.split("=") }.associate { it[0] to it[1] } + val specs = + deviceSpec.substringAfter("spec:") + .split(",").map { it.split("=") }.associate { it[0] to it[1] } val width = specs["width"]?.toInt() ?: 640 val height = specs["height"]?.toInt() ?: 480 val dpi = specs["dpi"]?.toInt() ?: 480 diff --git a/core/testing/src/main/java/com/wei/amazingtalker/core/testing/util/TestNetworkMonitor.kt b/core/testing/src/main/java/com/wei/amazingtalker/core/testing/util/TestNetworkMonitor.kt index 9eb79161..f8c4d661 100644 --- a/core/testing/src/main/java/com/wei/amazingtalker/core/testing/util/TestNetworkMonitor.kt +++ b/core/testing/src/main/java/com/wei/amazingtalker/core/testing/util/TestNetworkMonitor.kt @@ -5,7 +5,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow class TestNetworkMonitor : NetworkMonitor { - private val connectivityFlow = MutableStateFlow(true) override val isOnline: Flow = connectivityFlow diff --git a/feature/contactme/src/androidTest/java/com/wei/amazingtalker/feature/contactme/contactme/ContactMeScreenRobot.kt b/feature/contactme/src/androidTest/java/com/wei/amazingtalker/feature/contactme/contactme/ContactMeScreenRobot.kt index 7eb60df2..7fb7e7e0 100644 --- a/feature/contactme/src/androidTest/java/com/wei/amazingtalker/feature/contactme/contactme/ContactMeScreenRobot.kt +++ b/feature/contactme/src/androidTest/java/com/wei/amazingtalker/feature/contactme/contactme/ContactMeScreenRobot.kt @@ -38,8 +38,9 @@ internal fun contactMeScreenRobot( internal open class ContactMeScreenRobot( private val composeTestRule: AndroidComposeTestRule, ComponentActivity>, ) { - private fun AndroidComposeTestRule<*, *>.stringResource(@StringRes resId: Int) = - ReadOnlyProperty { _, _ -> activity.getString(resId) } + private fun AndroidComposeTestRule<*, *>.stringResource( + @StringRes resId: Int, + ) = ReadOnlyProperty { _, _ -> activity.getString(resId) } private val profilePictureDescription by composeTestRule.stringResource(R.string.profile_picture) private val linkedinString by composeTestRule.stringResource(R.string.linkedin) @@ -183,7 +184,6 @@ internal fun contactMeScreenCallRobot( internal open class ContactMeScreenCallRobot( private val composeTestRule: AndroidComposeTestRule, ComponentActivity>, ) { - private var isCallClicked: Boolean = false fun setIsCallClicked(backClicked: Boolean) { @@ -195,12 +195,13 @@ internal open class ContactMeScreenCallRobot( } } -val testUiState = ContactMeViewState( - nameTw = NAME_TW, - nameEng = NAME_ENG, - position = POSITION, - phone = PHONE, - linkedinUrl = LINKEDIN_URL, - email = EMAIL, - timeZone = TIME_ZONE, -) +val testUiState = + ContactMeViewState( + nameTw = NAME_TW, + nameEng = NAME_ENG, + position = POSITION, + phone = PHONE, + linkedinUrl = LINKEDIN_URL, + email = EMAIL, + timeZone = TIME_ZONE, + ) diff --git a/feature/contactme/src/androidTest/java/com/wei/amazingtalker/feature/contactme/contactme/ContactMeScreenTest.kt b/feature/contactme/src/androidTest/java/com/wei/amazingtalker/feature/contactme/contactme/ContactMeScreenTest.kt index 767523fe..bef07a91 100644 --- a/feature/contactme/src/androidTest/java/com/wei/amazingtalker/feature/contactme/contactme/ContactMeScreenTest.kt +++ b/feature/contactme/src/androidTest/java/com/wei/amazingtalker/feature/contactme/contactme/ContactMeScreenTest.kt @@ -11,7 +11,6 @@ import org.junit.Test * UI tests for [ContactMeScreen] composable. */ class ContactMeScreenTest { - /** * 通常我們使用 createComposeRule(),作為 composeTestRule * diff --git a/feature/contactme/src/main/java/com/wei/amazingtalker/feature/contactme/contactme/ContactMeScreen.kt b/feature/contactme/src/main/java/com/wei/amazingtalker/feature/contactme/contactme/ContactMeScreen.kt index bb39ca7a..f5998877 100644 --- a/feature/contactme/src/main/java/com/wei/amazingtalker/feature/contactme/contactme/ContactMeScreen.kt +++ b/feature/contactme/src/main/java/com/wei/amazingtalker/feature/contactme/contactme/ContactMeScreen.kt @@ -124,7 +124,8 @@ internal fun ContactMeScreen( onPhoneClick: () -> Unit, ) { Surface( - modifier = Modifier + modifier = + Modifier .fillMaxSize(), ) { if (contentType == AtContentType.DUAL_PANE) { @@ -153,13 +154,18 @@ internal fun ContactMeScreen( onPhoneClick = onPhoneClick, ) }, - strategy = HorizontalTwoPaneStrategy(splitFraction = 0.5f, gapWidth = SPACING_LARGE.dp), + strategy = + HorizontalTwoPaneStrategy( + splitFraction = 0.5f, + gapWidth = SPACING_LARGE.dp, + ), displayFeatures = displayFeatures, ) } } else { Box( - modifier = Modifier + modifier = + Modifier .fillMaxSize() .background(color = MaterialTheme.colorScheme.background), contentAlignment = Alignment.Center, @@ -190,10 +196,11 @@ internal fun ContactMeTwoPaneFirstContent( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center, ) { - val modifier = Modifier - .clip(CircleShape) - .size(200.dp) - .border(2.dp, MaterialTheme.colorScheme.onSurface, CircleShape) + val modifier = + Modifier + .clip(CircleShape) + .size(200.dp) + .border(2.dp, MaterialTheme.colorScheme.onSurface, CircleShape) DisplayHeadShot( modifier = modifier, @@ -260,10 +267,11 @@ internal fun ContactMeSinglePaneContent( } } item { - val modifier = Modifier - .clip(CircleShape) - .size(150.dp) - .border(2.dp, MaterialTheme.colorScheme.onBackground, CircleShape) + val modifier = + Modifier + .clip(CircleShape) + .size(150.dp) + .border(2.dp, MaterialTheme.colorScheme.onBackground, CircleShape) DisplayHeadShot( modifier = modifier, @@ -300,7 +308,8 @@ internal fun DisplayHeadShot( Image( painter = painter, contentDescription = profilePictureDescription, - modifier = modifier + modifier = + modifier .clip(CircleShape) .size(300.dp) .border(2.dp, MaterialTheme.colorScheme.onBackground, CircleShape), @@ -314,15 +323,18 @@ fun ContactMeCard( onPhoneClick: () -> Unit, ) { Card( - modifier = modifier + modifier = + modifier .padding(horizontal = SPACING_EXTRA_LARGE.dp) .clip(CardDefaults.shape), - colors = CardDefaults.cardColors( + colors = + CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surfaceVariant, ), ) { Column( - modifier = Modifier + modifier = + Modifier .padding(SPACING_LARGE.dp) .fillMaxWidth(), ) { @@ -371,7 +383,8 @@ private fun NameAndPosition( Text( text = name, style = MaterialTheme.typography.headlineSmall, - modifier = Modifier + modifier = + Modifier .baselineHeight(32.dp) .semantics { contentDescription = name }, ) @@ -380,7 +393,8 @@ private fun NameAndPosition( text = position, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier + modifier = + Modifier .padding(bottom = 20.dp) .baselineHeight(SPACING_EXTRA_LARGE.dp) .semantics { contentDescription = position }, @@ -396,9 +410,11 @@ private fun PhoneButton( val showPopup = remember { mutableStateOf(false) } if (showPopup.value) { - FunctionalityNotAvailablePopup(onDismiss = { - showPopup.value = false - }) + FunctionalityNotAvailablePopup( + onDismiss = { + showPopup.value = false + }, + ) } val phoneDescription = stringResource(id = R.string.call).format(name) @@ -407,7 +423,8 @@ private fun PhoneButton( showPopup.value = true onPhoneClick() }, - modifier = Modifier + modifier = + Modifier .clip(CircleShape) .background(MaterialTheme.colorScheme.surface) .semantics { contentDescription = phoneDescription }, @@ -448,11 +465,12 @@ fun ContactMeScreenSinglePanePreview() { } } -internal val previewUIState = ContactMeViewState( - nameEng = NAME_ENG, - position = POSITION, - phone = PHONE, - linkedinUrl = LINKEDIN_URL, - email = EMAIL, - timeZone = TIME_ZONE, -) +internal val previewUIState = + ContactMeViewState( + nameEng = NAME_ENG, + position = POSITION, + phone = PHONE, + linkedinUrl = LINKEDIN_URL, + email = EMAIL, + timeZone = TIME_ZONE, + ) diff --git a/feature/contactme/src/main/java/com/wei/amazingtalker/feature/contactme/contactme/ContactMeViewModel.kt b/feature/contactme/src/main/java/com/wei/amazingtalker/feature/contactme/contactme/ContactMeViewModel.kt index 0880e402..dc341e78 100644 --- a/feature/contactme/src/main/java/com/wei/amazingtalker/feature/contactme/contactme/ContactMeViewModel.kt +++ b/feature/contactme/src/main/java/com/wei/amazingtalker/feature/contactme/contactme/ContactMeViewModel.kt @@ -12,11 +12,12 @@ import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @HiltViewModel -class ContactMeViewModel @Inject constructor() : BaseViewModel< +class ContactMeViewModel +@Inject +constructor() : BaseViewModel< ContactMeViewAction, ContactMeViewState, >(ContactMeViewState()) { - init { getProfile() } diff --git a/feature/contactme/src/main/java/com/wei/amazingtalker/feature/contactme/contactme/ContactMeViewState.kt b/feature/contactme/src/main/java/com/wei/amazingtalker/feature/contactme/contactme/ContactMeViewState.kt index fd42ff78..6acb00b1 100644 --- a/feature/contactme/src/main/java/com/wei/amazingtalker/feature/contactme/contactme/ContactMeViewState.kt +++ b/feature/contactme/src/main/java/com/wei/amazingtalker/feature/contactme/contactme/ContactMeViewState.kt @@ -4,7 +4,7 @@ import com.wei.amazingtalker.core.base.Action import com.wei.amazingtalker.core.base.State sealed class ContactMeViewAction : Action { - object Call : ContactMeViewAction() + data object Call : ContactMeViewAction() } data class ContactMeViewState( diff --git a/feature/contactme/src/main/java/com/wei/amazingtalker/feature/contactme/contactme/navigation/ContactMeNavigation.kt b/feature/contactme/src/main/java/com/wei/amazingtalker/feature/contactme/contactme/navigation/ContactMeNavigation.kt index c9df5de2..fb4c7774 100644 --- a/feature/contactme/src/main/java/com/wei/amazingtalker/feature/contactme/contactme/navigation/ContactMeNavigation.kt +++ b/feature/contactme/src/main/java/com/wei/amazingtalker/feature/contactme/contactme/navigation/ContactMeNavigation.kt @@ -9,10 +9,10 @@ import com.wei.amazingtalker.core.designsystem.ui.AtContentType import com.wei.amazingtalker.core.designsystem.ui.AtNavigationType import com.wei.amazingtalker.feature.contactme.contactme.ContactMeRoute -const val contactMeRoute = "contact_me_route" +const val CONTACT_ME_ROUTE = "contact_me_route" fun NavController.navigateToContactMe(navOptions: NavOptions? = null) { - this.navigate(contactMeRoute, navOptions) + this.navigate(CONTACT_ME_ROUTE, navOptions) } fun NavGraphBuilder.contactMeGraph( @@ -22,7 +22,7 @@ fun NavGraphBuilder.contactMeGraph( navigationType: AtNavigationType, nestedGraphs: NavGraphBuilder.() -> Unit, ) { - composable(route = contactMeRoute) { + composable(route = CONTACT_ME_ROUTE) { ContactMeRoute( navController = navController, contentType = contentType, diff --git a/feature/contactme/src/main/java/com/wei/amazingtalker/feature/contactme/contactme/ui/DecorativeBackgroundText.kt b/feature/contactme/src/main/java/com/wei/amazingtalker/feature/contactme/contactme/ui/DecorativeBackgroundText.kt index 61a6839a..a2b3736d 100644 --- a/feature/contactme/src/main/java/com/wei/amazingtalker/feature/contactme/contactme/ui/DecorativeBackgroundText.kt +++ b/feature/contactme/src/main/java/com/wei/amazingtalker/feature/contactme/contactme/ui/DecorativeBackgroundText.kt @@ -21,16 +21,18 @@ internal fun DecorativeBackgroundText( repetitions: Int, rotationZ: Float = 0f, scale: Float = 1f, - textStyle: TextStyle = TextStyle( - fontSize = MaterialTheme.typography.displayLarge.fontSize, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f), - textAlign = TextAlign.Center, - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Bold, - ), + textStyle: TextStyle = + TextStyle( + fontSize = MaterialTheme.typography.displayLarge.fontSize, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f), + textAlign = TextAlign.Center, + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Bold, + ), ) { Column( - modifier = Modifier + modifier = + Modifier .graphicsLayer(rotationZ = rotationZ) .scale(scale), ) { @@ -45,10 +47,11 @@ internal fun DecorativeBackgroundText( } @Composable -fun decorativeTextStyle(color: Color) = TextStyle( - fontSize = MaterialTheme.typography.displayLarge.fontSize, - color = color.copy(alpha = 0.8f), - textAlign = TextAlign.Center, - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Bold, -) +fun decorativeTextStyle(color: Color) = + TextStyle( + fontSize = MaterialTheme.typography.displayLarge.fontSize, + color = color.copy(alpha = 0.8f), + textAlign = TextAlign.Center, + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Bold, + ) diff --git a/feature/contactme/src/main/java/com/wei/amazingtalker/feature/contactme/contactme/ui/ProfileProperty.kt b/feature/contactme/src/main/java/com/wei/amazingtalker/feature/contactme/contactme/ui/ProfileProperty.kt index 6afdabe9..1bc5f57a 100644 --- a/feature/contactme/src/main/java/com/wei/amazingtalker/feature/contactme/contactme/ui/ProfileProperty.kt +++ b/feature/contactme/src/main/java/com/wei/amazingtalker/feature/contactme/contactme/ui/ProfileProperty.kt @@ -15,25 +15,32 @@ import com.wei.amazingtalker.core.designsystem.theme.SPACING_EXTRA_LARGE import com.wei.amazingtalker.core.designsystem.theme.SPACING_MEDIUM @Composable -fun ProfileProperty(label: String, value: String, isLink: Boolean = false) { +fun ProfileProperty( + label: String, + value: String, + isLink: Boolean = false, +) { Column(modifier = Modifier.padding(vertical = SPACING_MEDIUM.dp)) { Divider(color = MaterialTheme.colorScheme.outline) Text( text = label, - modifier = Modifier + modifier = + Modifier .baselineHeight(SPACING_EXTRA_LARGE.dp) .semantics { contentDescription = label }, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) - val style = if (isLink) { - MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.primary) - } else { - MaterialTheme.typography.bodyLarge - } + val style = + if (isLink) { + MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.primary) + } else { + MaterialTheme.typography.bodyLarge + } Text( text = value, - modifier = Modifier + modifier = + Modifier .baselineHeight(SPACING_EXTRA_LARGE.dp) .semantics { contentDescription = value }, style = style, diff --git a/feature/home/src/androidTest/java/com/wei/amazingtalker/feature/home/home/HomeScreenRobot.kt b/feature/home/src/androidTest/java/com/wei/amazingtalker/feature/home/home/HomeScreenRobot.kt index f87980be..a74bc9c5 100644 --- a/feature/home/src/androidTest/java/com/wei/amazingtalker/feature/home/home/HomeScreenRobot.kt +++ b/feature/home/src/androidTest/java/com/wei/amazingtalker/feature/home/home/HomeScreenRobot.kt @@ -32,8 +32,9 @@ internal fun homeScreenRobot( internal open class HomeScreenRobot( private val composeTestRule: AndroidComposeTestRule, ComponentActivity>, ) { - private fun AndroidComposeTestRule<*, *>.stringResource(@StringRes resId: Int) = - ReadOnlyProperty { _, _ -> activity.getString(resId) } + private fun AndroidComposeTestRule<*, *>.stringResource( + @StringRes resId: Int, + ) = ReadOnlyProperty { _, _ -> activity.getString(resId) } // The strings used for matching in these tests private val userAvatarTag by composeTestRule.stringResource(R.string.tag_user_avatar) @@ -177,9 +178,7 @@ internal open class HomeScreenRobot( ) } - fun setHomeScreenContent( - uiStates: HomeViewState = testHomeViewState, - ) { + fun setHomeScreenContent(uiStates: HomeViewState = testHomeViewState) { composeTestRule.setContent { resetInteractionFlags() AtTheme { @@ -311,23 +310,25 @@ internal open class HomeScreenRobot( } } -val testHomeViewState: HomeViewState = HomeViewState( - loadingState = HomeViewLoadingState.Success, - userDisplayName = "Wei", - selectedTab = Tab.MY_COURSES, - chatCount = 102, - myCoursesContentState = MyCoursesContentState( - courseProgress = 20, - courseCount = 14, - pupilRating = 9.9, - tutorName = "TEST_TUTOR_NAME", - className = "TEST_CLASS_NAME", - lessonsCountDisplay = "30+", - ratingCount = 4.9, - startedDate = "11.04", - contacts = listOf(), - skillName = "TEST_SKILL_NAME", - skillLevel = "TEST_SKILL_LEVEL", - skillLevelProgress = 64, - ), -) +val testHomeViewState: HomeViewState = + HomeViewState( + loadingState = HomeViewLoadingState.Success, + userDisplayName = "Wei", + selectedTab = Tab.MY_COURSES, + chatCount = 102, + myCoursesContentState = + MyCoursesContentState( + courseProgress = 20, + courseCount = 14, + pupilRating = 9.9, + tutorName = "TEST_TUTOR_NAME", + className = "TEST_CLASS_NAME", + lessonsCountDisplay = "30+", + ratingCount = 4.9, + startedDate = "11.04", + contacts = listOf(), + skillName = "TEST_SKILL_NAME", + skillLevel = "TEST_SKILL_LEVEL", + skillLevelProgress = 64, + ), + ) diff --git a/feature/home/src/androidTest/java/com/wei/amazingtalker/feature/home/home/HomeScreenTest.kt b/feature/home/src/androidTest/java/com/wei/amazingtalker/feature/home/home/HomeScreenTest.kt index 066ce549..6aa9092e 100644 --- a/feature/home/src/androidTest/java/com/wei/amazingtalker/feature/home/home/HomeScreenTest.kt +++ b/feature/home/src/androidTest/java/com/wei/amazingtalker/feature/home/home/HomeScreenTest.kt @@ -9,7 +9,6 @@ import org.junit.Test * UI tests for [HomeScreen] composable. */ class HomeScreenTest { - /** * 通常我們使用 createComposeRule(),作為 composeTestRule * @@ -53,10 +52,11 @@ class HomeScreenTest { @Test fun checkLoadErrorElementsVisibility_whenLoadError() { - val loadingUiState = HomeViewState( - loadingState = HomeViewLoadingState.Error, - userDisplayName = "Wei", - ) + val loadingUiState = + HomeViewState( + loadingState = HomeViewLoadingState.Error, + userDisplayName = "Wei", + ) homeScreenRobot(composeTestRule) { setHomeScreenContent( @@ -69,10 +69,11 @@ class HomeScreenTest { @Test fun checkLoadingElementsVisibility_whenLoading() { - val loadingUiState = HomeViewState( - loadingState = HomeViewLoadingState.Loading, - userDisplayName = "Wei", - ) + val loadingUiState = + HomeViewState( + loadingState = HomeViewLoadingState.Loading, + userDisplayName = "Wei", + ) homeScreenRobot(composeTestRule) { setHomeScreenContent( @@ -106,11 +107,12 @@ class HomeScreenTest { @Test fun checkScreenNotAvailableVisibility_whenLoadingStateIsSuccess_andChatsSelected() { - val loadingUiState = HomeViewState( - loadingState = HomeViewLoadingState.Success, - userDisplayName = "Wei", - selectedTab = Tab.CHATS, - ) + val loadingUiState = + HomeViewState( + loadingState = HomeViewLoadingState.Success, + userDisplayName = "Wei", + selectedTab = Tab.CHATS, + ) homeScreenRobot(composeTestRule) { setHomeScreenContent(uiStates = loadingUiState) @@ -122,11 +124,12 @@ class HomeScreenTest { @Test fun checkScreenNotAvailableVisibility_whenLoadingStateIsSuccess_andTutorsSelected() { - val loadingUiState = HomeViewState( - loadingState = HomeViewLoadingState.Success, - userDisplayName = "Wei", - selectedTab = Tab.TUTORS, - ) + val loadingUiState = + HomeViewState( + loadingState = HomeViewLoadingState.Success, + userDisplayName = "Wei", + selectedTab = Tab.TUTORS, + ) homeScreenRobot(composeTestRule) { setHomeScreenContent(uiStates = loadingUiState) diff --git a/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/HomeScreen.kt b/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/HomeScreen.kt index 22665945..2288a257 100644 --- a/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/HomeScreen.kt +++ b/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/HomeScreen.kt @@ -119,15 +119,15 @@ internal fun HomeScreen( userName = uiStates.userDisplayName, avatarId = R.drawable.he_wei, onAddUserClick = { - /*TODO*/ + // TODO showPopup.value = true }, onUserProfileImageClick = { - /*TODO*/ + // TODO showPopup.value = true }, onMenuClick = { - /*TODO*/ + // TODO showPopup.value = true }, ) @@ -201,7 +201,8 @@ private fun UnavailableScreenContent() { text = screenNotAvailable, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.headlineMedium, - modifier = Modifier + modifier = + Modifier .semantics { contentDescription = screenNotAvailable }, ) Spacer(modifier = Modifier.weight(1f)) @@ -211,17 +212,20 @@ private fun UnavailableScreenContent() { @Composable private fun LoadingErrorContent() { Box( - modifier = Modifier + modifier = + Modifier .fillMaxSize() .testTag(stringResource(R.string.tag_loading_error_content)), ) { // TODO Error Content - } } + } +} @Composable private fun LoadingContent() { Box( - modifier = Modifier + modifier = + Modifier .fillMaxSize() .testTag(stringResource(R.string.tag_loading_content)), contentAlignment = Alignment.Center, @@ -235,7 +239,8 @@ private fun LoadingContent() { fun HomeScreenPreview() { AtTheme { HomeScreen( - uiStates = HomeViewState( + uiStates = + HomeViewState( loadingState = HomeViewLoadingState.Success, userDisplayName = "Wei", ), diff --git a/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/HomeViewModel.kt b/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/HomeViewModel.kt index 15bb1a52..069b72d5 100644 --- a/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/HomeViewModel.kt +++ b/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/HomeViewModel.kt @@ -15,13 +15,14 @@ import timber.log.Timber import javax.inject.Inject @HiltViewModel -class HomeViewModel @Inject constructor( +class HomeViewModel +@Inject +constructor( private val profileRepository: ProfileRepository, ) : BaseViewModel< HomeViewAction, HomeViewState, >(HomeViewState()) { - init { loadUserProfile() } @@ -58,20 +59,22 @@ class HomeViewModel @Inject constructor( } } - private fun CoursesContent.toMyCoursesContentState(skill: Skill) = MyCoursesContentState( - courseProgress = courseProgress, - courseCount = courseCount, - pupilRating = pupilRating, - tutorName = tutorName, - className = className, - lessonsCountDisplay = lessonsCountDisplay, - ratingCount = ratingCount, - startedDate = startedDate, - skillName = skill.skillName, - skillLevel = skill.skillLevel, - skillLevelProgress = skill.skillLevelProgress, - contacts = TestContacts, // TestData - ) + private fun CoursesContent.toMyCoursesContentState(skill: Skill) = + MyCoursesContentState( + courseProgress = courseProgress, + courseCount = courseCount, + pupilRating = pupilRating, + tutorName = tutorName, + className = className, + lessonsCountDisplay = lessonsCountDisplay, + ratingCount = ratingCount, + startedDate = startedDate, + skillName = skill.skillName, + skillLevel = skill.skillLevel, + skillLevelProgress = skill.skillLevelProgress, + // TestData + contacts = TestContacts, + ) private fun handleError() { updateState { copy(loadingState = HomeViewLoadingState.Error) } diff --git a/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/HomeViewState.kt b/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/HomeViewState.kt index 823ff54c..3ad12016 100644 --- a/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/HomeViewState.kt +++ b/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/HomeViewState.kt @@ -4,7 +4,9 @@ import com.wei.amazingtalker.core.base.Action import com.wei.amazingtalker.core.base.State enum class Tab { - MY_COURSES, CHATS, TUTORS + MY_COURSES, + CHATS, + TUTORS, } sealed class HomeViewAction : Action { @@ -27,6 +29,8 @@ data class HomeViewState( sealed interface HomeViewLoadingState : State { data object Success : HomeViewLoadingState + data object Error : HomeViewLoadingState + data object Loading : HomeViewLoadingState } diff --git a/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/MyCoursesContenState.kt b/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/MyCoursesContenState.kt index 7ae53b39..322a21bd 100644 --- a/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/MyCoursesContenState.kt +++ b/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/MyCoursesContenState.kt @@ -3,7 +3,9 @@ package com.wei.amazingtalker.feature.home.home import com.wei.amazingtalker.core.base.State enum class OnlineStatus { - FREE, BUSY, OFFLINE + FREE, + BUSY, + OFFLINE, } data class Contact( diff --git a/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/MyCoursesContent.kt b/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/MyCoursesContent.kt index ed7d7c3e..04317ec1 100644 --- a/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/MyCoursesContent.kt +++ b/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/MyCoursesContent.kt @@ -90,7 +90,8 @@ fun MyCoursesContent( ) Spacer(modifier = Modifier.width(SPACING_SMALL.dp)) SkillProgressCard( - modifier = Modifier + modifier = + Modifier .size(cardSize.dp) .weight(1f), skillName = uiStates.skillName, @@ -112,15 +113,17 @@ fun CourseProgressCard( onClick: () -> Unit, ) { val completed = stringResource(id = R.string.completed) - val contentCourseProgressCard = stringResource( - R.string.course_progress_card, - completed, - courseProgress, - courseCount, - ) + val contentCourseProgressCard = + stringResource( + R.string.course_progress_card, + completed, + courseProgress, + courseCount, + ) StatusCard( - modifier = modifier + modifier = + modifier .testTag(stringResource(R.string.tag_course_progress_card)) .semantics { contentDescription = contentCourseProgressCard @@ -159,7 +162,8 @@ fun PupilRatingCard( val contentPupilRatingCard = "$pupil $rating $pupilRating" StatusCard( - modifier = modifier + modifier = + modifier .testTag(stringResource(R.string.tag_pupil_rating_card)) .semantics { contentDescription = contentPupilRatingCard @@ -258,10 +262,12 @@ private fun ClassInfo( val lessons = stringResource(id = R.string.lessons) val rating = stringResource(id = R.string.rating) val started = stringResource(id = R.string.started) - val contentClassInfo = "$lessons $lessonsCountDisplay, $rating $ratingCount, $started $startedDate" + val contentClassInfo = + "$lessons $lessonsCountDisplay, $rating $ratingCount, $started $startedDate" Row( - modifier = Modifier + modifier = + Modifier .padding(horizontal = SPACING_SMALL.dp) .testTag(stringResource(R.string.tag_class_info)) .semantics { @@ -315,7 +321,8 @@ private fun ClassInfo( @Composable private fun ClassName(className: String) { Row( - modifier = Modifier + modifier = + Modifier .padding(horizontal = SPACING_SMALL.dp) .testTag(stringResource(R.string.tag_class_name)) .semantics { @@ -341,11 +348,13 @@ private fun TutorButton( Button( onClick = onTutorClick, - colors = ButtonDefaults.buttonColors( + colors = + ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.background, ), contentPadding = ButtonDefaults.TextButtonContentPadding, - modifier = Modifier + modifier = + Modifier .testTag( stringResource(R.string.tag_tutor_button), ) @@ -386,7 +395,8 @@ fun MyCoursesContentPreview() { Surface { MyCoursesContent( modifier = Modifier.padding(horizontal = SPACING_LARGE.dp), - uiStates = MyCoursesContentState( + uiStates = + MyCoursesContentState( courseProgress = 20, courseCount = 30, pupilRating = 9.9, diff --git a/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/navigation/HomeNavigation.kt b/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/navigation/HomeNavigation.kt index decbc231..63f2bb09 100644 --- a/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/navigation/HomeNavigation.kt +++ b/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/navigation/HomeNavigation.kt @@ -6,17 +6,17 @@ import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.wei.amazingtalker.feature.home.home.HomeRoute -const val homeRoute = "home_route" +const val HOME_ROUTE = "home_route" fun NavController.navigateToHome(navOptions: NavOptions? = null) { - this.navigate(homeRoute, navOptions) + this.navigate(HOME_ROUTE, navOptions) } fun NavGraphBuilder.homeGraph( navController: NavController, tokenInvalidNavigate: () -> Unit, ) { - composable(route = homeRoute) { + composable(route = HOME_ROUTE) { HomeRoute( navController = navController, tokenInvalidNavigate = tokenInvalidNavigate, diff --git a/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/ui/ContactListCard.kt b/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/ui/ContactListCard.kt index 734c7fdf..6e24fdca 100644 --- a/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/ui/ContactListCard.kt +++ b/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/ui/ContactListCard.kt @@ -52,12 +52,14 @@ fun ContactCard( val contactCard = stringResource(R.string.contact_card) Card( - modifier = modifier + modifier = + modifier .semantics { contentDescription = contactCard }, shape = shapes.extraLarge, - colors = CardDefaults.cardColors( + colors = + CardDefaults.cardColors( contentColor = MaterialTheme.colorScheme.onSecondary, containerColor = MaterialTheme.colorScheme.secondary, ), @@ -104,14 +106,16 @@ internal fun ContactAvatar( val profilePictureDescription = stringResource(R.string.profile_picture).format(name) Box( - modifier = modifier + modifier = + modifier .size(CONTACT_HEAD_SHOT_SIZE.dp) .statusIndicator(status), ) { Image( painter = painter, contentDescription = profilePictureDescription, - modifier = Modifier + modifier = + Modifier .matchParentSize() .clip(CircleShape), ) @@ -123,51 +127,52 @@ fun Modifier.statusIndicator( canvasSize: Dp = 16.dp, statusIndicatorOffset: Dp = 3.dp, offlineStatusIndicatorOffset: Dp = 6.dp, -): Modifier = composed { - val canvasBackground = MaterialTheme.colorScheme.secondary - - this.then( - drawWithContent { - drawContent() - - val circleColor = when (status) { - OnlineStatus.FREE -> FreeColor - OnlineStatus.BUSY -> BusyColor - OnlineStatus.OFFLINE -> OfflineColor - } - - val radius = size.minDimension.coerceAtMost(canvasSize.toPx()) / 2 - val center = Offset(size.width - radius, size.height - radius) - - drawCircle( - color = canvasBackground, - radius = radius, - center = center, - ) +): Modifier = + composed { + val canvasBackground = MaterialTheme.colorScheme.secondary + + this.then( + drawWithContent { + drawContent() + + val circleColor = + when (status) { + OnlineStatus.FREE -> FreeColor + OnlineStatus.BUSY -> BusyColor + OnlineStatus.OFFLINE -> OfflineColor + } - drawCircle( - color = circleColor, - radius = radius - statusIndicatorOffset.toPx(), - center = center, - ) + val radius = size.minDimension.coerceAtMost(canvasSize.toPx()) / 2 + val center = Offset(size.width - radius, size.height - radius) - if (status == OnlineStatus.OFFLINE) { drawCircle( color = canvasBackground, - radius = radius - offlineStatusIndicatorOffset.toPx(), + radius = radius, center = center, ) - } - }, - ) -} + + drawCircle( + color = circleColor, + radius = radius - statusIndicatorOffset.toPx(), + center = center, + ) + + if (status == OnlineStatus.OFFLINE) { + drawCircle( + color = canvasBackground, + radius = radius - offlineStatusIndicatorOffset.toPx(), + center = center, + ) + } + }, + ) + } @Composable -fun PlaceholderAvatar( - modifier: Modifier = Modifier, -) { +fun PlaceholderAvatar(modifier: Modifier = Modifier) { Box( - modifier = modifier + modifier = + modifier .background(MaterialTheme.colorScheme.secondary) .size(CONTACT_HEAD_SHOT_SIZE.dp), ) diff --git a/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/ui/HomeTabRow.kt b/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/ui/HomeTabRow.kt index 3797f783..83c5f118 100644 --- a/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/ui/HomeTabRow.kt +++ b/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/ui/HomeTabRow.kt @@ -73,7 +73,8 @@ fun MyCoursesTab( Tab( selected = isSelected, onClick = onTabClick, - modifier = Modifier.semantics { + modifier = + Modifier.semantics { contentDescription = myCourses }, ) { @@ -98,7 +99,8 @@ fun ChatTab( Tab( selected = isSelected, onClick = onTabClick, - modifier = Modifier.semantics { + modifier = + Modifier.semantics { contentDescription = chats }, ) { @@ -122,7 +124,8 @@ fun ChatTab( @Composable fun ChatCountBadge(count: String) { Box( - modifier = Modifier + modifier = + Modifier .clip(shape = shapes.medium) .background(color = MaterialTheme.colorScheme.primary), ) { @@ -136,13 +139,17 @@ fun ChatCountBadge(count: String) { } @Composable -fun TutorsTab(isSelected: Boolean, onTabClick: () -> Unit) { +fun TutorsTab( + isSelected: Boolean, + onTabClick: () -> Unit, +) { val tutors = stringResource(id = R.string.tutors) Tab( selected = isSelected, onClick = onTabClick, - modifier = Modifier.semantics { + modifier = + Modifier.semantics { contentDescription = tutors }, ) { @@ -160,7 +167,8 @@ fun TutorsTab(isSelected: Boolean, onTabClick: () -> Unit) { fun HomeTabRowPreview() { AtTheme { HomeTabRow( - uiStates = HomeViewState( + uiStates = + HomeViewState( selectedTab = Tab.MY_COURSES, chatCount = 999, ), diff --git a/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/ui/HomeTopBar.kt b/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/ui/HomeTopBar.kt index 60690477..247bf6ae 100644 --- a/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/ui/HomeTopBar.kt +++ b/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/ui/HomeTopBar.kt @@ -66,7 +66,8 @@ fun HomeTopBar( text = helloUserName, style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Normal, - modifier = Modifier + modifier = + Modifier .testTag(stringResource(R.string.tag_hello_user_name_text)) .semantics { contentDescription = helloUserName }, ) @@ -86,7 +87,8 @@ private fun AddUserButton( onClick = { onAddUserClick() }, - modifier = Modifier + modifier = + Modifier .clip(CircleShape) .background(MaterialTheme.colorScheme.surfaceVariant) .semantics { contentDescription = addUser }, @@ -112,7 +114,8 @@ internal fun UserAvatar( IconButton( onClick = onUserProfileImageClick, - modifier = modifier + modifier = + modifier .size(48.dp) .testTag(stringResource(R.string.tag_user_avatar)) .semantics { @@ -122,7 +125,8 @@ internal fun UserAvatar( Image( painter = painter, contentDescription = null, - modifier = modifier + modifier = + modifier .clip(CircleShape) .fillMaxSize(), ) @@ -130,14 +134,13 @@ internal fun UserAvatar( } @Composable -private fun MenuButton( - onMenuClick: () -> Unit, -) { +private fun MenuButton(onMenuClick: () -> Unit) { val menu = stringResource(R.string.menu) IconButton( onClick = onMenuClick, - modifier = Modifier + modifier = + Modifier .clip(CircleShape) .background(MaterialTheme.colorScheme.surfaceVariant) .semantics { contentDescription = menu }, diff --git a/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/ui/SkillProgressCard.kt b/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/ui/SkillProgressCard.kt index bd4a04c5..e8cb3067 100644 --- a/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/ui/SkillProgressCard.kt +++ b/feature/home/src/main/java/com/wei/amazingtalker/feature/home/home/ui/SkillProgressCard.kt @@ -38,18 +38,21 @@ fun SkillProgressCard( progress: Int, onClick: () -> Unit, ) { - val contentSkillProgressCard = stringResource( - R.string.skill_progress_card, - skillName, - skillLevel, - progress, - ) + val contentSkillProgressCard = + stringResource( + R.string.skill_progress_card, + skillName, + skillLevel, + progress, + ) Card( - modifier = modifier.semantics { + modifier = + modifier.semantics { contentDescription = contentSkillProgressCard }, shape = shapes.extraLarge, - colors = CardDefaults.cardColors( + colors = + CardDefaults.cardColors( contentColor = MaterialTheme.colorScheme.onPrimary, containerColor = MaterialTheme.colorScheme.primary, ), @@ -75,7 +78,8 @@ fun SkillProgressCard( ) Spacer(modifier = Modifier.height(SPACING_LARGE.dp)) CircularProgress( - modifier = Modifier + modifier = + Modifier .weight(1f) .testTag(stringResource(R.string.tag_circular_progress)), progress = progress, @@ -86,7 +90,10 @@ fun SkillProgressCard( } @Composable -private fun CircularProgress(modifier: Modifier = Modifier, progress: Int) { +private fun CircularProgress( + modifier: Modifier = Modifier, + progress: Int, +) { val strokeLineWidth = 4.dp val startAngle = -90f val sweepAngleFactor = 360 / 100f @@ -126,7 +133,8 @@ private fun CircularProgress(modifier: Modifier = Modifier, progress: Int) { fun HomeScreenPreview() { AtTheme { SkillProgressCard( - modifier = Modifier + modifier = + Modifier .width(200.dp) .height(152.dp), skillName = "Business English", diff --git a/feature/login/src/androidTest/java/com/wei/amazingtalker/feature/login/login/LoginScreenRobot.kt b/feature/login/src/androidTest/java/com/wei/amazingtalker/feature/login/login/LoginScreenRobot.kt index d39d936b..536624cc 100644 --- a/feature/login/src/androidTest/java/com/wei/amazingtalker/feature/login/login/LoginScreenRobot.kt +++ b/feature/login/src/androidTest/java/com/wei/amazingtalker/feature/login/login/LoginScreenRobot.kt @@ -39,14 +39,15 @@ internal fun loginScreenRobot( internal open class LoginScreenRobot( private val composeTestRule: AndroidComposeTestRule, ComponentActivity>, ) { - - private fun AndroidComposeTestRule<*, *>.stringResource(@StringRes resId: Int) = - ReadOnlyProperty { _, _ -> activity.getString(resId) } - - private fun withRole(role: Role) = SemanticsMatcher("${SemanticsProperties.Role.name} contains '$role'") { - val roleProperty = it.config.getOrNull(SemanticsProperties.Role) ?: false - roleProperty == role - } + private fun AndroidComposeTestRule<*, *>.stringResource( + @StringRes resId: Int, + ) = ReadOnlyProperty { _, _ -> activity.getString(resId) } + + private fun withRole(role: Role) = + SemanticsMatcher("${SemanticsProperties.Role.name} contains '$role'") { + val roleProperty = it.config.getOrNull(SemanticsProperties.Role) ?: false + roleProperty == role + } // The strings used for matching in these tests private val accountDescription by composeTestRule.stringResource(R.string.content_description_account) @@ -96,11 +97,12 @@ internal open class LoginScreenRobot( account = "", password = "", login = { account, password -> - loginResult = if (TEST_ACCOUNT == account && TEST_PASSWORD == password) { - LoginResultRobot.LoginResult.Success - } else { - LoginResultRobot.LoginResult.Failed - } + loginResult = + if (TEST_ACCOUNT == account && TEST_PASSWORD == password) { + LoginResultRobot.LoginResult.Success + } else { + LoginResultRobot.LoginResult.Failed + } }, ) } @@ -161,13 +163,13 @@ internal fun loginResultRobot( internal open class LoginResultRobot( private val composeTestRule: AndroidComposeTestRule, ComponentActivity>, ) { - enum class LoginResult { Success, Failed, } private lateinit var result: LoginResult + fun setResult(loginResult: LoginResult) { result = loginResult } diff --git a/feature/login/src/androidTest/java/com/wei/amazingtalker/feature/login/login/LoginScreenTest.kt b/feature/login/src/androidTest/java/com/wei/amazingtalker/feature/login/login/LoginScreenTest.kt index ba02bc88..cda2e176 100644 --- a/feature/login/src/androidTest/java/com/wei/amazingtalker/feature/login/login/LoginScreenTest.kt +++ b/feature/login/src/androidTest/java/com/wei/amazingtalker/feature/login/login/LoginScreenTest.kt @@ -11,7 +11,6 @@ import org.junit.Test * UI tests for [LoginScreen] composable. */ class LoginScreenTest { - /** * 通常我們使用 createComposeRule(),作為 composeTestRule * diff --git a/feature/login/src/androidTest/java/com/wei/amazingtalker/feature/login/welcome/WelcomeScreenRobot.kt b/feature/login/src/androidTest/java/com/wei/amazingtalker/feature/login/welcome/WelcomeScreenRobot.kt index 5af858fb..c764edf5 100644 --- a/feature/login/src/androidTest/java/com/wei/amazingtalker/feature/login/welcome/WelcomeScreenRobot.kt +++ b/feature/login/src/androidTest/java/com/wei/amazingtalker/feature/login/welcome/WelcomeScreenRobot.kt @@ -28,8 +28,9 @@ internal fun welcomeScreenRobot( internal open class WelcomeScreenRobot( private val composeTestRule: AndroidComposeTestRule, ComponentActivity>, ) { - private fun AndroidComposeTestRule<*, *>.stringResource(@StringRes resId: Int) = - ReadOnlyProperty { _, _ -> activity.getString(resId) } + private fun AndroidComposeTestRule<*, *>.stringResource( + @StringRes resId: Int, + ) = ReadOnlyProperty { _, _ -> activity.getString(resId) } // The strings used for matching in these tests private val scheduleListTag by composeTestRule.stringResource(R.string.tag_welcome_graphics) diff --git a/feature/login/src/androidTest/java/com/wei/amazingtalker/feature/login/welcome/WelcomeScreenTest.kt b/feature/login/src/androidTest/java/com/wei/amazingtalker/feature/login/welcome/WelcomeScreenTest.kt index df7a2c31..7a4e3a8b 100644 --- a/feature/login/src/androidTest/java/com/wei/amazingtalker/feature/login/welcome/WelcomeScreenTest.kt +++ b/feature/login/src/androidTest/java/com/wei/amazingtalker/feature/login/welcome/WelcomeScreenTest.kt @@ -9,7 +9,6 @@ import org.junit.Test * UI tests for [WelcomeScreen] composable. */ class WelcomeScreenTest { - /** * 通常我們使用 createComposeRule(),作為 composeTestRule * diff --git a/feature/login/src/main/java/com/wei/amazingtalker/feature/login/login/LoginScreen.kt b/feature/login/src/main/java/com/wei/amazingtalker/feature/login/login/LoginScreen.kt index 254bc571..4159e7db 100644 --- a/feature/login/src/main/java/com/wei/amazingtalker/feature/login/login/LoginScreen.kt +++ b/feature/login/src/main/java/com/wei/amazingtalker/feature/login/login/LoginScreen.kt @@ -96,20 +96,23 @@ internal fun LoginScreen( withTopSpacer: Boolean = true, withBottomSpacer: Boolean = true, ) { - val accountState = rememberSaveable { - mutableStateOf(account) - } + val accountState = + rememberSaveable { + mutableStateOf(account) + } - val passwordState = rememberSaveable { - mutableStateOf(password) - } + val passwordState = + rememberSaveable { + mutableStateOf(password) + } Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background, ) { Box( - modifier = Modifier + modifier = + Modifier .fillMaxSize() .padding(SPACING_LARGE.dp), contentAlignment = Alignment.Center, @@ -149,21 +152,21 @@ private fun Title(modifier: Modifier = Modifier) { Text( text = title, style = MaterialTheme.typography.displayMedium, - modifier = modifier + modifier = + modifier .semantics { contentDescription = "" }, ) } @Composable -internal fun AccountTextField( - accountState: MutableState, -) { +internal fun AccountTextField(accountState: MutableState) { val account = stringResource(R.string.account) val accountDescription = stringResource(R.string.content_description_account) TextField( value = accountState.value, - modifier = Modifier + modifier = + Modifier .semantics { contentDescription = accountDescription }, onValueChange = { accountState.value = it @@ -176,15 +179,14 @@ internal fun AccountTextField( } @Composable -internal fun PasswordTextField( - passwordState: MutableState, -) { +internal fun PasswordTextField(passwordState: MutableState) { val password = stringResource(R.string.password) val passwordDescription = stringResource(R.string.content_description_password) TextField( value = passwordState.value, - modifier = Modifier + modifier = + Modifier .semantics { contentDescription = passwordDescription }, onValueChange = { passwordState.value = it @@ -204,7 +206,8 @@ internal fun ForgotPasswordText(modifier: Modifier = Modifier) { Text( text = forgotPassword, style = MaterialTheme.typography.bodyMedium, - modifier = Modifier + modifier = + Modifier .padding(top = SPACING_LARGE.dp) .semantics { contentDescription = forgotPassword }, ) @@ -223,7 +226,8 @@ internal fun LoginButton( onClick = { login(accountState.value, passwordState.value) }, - modifier = Modifier + modifier = + Modifier .padding(top = SPACING_SMALL.dp) .semantics { contentDescription = loginTextDescription }, ) { diff --git a/feature/login/src/main/java/com/wei/amazingtalker/feature/login/login/LoginViewModel.kt b/feature/login/src/main/java/com/wei/amazingtalker/feature/login/login/LoginViewModel.kt index e0b777f1..6993e717 100644 --- a/feature/login/src/main/java/com/wei/amazingtalker/feature/login/login/LoginViewModel.kt +++ b/feature/login/src/main/java/com/wei/amazingtalker/feature/login/login/LoginViewModel.kt @@ -12,14 +12,18 @@ import java.time.OffsetDateTime import javax.inject.Inject @HiltViewModel -class LoginViewModel @Inject constructor( +class LoginViewModel +@Inject +constructor( private val userDataRepository: UserDataRepository, ) : BaseViewModel< LoginViewAction, LoginViewState, >(LoginViewState()) { - - private fun login(account: String, password: String) { + private fun login( + account: String, + password: String, + ) { // TODO 替換至 login API viewModelScope.launch { if (TEST_ACCOUNT == account && TEST_PASSWORD == password) { diff --git a/feature/login/src/main/java/com/wei/amazingtalker/feature/login/login/navigation/LoginNavigation.kt b/feature/login/src/main/java/com/wei/amazingtalker/feature/login/login/navigation/LoginNavigation.kt index 38e9936e..ce949a29 100644 --- a/feature/login/src/main/java/com/wei/amazingtalker/feature/login/login/navigation/LoginNavigation.kt +++ b/feature/login/src/main/java/com/wei/amazingtalker/feature/login/login/navigation/LoginNavigation.kt @@ -5,17 +5,15 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import com.wei.amazingtalker.feature.login.login.LoginRoute -const val loginRoute = "login_route" +const val LOGIN_ROUTE = "login_route" fun NavController.navigateToLogin() { this.popBackStack() - this.navigate(loginRoute) + this.navigate(LOGIN_ROUTE) } -fun NavGraphBuilder.loginScreen( - onLoginNav: () -> Unit, -) { - composable(route = loginRoute) { +fun NavGraphBuilder.loginScreen(onLoginNav: () -> Unit) { + composable(route = LOGIN_ROUTE) { LoginRoute( onLoginNav = onLoginNav, ) diff --git a/feature/login/src/main/java/com/wei/amazingtalker/feature/login/welcome/WelcomeScreen.kt b/feature/login/src/main/java/com/wei/amazingtalker/feature/login/welcome/WelcomeScreen.kt index 3be775b3..07479074 100644 --- a/feature/login/src/main/java/com/wei/amazingtalker/feature/login/welcome/WelcomeScreen.kt +++ b/feature/login/src/main/java/com/wei/amazingtalker/feature/login/welcome/WelcomeScreen.kt @@ -102,9 +102,10 @@ internal fun WelcomeRoute( ) } -val NotoSansFontFamily = FontFamily( - Font(DesignsystemR.font.noto_sans_tc_variablefont_wght), -) +val NotoSansFontFamily = + FontFamily( + Font(DesignsystemR.font.noto_sans_tc_variablefont_wght), + ) @Composable internal fun WelcomeScreen( @@ -116,7 +117,8 @@ internal fun WelcomeScreen( ) { Surface(modifier = Modifier.fillMaxSize()) { Column( - modifier = Modifier + modifier = + Modifier .fillMaxSize(), ) { if (withTopSpacer) { @@ -124,7 +126,8 @@ internal fun WelcomeScreen( } WelcomeScreenToolbar( - modifier = if (isPortrait) { + modifier = + if (isPortrait) { Modifier.padding(horizontal = SPACING_LARGE.dp) } else { Modifier.padding( @@ -210,7 +213,8 @@ fun WelcomeGraphics( Image( painter = painter, contentDescription = null, - modifier = modifier + modifier = + modifier .fillMaxSize() .testTag(stringResource(R.string.tag_welcome_graphics)), contentScale = ContentScale.Crop, @@ -223,11 +227,12 @@ fun WelcomeContent( modifier: Modifier = Modifier, isPortrait: Boolean, ) { - val style = TextStyle( - fontFamily = NotoSansFontFamily, - fontSize = MaterialTheme.typography.displaySmall.fontSize, - fontWeight = FontWeight.Bold, - ) + val style = + TextStyle( + fontFamily = NotoSansFontFamily, + fontSize = MaterialTheme.typography.displaySmall.fontSize, + fontWeight = FontWeight.Bold, + ) if (isPortrait) { WelcomeTitlePortrait(style = style) @@ -244,12 +249,14 @@ fun WelcomeTitlePortrait( val welcomeTitle = stringResource(R.string.welcome_title) Box( - modifier = modifier + modifier = + modifier .fillMaxWidth() .gradientBackgroundPortrait(), ) { Text( - modifier = Modifier + modifier = + Modifier .padding(vertical = SPACING_EXTRA_LARGE.dp) .semantics { contentDescription = welcomeTitle } .align(alignment = Alignment.Center), @@ -269,12 +276,14 @@ fun WelcomeTitleLandscape( val welcomeTitle = stringResource(R.string.welcome_title) Box( - modifier = modifier + modifier = + modifier .fillMaxHeight() .gradientBackgroundLandscape(), ) { Text( - modifier = Modifier + modifier = + Modifier .padding(horizontal = SPACING_EXTRA_LARGE.dp) .semantics { contentDescription = welcomeTitle } .align(alignment = Alignment.Center), @@ -286,23 +295,29 @@ fun WelcomeTitleLandscape( } } -internal fun Modifier.gradientBackgroundPortrait(): Modifier = this.background( - brush = Brush.verticalGradient( - colors = listOf( - Color.Black.copy(alpha = 0f), - Color.Black.copy(alpha = 0.5f), +internal fun Modifier.gradientBackgroundPortrait(): Modifier = + this.background( + brush = + Brush.verticalGradient( + colors = + listOf( + Color.Black.copy(alpha = 0f), + Color.Black.copy(alpha = 0.5f), + ), ), - ), -) + ) -internal fun Modifier.gradientBackgroundLandscape(): Modifier = this.background( - brush = Brush.horizontalGradient( - colors = listOf( - Color.White.copy(alpha = 0.5f), - Color.White.copy(alpha = 0f), +internal fun Modifier.gradientBackgroundLandscape(): Modifier = + this.background( + brush = + Brush.horizontalGradient( + colors = + listOf( + Color.White.copy(alpha = 0.5f), + Color.White.copy(alpha = 0f), + ), ), - ), -) + ) @DevicePortraitPreviews @Composable diff --git a/feature/login/src/main/java/com/wei/amazingtalker/feature/login/welcome/WelcomeViewModel.kt b/feature/login/src/main/java/com/wei/amazingtalker/feature/login/welcome/WelcomeViewModel.kt index 6b51bcdb..acf32e03 100644 --- a/feature/login/src/main/java/com/wei/amazingtalker/feature/login/welcome/WelcomeViewModel.kt +++ b/feature/login/src/main/java/com/wei/amazingtalker/feature/login/welcome/WelcomeViewModel.kt @@ -7,11 +7,12 @@ import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel -class WelcomeViewModel @Inject constructor() : BaseViewModel< +class WelcomeViewModel +@Inject +constructor() : BaseViewModel< WelcomeViewAction, WelcomeViewState, >(WelcomeViewState()) { - private fun navigateToLogin() { viewModelScope.launch { updateState { diff --git a/feature/login/src/main/java/com/wei/amazingtalker/feature/login/welcome/WelcomeViewState.kt b/feature/login/src/main/java/com/wei/amazingtalker/feature/login/welcome/WelcomeViewState.kt index 1e3f91ff..b9029e22 100644 --- a/feature/login/src/main/java/com/wei/amazingtalker/feature/login/welcome/WelcomeViewState.kt +++ b/feature/login/src/main/java/com/wei/amazingtalker/feature/login/welcome/WelcomeViewState.kt @@ -4,7 +4,7 @@ import com.wei.amazingtalker.core.base.Action import com.wei.amazingtalker.core.base.State sealed class WelcomeViewAction : Action { - object GetStarted : WelcomeViewAction() + data object GetStarted : WelcomeViewAction() } data class WelcomeViewState( diff --git a/feature/login/src/main/java/com/wei/amazingtalker/feature/login/welcome/navigation/WelcomeNavigation.kt b/feature/login/src/main/java/com/wei/amazingtalker/feature/login/welcome/navigation/WelcomeNavigation.kt index d66c331b..9117101f 100644 --- a/feature/login/src/main/java/com/wei/amazingtalker/feature/login/welcome/navigation/WelcomeNavigation.kt +++ b/feature/login/src/main/java/com/wei/amazingtalker/feature/login/welcome/navigation/WelcomeNavigation.kt @@ -8,11 +8,11 @@ import androidx.navigation.compose.composable import com.wei.amazingtalker.feature.login.welcome.WelcomeRoute import timber.log.Timber -const val welcomeRoute = "welcome_route" +const val WELCOME_ROUTE = "welcome_route" fun NavController.navigateToWelcome(navOptions: NavOptions? = null) { Timber.d("navigateToWelcome") - this.navigate(welcomeRoute, navOptions) + this.navigate(WELCOME_ROUTE, navOptions) } fun NavGraphBuilder.welcomeGraph( @@ -20,7 +20,7 @@ fun NavGraphBuilder.welcomeGraph( navController: NavHostController, nestedGraphs: NavGraphBuilder.() -> Unit, ) { - composable(route = welcomeRoute) { + composable(route = WELCOME_ROUTE) { WelcomeRoute( isPortrait = isPortrait, navController = navController, diff --git a/feature/teacherschedule/src/androidTest/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ScheduleScreenRobot.kt b/feature/teacherschedule/src/androidTest/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ScheduleScreenRobot.kt index dcbea6c5..a2969487 100644 --- a/feature/teacherschedule/src/androidTest/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ScheduleScreenRobot.kt +++ b/feature/teacherschedule/src/androidTest/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ScheduleScreenRobot.kt @@ -21,11 +21,11 @@ import androidx.compose.ui.unit.height import androidx.test.ext.junit.rules.ActivityScenarioRule import com.google.common.truth.Truth.assertThat import com.wei.amazingtalker.core.designsystem.theme.AtTheme +import com.wei.amazingtalker.core.testing.data.TEST_CURRENT_TIME import com.wei.amazingtalker.core.testing.data.fixedClock import com.wei.amazingtalker.core.testing.data.fixedClockUtc import com.wei.amazingtalker.core.testing.data.groupedTimeSlots import com.wei.amazingtalker.core.testing.data.testAvailableTimeSlot -import com.wei.amazingtalker.core.testing.data.testCurrentTime import com.wei.amazingtalker.core.testing.data.testUnavailableTimeSlot import com.wei.amazingtalker.feature.teacherschedule.R import com.wei.amazingtalker.feature.teacherschedule.schedule.ui.dateFormatter @@ -54,8 +54,9 @@ internal fun scheduleScreenRobot( internal open class ScheduleScreenRobot( private val composeTestRule: AndroidComposeTestRule, ComponentActivity>, ) { - private fun AndroidComposeTestRule<*, *>.stringResource(@StringRes resId: Int) = - ReadOnlyProperty { _, _ -> activity.getString(resId) } + private fun AndroidComposeTestRule<*, *>.stringResource( + @StringRes resId: Int, + ) = ReadOnlyProperty { _, _ -> activity.getString(resId) } // The strings used for matching in these tests private val morningString by composeTestRule.stringResource(R.string.morning) @@ -75,10 +76,11 @@ internal open class ScheduleScreenRobot( private val scheduleToolbarTag by composeTestRule.stringResource(R.string.tag_schedule_toolbar) private val scheduleListTag by composeTestRule.stringResource(R.string.tag_schedule_list) - private val scheduleViewState = ScheduleViewState( - currentClock = fixedClock, - queryClockUtc = fixedClockUtc, - ) + private val scheduleViewState = + ScheduleViewState( + currentClock = fixedClock, + queryClockUtc = fixedClockUtc, + ) private var isPreviousWeekClicked: Boolean = false private var isNextWeekClicked: Boolean = false @@ -149,7 +151,7 @@ internal open class ScheduleScreenRobot( ) } private val yourLocalTimeZone by lazy { - val fixedClock = Clock.fixed(Instant.parse(testCurrentTime), ZoneId.systemDefault()) + val fixedClock = Clock.fixed(Instant.parse(TEST_CURRENT_TIME), ZoneId.systemDefault()) composeTestRule.onNodeWithContentDescription( String.format( yourLocalTimeZoneString, @@ -208,9 +210,7 @@ internal open class ScheduleScreenRobot( ) } - fun setScheduleScreenContent( - uiStates: ScheduleViewState = scheduleViewState, - ) { + fun setScheduleScreenContent(uiStates: ScheduleViewState = scheduleViewState) { composeTestRule.setContent { resetInteractionFlags() AtTheme { diff --git a/feature/teacherschedule/src/androidTest/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ScheduleScreenTest.kt b/feature/teacherschedule/src/androidTest/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ScheduleScreenTest.kt index 89358a76..caf9b6cd 100644 --- a/feature/teacherschedule/src/androidTest/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ScheduleScreenTest.kt +++ b/feature/teacherschedule/src/androidTest/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ScheduleScreenTest.kt @@ -2,7 +2,7 @@ package com.wei.amazingtalker.feature.teacherschedule.schedule import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.createAndroidComposeRule -import com.wei.amazingtalker.core.testing.data.testCurrentTime +import com.wei.amazingtalker.core.testing.data.TEST_CURRENT_TIME import org.junit.Rule import org.junit.Test import java.time.Clock @@ -15,7 +15,6 @@ import java.time.ZoneOffset * UI tests for [ScheduleScreen] composable. */ class ScheduleScreenTest { - /** * 通常我們使用 createComposeRule(),作為 composeTestRule * @@ -90,9 +89,12 @@ class ScheduleScreenTest { fun checkPrevWeekClickNotInvoked_whenWeekStartIsLaterCurrent_afterClick() { scheduleScreenRobot(composeTestRule) { // mock currentTime - val fixedClock = Clock.fixed(Instant.parse(testCurrentTime), ZoneId.systemDefault()) + val fixedClock = Clock.fixed(Instant.parse(TEST_CURRENT_TIME), ZoneId.systemDefault()) val fixedClockUtc = - Clock.fixed(Instant.parse(testCurrentTime).plus(Period.ofWeeks(2)), ZoneOffset.UTC) + Clock.fixed( + Instant.parse(TEST_CURRENT_TIME).plus(Period.ofWeeks(2)), + ZoneOffset.UTC, + ) setScheduleScreenContent( ScheduleViewState( currentClock = fixedClock, @@ -150,8 +152,8 @@ class ScheduleScreenTest { @Test fun checkSuccessElementsExists_whenSuccess() { // mock currentTime - val fixedClock = Clock.fixed(Instant.parse(testCurrentTime), ZoneId.systemDefault()) - val fixedClockUtc = Clock.fixed(Instant.parse(testCurrentTime), ZoneOffset.UTC) + val fixedClock = Clock.fixed(Instant.parse(TEST_CURRENT_TIME), ZoneId.systemDefault()) + val fixedClockUtc = Clock.fixed(Instant.parse(TEST_CURRENT_TIME), ZoneOffset.UTC) scheduleScreenRobot(composeTestRule) { setScheduleScreenContent( ScheduleViewState( @@ -172,8 +174,8 @@ class ScheduleScreenTest { @Test fun checkTimeSlotClickInvoked_afterClickAvailableTimeSlot() { // mock currentTime - val fixedClock = Clock.fixed(Instant.parse(testCurrentTime), ZoneId.systemDefault()) - val fixedClockUtc = Clock.fixed(Instant.parse(testCurrentTime), ZoneOffset.UTC) + val fixedClock = Clock.fixed(Instant.parse(TEST_CURRENT_TIME), ZoneId.systemDefault()) + val fixedClockUtc = Clock.fixed(Instant.parse(TEST_CURRENT_TIME), ZoneOffset.UTC) scheduleScreenRobot(composeTestRule) { setScheduleScreenContent( ScheduleViewState( @@ -191,8 +193,8 @@ class ScheduleScreenTest { @Test fun checkTimeSlotClickNotInvoked_afterClickUnavailableTimeSlot() { // mock currentTime - val fixedClock = Clock.fixed(Instant.parse(testCurrentTime), ZoneId.systemDefault()) - val fixedClockUtc = Clock.fixed(Instant.parse(testCurrentTime), ZoneOffset.UTC) + val fixedClock = Clock.fixed(Instant.parse(TEST_CURRENT_TIME), ZoneId.systemDefault()) + val fixedClockUtc = Clock.fixed(Instant.parse(TEST_CURRENT_TIME), ZoneOffset.UTC) scheduleScreenRobot(composeTestRule) { setScheduleScreenContent( ScheduleViewState( @@ -209,9 +211,10 @@ class ScheduleScreenTest { @Test fun checkLoadingElementsVisibility_whenLoading() { - val loadingUiState = ScheduleViewState( - timeListUiState = TimeListUiState.Loading, - ) + val loadingUiState = + ScheduleViewState( + timeListUiState = TimeListUiState.Loading, + ) scheduleScreenRobot(composeTestRule) { setScheduleScreenContent(uiStates = loadingUiState) @@ -222,9 +225,10 @@ class ScheduleScreenTest { @Test fun checkLoadFailedElementsVisibility_whenLoadFailed() { - val loadFailedUiState = ScheduleViewState( - timeListUiState = TimeListUiState.LoadFailed, - ) + val loadFailedUiState = + ScheduleViewState( + timeListUiState = TimeListUiState.LoadFailed, + ) scheduleScreenRobot(composeTestRule) { setScheduleScreenContent(uiStates = loadFailedUiState) diff --git a/feature/teacherschedule/src/androidTest/java/com/wei/amazingtalker/feature/teacherschedule/scheduledetail/ScheduleDetailScreenRobot.kt b/feature/teacherschedule/src/androidTest/java/com/wei/amazingtalker/feature/teacherschedule/scheduledetail/ScheduleDetailScreenRobot.kt index 32dc0cbf..283986f8 100644 --- a/feature/teacherschedule/src/androidTest/java/com/wei/amazingtalker/feature/teacherschedule/scheduledetail/ScheduleDetailScreenRobot.kt +++ b/feature/teacherschedule/src/androidTest/java/com/wei/amazingtalker/feature/teacherschedule/scheduledetail/ScheduleDetailScreenRobot.kt @@ -34,8 +34,9 @@ internal fun scheduleDetailScreenRobot( internal open class ScheduleDetailScreenRobot( private val composeTestRule: AndroidComposeTestRule, ComponentActivity>, ) { - private fun AndroidComposeTestRule<*, *>.stringResource(@StringRes resId: Int) = - ReadOnlyProperty { _, _ -> activity.getString(resId) } + private fun AndroidComposeTestRule<*, *>.stringResource( + @StringRes resId: Int, + ) = ReadOnlyProperty { _, _ -> activity.getString(resId) } // The strings used for matching in these tests private val startTimeDescription by composeTestRule.stringResource(R.string.content_description_start_time) @@ -92,7 +93,8 @@ internal open class ScheduleDetailScreenRobot( AtTheme { ScheduleDetailScreen( uiStates = uiStates, - onBackClick = { backClicked = true }, // Handle back click + // Handle back click + onBackClick = { backClicked = true }, ) } } @@ -143,7 +145,6 @@ internal fun scheduleDetailBackRobot( internal open class ScheduleDetailBackRobot( private val composeTestRule: AndroidComposeTestRule, ComponentActivity>, ) { - private var isBackClicked: Boolean = false fun setIsBackClicked(backClicked: Boolean) { @@ -156,10 +157,11 @@ internal open class ScheduleDetailBackRobot( } val now: OffsetDateTime = OffsetDateTime.now() -val testUiState = ScheduleDetailViewState( - teacherName = "John Doe", - start = now, - end = now.plusMinutes(30), - state = ScheduleState.AVAILABLE, - duringDayType = DuringDayType.Afternoon, -) +val testUiState = + ScheduleDetailViewState( + teacherName = "John Doe", + start = now, + end = now.plusMinutes(30), + state = ScheduleState.AVAILABLE, + duringDayType = DuringDayType.Afternoon, + ) diff --git a/feature/teacherschedule/src/androidTest/java/com/wei/amazingtalker/feature/teacherschedule/scheduledetail/ScheduleDetailScreenTest.kt b/feature/teacherschedule/src/androidTest/java/com/wei/amazingtalker/feature/teacherschedule/scheduledetail/ScheduleDetailScreenTest.kt index 0c9e4478..67b6315b 100644 --- a/feature/teacherschedule/src/androidTest/java/com/wei/amazingtalker/feature/teacherschedule/scheduledetail/ScheduleDetailScreenTest.kt +++ b/feature/teacherschedule/src/androidTest/java/com/wei/amazingtalker/feature/teacherschedule/scheduledetail/ScheduleDetailScreenTest.kt @@ -9,7 +9,6 @@ import org.junit.Test * UI tests for [ScheduleDetailScreen] composable. */ class ScheduleDetailScreenTest { - /** * 通常我們使用 createComposeRule(),作為 composeTestRule * diff --git a/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/domain/GetTeacherScheduleUseCase.kt b/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/domain/GetTeacherScheduleUseCase.kt index 4e607ec1..84885467 100644 --- a/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/domain/GetTeacherScheduleUseCase.kt +++ b/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/domain/GetTeacherScheduleUseCase.kt @@ -17,11 +17,12 @@ internal val SCHEDULE_STATE_TIME_INTERVAL: TimeInterval = TimeInterval.INTERVAL_ * 用於獲取教師課程表的 use case。它從 [TeacherScheduleRepository] 獲取課程表信息, * 並使用 [IntervalizeScheduleUseCase] 將其分解成區間時段。 */ -class GetTeacherScheduleUseCase @Inject constructor( +class GetTeacherScheduleUseCase +@Inject +constructor( private val teacherScheduleRepository: TeacherScheduleRepository, private val intervalizeScheduleUseCase: IntervalizeScheduleUseCase, ) { - suspend operator fun invoke( teacherName: String, startedAtUtc: String, diff --git a/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ScheduleScreen.kt b/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ScheduleScreen.kt index d576e0c9..56a94117 100644 --- a/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ScheduleScreen.kt +++ b/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ScheduleScreen.kt @@ -173,49 +173,51 @@ internal fun ScheduleScreen( onListScroll: () -> Unit, onTimeSlotClick: (IntervalScheduleTimeSlot) -> Unit, ) { - val toolbarHeightRange = with(LocalDensity.current) { - MinToolbarHeight.roundToPx()..MaxToolbarHeight.roundToPx() - } + val toolbarHeightRange = + with(LocalDensity.current) { + MinToolbarHeight.roundToPx()..MaxToolbarHeight.roundToPx() + } val toolbarState = rememberToolbarState(toolbarHeightRange) val listState = rememberLazyListState() val scope = rememberCoroutineScope() - val nestedScrollConnection = remember { - object : NestedScrollConnection { - override fun onPreScroll( - available: Offset, - source: NestedScrollSource, - ): Offset { - toolbarState.scrollTopLimitReached = - listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset == 0 - toolbarState.scrollOffset = toolbarState.scrollOffset - available.y - return Offset(0f, toolbarState.consumed) - } + val nestedScrollConnection = + remember { + object : NestedScrollConnection { + override fun onPreScroll( + available: Offset, + source: NestedScrollSource, + ): Offset { + toolbarState.scrollTopLimitReached = + listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset == 0 + toolbarState.scrollOffset = toolbarState.scrollOffset - available.y + return Offset(0f, toolbarState.consumed) + } - override suspend fun onPostFling( - consumed: Velocity, - available: Velocity, - ): Velocity { - if (available.y > 0) { - scope.launch { - animateDecay( - initialValue = toolbarState.height + toolbarState.offset, - initialVelocity = available.y, - animationSpec = FloatExponentialDecaySpec(), - ) { value, _ -> - toolbarState.scrollTopLimitReached = - listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset == 0 - toolbarState.scrollOffset = - toolbarState.scrollOffset - (value - (toolbarState.height + toolbarState.offset)) - if (toolbarState.scrollOffset == 0f) scope.coroutineContext.cancelChildren() + override suspend fun onPostFling( + consumed: Velocity, + available: Velocity, + ): Velocity { + if (available.y > 0) { + scope.launch { + animateDecay( + initialValue = toolbarState.height + toolbarState.offset, + initialVelocity = available.y, + animationSpec = FloatExponentialDecaySpec(), + ) { value, _ -> + toolbarState.scrollTopLimitReached = + listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset == 0 + toolbarState.scrollOffset = + toolbarState.scrollOffset - (value - (toolbarState.height + toolbarState.offset)) + if (toolbarState.scrollOffset == 0f) scope.coroutineContext.cancelChildren() + } } } - } - return super.onPostFling(consumed, available) + return super.onPostFling(consumed, available) + } } } - } Column { ScheduleTopAppBar(title = uiStates._currentTeacherName) @@ -225,12 +227,14 @@ internal fun ScheduleScreen( * using the nestedScroll modifier. */ Box( - modifier = Modifier + modifier = + Modifier .fillMaxSize() .nestedScroll(nestedScrollConnection), ) { ScheduleList( - modifier = Modifier + modifier = + Modifier .background(MaterialTheme.colorScheme.background) .fillMaxSize() .graphicsLayer { translationY = toolbarState.height + toolbarState.offset } @@ -249,7 +253,8 @@ internal fun ScheduleScreen( ) ScheduleToolbar( progress = toolbarState.progress, - modifier = Modifier + modifier = + Modifier .fillMaxWidth() .height(with(LocalDensity.current) { toolbarState.height.toDp() }) .graphicsLayer { translationY = toolbarState.offset }, @@ -271,14 +276,16 @@ private fun ScheduleTopAppBar(title: String) { title = { Text( text = title, - modifier = Modifier + modifier = + Modifier .testTag(stringResource(id = R.string.tag_schedule_top_app_bar)) .semantics { contentDescription = title }, ) }, navigationIcon = { }, actions = { }, - colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + colors = + TopAppBarDefaults.centerAlignedTopAppBarColors( containerColor = Color.Transparent, ), ) @@ -337,28 +344,32 @@ internal fun ScheduleList( } } - is TimeListUiState.Loading -> item { - val loading = stringResource(R.string.loading) - Text( - text = loading, - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier - .padding(SPACING_LARGE.dp) - .semantics { contentDescription = loading }, - ) - } + is TimeListUiState.Loading -> + item { + val loading = stringResource(R.string.loading) + Text( + text = loading, + style = MaterialTheme.typography.bodyLarge, + modifier = + Modifier + .padding(SPACING_LARGE.dp) + .semantics { contentDescription = loading }, + ) + } - is TimeListUiState.LoadFailed -> item { - val loadFailed = stringResource(R.string.load_failed) - Text( - text = stringResource(R.string.load_failed), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.error, - modifier = Modifier - .padding(SPACING_LARGE.dp) - .semantics { contentDescription = loadFailed }, - ) - } + is TimeListUiState.LoadFailed -> + item { + val loadFailed = stringResource(R.string.load_failed) + Text( + text = stringResource(R.string.load_failed), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.error, + modifier = + Modifier + .padding(SPACING_LARGE.dp) + .semantics { contentDescription = loadFailed }, + ) + } } if (withBottomSpacer) { @@ -414,13 +425,15 @@ fun WeekActionBar( Box( contentAlignment = Alignment.Center, - modifier = Modifier + modifier = + Modifier .height(actionBarSizeDp.dp) .fillMaxWidth() .background(MaterialTheme.colorScheme.surfaceVariant), ) { Row( - modifier = Modifier + modifier = + Modifier .fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, @@ -437,7 +450,8 @@ fun WeekActionBar( Icon( imageVector = AtIcons.ArrowBackIosNew, contentDescription = null, - tint = if (uiStates.isAvailablePreviousWeek) { + tint = + if (uiStates.isAvailablePreviousWeek) { MaterialTheme.colorScheme.primary } else { LocalContentColor.current @@ -446,13 +460,15 @@ fun WeekActionBar( } val (weekStart, weekEnd) = uiStates.weekDateText - val weekDataDescription = stringResource(R.string.content_description_week_date).format( - weekStart, - weekEnd, - ) + val weekDataDescription = + stringResource(R.string.content_description_week_date).format( + weekStart, + weekEnd, + ) val weekDateText = "$weekStart - $weekEnd" TextButton( - modifier = Modifier + modifier = + Modifier .weight(1f) .semantics { contentDescription = weekDataDescription }, onClick = { @@ -492,7 +508,8 @@ private fun WeekActionBarBottom( ) { Box( contentAlignment = Alignment.Center, - modifier = Modifier + modifier = + Modifier .fillMaxSize() .padding(horizontal = SPACING_LARGE.dp), ) { @@ -514,7 +531,8 @@ private fun WeekActionBarBottom( } enum class ToolbarStatus { - Hidden, Visible + Hidden, + Visible, } @Composable @@ -523,11 +541,12 @@ fun AnimateToolbarOffset( listState: LazyListState, toolbarHeightRange: IntRange, ) { - val toolbarStatus = remember { - derivedStateOf { - deriveToolbarStatus(topAppBarState.scrollOffset, toolbarHeightRange) + val toolbarStatus = + remember { + derivedStateOf { + deriveToolbarStatus(topAppBarState.scrollOffset, toolbarHeightRange) + } } - } val isScrollInProgress = rememberUpdatedState(newValue = listState.isScrollInProgress) LaunchedEffect(topAppBarState, isScrollInProgress.value) { @@ -545,7 +564,10 @@ fun AnimateToolbarOffset( } } -private fun deriveToolbarStatus(scrollOffset: Float, toolbarHeightRange: IntRange): ToolbarStatus { +private fun deriveToolbarStatus( + scrollOffset: Float, + toolbarHeightRange: IntRange, +): ToolbarStatus { val largeToolbarHalf = toolbarHeightRange.last / 2f return when { @@ -554,7 +576,10 @@ private fun deriveToolbarStatus(scrollOffset: Float, toolbarHeightRange: IntRang } } -private suspend fun animateTo(topAppBarState: TopAppBarState, targetValue: Float) { +private suspend fun animateTo( + topAppBarState: TopAppBarState, + targetValue: Float, +) { animate( initialValue = topAppBarState.scrollOffset, targetValue = targetValue, @@ -596,7 +621,8 @@ fun ScheduleListPreview( ) { AtTheme { ScheduleList( - modifier = Modifier + modifier = + Modifier .background(MaterialTheme.colorScheme.background) .fillMaxSize(), timeListUiState = timeListUiState, diff --git a/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ScheduleViewModel.kt b/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ScheduleViewModel.kt index 9aef8c69..d6a1a5b6 100644 --- a/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ScheduleViewModel.kt +++ b/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ScheduleViewModel.kt @@ -27,7 +27,9 @@ import java.time.OffsetDateTime import javax.inject.Inject @HiltViewModel -class ScheduleViewModel @Inject constructor( +class ScheduleViewModel +@Inject +constructor( @Clocks(AtClocks.DefaultClock) private val clock: Clock, @Clocks(AtClocks.UtcClock) private val clockUtc: Clock, private val getTeacherScheduleUseCase: GetTeacherScheduleUseCase, @@ -42,8 +44,7 @@ class ScheduleViewModel @Inject constructor( queryClockUtc = clockUtc, ), ) { - - private val _scheduleTimeList = + private val scheduleTimeList = MutableStateFlow>>(DataSourceResult.Loading) private var getScheduleJob: Job? = null @@ -59,11 +60,15 @@ class ScheduleViewModel @Inject constructor( fetchTeacherSchedule() } - private fun updateWeekData(queryDateLocal: OffsetDateTime, resetToStartOfDay: Boolean) { + private fun updateWeekData( + queryDateLocal: OffsetDateTime, + resetToStartOfDay: Boolean, + ) { updateState { copy( selectedIndex = 0, - _queryDateUtc = weekDataHelper.getQueryDateUtc( + _queryDateUtc = + weekDataHelper.getQueryDateUtc( queryDateLocal = queryDateLocal, resetToStartOfDay = resetToStartOfDay, ), @@ -74,31 +79,35 @@ class ScheduleViewModel @Inject constructor( private fun fetchTeacherSchedule() { getScheduleJob?.cancel() - getScheduleJob = viewModelScope.launch { - getTeacherScheduleUseCase( - teacherName = states.value._currentTeacherName, - startedAtUtc = states.value._queryDateUtc.toString(), - ).stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = DataSourceResult.Loading, - ).collect { result -> - _scheduleTimeList.value = result - filterTimeListByDate( - _scheduleTimeList.value, - states.value.dateTabs[states.value.selectedIndex], - ) + getScheduleJob = + viewModelScope.launch { + getTeacherScheduleUseCase( + teacherName = states.value._currentTeacherName, + startedAtUtc = states.value._queryDateUtc.toString(), + ).stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = DataSourceResult.Loading, + ).collect { result -> + scheduleTimeList.value = result + filterTimeListByDate( + scheduleTimeList.value, + states.value.dateTabs[states.value.selectedIndex], + ) + } } - } } - private fun onTabSelected(position: Int, date: OffsetDateTime) { + private fun onTabSelected( + position: Int, + date: OffsetDateTime, + ) { Timber.d("onTabSelected $date $position") updateState { copy(selectedIndex = position) } filterTimeListByDate( - _scheduleTimeList.value, + scheduleTimeList.value, states.value.dateTabs[states.value.selectedIndex], ) } @@ -110,11 +119,12 @@ class ScheduleViewModel @Inject constructor( ) { when (result) { is DataSourceResult.Success -> { - val groupedTimeSlots = result.data - .filter { item -> - item.start.dayOfYear == date.dayOfYear - } - .groupBy { it.duringDayType } + val groupedTimeSlots = + result.data + .filter { item -> + item.start.dayOfYear == date.dayOfYear + } + .groupBy { it.duringDayType } updateState { Timber.d("filterTimeListByDate Success $date \n $groupedTimeSlots") @@ -188,7 +198,8 @@ class ScheduleViewModel @Inject constructor( } else { snackbarManager.showMessage( state = snackbarState, - uiText = UiText.StringResource( + uiText = + UiText.StringResource( resId, message.map { UiText.StringResource.Args.DynamicString(it) diff --git a/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ScheduleViewState.kt b/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ScheduleViewState.kt index 35696788..5fa81f19 100644 --- a/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ScheduleViewState.kt +++ b/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ScheduleViewState.kt @@ -13,7 +13,8 @@ import java.time.OffsetDateTime import java.time.ZoneOffset enum class WeekAction { - PREVIOUS_WEEK, NEXT_WEEK + PREVIOUS_WEEK, + NEXT_WEEK, } sealed class ScheduleViewAction : Action { @@ -23,8 +24,10 @@ sealed class ScheduleViewAction : Action { ) : ScheduleViewAction() data class UpdateWeek(val weekAction: WeekAction) : ScheduleViewAction() + data class SelectedTab(val date: Pair) : ScheduleViewAction() - object ListScrolled : ScheduleViewAction() + + data object ListScrolled : ScheduleViewAction() } data class ScheduleViewState( @@ -51,7 +54,10 @@ data class ScheduleViewState( } sealed interface TimeListUiState { - data class Success(val groupedTimeSlots: Map>) : TimeListUiState + data class Success(val groupedTimeSlots: Map>) : + TimeListUiState + object LoadFailed : TimeListUiState + object Loading : TimeListUiState } diff --git a/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/schedule/navigation/ScheduleNavigation.kt b/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/schedule/navigation/ScheduleNavigation.kt index 50e675cb..20575f95 100644 --- a/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/schedule/navigation/ScheduleNavigation.kt +++ b/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/schedule/navigation/ScheduleNavigation.kt @@ -6,10 +6,10 @@ import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.wei.amazingtalker.feature.teacherschedule.schedule.ScheduleRoute -const val scheduleRoute = "schedule_route" +const val SCHEDULE_ROUTE = "schedule_route" fun NavController.navigateToSchedule(navOptions: NavOptions? = null) { - this.navigate(scheduleRoute, navOptions) + this.navigate(SCHEDULE_ROUTE, navOptions) } fun NavGraphBuilder.scheduleGraph( @@ -17,7 +17,7 @@ fun NavGraphBuilder.scheduleGraph( tokenInvalidNavigate: () -> Unit, nestedGraphs: NavGraphBuilder.() -> Unit, ) { - composable(route = scheduleRoute) { + composable(route = SCHEDULE_ROUTE) { ScheduleRoute( navController = navController, tokenInvalidNavigate = tokenInvalidNavigate, diff --git a/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ui/DateTabLayout.kt b/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ui/DateTabLayout.kt index 195fa8bc..0b5c5de4 100644 --- a/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ui/DateTabLayout.kt +++ b/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ui/DateTabLayout.kt @@ -86,7 +86,8 @@ private fun DateTab( onClick = { onTabClick(index, tab) }, - modifier = Modifier + modifier = + Modifier .height(70.dp) .width(tabWidth()) .semantics { contentDescription = date }, @@ -112,7 +113,8 @@ fun DateTabLayoutPreview() { DateTabLayout( modifier = Modifier.fillMaxSize(), selectedIndex = 0, - tabs = listOf( + tabs = + listOf( OffsetDateTime.parse("2023-06-26T00:00+08:00"), OffsetDateTime.parse("2023-06-27T00:00+08:00"), OffsetDateTime.parse("2023-06-28T00:00+08:00"), diff --git a/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ui/ScheduleListPreviewParameterProvider.kt b/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ui/ScheduleListPreviewParameterProvider.kt index 8de96d59..b6293e0f 100644 --- a/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ui/ScheduleListPreviewParameterProvider.kt +++ b/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ui/ScheduleListPreviewParameterProvider.kt @@ -9,40 +9,40 @@ import com.wei.amazingtalker.feature.teacherschedule.schedule.ui.PreviewParamete import java.time.OffsetDateTime class ScheduleListPreviewParameterProvider : PreviewParameterProvider { - - override val values: Sequence = sequenceOf( - TimeListUiState.Success( - groupedTimeSlots = timeSlotList.groupBy { it.duringDayType }, - ), - ) + override val values: Sequence = + sequenceOf( + TimeListUiState.Success( + groupedTimeSlots = timeSlotList.groupBy { it.duringDayType }, + ), + ) } object PreviewParameterData { - - val timeSlotList = listOf( - IntervalScheduleTimeSlot( - start = OffsetDateTime.parse("2023-06-23T02:00+08:00"), - end = OffsetDateTime.parse("2023-06-23T02:30+08:00"), - state = ScheduleState.AVAILABLE, - duringDayType = DuringDayType.Morning, - ), - IntervalScheduleTimeSlot( - start = OffsetDateTime.parse("2023-06-23T16:30+08:00"), - end = OffsetDateTime.parse("2023-06-23T17:00+08:00"), - state = ScheduleState.AVAILABLE, - duringDayType = DuringDayType.Afternoon, - ), - IntervalScheduleTimeSlot( - start = OffsetDateTime.parse("2023-06-23T17:00+08:00"), - end = OffsetDateTime.parse("2023-06-23T17:30+08:00"), - state = ScheduleState.AVAILABLE, - duringDayType = DuringDayType.Afternoon, - ), - IntervalScheduleTimeSlot( - start = OffsetDateTime.parse("2023-06-23T19:00+08:00"), - end = OffsetDateTime.parse("2023-06-23T19:30+08:00"), - state = ScheduleState.BOOKED, - duringDayType = DuringDayType.Evening, - ), - ) + val timeSlotList = + listOf( + IntervalScheduleTimeSlot( + start = OffsetDateTime.parse("2023-06-23T02:00+08:00"), + end = OffsetDateTime.parse("2023-06-23T02:30+08:00"), + state = ScheduleState.AVAILABLE, + duringDayType = DuringDayType.Morning, + ), + IntervalScheduleTimeSlot( + start = OffsetDateTime.parse("2023-06-23T16:30+08:00"), + end = OffsetDateTime.parse("2023-06-23T17:00+08:00"), + state = ScheduleState.AVAILABLE, + duringDayType = DuringDayType.Afternoon, + ), + IntervalScheduleTimeSlot( + start = OffsetDateTime.parse("2023-06-23T17:00+08:00"), + end = OffsetDateTime.parse("2023-06-23T17:30+08:00"), + state = ScheduleState.AVAILABLE, + duringDayType = DuringDayType.Afternoon, + ), + IntervalScheduleTimeSlot( + start = OffsetDateTime.parse("2023-06-23T19:00+08:00"), + end = OffsetDateTime.parse("2023-06-23T19:30+08:00"), + state = ScheduleState.BOOKED, + duringDayType = DuringDayType.Evening, + ), + ) } diff --git a/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ui/TimeList.kt b/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ui/TimeList.kt index 15494e13..b4264893 100644 --- a/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ui/TimeList.kt +++ b/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ui/TimeList.kt @@ -33,17 +33,19 @@ val timeSlotFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("H:mm") @Composable internal fun YourLocalTimeZoneText(clock: Clock = Clock.systemDefaultZone()) { - val yourLocalTimeZone = String.format( - stringResource(R.string.your_local_time_zone), - clock.zone, - yourLocalTimeZoneFormatter.format(OffsetDateTime.now(clock).offset), - ) + val yourLocalTimeZone = + String.format( + stringResource(R.string.your_local_time_zone), + clock.zone, + yourLocalTimeZoneFormatter.format(OffsetDateTime.now(clock).offset), + ) Text( text = yourLocalTimeZone, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodySmall, - modifier = Modifier + modifier = + Modifier .padding(top = SPACING_LARGE.dp) .padding(horizontal = SPACING_LARGE.dp) .semantics { contentDescription = yourLocalTimeZone }, @@ -52,18 +54,20 @@ internal fun YourLocalTimeZoneText(clock: Clock = Clock.systemDefaultZone()) { @Composable internal fun DuringDay(duringDayType: DuringDayType) { - val duringDay = when (duringDayType) { - DuringDayType.Morning -> stringResource(R.string.morning) - DuringDayType.Afternoon -> stringResource(R.string.afternoon) - DuringDayType.Evening -> stringResource(R.string.evening) - else -> stringResource(R.string.morning) - } + val duringDay = + when (duringDayType) { + DuringDayType.Morning -> stringResource(R.string.morning) + DuringDayType.Afternoon -> stringResource(R.string.afternoon) + DuringDayType.Evening -> stringResource(R.string.evening) + else -> stringResource(R.string.morning) + } Text( text = duringDay, color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.bodyMedium, - modifier = Modifier + modifier = + Modifier .padding(top = SPACING_LARGE.dp) .padding(horizontal = SPACING_LARGE.dp) .semantics { contentDescription = duringDay }, @@ -102,7 +106,8 @@ private fun AvailableTimeSlot( Button( onClick = { onTimeSlotClick() }, - modifier = modifier + modifier = + modifier .padding(top = SPACING_LARGE.dp) .padding(horizontal = SPACING_LARGE.dp) .fillMaxWidth(0.5f) @@ -131,13 +136,15 @@ private fun UnavailableTimeSlot( OutlinedButton( onClick = {}, enabled = false, - modifier = modifier + modifier = + modifier .padding(top = SPACING_LARGE.dp) .padding(horizontal = SPACING_LARGE.dp) .fillMaxWidth(0.5f) .semantics { contentDescription = unavailableDescription }, shape = shapes.medium, - colors = ButtonDefaults.outlinedButtonColors( + colors = + ButtonDefaults.outlinedButtonColors( containerColor = MaterialTheme.colorScheme.background, contentColor = MaterialTheme.colorScheme.onBackground, ), @@ -173,7 +180,8 @@ fun AvailableTimeSlotPreview() { AtTheme { Box(modifier = Modifier.fillMaxWidth()) { AvailableTimeSlot( - timeSlot = IntervalScheduleTimeSlot( + timeSlot = + IntervalScheduleTimeSlot( start = OffsetDateTime.now(), end = OffsetDateTime.now(), state = ScheduleState.AVAILABLE, @@ -191,7 +199,8 @@ fun UnavailableTimeSlotPreview() { AtTheme { Box(modifier = Modifier.fillMaxWidth()) { UnavailableTimeSlot( - timeSlot = IntervalScheduleTimeSlot( + timeSlot = + IntervalScheduleTimeSlot( start = OffsetDateTime.now(), end = OffsetDateTime.now(), state = ScheduleState.BOOKED, diff --git a/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/scheduledetail/ScheduleDetailScreen.kt b/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/scheduledetail/ScheduleDetailScreen.kt index 4c8f68c7..cd181caf 100644 --- a/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/scheduledetail/ScheduleDetailScreen.kt +++ b/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/scheduledetail/ScheduleDetailScreen.kt @@ -108,7 +108,8 @@ internal fun ScheduleDetailScreen( Text( text = teacherName, style = MaterialTheme.typography.headlineLarge, - modifier = Modifier + modifier = + Modifier .padding(horizontal = SPACING_LARGE.dp) .testTag(stringResource(id = R.string.tag_teacher_name)) .semantics { contentDescription = teacherNameDescription }, @@ -119,7 +120,8 @@ internal fun ScheduleDetailScreen( Text( text = startTimeDescription, style = MaterialTheme.typography.bodyMedium, - modifier = Modifier + modifier = + Modifier .padding(horizontal = SPACING_LARGE.dp) .padding(top = SPACING_MEDIUM.dp) .testTag(stringResource(id = R.string.tag_start_time)) @@ -131,7 +133,8 @@ internal fun ScheduleDetailScreen( Text( text = endTimeDescription, style = MaterialTheme.typography.bodyMedium, - modifier = Modifier + modifier = + Modifier .padding(horizontal = SPACING_LARGE.dp) .padding(top = SPACING_MEDIUM.dp) .testTag(stringResource(id = R.string.tag_end_time)) @@ -143,7 +146,8 @@ internal fun ScheduleDetailScreen( Text( text = state, style = MaterialTheme.typography.bodyMedium, - modifier = Modifier + modifier = + Modifier .padding(horizontal = SPACING_LARGE.dp) .padding(top = SPACING_MEDIUM.dp) .testTag(stringResource(id = R.string.tag_state)) @@ -156,7 +160,8 @@ internal fun ScheduleDetailScreen( Text( text = duringDayType, style = MaterialTheme.typography.bodyMedium, - modifier = Modifier + modifier = + Modifier .padding(horizontal = SPACING_LARGE.dp) .padding(top = SPACING_MEDIUM.dp) .testTag(stringResource(id = R.string.tag_during_day_type)) @@ -195,7 +200,8 @@ fun ScheduleDetailScreenPreview() { AtTheme { ScheduleDetailScreen( - uiStates = ScheduleDetailViewState( + uiStates = + ScheduleDetailViewState( teacherName = "Teacher Name", start = nowTime, end = nowTime.plusMinutes(30), diff --git a/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/scheduledetail/ScheduleDetailViewModel.kt b/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/scheduledetail/ScheduleDetailViewModel.kt index 2b42ceb1..b7aa7805 100644 --- a/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/scheduledetail/ScheduleDetailViewModel.kt +++ b/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/scheduledetail/ScheduleDetailViewModel.kt @@ -6,11 +6,12 @@ import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @HiltViewModel -class ScheduleDetailViewModel @Inject constructor() : BaseViewModel< +class ScheduleDetailViewModel +@Inject +constructor() : BaseViewModel< ScheduleDetailViewAction, ScheduleDetailViewState, >(ScheduleDetailViewState()) { - private fun initNavData( teacherName: String, intervalScheduleTimeSlot: IntervalScheduleTimeSlot, @@ -28,10 +29,11 @@ class ScheduleDetailViewModel @Inject constructor() : BaseViewModel< override fun dispatch(action: ScheduleDetailViewAction) { when (action) { - is ScheduleDetailViewAction.InitNavData -> initNavData( - teacherName = action.teacherName, - intervalScheduleTimeSlot = action.intervalScheduleTimeSlot, - ) + is ScheduleDetailViewAction.InitNavData -> + initNavData( + teacherName = action.teacherName, + intervalScheduleTimeSlot = action.intervalScheduleTimeSlot, + ) } } } diff --git a/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/scheduledetail/navigation/ScheduleDetailNavigation.kt b/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/scheduledetail/navigation/ScheduleDetailNavigation.kt index 5e91c5c0..e4dd1966 100644 --- a/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/scheduledetail/navigation/ScheduleDetailNavigation.kt +++ b/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/scheduledetail/navigation/ScheduleDetailNavigation.kt @@ -8,9 +8,9 @@ import androidx.navigation.compose.composable import com.wei.amazingtalker.core.model.data.IntervalScheduleTimeSlot import com.wei.amazingtalker.feature.teacherschedule.scheduledetail.ScheduleDetailRoute -const val scheduleDetailRoute = "schedule_detail_route" -const val teacherNameArg = "teacherName" -const val timeSlotArg = "timeSlot" +const val SCHEDULE_DETAIL_ROUTE = "schedule_detail_route" +const val TEACHER_NAME_ARG = "teacherName" +const val TIME_SLOT_ARG = "timeSlot" fun NavController.navigateToScheduleDetail( navOptions: NavOptions? = null, @@ -18,25 +18,25 @@ fun NavController.navigateToScheduleDetail( timeSlot: IntervalScheduleTimeSlot, ) { this.currentBackStackEntry?.savedStateHandle?.set( - key = teacherNameArg, + key = TEACHER_NAME_ARG, value = teacherName, ) this.currentBackStackEntry?.savedStateHandle?.set( - key = timeSlotArg, + key = TIME_SLOT_ARG, value = timeSlot, ) - this.navigate(scheduleDetailRoute, navOptions) + this.navigate(SCHEDULE_DETAIL_ROUTE, navOptions) } fun NavGraphBuilder.scheduleDetailScreen(navController: NavHostController) { - composable(route = scheduleDetailRoute) { + composable(route = SCHEDULE_DETAIL_ROUTE) { val teacherName = navController.previousBackStackEntry?.savedStateHandle?.get( - teacherNameArg, + TEACHER_NAME_ARG, ) ?: "" val timeSlot = navController.previousBackStackEntry?.savedStateHandle?.get( - timeSlotArg, + TIME_SLOT_ARG, ) if (teacherName.isNotBlank() && timeSlot != null) { diff --git a/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/utilities/WeekDataHelper.kt b/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/utilities/WeekDataHelper.kt index a0ac1c8e..26fb8bf5 100644 --- a/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/utilities/WeekDataHelper.kt +++ b/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/utilities/WeekDataHelper.kt @@ -9,8 +9,13 @@ import javax.inject.Inject /** * 輔助處理關於週數據的類別。 */ -class WeekDataHelper @Inject constructor() { - fun getQueryDateUtc(queryDateLocal: OffsetDateTime, resetToStartOfDay: Boolean): OffsetDateTime { +class WeekDataHelper +@Inject +constructor() { + fun getQueryDateUtc( + queryDateLocal: OffsetDateTime, + resetToStartOfDay: Boolean, + ): OffsetDateTime { return if (resetToStartOfDay) { queryDateLocal .withHour(0) @@ -35,7 +40,10 @@ class WeekDataHelper @Inject constructor() { return localTime.plusDays(betweenWeekSunday.toLong()) } - fun getWeekDateText(weekStart: OffsetDateTime, weekEnd: OffsetDateTime): Pair { + fun getWeekDateText( + weekStart: OffsetDateTime, + weekEnd: OffsetDateTime, + ): Pair { val weekStartFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") val weekEndFormatter = DateTimeFormatter.ofPattern("MM-dd") return Pair(weekStartFormatter.format(weekStart), weekEndFormatter.format(weekEnd)) diff --git a/feature/teacherschedule/src/test/java/com/wei/amazingtalker/feature/teacherschedule/domain/GetTeacherScheduleUseCaseTest.kt b/feature/teacherschedule/src/test/java/com/wei/amazingtalker/feature/teacherschedule/domain/GetTeacherScheduleUseCaseTest.kt index 2378457b..6301f8ce 100644 --- a/feature/teacherschedule/src/test/java/com/wei/amazingtalker/feature/teacherschedule/domain/GetTeacherScheduleUseCaseTest.kt +++ b/feature/teacherschedule/src/test/java/com/wei/amazingtalker/feature/teacherschedule/domain/GetTeacherScheduleUseCaseTest.kt @@ -29,7 +29,6 @@ import org.junit.Test */ @OptIn(ExperimentalCoroutinesApi::class) class GetTeacherScheduleUseCaseTest { - // 使用 TestTeacherScheduleRepository private lateinit var testTeacherScheduleRepo: TestTeacherScheduleRepository private lateinit var intervalizeScheduleUseCase: IntervalizeScheduleUseCase @@ -42,86 +41,97 @@ class GetTeacherScheduleUseCaseTest { fun setUp() { testTeacherScheduleRepo = TestTeacherScheduleRepository() intervalizeScheduleUseCase = IntervalizeScheduleUseCase() - getTeacherScheduleUseCase = GetTeacherScheduleUseCase( - teacherScheduleRepository = testTeacherScheduleRepo, - intervalizeScheduleUseCase = intervalizeScheduleUseCase, - ) + getTeacherScheduleUseCase = + GetTeacherScheduleUseCase( + teacherScheduleRepository = testTeacherScheduleRepo, + intervalizeScheduleUseCase = intervalizeScheduleUseCase, + ) } @Test - fun `getTeacherScheduleUseCase should return sorted interval schedule`() = runTest { - // Arrange - val scheduleStateTimeInterval = SCHEDULE_STATE_TIME_INTERVAL + fun `getTeacherScheduleUseCase should return sorted interval schedule`() = + runTest { + // Arrange + val scheduleStateTimeInterval = SCHEDULE_STATE_TIME_INTERVAL - /** - * 使用 testSchedules.copy() 確保了每次設置前置條件時都是對原始數據的深度複製, - * 這確保了每個測試的獨立性,避免了因數據共享而產生的潛在問題。 - */ - val expectedTeacherSchedule = testTeacherSchedule.copy() - val expectedIntervalSchedule = generateExpectedIntervalSchedule(expectedTeacherSchedule, scheduleStateTimeInterval) + /** + * 使用 testSchedules.copy() 確保了每次設置前置條件時都是對原始數據的深度複製, + * 這確保了每個測試的獨立性,避免了因數據共享而產生的潛在問題。 + */ + val expectedTeacherSchedule = testTeacherSchedule.copy() + val expectedIntervalSchedule = + generateExpectedIntervalSchedule(expectedTeacherSchedule, scheduleStateTimeInterval) - // Act - testTeacherScheduleRepo.sendTeacherSchedule(testTeacherSchedule) - val resultFlow = getTeacherScheduleUseCase("testTeacherName", "testStartedAtUtc").take(2) + // Act + testTeacherScheduleRepo.sendTeacherSchedule(testTeacherSchedule) + val resultFlow = + getTeacherScheduleUseCase("TEST_TEACHER_NAME", "testStartedAtUtc").take(2) - // Assert - val results = resultFlow.toList() - assertThat(results[0]).isInstanceOf(DataSourceResult.Loading::class.java) - val result = results[1] as DataSourceResult.Success - assertThat(result.data).isEqualTo(expectedIntervalSchedule) - } + // Assert + val results = resultFlow.toList() + assertThat(results[0]).isInstanceOf(DataSourceResult.Loading::class.java) + val result = results[1] as DataSourceResult.Success + assertThat(result.data).isEqualTo(expectedIntervalSchedule) + } @Test - fun `getTeacherScheduleUseCase should return error when repository returns error`() = runTest { - // Act - testTeacherScheduleRepo.causeError() - val resultFlow = getTeacherScheduleUseCase("testTeacherName", "testStartedAtUtc").take(2) + fun `getTeacherScheduleUseCase should return error when repository returns error`() = + runTest { + // Act + testTeacherScheduleRepo.causeError() + val resultFlow = + getTeacherScheduleUseCase("TEST_TEACHER_NAME", "testStartedAtUtc").take(2) - // Assert - val results = resultFlow.toList() - assertThat(results[0]).isInstanceOf(DataSourceResult.Loading::class.java) - val result = results[1] as DataSourceResult.Error - assertThat(result.exception!!.message).isEqualTo(TestTeacherScheduleRepository.ErrorExceptionMessage) - } + // Assert + val results = resultFlow.toList() + assertThat(results[0]).isInstanceOf(DataSourceResult.Loading::class.java) + val result = results[1] as DataSourceResult.Error + assertThat(result.exception!!.message).isEqualTo(TestTeacherScheduleRepository.ERROR_EXCEPTION_MESSAGE) + } - private fun generateExpectedIntervalSchedule(expectedTeacherSchedule: TeacherSchedule, scheduleStateTimeInterval: TimeInterval) = - mutableListOf().apply { - addAll( - intervalizeScheduleUseCase( - expectedTeacherSchedule.available, - scheduleStateTimeInterval, - ScheduleState.AVAILABLE, - ), - ) - addAll( - intervalizeScheduleUseCase( - expectedTeacherSchedule.booked, - scheduleStateTimeInterval, - ScheduleState.BOOKED, - ), - ) - }.sortedBy { it.start }.toMutableList() + private fun generateExpectedIntervalSchedule( + expectedTeacherSchedule: TeacherSchedule, + scheduleStateTimeInterval: TimeInterval, + ) = mutableListOf().apply { + addAll( + intervalizeScheduleUseCase( + expectedTeacherSchedule.available, + scheduleStateTimeInterval, + ScheduleState.AVAILABLE, + ), + ) + addAll( + intervalizeScheduleUseCase( + expectedTeacherSchedule.booked, + scheduleStateTimeInterval, + ScheduleState.BOOKED, + ), + ) + }.sortedBy { it.start }.toMutableList() } -private val testTeacherSchedule = TeacherSchedule( - available = listOf( - TimeSlots( - startUtc = "2023-07-31T04:30:00Z", - endUtc = "2023-07-31T09:30:00Z", - ), - TimeSlots( - startUtc = "2023-07-31T13:30:00Z", - endUtc = "2023-07-31T18:30:00Z", - ), - ), - booked = listOf( - TimeSlots( - startUtc = "2023-07-31T09:30:00Z", - endUtc = "2023-07-31T10:00:00Z", +private val testTeacherSchedule = + TeacherSchedule( + available = + listOf( + TimeSlots( + startUtc = "2023-07-31T04:30:00Z", + endUtc = "2023-07-31T09:30:00Z", + ), + TimeSlots( + startUtc = "2023-07-31T13:30:00Z", + endUtc = "2023-07-31T18:30:00Z", + ), ), - TimeSlots( - startUtc = "2023-07-31T11:30:00Z", - endUtc = "2023-07-31T13:30:00Z", + booked = + listOf( + TimeSlots( + startUtc = "2023-07-31T09:30:00Z", + endUtc = "2023-07-31T10:00:00Z", + ), + TimeSlots( + startUtc = "2023-07-31T11:30:00Z", + endUtc = "2023-07-31T13:30:00Z", + ), ), - ), -) + ) diff --git a/feature/teacherschedule/src/test/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ScheduleViewModelTest.kt b/feature/teacherschedule/src/test/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ScheduleViewModelTest.kt index 8d54b04d..b1f2040a 100644 --- a/feature/teacherschedule/src/test/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ScheduleViewModelTest.kt +++ b/feature/teacherschedule/src/test/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ScheduleViewModelTest.kt @@ -38,7 +38,6 @@ import java.time.ZoneOffset * {Arrange}{Act}{Assert} */ class ScheduleViewModelTest { - private lateinit var getTeacherScheduleUseCase: GetTeacherScheduleUseCase private lateinit var testTeacherScheduleRepo: TestTeacherScheduleRepository private lateinit var intervalizeScheduleUseCase: IntervalizeScheduleUseCase @@ -50,8 +49,8 @@ class ScheduleViewModelTest { private lateinit var expectedState: MutableStateFlow // mock currentTime - private val fixedClock = Clock.fixed(Instant.parse(testCurrentTime), ZoneId.systemDefault()) - private val fixedClockUtc = Clock.fixed(Instant.parse(testCurrentTime), ZoneOffset.UTC) + private val fixedClock = Clock.fixed(Instant.parse(TEST_CURRENT_TIME), ZoneId.systemDefault()) + private val fixedClockUtc = Clock.fixed(Instant.parse(TEST_CURRENT_TIME), ZoneOffset.UTC) @get:Rule val mainDispatcherRule = MainDispatcherRule() @@ -60,20 +59,22 @@ class ScheduleViewModelTest { fun setUp() { testTeacherScheduleRepo = TestTeacherScheduleRepository() intervalizeScheduleUseCase = IntervalizeScheduleUseCase() - getTeacherScheduleUseCase = GetTeacherScheduleUseCase( - teacherScheduleRepository = testTeacherScheduleRepo, - intervalizeScheduleUseCase = intervalizeScheduleUseCase, - ) + getTeacherScheduleUseCase = + GetTeacherScheduleUseCase( + teacherScheduleRepository = testTeacherScheduleRepo, + intervalizeScheduleUseCase = intervalizeScheduleUseCase, + ) weekDataHelper = WeekDataHelper() snackbarManager = SnackbarManager() - viewModel = ScheduleViewModel( - clock = fixedClock, - clockUtc = fixedClockUtc, - getTeacherScheduleUseCase = getTeacherScheduleUseCase, - weekDataHelper = weekDataHelper, - snackbarManager = snackbarManager, - ) + viewModel = + ScheduleViewModel( + clock = fixedClock, + clockUtc = fixedClockUtc, + getTeacherScheduleUseCase = getTeacherScheduleUseCase, + weekDataHelper = weekDataHelper, + snackbarManager = snackbarManager, + ) expectedState = MutableStateFlow(viewModel.states.value) } @@ -100,19 +101,21 @@ class ScheduleViewModelTest { runTest { // Arrange val resId = R.string.clickWeekDate - val message = listOf(testTeacherName) - val testUiText = UiText.StringResource( - resId, - message.map { - UiText.StringResource.Args.DynamicString(it) - }.toList(), - ) + val message = listOf(TEST_TEACHER_NAME) + val testUiText = + UiText.StringResource( + resId, + message.map { + UiText.StringResource.Args.DynamicString(it) + }.toList(), + ) // Act - val action = ScheduleViewAction.ShowSnackBar( - resId, - message = message, - ) + val action = + ScheduleViewAction.ShowSnackBar( + resId, + message = message, + ) viewModel.dispatch(action) // Assert @@ -126,19 +129,21 @@ class ScheduleViewModelTest { runTest { // Arrange val resId = R.string.inquirying_teacher_calendar - val message = listOf(testTeacherName, testWeekDateText) - val testUiText = UiText.StringResource( - resId, - message.map { - UiText.StringResource.Args.DynamicString(it) - }.toList(), - ) + val message = listOf(TEST_TEACHER_NAME, TEST_WEEK_DATE_TEXT) + val testUiText = + UiText.StringResource( + resId, + message.map { + UiText.StringResource.Args.DynamicString(it) + }.toList(), + ) // Act - val action = ScheduleViewAction.ShowSnackBar( - resId, - message = message, - ) + val action = + ScheduleViewAction.ShowSnackBar( + resId, + message = message, + ) viewModel.dispatch(action) // Assert @@ -147,6 +152,7 @@ class ScheduleViewModelTest { Truth.assertThat(lastMessage?.uiText).isEqualTo(testUiText) } + @Suppress("ktlint:standard:max-line-length") @Test fun `dispatch updateWeek action should update correct _queryDateUtc and resetToStartOfDay when weekAction is PREVIOUS_WEEK and previousWeekMondayLocalDate is the same as or later than the current local time`() = runTest { @@ -160,7 +166,8 @@ class ScheduleViewModelTest { expectedState.setState { copy( selectedIndex = 0, - _queryDateUtc = weekDataHelper.getQueryDateUtc( + _queryDateUtc = + weekDataHelper.getQueryDateUtc( queryDateLocal = previousWeekMondayLocalDate, resetToStartOfDay = true, ), @@ -171,10 +178,13 @@ class ScheduleViewModelTest { viewModel.dispatch(ScheduleViewAction.UpdateWeek(WeekAction.PREVIOUS_WEEK)) // Assert - Truth.assertThat(viewModel.states.value._queryDateUtc).isEqualTo(expectedState.value._queryDateUtc) - Truth.assertThat(viewModel.states.value.selectedIndex).isEqualTo(expectedState.value.selectedIndex) + Truth.assertThat(viewModel.states.value._queryDateUtc) + .isEqualTo(expectedState.value._queryDateUtc) + Truth.assertThat(viewModel.states.value.selectedIndex) + .isEqualTo(expectedState.value.selectedIndex) } + @Suppress("ktlint:standard:max-line-length") @Test fun `dispatch updateWeek action update correct _queryDateUtc when weekAction is PREVIOUS_WEEK and previousWeekMondayLocalDate is earlier than the current time`() = runTest { @@ -182,7 +192,8 @@ class ScheduleViewModelTest { expectedState.setState { copy( selectedIndex = 0, - _queryDateUtc = weekDataHelper.getQueryDateUtc( + _queryDateUtc = + weekDataHelper.getQueryDateUtc( queryDateLocal = OffsetDateTime.now(fixedClock).getLocalOffsetDateTime(), resetToStartOfDay = false, ), @@ -193,8 +204,10 @@ class ScheduleViewModelTest { viewModel.dispatch(ScheduleViewAction.UpdateWeek(WeekAction.PREVIOUS_WEEK)) // Assert - Truth.assertThat(viewModel.states.value._queryDateUtc).isEqualTo(expectedState.value._queryDateUtc) - Truth.assertThat(viewModel.states.value.selectedIndex).isEqualTo(expectedState.value.selectedIndex) + Truth.assertThat(viewModel.states.value._queryDateUtc) + .isEqualTo(expectedState.value._queryDateUtc) + Truth.assertThat(viewModel.states.value.selectedIndex) + .isEqualTo(expectedState.value.selectedIndex) } @Test @@ -204,7 +217,8 @@ class ScheduleViewModelTest { expectedState.setState { copy( selectedIndex = 0, - _queryDateUtc = weekDataHelper.getQueryDateUtc( + _queryDateUtc = + weekDataHelper.getQueryDateUtc( queryDateLocal = viewModel.states.value.weekStart.plusWeeks(1), resetToStartOfDay = true, ), @@ -215,8 +229,10 @@ class ScheduleViewModelTest { viewModel.dispatch(ScheduleViewAction.UpdateWeek(WeekAction.NEXT_WEEK)) // Assert - Truth.assertThat(viewModel.states.value._queryDateUtc).isEqualTo(expectedState.value._queryDateUtc) - Truth.assertThat(viewModel.states.value.selectedIndex).isEqualTo(expectedState.value.selectedIndex) + Truth.assertThat(viewModel.states.value._queryDateUtc) + .isEqualTo(expectedState.value._queryDateUtc) + Truth.assertThat(viewModel.states.value.selectedIndex) + .isEqualTo(expectedState.value.selectedIndex) } @Test @@ -226,7 +242,8 @@ class ScheduleViewModelTest { expectedState.setState { copy( selectedIndex = 0, - _queryDateUtc = weekDataHelper.getQueryDateUtc( + _queryDateUtc = + weekDataHelper.getQueryDateUtc( queryDateLocal = viewModel.states.value.weekStart.plusWeeks(1), resetToStartOfDay = true, ), @@ -237,8 +254,10 @@ class ScheduleViewModelTest { viewModel.dispatch(ScheduleViewAction.UpdateWeek(WeekAction.NEXT_WEEK)) // Assert - Truth.assertThat(viewModel.states.value._queryDateUtc).isEqualTo(expectedState.value._queryDateUtc) - Truth.assertThat(viewModel.states.value.selectedIndex).isEqualTo(expectedState.value.selectedIndex) + Truth.assertThat(viewModel.states.value._queryDateUtc) + .isEqualTo(expectedState.value._queryDateUtc) + Truth.assertThat(viewModel.states.value.selectedIndex) + .isEqualTo(expectedState.value.selectedIndex) } @Test @@ -249,27 +268,30 @@ class ScheduleViewModelTest { val testDate = OffsetDateTime.parse("2023-08-25T01:00+08:00") val expectedGroupedTimeSlots: Map> = mapOf( - DuringDayType.Morning to listOf( - IntervalScheduleTimeSlot( - start = OffsetDateTime.parse("2023-08-25T01:00+08:00"), - end = OffsetDateTime.parse("2023-08-25T01:30+08:00"), - state = ScheduleState.AVAILABLE, - duringDayType = DuringDayType.Morning, - ), - IntervalScheduleTimeSlot( - start = OffsetDateTime.parse("2023-08-25T01:30+08:00"), - end = OffsetDateTime.parse("2023-08-25T02:00+08:00"), - state = ScheduleState.AVAILABLE, - duringDayType = DuringDayType.Morning, + DuringDayType.Morning to + listOf( + IntervalScheduleTimeSlot( + start = OffsetDateTime.parse("2023-08-25T01:00+08:00"), + end = OffsetDateTime.parse("2023-08-25T01:30+08:00"), + state = ScheduleState.AVAILABLE, + duringDayType = DuringDayType.Morning, + ), + IntervalScheduleTimeSlot( + start = OffsetDateTime.parse("2023-08-25T01:30+08:00"), + end = OffsetDateTime.parse("2023-08-25T02:00+08:00"), + state = ScheduleState.AVAILABLE, + duringDayType = DuringDayType.Morning, + ), ), - ), ) // Act viewModel.filterTimeListByDate(testResult, testDate) // Assert - Truth.assertThat(viewModel.states.value.timeListUiState).isEqualTo(TimeListUiState.Success(groupedTimeSlots = expectedGroupedTimeSlots)) + Truth.assertThat( + viewModel.states.value.timeListUiState, + ).isEqualTo(TimeListUiState.Success(groupedTimeSlots = expectedGroupedTimeSlots)) } @Test @@ -283,7 +305,8 @@ class ScheduleViewModelTest { viewModel.filterTimeListByDate(testResult, testDate) // Assert - Truth.assertThat(viewModel.states.value.timeListUiState).isEqualTo(TimeListUiState.LoadFailed) + Truth.assertThat(viewModel.states.value.timeListUiState) + .isEqualTo(TimeListUiState.LoadFailed) } @Test @@ -297,42 +320,44 @@ class ScheduleViewModelTest { viewModel.filterTimeListByDate(testResult, testDate) // Assert - Truth.assertThat(viewModel.states.value.timeListUiState).isEqualTo(TimeListUiState.Loading) + Truth.assertThat(viewModel.states.value.timeListUiState) + .isEqualTo(TimeListUiState.Loading) } } -const val testTeacherName = TEST_DATA_TEACHER_NAME -const val testWeekDateText = "2023-08-14 - 08-20" -const val testCurrentTime = "2023-08-18T00:00:00.00Z" -private val testTimeSlotList = mutableListOf( - IntervalScheduleTimeSlot( - start = OffsetDateTime.parse("2023-08-19T16:00+08:00"), - end = OffsetDateTime.parse("2023-08-19T16:30+08:00"), - state = ScheduleState.AVAILABLE, - duringDayType = DuringDayType.Afternoon, - ), - IntervalScheduleTimeSlot( - start = OffsetDateTime.parse("2023-08-20T13:30+08:00"), - end = OffsetDateTime.parse("2023-08-20T14:00+08:00"), - state = ScheduleState.AVAILABLE, - duringDayType = DuringDayType.Afternoon, - ), - IntervalScheduleTimeSlot( - start = OffsetDateTime.parse("2023-08-20T14:00+08:00"), - end = OffsetDateTime.parse("2023-08-20T14:30+08:00"), - state = ScheduleState.AVAILABLE, - duringDayType = DuringDayType.Afternoon, - ), - IntervalScheduleTimeSlot( - start = OffsetDateTime.parse("2023-08-25T01:00+08:00"), - end = OffsetDateTime.parse("2023-08-25T01:30+08:00"), - state = ScheduleState.AVAILABLE, - duringDayType = DuringDayType.Morning, - ), - IntervalScheduleTimeSlot( - start = OffsetDateTime.parse("2023-08-25T01:30+08:00"), - end = OffsetDateTime.parse("2023-08-25T02:00+08:00"), - state = ScheduleState.AVAILABLE, - duringDayType = DuringDayType.Morning, - ), -) +const val TEST_TEACHER_NAME = TEST_DATA_TEACHER_NAME +const val TEST_WEEK_DATE_TEXT = "2023-08-14 - 08-20" +const val TEST_CURRENT_TIME = "2023-08-18T00:00:00.00Z" +private val testTimeSlotList = + mutableListOf( + IntervalScheduleTimeSlot( + start = OffsetDateTime.parse("2023-08-19T16:00+08:00"), + end = OffsetDateTime.parse("2023-08-19T16:30+08:00"), + state = ScheduleState.AVAILABLE, + duringDayType = DuringDayType.Afternoon, + ), + IntervalScheduleTimeSlot( + start = OffsetDateTime.parse("2023-08-20T13:30+08:00"), + end = OffsetDateTime.parse("2023-08-20T14:00+08:00"), + state = ScheduleState.AVAILABLE, + duringDayType = DuringDayType.Afternoon, + ), + IntervalScheduleTimeSlot( + start = OffsetDateTime.parse("2023-08-20T14:00+08:00"), + end = OffsetDateTime.parse("2023-08-20T14:30+08:00"), + state = ScheduleState.AVAILABLE, + duringDayType = DuringDayType.Afternoon, + ), + IntervalScheduleTimeSlot( + start = OffsetDateTime.parse("2023-08-25T01:00+08:00"), + end = OffsetDateTime.parse("2023-08-25T01:30+08:00"), + state = ScheduleState.AVAILABLE, + duringDayType = DuringDayType.Morning, + ), + IntervalScheduleTimeSlot( + start = OffsetDateTime.parse("2023-08-25T01:30+08:00"), + end = OffsetDateTime.parse("2023-08-25T02:00+08:00"), + state = ScheduleState.AVAILABLE, + duringDayType = DuringDayType.Morning, + ), + ) diff --git a/feature/teacherschedule/src/test/java/com/wei/amazingtalker/feature/teacherschedule/utilities/WeekDataHelperTest.kt b/feature/teacherschedule/src/test/java/com/wei/amazingtalker/feature/teacherschedule/utilities/WeekDataHelperTest.kt index 3723da82..76a9b0bd 100644 --- a/feature/teacherschedule/src/test/java/com/wei/amazingtalker/feature/teacherschedule/utilities/WeekDataHelperTest.kt +++ b/feature/teacherschedule/src/test/java/com/wei/amazingtalker/feature/teacherschedule/utilities/WeekDataHelperTest.kt @@ -13,7 +13,6 @@ import java.time.ZoneOffset * {Arrange}{Act}{Assert} */ class WeekDataHelperTest { - private lateinit var weekDataHelper: WeekDataHelper @Before @@ -54,7 +53,18 @@ class WeekDataHelperTest { val result = weekDataHelper.getWeekStart(localTime) // Assert - assertThat(result).isEqualTo(OffsetDateTime.of(2023, 7, 31, 10, 30, 0, 0, ZoneOffset.UTC)) // Monday + assertThat(result).isEqualTo( + OffsetDateTime.of( + 2023, + 7, + 31, + 10, + 30, + 0, + 0, + ZoneOffset.UTC, + ), + ) // Monday } @Test @@ -78,7 +88,18 @@ class WeekDataHelperTest { val result = weekDataHelper.getWeekEnd(localTime) // Assert - assertThat(result).isEqualTo(OffsetDateTime.of(2023, 8, 6, 10, 30, 0, 0, ZoneOffset.UTC)) // Sunday + assertThat(result).isEqualTo( + OffsetDateTime.of( + 2023, + 8, + 6, + 10, + 30, + 0, + 0, + ZoneOffset.UTC, + ), + ) // Sunday } @Test diff --git a/gradle/init.gradle.kts b/gradle/init.gradle.kts index 74ce6dd3..c75b66f5 100644 --- a/gradle/init.gradle.kts +++ b/gradle/init.gradle.kts @@ -1,7 +1,7 @@ -val ktlintVersion = "0.48.1" +val ktlintVersion = "1.0.1" initscript { - val spotlessVersion = "6.13.0" + val spotlessVersion = "6.23.3" repositories { mavenCentral() @@ -19,7 +19,11 @@ rootProject { kotlin { target("**/*.kt") targetExclude("**/build/**/*.kt") - ktlint(ktlintVersion).userData(mapOf("android" to "true")) + ktlint(ktlintVersion).editorConfigOverride( + mapOf( + "android" to "true", + ), + ) } format("kts") { target("**/*.kts")