From fd9553227005d4a315d6160e6ae9e57d31cdcd97 Mon Sep 17 00:00:00 2001 From: skydoves Date: Sat, 13 Aug 2022 15:57:10 +0900 Subject: [PATCH] Introduce SealedX KSP library --- core-data/build.gradle.kts | 10 +++++ .../nowinandroid/core/data/model/UiState.kt | 39 +++++++++++++++++++ .../core/model/data/NewsResource.kt | 8 ++++ .../core/model/data/SaveableNewsResource.kt | 8 ++++ .../feature/author/AuthorScreen.kt | 27 +++++++------ .../feature/author/AuthorViewModel.kt | 31 ++++++--------- .../nowinandroid/feature/topic/TopicScreen.kt | 21 +++++----- .../feature/topic/TopicViewModel.kt | 19 +++------ gradle/libs.versions.toml | 3 ++ 9 files changed, 111 insertions(+), 55 deletions(-) create mode 100644 core-data/src/main/java/com/google/samples/apps/nowinandroid/core/data/model/UiState.kt diff --git a/core-data/build.gradle.kts b/core-data/build.gradle.kts index 66fa5bcb4..9d2823b35 100644 --- a/core-data/build.gradle.kts +++ b/core-data/build.gradle.kts @@ -20,6 +20,13 @@ plugins { id("kotlinx-serialization") id("dagger.hilt.android.plugin") id("nowinandroid.spotless") + alias(libs.plugins.ksp) +} + +kotlin { + sourceSets.configureEach { + kotlin.srcDir("$buildDir/generated/ksp/$name/kotlin/") + } } dependencies { @@ -38,4 +45,7 @@ dependencies { implementation(libs.hilt.android) kapt(libs.hilt.compiler) + + implementation(libs.sealedx.core) + ksp(libs.sealedx.processor) } diff --git a/core-data/src/main/java/com/google/samples/apps/nowinandroid/core/data/model/UiState.kt b/core-data/src/main/java/com/google/samples/apps/nowinandroid/core/data/model/UiState.kt new file mode 100644 index 000000000..4d8c8ccbe --- /dev/null +++ b/core-data/src/main/java/com/google/samples/apps/nowinandroid/core/data/model/UiState.kt @@ -0,0 +1,39 @@ +/* + * 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.data.model + +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.NewsResourceExtensive +import com.google.samples.apps.nowinandroid.core.model.data.SaveableNewsResourceExtensive +import com.skydoves.sealedx.core.Extensive +import com.skydoves.sealedx.core.annotations.ExtensiveModel +import com.skydoves.sealedx.core.annotations.ExtensiveSealed + +@ExtensiveSealed( + models = [ + ExtensiveModel(type = FollowableTopic::class, name = "Topic"), + ExtensiveModel(type = FollowableAuthor::class, name = "Author"), + ExtensiveModel(type = SaveableNewsResourceExtensive::class, name = "SaveableNews"), + ExtensiveModel(type = NewsResourceExtensive::class, name = "News"), + ] +) +sealed interface UiState { + data class Success(val data: Extensive) : UiState + object Error : UiState + object Loading : UiState +} diff --git a/core-model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/NewsResource.kt b/core-model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/NewsResource.kt index fb3ee92d2..e5fdce9b8 100644 --- a/core-model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/NewsResource.kt +++ b/core-model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/NewsResource.kt @@ -41,6 +41,14 @@ data class NewsResource( val topics: List ) +data class NewsResourceExtensive( + val newsResources: List +) + +fun List.toExtensive(): NewsResourceExtensive { + return NewsResourceExtensive(this) +} + val previewNewsResources = listOf( NewsResource( id = "1", diff --git a/core-model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/SaveableNewsResource.kt b/core-model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/SaveableNewsResource.kt index c886e81de..e2ff893bb 100644 --- a/core-model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/SaveableNewsResource.kt +++ b/core-model/src/main/java/com/google/samples/apps/nowinandroid/core/model/data/SaveableNewsResource.kt @@ -23,3 +23,11 @@ data class SaveableNewsResource( val newsResource: NewsResource, val isSaved: Boolean, ) + +data class SaveableNewsResourceExtensive( + val newsResources: List +) + +fun List.toExtensive(): SaveableNewsResourceExtensive { + return SaveableNewsResourceExtensive(this) +} 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..d28e59d9b 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 @@ -50,6 +50,8 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.AsyncImage +import com.google.samples.apps.nowinandroid.core.data.model.AuthorUiState +import com.google.samples.apps.nowinandroid.core.data.model.SaveableNewsUiState import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaFilterChip import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel @@ -59,6 +61,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.model.data.toExtensive import com.google.samples.apps.nowinandroid.core.ui.newsResourceCardItems @OptIn(ExperimentalLifecycleComposeApi::class) @@ -69,7 +72,7 @@ fun AuthorRoute( viewModel: AuthorViewModel = hiltViewModel(), ) { val authorUiState: AuthorUiState by viewModel.authorUiState.collectAsStateWithLifecycle() - val newsUiState: NewsUiState by viewModel.newUiState.collectAsStateWithLifecycle() + val newsUiState: SaveableNewsUiState by viewModel.newUiState.collectAsStateWithLifecycle() AuthorScreen( authorUiState = authorUiState, @@ -85,7 +88,7 @@ fun AuthorRoute( @Composable internal fun AuthorScreen( authorUiState: AuthorUiState, - newsUiState: NewsUiState, + newsUiState: SaveableNewsUiState, onBackClick: () -> Unit, onFollowClick: (Boolean) -> Unit, onBookmarkChanged: (String, Boolean) -> Unit, @@ -115,11 +118,11 @@ internal fun AuthorScreen( AuthorToolbar( onBackClick = onBackClick, onFollowClick = onFollowClick, - uiState = authorUiState.followableAuthor, + uiState = authorUiState.data, ) } authorBody( - author = authorUiState.followableAuthor.author, + author = authorUiState.data.author, news = newsUiState, onBookmarkChanged = onBookmarkChanged, ) @@ -133,7 +136,7 @@ internal fun AuthorScreen( private fun LazyListScope.authorBody( author: Author, - news: NewsUiState, + news: SaveableNewsUiState, onBookmarkChanged: (String, Boolean) -> Unit ) { item { @@ -170,20 +173,20 @@ private fun AuthorHeader(author: Author) { } private fun LazyListScope.authorCards( - news: NewsUiState, + news: SaveableNewsUiState, onBookmarkChanged: (String, Boolean) -> Unit ) { when (news) { - is NewsUiState.Success -> { + is SaveableNewsUiState.Success -> { newsResourceCardItems( - items = news.news, + items = news.data.newsResources, newsResourceMapper = { it.newsResource }, isBookmarkedMapper = { it.isSaved }, onToggleBookmark = { onBookmarkChanged(it.newsResource.id, !it.isSaved) }, itemModifier = Modifier.padding(24.dp) ) } - is NewsUiState.Loading -> item { + is SaveableNewsUiState.Loading -> item { NiaLoadingWheel(contentDesc = "Loading news") // TODO } else -> item { @@ -237,13 +240,13 @@ fun AuthorScreenPopulated() { NiaBackground { AuthorScreen( authorUiState = AuthorUiState.Success(FollowableAuthor(previewAuthors[0], false)), - newsUiState = NewsUiState.Success( + newsUiState = SaveableNewsUiState.Success( previewNewsResources.mapIndexed { index, newsResource -> SaveableNewsResource( newsResource = newsResource, isSaved = index % 2 == 0, ) - } + }.toExtensive() ), onBackClick = {}, onFollowClick = {}, @@ -263,7 +266,7 @@ fun AuthorScreenLoading() { NiaBackground { AuthorScreen( authorUiState = AuthorUiState.Loading, - newsUiState = NewsUiState.Loading, + newsUiState = SaveableNewsUiState.Loading, onBackClick = {}, onFollowClick = {}, onBookmarkChanged = { _, _ -> }, diff --git a/feature-author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModel.kt b/feature-author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModel.kt index 889c3a326..44aef9cb3 100644 --- a/feature-author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModel.kt +++ b/feature-author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModel.kt @@ -19,6 +19,8 @@ package com.google.samples.apps.nowinandroid.feature.author import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.google.samples.apps.nowinandroid.core.data.model.AuthorUiState +import com.google.samples.apps.nowinandroid.core.data.model.SaveableNewsUiState import com.google.samples.apps.nowinandroid.core.data.repository.AuthorsRepository import com.google.samples.apps.nowinandroid.core.data.repository.NewsRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository @@ -26,6 +28,7 @@ import com.google.samples.apps.nowinandroid.core.model.data.Author import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.SaveableNewsResource +import com.google.samples.apps.nowinandroid.core.model.data.toExtensive import com.google.samples.apps.nowinandroid.core.result.Result import com.google.samples.apps.nowinandroid.core.result.asResult import com.google.samples.apps.nowinandroid.feature.author.navigation.AuthorDestination @@ -62,7 +65,7 @@ class AuthorViewModel @Inject constructor( initialValue = AuthorUiState.Loading ) - val newUiState: StateFlow = newsUiStateStream( + val newUiState: StateFlow = newsUiStateStream( authorId = authorId, userDataRepository = userDataRepository, newsRepository = newsRepository @@ -70,7 +73,7 @@ class AuthorViewModel @Inject constructor( .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), - initialValue = NewsUiState.Loading + initialValue = SaveableNewsUiState.Loading ) fun followAuthorToggle(followed: Boolean) { @@ -113,7 +116,7 @@ private fun authorUiStateStream( val (followedAuthors, author) = followedAuthorToAuthorResult.data val followed = followedAuthors.contains(authorId) AuthorUiState.Success( - followableAuthor = FollowableAuthor( + data = FollowableAuthor( author = author, isFollowed = followed ) @@ -133,7 +136,7 @@ private fun newsUiStateStream( authorId: String, newsRepository: NewsRepository, userDataRepository: UserDataRepository, -): Flow { +): Flow { // Observe news val newsStream: Flow> = newsRepository.getNewsResourcesStream( filterAuthorIds = setOf(element = authorId), @@ -154,33 +157,21 @@ private fun newsUiStateStream( when (newsToBookmarksResult) { is Result.Success -> { val (news, bookmarks) = newsToBookmarksResult.data - NewsUiState.Success( + SaveableNewsUiState.Success( news.map { newsResource -> SaveableNewsResource( newsResource, isSaved = bookmarks.contains(newsResource.id) ) - } + }.toExtensive() ) } is Result.Loading -> { - NewsUiState.Loading + SaveableNewsUiState.Loading } is Result.Error -> { - NewsUiState.Error + SaveableNewsUiState.Error } } } } - -sealed interface AuthorUiState { - data class Success(val followableAuthor: FollowableAuthor) : AuthorUiState - object Error : AuthorUiState - object Loading : AuthorUiState -} - -sealed interface NewsUiState { - data class Success(val news: List) : NewsUiState - object Error : NewsUiState - object Loading : NewsUiState -} 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..c48a01781 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 @@ -47,16 +47,19 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.AsyncImage +import com.google.samples.apps.nowinandroid.core.data.model.NewsUiState +import com.google.samples.apps.nowinandroid.core.data.model.TopicUiState import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaFilterChip import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel 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.NewsResourceExtensive 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.model.data.toExtensive 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 @OptIn(ExperimentalLifecycleComposeApi::class) @Composable @@ -93,7 +96,7 @@ internal fun TopicScreen( Spacer(Modifier.windowInsetsTopHeight(WindowInsets.safeDrawing)) } when (topicState) { - Loading -> item { + TopicUiState.Loading -> item { NiaLoadingWheel( modifier = modifier, contentDesc = stringResource(id = string.topic_loading), @@ -105,14 +108,14 @@ internal fun TopicScreen( TopicToolbar( onBackClick = onBackClick, onFollowClick = onFollowClick, - uiState = topicState.followableTopic, + uiState = topicState.data, ) } TopicBody( - name = topicState.followableTopic.topic.name, - description = topicState.followableTopic.topic.longDescription, + name = topicState.data.topic.name, + description = topicState.data.topic.longDescription, news = newsState, - imageUrl = topicState.followableTopic.topic.imageUrl + imageUrl = topicState.data.topic.imageUrl ) } } @@ -164,7 +167,7 @@ private fun LazyListScope.TopicCards(news: NewsUiState) { when (news) { is NewsUiState.Success -> { newsResourceCardItems( - items = news.news, + items = news.data.newsResources, newsResourceMapper = { it }, isBookmarkedMapper = { /* TODO */ false }, onToggleBookmark = { /* TODO */ }, @@ -187,7 +190,7 @@ private fun TopicBodyPreview() { LazyColumn { TopicBody( "Jetpack Compose", "Lorem ipsum maximum", - NewsUiState.Success(emptyList()), "" + NewsUiState.Success(NewsResourceExtensive(emptyList())), "" ) } } @@ -238,7 +241,7 @@ fun TopicScreenPopulated() { NiaBackground { TopicScreen( topicState = TopicUiState.Success(FollowableTopic(previewTopics[0], false)), - newsState = NewsUiState.Success(previewNewsResources), + newsState = NewsUiState.Success(previewNewsResources.toExtensive()), onBackClick = {}, onFollowClick = {} ) diff --git a/feature-topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt b/feature-topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt index d647e4bf7..ac950d316 100644 --- a/feature-topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt +++ b/feature-topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt @@ -19,12 +19,15 @@ package com.google.samples.apps.nowinandroid.feature.topic import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.google.samples.apps.nowinandroid.core.data.model.NewsUiState +import com.google.samples.apps.nowinandroid.core.data.model.TopicUiState import com.google.samples.apps.nowinandroid.core.data.repository.NewsRepository import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.Topic +import com.google.samples.apps.nowinandroid.core.model.data.toExtensive import com.google.samples.apps.nowinandroid.core.result.Result import com.google.samples.apps.nowinandroid.core.result.asResult import com.google.samples.apps.nowinandroid.feature.topic.navigation.TopicDestination @@ -73,7 +76,7 @@ class TopicViewModel @Inject constructor( if (topicResult is Result.Success && followedTopicsResult is Result.Success) { val followed = followedTopicsResult.data.contains(topicId) TopicUiState.Success( - followableTopic = FollowableTopic( + data = FollowableTopic( topic = topicResult.data, isFollowed = followed ) @@ -87,7 +90,7 @@ class TopicViewModel @Inject constructor( } val news: NewsUiState = when (newsResult) { - is Result.Success -> NewsUiState.Success(newsResult.data) + is Result.Success -> NewsUiState.Success(newsResult.data.toExtensive()) is Result.Loading -> NewsUiState.Loading is Result.Error -> NewsUiState.Error } @@ -107,18 +110,6 @@ class TopicViewModel @Inject constructor( } } -sealed interface TopicUiState { - data class Success(val followableTopic: FollowableTopic) : TopicUiState - object Error : TopicUiState - object Loading : TopicUiState -} - -sealed interface NewsUiState { - data class Success(val news: List) : NewsUiState - object Error : NewsUiState - object Loading : NewsUiState -} - data class TopicScreenUiState( val topicState: TopicUiState, val newsState: NewsUiState diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 50f472382..b4e1c2ebc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,6 +36,7 @@ kotlinxCoroutines = "1.6.3" kotlinxDatetime = "0.3.3" kotlinxSerializationJson = "1.3.3" ksp = "1.7.0-1.0.6" +sealedx = "1.0.0" ktlint = "0.43.0" lint = "30.2.1" okhttp = "4.10.0" @@ -120,6 +121,8 @@ room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } secrets-gradlePlugin = { group = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", name = "secrets-gradle-plugin", version.ref = "secrets" } spotless-gradlePlugin = { group = "com.diffplug.spotless", name = "spotless-plugin-gradle", version.ref = "spotless" } +sealedx-core = { group = "com.github.skydoves", name = "sealedx-core", version.ref = "sealedx" } +sealedx-processor = { group = "com.github.skydoves", name = "sealedx-processor", version.ref = "sealedx" } [plugins] protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" }