diff --git a/README.md b/README.md index a6b05489c..1a418cfef 100644 --- a/README.md +++ b/README.md @@ -39,36 +39,36 @@ As Firebase Analytics does not yet support Kotlin Multiplatform, the implementat ## Status for modules -| Module | Progress | Desktop supported | Android supported | iOS supported | Web supported | -|---------------------------|------------------|-------------------|-------------------|---------------|---------------| -| app | Not started | ❌ | ❌ | ❌ | ❌ | -| app-nia-catalog | Done | ✅ | ✅ | ❔ | ✅ | -| :core:analytics | Done | ✔️ | ✔️ | ✔️ | ❌ | -| :core:common | Done | ✔️ | ✔️ | ✔️ | ❌ | -| :core:data | Done | ✔️ | ✔️ | ✔️ | ❌ | -| :core:data-test | Not started | ❌ | ❌ | ❌ | ❌ | -| :core:database | Done | ✔️ | ✔️ | ✔️ | ❌ | -| :core:datastore | Done | ✔️ | ✔️ | ✔️ | ❌ | -| :core:datastore-proto | Done | ✔️ | ✔️ | ✔️ | ❌ | -| :core:datastore-test | Removed | ❌ | ❌ | ❌ | ❌ | -| :core:designsystem | Done | ✅ | ✅ | ❔ | ✅ | -| :core:domain | Done | ✔️ | ✔️ | ✔️ | ❌ | -| :core:model | Done | ✔️ | ✔️ | ✔️ | ❌ | -| :core:network | Done | ✔️ | ✔️ | ✔️ | ❌ | -| :core:notification | Done | No implmentaion | ✔️ |No implmentaion| ❌ | -| :core:screenshot-testing | Not started | ❌ | ❌ | ❌ | ❌ | -| :core:testing | Done | ✔️ | ✔️ | ✔️ | ❌ | -| :core:ui | In progress | ✔️ | ✔️ | ✔️ | ❌ | -| :feature:bookmarks | Not started | ❌ | ❌ | ❌ | ❌ | -| :feature:foryou | Not started | ❌ | ❌ | ❌ | ❌ | -| :feature:interests | Not started | ❌ | ❌ | ❌ | ❌ | -| :feature:search | Not started | ❌ | ❌ | ❌ | ❌ | -| :feature:settings | Not started | ❌ | ❌ | ❌ | ❌ | -| :feature:topic | Not started | ❌ | ❌ | ❌ | ❌ | -| lint | Not started | ❌ | ❌ | ❌ | ❌ | -| :sync:sync-test | Not started | ❌ | ❌ | ❌ | ❌ | -| :sync:work | Not started | ❌ | ❌ | ❌ | ❌ | -| ui-test-manifest | Not started | ❌ | ❌ | ❌ | ❌ | +| Module | Progress | Desktop supported | Android supported | iOS supported | Web supported | +|---------------------------|-------------|-------------------|-------------------|---------------|---------------| +| app | Not started | ❌ | ❌ | ❌ | ❌ | +| app-nia-catalog | Done | ✅ | ✅ | ❔ | ✅ | +| :core:analytics | Done | ✔️ | ✔️ | ✔️ | ❌ | +| :core:common | Done | ✔️ | ✔️ | ✔️ | ❌ | +| :core:data | Done | ✔️ | ✔️ | ✔️ | ❌ | +| :core:data-test | Not started | ❌ | ❌ | ❌ | ❌ | +| :core:database | Done | ✔️ | ✔️ | ✔️ | ❌ | +| :core:datastore | Done | ✔️ | ✔️ | ✔️ | ❌ | +| :core:datastore-proto | Done | ✔️ | ✔️ | ✔️ | ❌ | +| :core:datastore-test | Removed | ❌ | ❌ | ❌ | ❌ | +| :core:designsystem | Done | ✅ | ✅ | ❔ | ✅ | +| :core:domain | Done | ✔️ | ✔️ | ✔️ | ❌ | +| :core:model | Done | ✔️ | ✔️ | ✔️ | ❌ | +| :core:network | Done | ✔️ | ✔️ | ✔️ | ❌ | +| :core:notification | Done | No implmentaion | ✔️ |No implmentaion| ❌ | +| :core:screenshot-testing | Not started | ❌ | ❌ | ❌ | ❌ | +| :core:testing | Done | ✔️ | ✔️ | ✔️ | ❌ | +| :core:ui | Done | ✔️ | ✔️ | ✔️ | ❌ | +| :feature:bookmarks | In progress | ❌ | ❌ | ❌ | ❌ | +| :feature:foryou | Not started | ❌ | ❌ | ❌ | ❌ | +| :feature:interests | Not started | ❌ | ❌ | ❌ | ❌ | +| :feature:search | Not started | ❌ | ❌ | ❌ | ❌ | +| :feature:settings | Not started | ❌ | ❌ | ❌ | ❌ | +| :feature:topic | Not started | ❌ | ❌ | ❌ | ❌ | +| lint | Not started | ❌ | ❌ | ❌ | ❌ | +| :sync:sync-test | Not started | ❌ | ❌ | ❌ | ❌ | +| :sync:work | Not started | ❌ | ❌ | ❌ | ❌ | +| ui-test-manifest | Not started | ❌ | ❌ | ❌ | ❌ | diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index 4a176cd06..f43404839 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -125,5 +125,9 @@ gradlePlugin { id = "nowinandroid.sqldelight" implementationClass = "SqlDelightConventionPlugin" } + register("cmpFeature") { + id = "nowinandroid.cmp.feature" + implementationClass = "CmpFeatureConventionPlugin" + } } } diff --git a/build-logic/convention/src/main/kotlin/CmpFeatureConventionPlugin.kt b/build-logic/convention/src/main/kotlin/CmpFeatureConventionPlugin.kt new file mode 100644 index 000000000..53fc3ff3e --- /dev/null +++ b/build-logic/convention/src/main/kotlin/CmpFeatureConventionPlugin.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2024 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. + */ + +import com.android.build.gradle.LibraryExtension +import com.google.samples.apps.nowinandroid.configureGradleManagedDevices +import com.google.samples.apps.nowinandroid.libs +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.dependencies + +// Convention plugin for the Compose Multiplatform feature module +class CmpFeatureConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + pluginManager.apply { + apply("nowinandroid.kmp.library") + apply("nowinandroid.kmp.inject") + } + extensions.configure { + defaultConfig { + testInstrumentationRunner = + "com.google.samples.apps.nowinandroid.core.testing.NiaTestRunner" + } + testOptions.animationsDisabled = true + configureGradleManagedDevices(this) + } + + dependencies { + add("commonMainImplementation", project(":core:ui")) + add("commonMainImplementation", project(":core:designsystem")) + add("commonMainImplementation", libs.findLibrary("jetbrains.compose.viewmodel").get()) + add("commonMainImplementation", libs.findLibrary("jetbrains.compose.navigation").get()) + + add("androidMainImplementation", libs.findLibrary("androidx.hilt.navigation.compose").get()) + add("androidMainImplementation", libs.findLibrary("androidx.lifecycle.runtimeCompose").get()) + add("androidMainImplementation", libs.findLibrary("androidx.lifecycle.viewModelCompose").get()) + add("androidMainImplementation", libs.findLibrary("androidx.tracing.ktx").get()) + + add("androidInstrumentedTestImplementation", libs.findLibrary("androidx.lifecycle.runtimeTesting").get()) + } + } + } +} diff --git a/core/analytics/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/analytics/StubAnalyticsHelper.kt b/core/analytics/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/analytics/StubAnalyticsHelper.kt index ec42eaa64..d6dec97fc 100644 --- a/core/analytics/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/analytics/StubAnalyticsHelper.kt +++ b/core/analytics/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/analytics/StubAnalyticsHelper.kt @@ -17,11 +17,13 @@ package com.google.samples.apps.nowinandroid.core.analytics import co.touchlab.kermit.Logger +import me.tatarka.inject.annotations.Inject /** * An implementation of AnalyticsHelper just writes the events to logcat. Used in builds where no * analytics events should be sent to a backend. */ +@Inject internal class StubAnalyticsHelper : AnalyticsHelper { override fun logEvent(event: AnalyticsEvent) { Logger.d { "Received analytics event: $event" } diff --git a/core/data/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/di/DataModule.kt b/core/data/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/di/DataModule.kt index 496f08e4d..bb29ee7a0 100644 --- a/core/data/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/di/DataModule.kt +++ b/core/data/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/di/DataModule.kt @@ -33,27 +33,27 @@ import me.tatarka.inject.annotations.Provides abstract class DataModule { @Provides - internal fun bindsTopicRepository( + fun bindsTopicRepository( topicsRepository: OfflineFirstTopicsRepository, ): TopicsRepository = topicsRepository @Provides - internal fun bindsNewsResourceRepository( + fun bindsNewsResourceRepository( newsRepository: OfflineFirstNewsRepository, ): NewsRepository = newsRepository @Provides - internal fun bindsUserDataRepository( + fun userDataRepository( userDataRepository: OfflineFirstUserDataRepository, ): UserDataRepository = userDataRepository @Provides - internal fun bindsRecentSearchRepository( + fun bindsRecentSearchRepository( recentSearchRepository: DefaultRecentSearchRepository, ): RecentSearchRepository = recentSearchRepository @Provides - internal fun bindsSearchContentsRepository( + fun bindsSearchContentsRepository( searchContentsRepository: DefaultSearchContentsRepository, ): SearchContentsRepository = searchContentsRepository } diff --git a/core/data/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/DefaultRecentSearchRepository.kt b/core/data/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/DefaultRecentSearchRepository.kt index 62b8691da..7fdeb6251 100644 --- a/core/data/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/DefaultRecentSearchRepository.kt +++ b/core/data/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/DefaultRecentSearchRepository.kt @@ -26,7 +26,7 @@ import kotlinx.datetime.Clock import me.tatarka.inject.annotations.Inject @Inject -internal class DefaultRecentSearchRepository( +class DefaultRecentSearchRepository( private val recentSearchQueryDao: RecentSearchQueryDao, ) : RecentSearchRepository { override suspend fun insertOrReplaceRecentSearch(searchQuery: String) { diff --git a/core/data/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/DefaultSearchContentsRepository.kt b/core/data/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/DefaultSearchContentsRepository.kt index e76093691..cfa582586 100644 --- a/core/data/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/DefaultSearchContentsRepository.kt +++ b/core/data/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/DefaultSearchContentsRepository.kt @@ -37,7 +37,7 @@ import me.tatarka.inject.annotations.Inject @OptIn(ExperimentalCoroutinesApi::class) @Inject -internal class DefaultSearchContentsRepository( +class DefaultSearchContentsRepository( private val newsResourceDao: NewsResourceDao, private val newsResourceFtsDao: NewsResourceFtsDao, private val topicDao: TopicDao, diff --git a/core/data/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstNewsRepository.kt b/core/data/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstNewsRepository.kt index 67e1e7e12..acf4f5aba 100644 --- a/core/data/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstNewsRepository.kt +++ b/core/data/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstNewsRepository.kt @@ -46,7 +46,7 @@ private const val SYNC_BATCH_SIZE = 40 * Reads are exclusively from local storage to support offline access. */ @Inject -internal class OfflineFirstNewsRepository( +class OfflineFirstNewsRepository( private val niaPreferencesDataSource: NiaPreferencesDataSource, private val newsResourceDao: NewsResourceDaoInterface, private val topicDao: TopicDaoInterface, diff --git a/core/data/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstTopicsRepository.kt b/core/data/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstTopicsRepository.kt index 485370927..46c5e0d38 100644 --- a/core/data/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstTopicsRepository.kt +++ b/core/data/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstTopicsRepository.kt @@ -35,7 +35,7 @@ import me.tatarka.inject.annotations.Inject * Reads are exclusively from local storage to support offline access. */ @Inject -internal class OfflineFirstTopicsRepository( +class OfflineFirstTopicsRepository( private val topicDao: TopicDaoInterface, private val network: NiaNetworkDataSource, ) : TopicsRepository { diff --git a/core/data/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepository.kt b/core/data/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepository.kt index 68c6efb36..7ea7e52c7 100644 --- a/core/data/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepository.kt +++ b/core/data/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepository.kt @@ -25,7 +25,7 @@ import kotlinx.coroutines.flow.Flow import me.tatarka.inject.annotations.Inject @Inject -internal class OfflineFirstUserDataRepository( +class OfflineFirstUserDataRepository( private val niaPreferencesDataSource: NiaPreferencesDataSource, private val analyticsHelper: AnalyticsHelper, ) : UserDataRepository { diff --git a/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceDao.kt b/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceDao.kt index 164cd908d..f5436443d 100644 --- a/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceDao.kt +++ b/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceDao.kt @@ -26,10 +26,12 @@ import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.datetime.Instant +import me.tatarka.inject.annotations.Inject /** * DAO for [NewsResource] and [NewsResourceEntity] access */ +@Inject class NewsResourceDao(db: NiaDatabase, private val dispatcher: CoroutineDispatcher) : NewsResourceDaoInterface { private val query = db.newsResourceQueries diff --git a/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceFtsDao.kt b/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceFtsDao.kt index 9b6b5e5ca..ee2419e77 100644 --- a/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceFtsDao.kt +++ b/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceFtsDao.kt @@ -24,10 +24,12 @@ import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceFtsE import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import me.tatarka.inject.annotations.Inject /** * DAO for [NewsResourceFtsEntity] access. */ +@Inject class NewsResourceFtsDao(db: NiaDatabase, private val dispatcher: CoroutineDispatcher) : NewsResourceFtsDaoInterface { private val dbQuery = db.newsResourceFtsQueries override suspend fun insertAll(newsResources: List) { diff --git a/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/RecentSearchQueryDao.kt b/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/RecentSearchQueryDao.kt index b1b005a2c..58e23a1de 100644 --- a/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/RecentSearchQueryDao.kt +++ b/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/RecentSearchQueryDao.kt @@ -24,10 +24,12 @@ import com.google.samples.apps.nowinandroid.core.database.model.RecentSearchQuer import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.datetime.Instant +import me.tatarka.inject.annotations.Inject /** * DAO for [RecentSearchQueryEntity] access */ +@Inject class RecentSearchQueryDao(db: NiaDatabase, private val dispatcher: CoroutineDispatcher) : RecentSearchQueryDaoInterface { private val query = db.recentSearchQueryQueries diff --git a/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/TopicDao.kt b/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/TopicDao.kt index 24b946b3e..4f3796f7f 100644 --- a/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/TopicDao.kt +++ b/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/TopicDao.kt @@ -24,11 +24,12 @@ import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import me.tatarka.inject.annotations.Inject /** * DAO for [TopicEntity] access */ - +@Inject class TopicDao(db: NiaDatabase, private val dispatcher: CoroutineDispatcher) : TopicDaoInterface { private val query = db.topicsQueries diff --git a/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/TopicFtsDao.kt b/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/TopicFtsDao.kt index de96556a0..dd5a86473 100644 --- a/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/TopicFtsDao.kt +++ b/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/TopicFtsDao.kt @@ -24,10 +24,12 @@ import com.google.samples.apps.nowinandroid.core.database.model.TopicFtsEntity import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import me.tatarka.inject.annotations.Inject /** * DAO for [TopicFtsEntity] access. */ +@Inject class TopicFtsDao(db: NiaDatabase, private val dispatcher: CoroutineDispatcher) : TopicFtsDaoInterface { private val dbQuery = db.topicFtsQueries diff --git a/core/datastore/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt b/core/datastore/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt index 59ac0433d..9cc91745a 100644 --- a/core/datastore/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt +++ b/core/datastore/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt @@ -30,10 +30,12 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import kotlinx.serialization.ExperimentalSerializationApi +import me.tatarka.inject.annotations.Inject private const val USER_DATA_KEY = "userData" @OptIn(ExperimentalSerializationApi::class, ExperimentalSettingsApi::class) +@Inject class NiaPreferencesDataSource( private val settings: Settings, private val dispatcher: IODispatcher, diff --git a/core/ui/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/ui/StateExtension.kt b/core/ui/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/ui/StateExtension.kt new file mode 100644 index 000000000..8336bf863 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/ui/StateExtension.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2024 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.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import kotlinx.coroutines.flow.StateFlow + +// collectAsStateWithLifecycle is an Android-only API +// Replaced with collectAsState in shared code +@Composable +fun StateFlow.collectAsStateWithLifecycle(): State = collectAsState() diff --git a/feature/bookmarks/build.gradle.kts b/feature/bookmarks/build.gradle.kts index 4e97176a2..bb53f731e 100644 --- a/feature/bookmarks/build.gradle.kts +++ b/feature/bookmarks/build.gradle.kts @@ -15,8 +15,9 @@ */ plugins { - alias(libs.plugins.nowinandroid.android.feature) - alias(libs.plugins.nowinandroid.android.library.compose) + alias(libs.plugins.nowinandroid.cmp.feature) + alias(libs.plugins.jetbrains.compose) + alias(libs.plugins.compose) alias(libs.plugins.nowinandroid.android.library.jacoco) } @@ -24,10 +25,22 @@ android { namespace = "com.google.samples.apps.nowinandroid.feature.bookmarks" } -dependencies { - implementation(projects.core.data) +kotlin { + sourceSets { + commonMain.dependencies { + implementation(projects.core.data) + implementation(compose.material3) + implementation(compose.foundation) + implementation(compose.ui) + implementation(compose.components.resources) + implementation(compose.components.uiToolingPreview) - testImplementation(projects.core.testing) - - androidTestImplementation(projects.core.testing) + } + commonTest.dependencies { + implementation(projects.core.testing) + } + androidInstrumentedTest.dependencies { + implementation(projects.core.testing) + } + } } diff --git a/feature/bookmarks/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt b/feature/bookmarks/src/androidInstrumentedTest/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt similarity index 100% rename from feature/bookmarks/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt rename to feature/bookmarks/src/androidInstrumentedTest/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt diff --git a/feature/bookmarks/src/main/res/drawable/feature_bookmarks_img_empty_bookmarks.xml b/feature/bookmarks/src/commonMain/composeResources/drawable/feature_bookmarks_img_empty_bookmarks.xml similarity index 100% rename from feature/bookmarks/src/main/res/drawable/feature_bookmarks_img_empty_bookmarks.xml rename to feature/bookmarks/src/commonMain/composeResources/drawable/feature_bookmarks_img_empty_bookmarks.xml diff --git a/feature/bookmarks/src/main/res/values/strings.xml b/feature/bookmarks/src/commonMain/composeResources/values/strings.xml similarity index 100% rename from feature/bookmarks/src/main/res/values/strings.xml rename to feature/bookmarks/src/commonMain/composeResources/values/strings.xml diff --git a/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt b/feature/bookmarks/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt similarity index 85% rename from feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt rename to feature/bookmarks/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt index 7c229c5ea..87be463f7 100644 --- a/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt +++ b/feature/bookmarks/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt @@ -49,17 +49,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.compose.LifecycleEventEffect -import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.DraggableScrollbar import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberDraggableScroller @@ -71,16 +64,29 @@ import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Loading import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Success import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent -import com.google.samples.apps.nowinandroid.core.ui.TrackScrollJank import com.google.samples.apps.nowinandroid.core.ui.UserNewsResourcePreviewParameterProvider +import com.google.samples.apps.nowinandroid.core.ui.collectAsStateWithLifecycle import com.google.samples.apps.nowinandroid.core.ui.newsFeed +import me.tatarka.inject.annotations.Inject +import nowinandroid.feature.bookmarks.generated.resources.Res +import nowinandroid.feature.bookmarks.generated.resources.feature_bookmarks_empty_description +import nowinandroid.feature.bookmarks.generated.resources.feature_bookmarks_empty_error +import nowinandroid.feature.bookmarks.generated.resources.feature_bookmarks_img_empty_bookmarks +import nowinandroid.feature.bookmarks.generated.resources.feature_bookmarks_loading +import nowinandroid.feature.bookmarks.generated.resources.feature_bookmarks_removed +import nowinandroid.feature.bookmarks.generated.resources.feature_bookmarks_undo +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.PreviewParameter @Composable +@Inject internal fun BookmarksRoute( onTopicClick: (String) -> Unit, onShowSnackbar: suspend (String, String?) -> Boolean, modifier: Modifier = Modifier, - viewModel: BookmarksViewModel = hiltViewModel(), + viewModel: BookmarksViewModel, ) { val feedState by viewModel.feedUiState.collectAsStateWithLifecycle() BookmarksScreen( @@ -112,8 +118,8 @@ internal fun BookmarksScreen( undoBookmarkRemoval: () -> Unit = {}, clearUndoState: () -> Unit = {}, ) { - val bookmarkRemovedMessage = stringResource(id = R.string.feature_bookmarks_removed) - val undoText = stringResource(id = R.string.feature_bookmarks_undo) + val bookmarkRemovedMessage = stringResource(Res.string.feature_bookmarks_removed) + val undoText = stringResource(Res.string.feature_bookmarks_undo) LaunchedEffect(shouldDisplayUndoBookmark) { if (shouldDisplayUndoBookmark) { @@ -125,10 +131,10 @@ internal fun BookmarksScreen( } } } - - LifecycleEventEffect(Lifecycle.Event.ON_STOP) { - clearUndoState() - } + // Could not import LifecycleEventEffect +// LifecycleEventEffect(Lifecycle.Event.ON_STOP) { +// clearUndoState() +// } when (feedState) { Loading -> LoadingState(modifier) @@ -155,7 +161,7 @@ private fun LoadingState(modifier: Modifier = Modifier) { .fillMaxWidth() .wrapContentSize() .testTag("forYou:loading"), - contentDesc = stringResource(id = R.string.feature_bookmarks_loading), + contentDesc = stringResource(Res.string.feature_bookmarks_loading), ) } @@ -168,7 +174,6 @@ private fun BookmarksGrid( modifier: Modifier = Modifier, ) { val scrollableState = rememberLazyStaggeredGridState() - TrackScrollJank(scrollableState = scrollableState, stateName = "bookmarks:grid") Box( modifier = modifier .fillMaxSize(), @@ -228,7 +233,7 @@ private fun EmptyState(modifier: Modifier = Modifier) { val iconTint = LocalTintTheme.current.iconTint Image( modifier = Modifier.fillMaxWidth(), - painter = painterResource(id = R.drawable.feature_bookmarks_img_empty_bookmarks), + painter = painterResource(Res.drawable.feature_bookmarks_img_empty_bookmarks), colorFilter = if (iconTint != Color.Unspecified) ColorFilter.tint(iconTint) else null, contentDescription = null, ) @@ -236,7 +241,7 @@ private fun EmptyState(modifier: Modifier = Modifier) { Spacer(modifier = Modifier.height(48.dp)) Text( - text = stringResource(id = R.string.feature_bookmarks_empty_error), + text = stringResource(Res.string.feature_bookmarks_empty_error), modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center, style = MaterialTheme.typography.titleMedium, @@ -246,7 +251,7 @@ private fun EmptyState(modifier: Modifier = Modifier) { Spacer(modifier = Modifier.height(8.dp)) Text( - text = stringResource(id = R.string.feature_bookmarks_empty_description), + text = stringResource(Res.string.feature_bookmarks_empty_description), modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center, style = MaterialTheme.typography.bodyMedium, diff --git a/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt b/feature/bookmarks/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt similarity index 95% rename from feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt rename to feature/bookmarks/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt index f93602485..e89dbb6a4 100644 --- a/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt +++ b/feature/bookmarks/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt @@ -26,17 +26,16 @@ import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourc import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Loading -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import javax.inject.Inject +import me.tatarka.inject.annotations.Inject -@HiltViewModel -class BookmarksViewModel @Inject constructor( +@Inject +class BookmarksViewModel( private val userDataRepository: UserDataRepository, userNewsResourceRepository: UserNewsResourceRepository, ) : ViewModel() { diff --git a/feature/bookmarks/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/di/BookmarkComponent.kt b/feature/bookmarks/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/di/BookmarkComponent.kt new file mode 100644 index 000000000..68853dc22 --- /dev/null +++ b/feature/bookmarks/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/di/BookmarkComponent.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2024 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.feature.bookmarks.di + +import com.google.samples.apps.nowinandroid.feature.bookmarks.BookmarksViewModel +import me.tatarka.inject.annotations.Component + +@Component +abstract class BookmarkComponent { + abstract val viewModel: BookmarksViewModel +} diff --git a/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/navigation/BookmarksNavigation.kt b/feature/bookmarks/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/navigation/BookmarksNavigation.kt similarity index 85% rename from feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/navigation/BookmarksNavigation.kt rename to feature/bookmarks/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/navigation/BookmarksNavigation.kt index 13d0baef0..64cf40df7 100644 --- a/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/navigation/BookmarksNavigation.kt +++ b/feature/bookmarks/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/navigation/BookmarksNavigation.kt @@ -21,6 +21,7 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.google.samples.apps.nowinandroid.feature.bookmarks.BookmarksRoute +import com.google.samples.apps.nowinandroid.feature.bookmarks.di.BookmarkComponent const val BOOKMARKS_ROUTE = "bookmarks_route" @@ -30,7 +31,8 @@ fun NavGraphBuilder.bookmarksScreen( onTopicClick: (String) -> Unit, onShowSnackbar: suspend (String, String?) -> Boolean, ) { + val viewModel = BookmarkComponent::class.create().viewModel composable(route = BOOKMARKS_ROUTE) { - BookmarksRoute(onTopicClick, onShowSnackbar) + BookmarksRoute(onTopicClick, onShowSnackbar, viewModel) } } diff --git a/feature/bookmarks/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModelTest.kt b/feature/bookmarks/src/commonTest/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModelTest.kt similarity index 100% rename from feature/bookmarks/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModelTest.kt rename to feature/bookmarks/src/commonTest/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModelTest.kt diff --git a/feature/bookmarks/src/main/AndroidManifest.xml b/feature/bookmarks/src/main/AndroidManifest.xml deleted file mode 100644 index 51d0cfc2e..000000000 --- a/feature/bookmarks/src/main/AndroidManifest.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5590fa0d1..aa41c55d7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ androidxCoreSplashscreen = "1.0.1" androidxDataStore = "1.1.1" androidxEspresso = "3.5.1" androidxHiltNavigationCompose = "1.2.0" -androidxLifecycle = "2.7.0" +androidxLifecycle = "2.8.2" androidxMacroBenchmark = "1.2.4" androidxMetrics = "1.0.0-beta01" androidxNavigation = "2.8.0-beta03" @@ -72,6 +72,8 @@ kermit = "2.0.4" ktor = "2.3.11" ktrofit = "1.14.0" buildKonfig = "0.15.1" +lifecycle-viewmodel-compose = "2.8.0" +navigation-compose = "2.7.0-alpha07" [libraries] accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanist" } @@ -194,6 +196,8 @@ ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serializatio ktorfit-ksp = { group = "de.jensklingenberg.ktorfit", name = "ktorfit-ksp", version.ref = "ktrofit" } ktorfit-lib = { group = "de.jensklingenberg.ktorfit", name = "ktorfit-lib", version.ref = "ktrofit" } buildkonfig-gradlePlugin = { group = "com.codingfeline.buildkonfig", name = "buildkonfig-gradle-plugin", version.ref = "buildKonfig" } +jetbrains-compose-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle-viewmodel-compose" } +jetbrains-compose-navigation = { group = "org.jetbrains.androidx.navigation", name = "navigation-compose", version.ref = "navigation-compose" } # Dependencies of the included build-logic android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" } @@ -249,3 +253,4 @@ nowinandroid-jvm-library = { id = "nowinandroid.jvm.library", version = "unspeci nowinandroid-kmp-library = { id = "nowinandroid.kmp.library", version = "unspecified" } nowinandroid-kotlin-inject = { id = "nowinandroid.kmp.inject", version = "unspecified" } nowinandroid-sqldelight = { id = "nowinandroid.sqldelight", version = "unspecified" } +nowinandroid-cmp-feature = { id = "nowinandroid.cmp.feature", version = "unspecified" }