From 821001cfcc294f62ed07d6c6870c1011da275eae Mon Sep 17 00:00:00 2001 From: Takeshi Hagikura Date: Tue, 20 Sep 2022 16:46:18 +0900 Subject: [PATCH 1/4] Introduce Multipreview annotation to simplify the preview configurations - Bump the AGP version to 7.3.0 - Introduce Multipreview annotation class that represents device previews and theme previews Fixes #282 --- .../core/designsystem/component/Background.kt | 22 +++++++------- .../core/ui/MultipreviewAnnotations.kt | 29 +++++++++++++++++++ .../feature/author/AuthorScreen.kt | 11 ++----- .../feature/foryou/ForYouScreen.kt | 16 +++------- .../feature/interests/InterestsScreen.kt | 16 +++------- .../nowinandroid/feature/topic/TopicScreen.kt | 11 ++----- gradle/libs.versions.toml | 2 +- 7 files changed, 56 insertions(+), 51 deletions(-) create mode 100644 core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/MultipreviewAnnotations.kt diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/Background.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/Background.kt index a3af38f23..6525acc0a 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/Background.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/Background.kt @@ -133,8 +133,15 @@ fun NiaGradientBackground( } } +/** + * Multipreview annotation that represents light and dark themes. Add this annotation to a + * composable to render the both themes. + */ @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light theme") @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark theme") +private annotation class ThemePreviews + +@ThemePreviews @Composable fun BackgroundDefault() { NiaTheme { @@ -142,8 +149,7 @@ fun BackgroundDefault() { } } -@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light theme") -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark theme") +@ThemePreviews @Composable fun BackgroundDynamic() { NiaTheme(dynamicColor = true) { @@ -151,8 +157,7 @@ fun BackgroundDynamic() { } } -@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light theme") -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark theme") +@ThemePreviews @Composable fun BackgroundAndroid() { NiaTheme(androidTheme = true) { @@ -160,8 +165,7 @@ fun BackgroundAndroid() { } } -@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light theme") -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark theme") +@ThemePreviews @Composable fun GradientBackgroundDefault() { NiaTheme { @@ -169,8 +173,7 @@ fun GradientBackgroundDefault() { } } -@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light theme") -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark theme") +@ThemePreviews @Composable fun GradientBackgroundDynamic() { NiaTheme(dynamicColor = true) { @@ -178,8 +181,7 @@ fun GradientBackgroundDynamic() { } } -@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light theme") -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark theme") +@ThemePreviews @Composable fun GradientBackgroundAndroid() { NiaTheme(androidTheme = true) { diff --git a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/MultipreviewAnnotations.kt b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/MultipreviewAnnotations.kt new file mode 100644 index 000000000..1daa4a70b --- /dev/null +++ b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/MultipreviewAnnotations.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.core.ui + +import androidx.compose.ui.tooling.preview.Preview + +/** + * Multipreview annotation that represents various device sizes. Add this annotation to a composable + * to render various devices. + */ +@Preview(name = "phone", device = "spec:shape=Normal,width=360,height=640,unit=dp,dpi=480") +@Preview(name = "landscape", device = "spec:shape=Normal,width=640,height=360,unit=dp,dpi=480") +@Preview(name = "foldable", device = "spec:shape=Normal,width=673,height=841,unit=dp,dpi=480") +@Preview(name = "tablet", device = "spec:shape=Normal,width=1280,height=800,unit=dp,dpi=480") +annotation class DevicePreviews diff --git a/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorScreen.kt b/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorScreen.kt index b1f45c9eb..22e704f3f 100644 --- a/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorScreen.kt +++ b/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorScreen.kt @@ -59,6 +59,7 @@ import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor import com.google.samples.apps.nowinandroid.core.model.data.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.model.data.previewAuthors import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources +import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews import com.google.samples.apps.nowinandroid.core.ui.newsResourceCardItems @OptIn(ExperimentalLifecycleComposeApi::class) @@ -227,10 +228,7 @@ private fun AuthorToolbar( } } -@Preview(name = "phone", device = "spec:shape=Normal,width=360,height=640,unit=dp,dpi=480") -@Preview(name = "landscape", device = "spec:shape=Normal,width=640,height=360,unit=dp,dpi=480") -@Preview(name = "foldable", device = "spec:shape=Normal,width=673,height=841,unit=dp,dpi=480") -@Preview(name = "tablet", device = "spec:shape=Normal,width=1280,height=800,unit=dp,dpi=480") +@DevicePreviews @Composable fun AuthorScreenPopulated() { NiaTheme { @@ -253,10 +251,7 @@ fun AuthorScreenPopulated() { } } -@Preview(name = "phone", device = "spec:shape=Normal,width=360,height=640,unit=dp,dpi=480") -@Preview(name = "landscape", device = "spec:shape=Normal,width=640,height=360,unit=dp,dpi=480") -@Preview(name = "foldable", device = "spec:shape=Normal,width=673,height=841,unit=dp,dpi=480") -@Preview(name = "tablet", device = "spec:shape=Normal,width=1280,height=800,unit=dp,dpi=480") +@DevicePreviews @Composable fun AuthorScreenLoading() { NiaTheme { diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt index 27372beb1..014797159 100644 --- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt +++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt @@ -93,6 +93,7 @@ import com.google.samples.apps.nowinandroid.core.model.data.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.model.data.previewAuthors import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources import com.google.samples.apps.nowinandroid.core.model.data.previewTopics +import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState import com.google.samples.apps.nowinandroid.core.ui.TrackScrollJank import com.google.samples.apps.nowinandroid.core.ui.newsFeed @@ -421,10 +422,7 @@ fun TopicIcon( ) } -@Preview(name = "phone", device = "spec:shape=Normal,width=360,height=640,unit=dp,dpi=480") -@Preview(name = "landscape", device = "spec:shape=Normal,width=640,height=360,unit=dp,dpi=480") -@Preview(name = "foldable", device = "spec:shape=Normal,width=673,height=841,unit=dp,dpi=480") -@Preview(name = "tablet", device = "spec:shape=Normal,width=1280,height=800,unit=dp,dpi=480") +@DevicePreviews @Composable fun ForYouScreenPopulatedFeed() { BoxWithConstraints { @@ -445,10 +443,7 @@ fun ForYouScreenPopulatedFeed() { } } -@Preview(name = "phone", device = "spec:shape=Normal,width=360,height=640,unit=dp,dpi=480") -@Preview(name = "landscape", device = "spec:shape=Normal,width=640,height=360,unit=dp,dpi=480") -@Preview(name = "foldable", device = "spec:shape=Normal,width=673,height=841,unit=dp,dpi=480") -@Preview(name = "tablet", device = "spec:shape=Normal,width=1280,height=800,unit=dp,dpi=480") +@DevicePreviews @Composable fun ForYouScreenTopicSelection() { BoxWithConstraints { @@ -472,10 +467,7 @@ fun ForYouScreenTopicSelection() { } } -@Preview(name = "phone", device = "spec:shape=Normal,width=360,height=640,unit=dp,dpi=480") -@Preview(name = "landscape", device = "spec:shape=Normal,width=640,height=360,unit=dp,dpi=480") -@Preview(name = "foldable", device = "spec:shape=Normal,width=673,height=841,unit=dp,dpi=480") -@Preview(name = "tablet", device = "spec:shape=Normal,width=1280,height=800,unit=dp,dpi=480") +@DevicePreviews @Composable fun ForYouScreenLoading() { BoxWithConstraints { diff --git a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt index 06d5c8e9f..6b739e2f8 100644 --- a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt +++ b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt @@ -44,6 +44,7 @@ import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.previewAuthors import com.google.samples.apps.nowinandroid.core.model.data.previewTopics +import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews import com.google.samples.apps.nowinandroid.core.ui.JankMetricDisposableEffect @OptIn(ExperimentalLifecycleComposeApi::class) @@ -174,10 +175,7 @@ private fun InterestsEmptyScreen() { Text(text = stringResource(id = R.string.interests_empty_header)) } -@Preview(name = "phone", device = "spec:shape=Normal,width=360,height=640,unit=dp,dpi=480") -@Preview(name = "landscape", device = "spec:shape=Normal,width=640,height=360,unit=dp,dpi=480") -@Preview(name = "foldable", device = "spec:shape=Normal,width=673,height=841,unit=dp,dpi=480") -@Preview(name = "tablet", device = "spec:shape=Normal,width=1280,height=800,unit=dp,dpi=480") +@DevicePreviews @Composable fun InterestsScreenPopulated() { NiaTheme { @@ -201,10 +199,7 @@ fun InterestsScreenPopulated() { } } -@Preview(name = "phone", device = "spec:shape=Normal,width=360,height=640,unit=dp,dpi=480") -@Preview(name = "landscape", device = "spec:shape=Normal,width=640,height=360,unit=dp,dpi=480") -@Preview(name = "foldable", device = "spec:shape=Normal,width=673,height=841,unit=dp,dpi=480") -@Preview(name = "tablet", device = "spec:shape=Normal,width=1280,height=800,unit=dp,dpi=480") +@DevicePreviews @Composable fun InterestsScreenLoading() { NiaTheme { @@ -225,10 +220,7 @@ fun InterestsScreenLoading() { } } -@Preview(name = "phone", device = "spec:shape=Normal,width=360,height=640,unit=dp,dpi=480") -@Preview(name = "landscape", device = "spec:shape=Normal,width=640,height=360,unit=dp,dpi=480") -@Preview(name = "foldable", device = "spec:shape=Normal,width=673,height=841,unit=dp,dpi=480") -@Preview(name = "tablet", device = "spec:shape=Normal,width=1280,height=800,unit=dp,dpi=480") +@DevicePreviews @Composable fun InterestsScreenEmpty() { NiaTheme { diff --git a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt index 08cd7a8ad..b85fc6817 100644 --- a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt +++ b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt @@ -54,6 +54,7 @@ import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources import com.google.samples.apps.nowinandroid.core.model.data.previewTopics +import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews import com.google.samples.apps.nowinandroid.core.ui.newsResourceCardItems import com.google.samples.apps.nowinandroid.feature.topic.R.string import com.google.samples.apps.nowinandroid.feature.topic.TopicUiState.Loading @@ -228,10 +229,7 @@ private fun TopicToolbar( } } -@Preview(name = "phone", device = "spec:shape=Normal,width=360,height=640,unit=dp,dpi=480") -@Preview(name = "landscape", device = "spec:shape=Normal,width=640,height=360,unit=dp,dpi=480") -@Preview(name = "foldable", device = "spec:shape=Normal,width=673,height=841,unit=dp,dpi=480") -@Preview(name = "tablet", device = "spec:shape=Normal,width=1280,height=800,unit=dp,dpi=480") +@DevicePreviews @Composable fun TopicScreenPopulated() { NiaTheme { @@ -246,10 +244,7 @@ fun TopicScreenPopulated() { } } -@Preview(name = "phone", device = "spec:shape=Normal,width=360,height=640,unit=dp,dpi=480") -@Preview(name = "landscape", device = "spec:shape=Normal,width=640,height=360,unit=dp,dpi=480") -@Preview(name = "foldable", device = "spec:shape=Normal,width=673,height=841,unit=dp,dpi=480") -@Preview(name = "tablet", device = "spec:shape=Normal,width=1280,height=800,unit=dp,dpi=480") +@DevicePreviews @Composable fun TopicScreenLoading() { NiaTheme { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0c09ab117..e61b222fd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] accompanist = "0.24.8-beta" androidDesugarJdkLibs = "1.1.5" -androidGradlePlugin = "7.2.2" +androidGradlePlugin = "7.3.0" androidxActivity = "1.5.1" androidxAppCompat = "1.5.1" androidxCompose = "1.3.0-beta02" From ee27ea6f87b4806430b32a7db0c14c3049fe5d7e Mon Sep 17 00:00:00 2001 From: Takeshi Hagikura Date: Tue, 20 Sep 2022 17:30:49 +0900 Subject: [PATCH 2/4] Fix the lint warnings --- .idea/codeStyles/codeStyleConfig.xml | 2 +- ...previewAnnotations.kt => DevicePreviews.kt} | 18 +++++++++--------- .../feature/author/AuthorScreen.kt | 1 - .../feature/foryou/ForYouScreen.kt | 1 - .../feature/interests/InterestsScreen.kt | 1 - 5 files changed, 10 insertions(+), 13 deletions(-) rename core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/{MultipreviewAnnotations.kt => DevicePreviews.kt} (55%) diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml index 79ee123c2..94e97212b 100644 --- a/.idea/codeStyles/codeStyleConfig.xml +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -1,5 +1,5 @@ - \ No newline at end of file diff --git a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/MultipreviewAnnotations.kt b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/DevicePreviews.kt similarity index 55% rename from core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/MultipreviewAnnotations.kt rename to core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/DevicePreviews.kt index 1daa4a70b..bb2b59466 100644 --- a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/MultipreviewAnnotations.kt +++ b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/DevicePreviews.kt @@ -1,17 +1,17 @@ /* * Copyright 2022 The Android Open Source Project * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package com.google.samples.apps.nowinandroid.core.ui diff --git a/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorScreen.kt b/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorScreen.kt index 22e704f3f..b7c357621 100644 --- a/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorScreen.kt +++ b/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorScreen.kt @@ -44,7 +44,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt index 014797159..c9940bcd7 100644 --- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt +++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt @@ -70,7 +70,6 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.max import androidx.compose.ui.unit.sp diff --git a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt index 6b739e2f8..3742b47ae 100644 --- a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt +++ b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt @@ -29,7 +29,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi import androidx.lifecycle.compose.collectAsStateWithLifecycle From 543f36abe05dc1eb91ddc5e66dea58e499d3bb65 Mon Sep 17 00:00:00 2001 From: Takeshi Hagikura Date: Tue, 20 Sep 2022 17:57:55 +0900 Subject: [PATCH 3/4] Revert the unwanted change for the codeStyleConfig --- .idea/codeStyles/codeStyleConfig.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml index 94e97212b..79ee123c2 100644 --- a/.idea/codeStyles/codeStyleConfig.xml +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -1,5 +1,5 @@ - \ No newline at end of file From 750c43fb05b3038bcc51d11e5398872ccc08e424 Mon Sep 17 00:00:00 2001 From: Don Turner Date: Mon, 19 Sep 2022 21:15:54 +0100 Subject: [PATCH 4/4] Change user data fields to use maps instead of lists. --- .../core/datastore/IntToStringIdsMigration.kt | 8 +- .../core/datastore/ListToMapMigration.kt | 58 +++++++ .../datastore/NiaPreferencesDataSource.kt | 159 ++++++++---------- .../nowinandroid/data/user_preferences.proto | 13 +- .../datastore/IntToStringIdsMigrationTest.kt | 8 +- .../core/datastore/ListToMapMigrationTest.kt | 102 +++++++++++ .../UserPreferencesSerializerTest.kt | 4 +- 7 files changed, 252 insertions(+), 100 deletions(-) create mode 100644 core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/ListToMapMigration.kt create mode 100644 core/datastore/src/test/java/com/google/samples/apps/nowinandroid/core/datastore/ListToMapMigrationTest.kt diff --git a/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/IntToStringIdsMigration.kt b/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/IntToStringIdsMigration.kt index 4e6e49300..cd27bb015 100644 --- a/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/IntToStringIdsMigration.kt +++ b/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/IntToStringIdsMigration.kt @@ -28,15 +28,15 @@ object IntToStringIdsMigration : DataMigration { override suspend fun migrate(currentData: UserPreferences): UserPreferences = currentData.copy { // Migrate topic ids - followedTopicIds.clear() - followedTopicIds.addAll( + deprecatedFollowedTopicIds.clear() + deprecatedFollowedTopicIds.addAll( currentData.deprecatedIntFollowedTopicIdsList.map(Int::toString) ) deprecatedIntFollowedTopicIds.clear() // Migrate author ids - followedAuthorIds.clear() - followedAuthorIds.addAll( + deprecatedFollowedAuthorIds.clear() + deprecatedFollowedAuthorIds.addAll( currentData.deprecatedIntFollowedAuthorIdsList.map(Int::toString) ) deprecatedIntFollowedAuthorIds.clear() diff --git a/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/ListToMapMigration.kt b/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/ListToMapMigration.kt new file mode 100644 index 000000000..b88b27f74 --- /dev/null +++ b/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/ListToMapMigration.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.core.datastore + +import androidx.datastore.core.DataMigration + +/** + * Migrates from using lists to maps for user data. + */ +object ListToMapMigration : DataMigration { + + override suspend fun cleanUp() = Unit + + override suspend fun migrate(currentData: UserPreferences): UserPreferences = + currentData.copy { + // Migrate topic id lists + followedTopicIds.clear() + followedTopicIds.putAll( + currentData.deprecatedFollowedTopicIdsList.associateWith { true } + ) + deprecatedFollowedTopicIds.clear() + + // Migrate author ids + followedAuthorIds.clear() + followedAuthorIds.putAll( + currentData.deprecatedFollowedAuthorIdsList.associateWith { true } + ) + deprecatedFollowedAuthorIds.clear() + + // Migrate bookmarks + bookmarkedNewsResourceIds.clear() + bookmarkedNewsResourceIds.putAll( + currentData.deprecatedBookmarkedNewsResourceIdsList.associateWith { true } + ) + deprecatedBookmarkedNewsResourceIds.clear() + + // Mark migration as complete + hasDoneListToMapMigration = true + } + + override suspend fun shouldMigrate(currentData: UserPreferences): Boolean { + return !currentData.hasDoneListToMapMigration + } +} diff --git a/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt b/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt index 6d7028f0c..28e1df069 100644 --- a/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt +++ b/core/datastore/src/main/java/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt @@ -18,8 +18,6 @@ package com.google.samples.apps.nowinandroid.core.datastore import android.util.Log import androidx.datastore.core.DataStore -import com.google.protobuf.kotlin.DslList -import com.google.protobuf.kotlin.DslProxy import com.google.samples.apps.nowinandroid.core.model.data.UserData import java.io.IOException import javax.inject.Inject @@ -33,54 +31,85 @@ class NiaPreferencesDataSource @Inject constructor( val userDataStream = userPreferences.data .map { UserData( - bookmarkedNewsResources = it.bookmarkedNewsResourceIdsList.toSet(), - followedTopics = it.followedTopicIdsList.toSet(), - followedAuthors = it.followedAuthorIdsList.toSet(), + bookmarkedNewsResources = it.bookmarkedNewsResourceIdsMap.keys, + followedTopics = it.followedTopicIdsMap.keys, + followedAuthors = it.followedAuthorIdsMap.keys, ) } - suspend fun setFollowedTopicIds(followedTopicIds: Set) = - userPreferences.setList( - listGetter = { it.followedTopicIds }, - listModifier = { followedTopicIds.toList() }, - clear = { it.clear() }, - addAll = { dslList, editedList -> dslList.addAll(editedList) } - ) + suspend fun setFollowedTopicIds(topicIds: Set) { + try { + userPreferences.updateData { + it.copy { + followedTopicIds.clear() + followedTopicIds.putAll(topicIds.associateWith { true }) + } + } + } catch (ioException: IOException) { + Log.e("NiaPreferences", "Failed to update user preferences", ioException) + } + } - suspend fun toggleFollowedTopicId(followedTopicId: String, followed: Boolean) = - userPreferences.editList( - add = followed, - value = followedTopicId, - listGetter = { it.followedTopicIds }, - clear = { it.clear() }, - addAll = { dslList, editedList -> dslList.addAll(editedList) } - ) + suspend fun toggleFollowedTopicId(topicId: String, followed: Boolean) { + try { + userPreferences.updateData { + it.copy { + if (followed) { + followedTopicIds.put(topicId, true) + } else { + followedTopicIds.remove(topicId) + } + } + } + } catch (ioException: IOException) { + Log.e("NiaPreferences", "Failed to update user preferences", ioException) + } + } - suspend fun setFollowedAuthorIds(followedAuthorIds: Set) = - userPreferences.setList( - listGetter = { it.followedAuthorIds }, - listModifier = { followedAuthorIds.toList() }, - clear = { it.clear() }, - addAll = { dslList, editedList -> dslList.addAll(editedList) } - ) + suspend fun setFollowedAuthorIds(authorIds: Set) { + try { + userPreferences.updateData { + it.copy { + followedAuthorIds.clear() + followedAuthorIds.putAll(authorIds.associateWith { true }) + } + } + } catch (ioException: IOException) { + Log.e("NiaPreferences", "Failed to update user preferences", ioException) + } + } - suspend fun toggleFollowedAuthorId(followedAuthorId: String, followed: Boolean) = - userPreferences.editList( - add = followed, - value = followedAuthorId, - listGetter = { it.followedAuthorIds }, - clear = { it.clear() }, - addAll = { dslList, editedList -> dslList.addAll(editedList) } - ) + suspend fun toggleFollowedAuthorId(authorId: String, followed: Boolean) { + try { + userPreferences.updateData { + it.copy { + if (followed) { + followedAuthorIds.put(authorId, true) + } else { + followedAuthorIds.remove(authorId) + } + } + } + } catch (ioException: IOException) { + Log.e("NiaPreferences", "Failed to update user preferences", ioException) + } + } - suspend fun toggleNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean) = - userPreferences.editList( - add = bookmarked, - value = newsResourceId, - listGetter = { it.bookmarkedNewsResourceIds }, - clear = { it.clear() }, - addAll = { dslList, editedList -> dslList.addAll(editedList) } - ) + suspend fun toggleNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean) { + try { + userPreferences.updateData { + it.copy { + if (bookmarked) { + bookmarkedNewsResourceIds.put(newsResourceId, true) + } else { + bookmarkedNewsResourceIds.remove(newsResourceId) + } + } + } + } catch (ioException: IOException) { + Log.e("NiaPreferences", "Failed to update user preferences", ioException) + } + } suspend fun getChangeListVersions() = userPreferences.data .map { @@ -119,48 +148,4 @@ class NiaPreferencesDataSource @Inject constructor( Log.e("NiaPreferences", "Failed to update user preferences", ioException) } } - - /** - * Adds or removes [value] from the [DslList] provided by [listGetter] - */ - private suspend fun DataStore.editList( - add: Boolean, - value: String, - listGetter: (UserPreferencesKt.Dsl) -> DslList, - clear: UserPreferencesKt.Dsl.(DslList) -> Unit, - addAll: UserPreferencesKt.Dsl.(DslList, Iterable) -> Unit - ) { - setList( - listGetter = listGetter, - listModifier = { currentList -> - if (add) currentList + value - else currentList - value - }, - clear = clear, - addAll = addAll - ) - } - - /** - * Sets the value provided by [listModifier] into the [DslList] read by [listGetter] - */ - private suspend fun DataStore.setList( - listGetter: (UserPreferencesKt.Dsl) -> DslList, - listModifier: (DslList) -> List, - clear: UserPreferencesKt.Dsl.(DslList) -> Unit, - addAll: UserPreferencesKt.Dsl.(DslList, List) -> Unit - ) { - try { - updateData { - it.copy { - val dslList = listGetter(this) - val newList = listModifier(dslList) - clear(dslList) - addAll(dslList, newList) - } - } - } catch (ioException: IOException) { - Log.e("NiaPreferences", "Failed to update user preferences", ioException) - } - } } diff --git a/core/datastore/src/main/proto/com/google/samples/apps/nowinandroid/data/user_preferences.proto b/core/datastore/src/main/proto/com/google/samples/apps/nowinandroid/data/user_preferences.proto index 407031215..c7b199a33 100644 --- a/core/datastore/src/main/proto/com/google/samples/apps/nowinandroid/data/user_preferences.proto +++ b/core/datastore/src/main/proto/com/google/samples/apps/nowinandroid/data/user_preferences.proto @@ -28,7 +28,14 @@ message UserPreferences { int32 newsResourceChangeListVersion = 6; repeated int32 deprecated_int_followed_author_ids = 7; bool has_done_int_to_string_id_migration = 8; - repeated string followed_topic_ids = 9; - repeated string followed_author_ids = 10; - repeated string bookmarked_news_resource_ids = 11; + repeated string deprecated_followed_topic_ids = 9; + repeated string deprecated_followed_author_ids = 10; + repeated string deprecated_bookmarked_news_resource_ids = 11; + bool has_done_list_to_map_migration = 12; + + // Each map is used to store a set of string IDs. The bool has no meaning, but proto3 doesn't + // have a Set type so this is the closest we can get to a Set. + map followed_topic_ids = 13; + map followed_author_ids = 14; + map bookmarked_news_resource_ids = 15; } diff --git a/core/datastore/src/test/java/com/google/samples/apps/nowinandroid/core/datastore/IntToStringIdsMigrationTest.kt b/core/datastore/src/test/java/com/google/samples/apps/nowinandroid/core/datastore/IntToStringIdsMigrationTest.kt index 8c62452b8..89035dc91 100644 --- a/core/datastore/src/test/java/com/google/samples/apps/nowinandroid/core/datastore/IntToStringIdsMigrationTest.kt +++ b/core/datastore/src/test/java/com/google/samples/apps/nowinandroid/core/datastore/IntToStringIdsMigrationTest.kt @@ -35,7 +35,7 @@ class IntToStringIdsMigrationTest { // Assert that there are no string topic ids yet assertEquals( emptyList(), - preMigrationUserPreferences.followedTopicIdsList + preMigrationUserPreferences.deprecatedFollowedTopicIdsList ) // Run the migration @@ -45,7 +45,7 @@ class IntToStringIdsMigrationTest { // Assert the deprecated int topic ids have been migrated to the string topic ids assertEquals( userPreferences { - followedTopicIds.addAll(listOf("1", "2", "3")) + deprecatedFollowedTopicIds.addAll(listOf("1", "2", "3")) hasDoneIntToStringIdMigration = true }, postMigrationUserPreferences @@ -64,7 +64,7 @@ class IntToStringIdsMigrationTest { // Assert that there are no string author ids yet assertEquals( emptyList(), - preMigrationUserPreferences.followedAuthorIdsList + preMigrationUserPreferences.deprecatedFollowedAuthorIdsList ) // Run the migration @@ -74,7 +74,7 @@ class IntToStringIdsMigrationTest { // Assert the deprecated int author ids have been migrated to the string author ids assertEquals( userPreferences { - followedAuthorIds.addAll(listOf("4", "5", "6")) + deprecatedFollowedAuthorIds.addAll(listOf("4", "5", "6")) hasDoneIntToStringIdMigration = true }, postMigrationUserPreferences diff --git a/core/datastore/src/test/java/com/google/samples/apps/nowinandroid/core/datastore/ListToMapMigrationTest.kt b/core/datastore/src/test/java/com/google/samples/apps/nowinandroid/core/datastore/ListToMapMigrationTest.kt new file mode 100644 index 000000000..6fc06f585 --- /dev/null +++ b/core/datastore/src/test/java/com/google/samples/apps/nowinandroid/core/datastore/ListToMapMigrationTest.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.core.datastore + +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Test + +class ListToMapMigrationTest { + + @Test + fun ListToMapMigration_should_migrate_topic_ids() = runTest { + // Set up existing preferences with topic ids + val preMigrationUserPreferences = userPreferences { + deprecatedFollowedTopicIds.addAll(listOf("1", "2", "3")) + } + // Assert that there are no topic ids in the map yet + Assert.assertEquals( + emptyMap(), + preMigrationUserPreferences.followedTopicIdsMap + ) + + // Run the migration + val postMigrationUserPreferences = + ListToMapMigration.migrate(preMigrationUserPreferences) + + // Assert the deprecated topic ids have been migrated to the topic ids map + Assert.assertEquals( + mapOf("1" to true, "2" to true, "3" to true), + postMigrationUserPreferences.followedTopicIdsMap + ) + + // Assert that the migration has been marked complete + Assert.assertTrue(postMigrationUserPreferences.hasDoneListToMapMigration) + } + + @Test + fun ListToMapMigration_should_migrate_author_ids() = runTest { + // Set up existing preferences with author ids + val preMigrationUserPreferences = userPreferences { + deprecatedFollowedAuthorIds.addAll(listOf("4", "5", "6")) + } + // Assert that there are no author ids in the map yet + Assert.assertEquals( + emptyMap(), + preMigrationUserPreferences.followedAuthorIdsMap + ) + + // Run the migration + val postMigrationUserPreferences = + ListToMapMigration.migrate(preMigrationUserPreferences) + + // Assert the deprecated author ids have been migrated to the author ids map + Assert.assertEquals( + mapOf("4" to true, "5" to true, "6" to true), + postMigrationUserPreferences.followedAuthorIdsMap + ) + + // Assert that the migration has been marked complete + Assert.assertTrue(postMigrationUserPreferences.hasDoneListToMapMigration) + } + + @Test + fun ListToMapMigration_should_migrate_bookmarks() = runTest { + // Set up existing preferences with bookmarks + val preMigrationUserPreferences = userPreferences { + deprecatedBookmarkedNewsResourceIds.addAll(listOf("7", "8", "9")) + } + // Assert that there are no bookmarks in the map yet + Assert.assertEquals( + emptyMap(), + preMigrationUserPreferences.bookmarkedNewsResourceIdsMap + ) + + // Run the migration + val postMigrationUserPreferences = + ListToMapMigration.migrate(preMigrationUserPreferences) + + // Assert the deprecated bookmarks have been migrated to the bookmarks map + Assert.assertEquals( + mapOf("7" to true, "8" to true, "9" to true), + postMigrationUserPreferences.bookmarkedNewsResourceIdsMap + ) + + // Assert that the migration has been marked complete + Assert.assertTrue(postMigrationUserPreferences.hasDoneListToMapMigration) + } +} diff --git a/core/datastore/src/test/java/com/google/samples/apps/nowinandroid/core/datastore/UserPreferencesSerializerTest.kt b/core/datastore/src/test/java/com/google/samples/apps/nowinandroid/core/datastore/UserPreferencesSerializerTest.kt index 3b4015495..cf3fcf338 100644 --- a/core/datastore/src/test/java/com/google/samples/apps/nowinandroid/core/datastore/UserPreferencesSerializerTest.kt +++ b/core/datastore/src/test/java/com/google/samples/apps/nowinandroid/core/datastore/UserPreferencesSerializerTest.kt @@ -39,8 +39,8 @@ class UserPreferencesSerializerTest { @Test fun writingAndReadingUserPreferences_outputsCorrectValue() = runTest { val expectedUserPreferences = userPreferences { - followedTopicIds.add("0") - followedTopicIds.add("1") + followedTopicIds.put("0", true) + followedTopicIds.put("1", true) } val outputStream = ByteArrayOutputStream()