diff --git a/.github/workflows/Build.yaml b/.github/workflows/Build.yaml index af0fc830b..001140a87 100644 --- a/.github/workflows/Build.yaml +++ b/.github/workflows/Build.yaml @@ -147,6 +147,17 @@ jobs: api-level: [26, 30] steps: + - name: Delete unnecessary tools 🔧 + uses: jlumbroso/free-disk-space@v1.3.1 + with: + android: false # Don't remove Android tools + tool-cache: true # Remove image tool cache - rm -rf "$AGENT_TOOLSDIRECTORY" + dotnet: true # rm -rf /usr/share/dotnet + haskell: true # rm -rf /opt/ghc... + swap-storage: true # rm -f /mnt/swapfile (4GiB) + docker-images: false # Takes 16s, enable if needed in the future + large-packages: false # includes google-cloud-sdk and it's slow + - name: Enable KVM group perms run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules diff --git a/app/dependencies/prodReleaseRuntimeClasspath.txt b/app/dependencies/prodReleaseRuntimeClasspath.txt index 7e9fc3416..6cbde1cae 100644 --- a/app/dependencies/prodReleaseRuntimeClasspath.txt +++ b/app/dependencies/prodReleaseRuntimeClasspath.txt @@ -71,19 +71,20 @@ androidx.hilt:hilt-navigation:1.0.0 androidx.hilt:hilt-work:1.1.0 androidx.interpolator:interpolator:1.0.0 androidx.legacy:legacy-support-core-utils:1.0.0 -androidx.lifecycle:lifecycle-common-java8:2.6.2 -androidx.lifecycle:lifecycle-common:2.6.2 -androidx.lifecycle:lifecycle-livedata-core:2.6.2 -androidx.lifecycle:lifecycle-livedata:2.6.2 -androidx.lifecycle:lifecycle-process:2.6.2 -androidx.lifecycle:lifecycle-runtime-compose:2.6.2 -androidx.lifecycle:lifecycle-runtime-ktx:2.6.2 -androidx.lifecycle:lifecycle-runtime:2.6.2 -androidx.lifecycle:lifecycle-service:2.6.2 -androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2 -androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2 -androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.2 -androidx.lifecycle:lifecycle-viewmodel:2.6.2 +androidx.lifecycle:lifecycle-common-java8:2.7.0 +androidx.lifecycle:lifecycle-common:2.7.0 +androidx.lifecycle:lifecycle-livedata-core-ktx:2.7.0 +androidx.lifecycle:lifecycle-livedata-core:2.7.0 +androidx.lifecycle:lifecycle-livedata:2.7.0 +androidx.lifecycle:lifecycle-process:2.7.0 +androidx.lifecycle:lifecycle-runtime-compose:2.7.0 +androidx.lifecycle:lifecycle-runtime-ktx:2.7.0 +androidx.lifecycle:lifecycle-runtime:2.7.0 +androidx.lifecycle:lifecycle-service:2.7.0 +androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0 +androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0 +androidx.lifecycle:lifecycle-viewmodel-savedstate:2.7.0 +androidx.lifecycle:lifecycle-viewmodel:2.7.0 androidx.loader:loader:1.0.0 androidx.localbroadcastmanager:localbroadcastmanager:1.0.0 androidx.metrics:metrics-performance:1.0.0-alpha04 diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/util/ProfileVerifierLogger.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/util/ProfileVerifierLogger.kt index 595166f03..9ca4b3373 100644 --- a/app/src/main/java/com/google/samples/apps/nowinandroid/util/ProfileVerifierLogger.kt +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/util/ProfileVerifierLogger.kt @@ -18,7 +18,7 @@ package com.google.samples.apps.nowinandroid.util import android.util.Log import androidx.profileinstaller.ProfileVerifier -import com.google.samples.apps.nowinandroid.core.network.di.ApplicationScope +import com.google.samples.apps.nowinandroid.core.di.ApplicationScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.guava.await import kotlinx.coroutines.launch diff --git a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt index 8a76a1ac9..b8699a05d 100644 --- a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt @@ -44,8 +44,9 @@ class AndroidFeatureConventionPlugin : Plugin { add("implementation", libs.findLibrary("androidx.hilt.navigation.compose").get()) add("implementation", libs.findLibrary("androidx.lifecycle.runtimeCompose").get()) add("implementation", libs.findLibrary("androidx.lifecycle.viewModelCompose").get()) - add("implementation", libs.findLibrary("androidx.tracing.ktx").get()) + + add("androidTestImplementation", libs.findLibrary("androidx.lifecycle.runtimeTesting").get()) } } } diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinMultiplatform.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinMultiplatform.kt index cc99af1f7..5f3fb97c0 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinMultiplatform.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinMultiplatform.kt @@ -85,13 +85,12 @@ internal fun Project.configureKotlinMultiplatform() { project.tasks.named("linuxX64Test") { enabled = HostManager.hostIsLinux } project.tasks.named("linkDebugTestLinuxX64") { enabled = HostManager.hostIsLinux } - tasks.withType().configureEach { - kotlinOptions { - freeCompilerArgs = freeCompilerArgs + listOf( - // Suppress warning:'expect'/'actual' classes (including interfaces, objects, - // annotations, enums, and 'actual' typealiases) are in Beta. - "-Xexpect-actual-classes", - ) + // Suppress 'expect'/'actual' classes are in Beta. + targets.configureEach { + compilations.configureEach { + compilerOptions.configure { + freeCompilerArgs.addAll("-Xexpect-actual-classes") + } } } diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index 51ae627dc..cd2b1fbce 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Android Open Source Project + * 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. @@ -14,16 +14,24 @@ * limitations under the License. */ plugins { - alias(libs.plugins.nowinandroid.android.library) + alias(libs.plugins.nowinandroid.kmp.library) + alias(libs.plugins.nowinandroid.kotlin.inject) alias(libs.plugins.nowinandroid.android.library.jacoco) - alias(libs.plugins.nowinandroid.android.hilt) } android { namespace = "com.google.samples.apps.nowinandroid.core.common" } -dependencies { - testImplementation(libs.kotlinx.coroutines.test) - testImplementation(libs.turbine) -} \ No newline at end of file +kotlin { + sourceSets { + commonMain.dependencies { + implementation(libs.kotlinx.coroutines.core) + } + commonTest.dependencies { + implementation(libs.kotlin.test) + implementation(libs.turbine) + implementation(libs.kotlinx.coroutines.test) + } + } +} diff --git a/core/common/src/main/AndroidManifest.xml b/core/common/src/androidMain/AndroidManifest.xml similarity index 100% rename from core/common/src/main/AndroidManifest.xml rename to core/common/src/androidMain/AndroidManifest.xml diff --git a/core/common/src/androidMain/kotlin/com/google/samples/apps/nowinandroid/core/di/DispatchersComponent.kt b/core/common/src/androidMain/kotlin/com/google/samples/apps/nowinandroid/core/di/DispatchersComponent.kt new file mode 100644 index 000000000..6ae0c863d --- /dev/null +++ b/core/common/src/androidMain/kotlin/com/google/samples/apps/nowinandroid/core/di/DispatchersComponent.kt @@ -0,0 +1,30 @@ +/* + * 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.di + +import kotlinx.coroutines.Dispatchers +import me.tatarka.inject.annotations.Component +import me.tatarka.inject.annotations.Provides + +@Component +actual abstract class DispatchersComponent { + @Provides + actual fun providesIODispatcher(): IODispatcher = Dispatchers.IO + + @Provides + actual fun providesDefaultDispatcher(): DefaultDispatcher = Dispatchers.Default +} diff --git a/core/common/src/main/res/drawable-anydpi-v24/core_common_ic_nia_notification.xml b/core/common/src/androidMain/res/drawable-anydpi-v24/core_common_ic_nia_notification.xml similarity index 100% rename from core/common/src/main/res/drawable-anydpi-v24/core_common_ic_nia_notification.xml rename to core/common/src/androidMain/res/drawable-anydpi-v24/core_common_ic_nia_notification.xml diff --git a/core/common/src/main/res/drawable-hdpi/core_common_ic_nia_notification.png b/core/common/src/androidMain/res/drawable-hdpi/core_common_ic_nia_notification.png similarity index 100% rename from core/common/src/main/res/drawable-hdpi/core_common_ic_nia_notification.png rename to core/common/src/androidMain/res/drawable-hdpi/core_common_ic_nia_notification.png diff --git a/core/common/src/main/res/drawable-mdpi/core_common_ic_nia_notification.png b/core/common/src/androidMain/res/drawable-mdpi/core_common_ic_nia_notification.png similarity index 100% rename from core/common/src/main/res/drawable-mdpi/core_common_ic_nia_notification.png rename to core/common/src/androidMain/res/drawable-mdpi/core_common_ic_nia_notification.png diff --git a/core/common/src/main/res/drawable-xhdpi/core_common_ic_nia_notification.png b/core/common/src/androidMain/res/drawable-xhdpi/core_common_ic_nia_notification.png similarity index 100% rename from core/common/src/main/res/drawable-xhdpi/core_common_ic_nia_notification.png rename to core/common/src/androidMain/res/drawable-xhdpi/core_common_ic_nia_notification.png diff --git a/core/common/src/main/res/drawable-xxhdpi/core_common_ic_nia_notification.png b/core/common/src/androidMain/res/drawable-xxhdpi/core_common_ic_nia_notification.png similarity index 100% rename from core/common/src/main/res/drawable-xxhdpi/core_common_ic_nia_notification.png rename to core/common/src/androidMain/res/drawable-xxhdpi/core_common_ic_nia_notification.png diff --git a/core/common/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/di/CoroutineScopeComponent.kt b/core/common/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/di/CoroutineScopeComponent.kt new file mode 100644 index 000000000..c23890dd0 --- /dev/null +++ b/core/common/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/di/CoroutineScopeComponent.kt @@ -0,0 +1,32 @@ +/* + * 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.di + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import me.tatarka.inject.annotations.Component +import me.tatarka.inject.annotations.Provides + +typealias ApplicationScope = CoroutineScope + +@Component +abstract class CoroutineScopeComponent { + @Provides + fun providesCoroutineScope( + dispatcher: DefaultDispatcher, + ): ApplicationScope = CoroutineScope(SupervisorJob() + dispatcher) +} diff --git a/core/common/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/NiaDispatchers.kt b/core/common/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/di/DispatchersComponent.kt similarity index 55% rename from core/common/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/NiaDispatchers.kt rename to core/common/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/di/DispatchersComponent.kt index 9c21dd69a..3de817939 100644 --- a/core/common/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/NiaDispatchers.kt +++ b/core/common/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/di/DispatchersComponent.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Android Open Source Project + * 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. @@ -14,16 +14,18 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.core.network +package com.google.samples.apps.nowinandroid.core.di -import javax.inject.Qualifier -import kotlin.annotation.AnnotationRetention.RUNTIME +import kotlinx.coroutines.CoroutineDispatcher +import me.tatarka.inject.annotations.Provides -@Qualifier -@Retention(RUNTIME) -annotation class Dispatcher(val niaDispatcher: NiaDispatchers) +typealias DefaultDispatcher = CoroutineDispatcher +typealias IODispatcher = CoroutineDispatcher -enum class NiaDispatchers { - Default, - IO, +expect abstract class DispatchersComponent { + @Provides + fun providesIODispatcher(): IODispatcher + + @Provides + fun providesDefaultDispatcher(): DefaultDispatcher } diff --git a/core/common/src/main/kotlin/com/google/samples/apps/nowinandroid/core/result/Result.kt b/core/common/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/result/Result.kt similarity index 100% rename from core/common/src/main/kotlin/com/google/samples/apps/nowinandroid/core/result/Result.kt rename to core/common/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/result/Result.kt diff --git a/core/common/src/test/kotlin/com/google/samples/apps/nowinandroid/core/result/ResultKtTest.kt b/core/common/src/commonTest/kotlin/com/google/samples/apps/nowinandroid/core/result/ResultKtTest.kt similarity index 95% rename from core/common/src/test/kotlin/com/google/samples/apps/nowinandroid/core/result/ResultKtTest.kt rename to core/common/src/commonTest/kotlin/com/google/samples/apps/nowinandroid/core/result/ResultKtTest.kt index 4f1229e9d..5cd23a973 100644 --- a/core/common/src/test/kotlin/com/google/samples/apps/nowinandroid/core/result/ResultKtTest.kt +++ b/core/common/src/commonTest/kotlin/com/google/samples/apps/nowinandroid/core/result/ResultKtTest.kt @@ -19,7 +19,7 @@ package com.google.samples.apps.nowinandroid.core.result import app.cash.turbine.test import kotlinx.coroutines.flow.flow import kotlinx.coroutines.test.runTest -import org.junit.Test +import kotlin.test.Test import kotlin.test.assertEquals class ResultKtTest { @@ -38,7 +38,7 @@ class ResultKtTest { when (val errorResult = awaitItem()) { is Result.Error -> assertEquals( "Test Done", - errorResult.exception?.message, + errorResult.exception.message, ) Result.Loading, is Result.Success, diff --git a/core/common/src/jsMain/kotlin/com/google/samples/apps/nowinandroid/core/di/DispatchersComponent.kt b/core/common/src/jsMain/kotlin/com/google/samples/apps/nowinandroid/core/di/DispatchersComponent.kt new file mode 100644 index 000000000..e4edc24a7 --- /dev/null +++ b/core/common/src/jsMain/kotlin/com/google/samples/apps/nowinandroid/core/di/DispatchersComponent.kt @@ -0,0 +1,32 @@ +/* + * 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.di + +import kotlinx.coroutines.Dispatchers +import me.tatarka.inject.annotations.Component +import me.tatarka.inject.annotations.Provides + +@Component +actual abstract class DispatchersComponent { + + // TODO Provides an actual IODispatcher + @Provides + actual fun providesIODispatcher(): IODispatcher = Dispatchers.Default + + @Provides + actual fun providesDefaultDispatcher(): DefaultDispatcher = Dispatchers.Default +} diff --git a/core/common/src/jvmMain/kotlin/com/google/samples/apps/nowinandroid/core/di/DispatchersComponent.kt b/core/common/src/jvmMain/kotlin/com/google/samples/apps/nowinandroid/core/di/DispatchersComponent.kt new file mode 100644 index 000000000..6ae0c863d --- /dev/null +++ b/core/common/src/jvmMain/kotlin/com/google/samples/apps/nowinandroid/core/di/DispatchersComponent.kt @@ -0,0 +1,30 @@ +/* + * 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.di + +import kotlinx.coroutines.Dispatchers +import me.tatarka.inject.annotations.Component +import me.tatarka.inject.annotations.Provides + +@Component +actual abstract class DispatchersComponent { + @Provides + actual fun providesIODispatcher(): IODispatcher = Dispatchers.IO + + @Provides + actual fun providesDefaultDispatcher(): DefaultDispatcher = Dispatchers.Default +} diff --git a/core/common/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/di/CoroutineScopesModule.kt b/core/common/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/di/CoroutineScopesModule.kt deleted file mode 100644 index 6e7ca6bb3..000000000 --- a/core/common/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/di/CoroutineScopesModule.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2023 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.network.di - -import com.google.samples.apps.nowinandroid.core.network.Dispatcher -import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.Default -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import javax.inject.Qualifier -import javax.inject.Singleton - -@Retention(AnnotationRetention.RUNTIME) -@Qualifier -annotation class ApplicationScope - -@Module -@InstallIn(SingletonComponent::class) -internal object CoroutineScopesModule { - @Provides - @Singleton - @ApplicationScope - fun providesCoroutineScope( - @Dispatcher(Default) dispatcher: CoroutineDispatcher, - ): CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher) -} diff --git a/core/common/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/di/DispatchersModule.kt b/core/common/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/di/DispatchersModule.kt deleted file mode 100644 index 95ec07049..000000000 --- a/core/common/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/di/DispatchersModule.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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.network.di - -import com.google.samples.apps.nowinandroid.core.network.Dispatcher -import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.Default -import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers - -@Module -@InstallIn(SingletonComponent::class) -object DispatchersModule { - @Provides - @Dispatcher(IO) - fun providesIODispatcher(): CoroutineDispatcher = Dispatchers.IO - - @Provides - @Dispatcher(Default) - fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default -} diff --git a/core/common/src/nativeMain/kotlin/com/google/samples/apps/nowinandroid/core/di/DispatchersComponent.kt b/core/common/src/nativeMain/kotlin/com/google/samples/apps/nowinandroid/core/di/DispatchersComponent.kt new file mode 100644 index 000000000..e4edc24a7 --- /dev/null +++ b/core/common/src/nativeMain/kotlin/com/google/samples/apps/nowinandroid/core/di/DispatchersComponent.kt @@ -0,0 +1,32 @@ +/* + * 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.di + +import kotlinx.coroutines.Dispatchers +import me.tatarka.inject.annotations.Component +import me.tatarka.inject.annotations.Provides + +@Component +actual abstract class DispatchersComponent { + + // TODO Provides an actual IODispatcher + @Provides + actual fun providesIODispatcher(): IODispatcher = Dispatchers.Default + + @Provides + actual fun providesDefaultDispatcher(): DefaultDispatcher = Dispatchers.Default +} diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts index 27cf0206b..ac81f18d7 100644 --- a/core/database/build.gradle.kts +++ b/core/database/build.gradle.kts @@ -32,7 +32,9 @@ kotlin { sourceSets { commonMain.dependencies { api(projects.core.model) + implementation(projects.core.common) implementation(libs.kotlinx.datetime) + implementation(libs.kotlinx.coroutines.core) implementation(libs.sqldelight.coroutines.extensions) implementation(libs.sqldelight.primitive.adapters) } @@ -50,7 +52,8 @@ kotlin { } jsMain.dependencies { implementation(libs.sqldelight.webworker.driver) - implementation(npm("sql.js", "1.6.2")) + implementation(npm("@cashapp/sqldelight-sqljs-worker", "2.0.1")) + implementation(npm("sql.js", "1.8.0")) implementation(devNpm("copy-webpack-plugin", "9.1.0")) } commonTest.dependencies { @@ -69,3 +72,14 @@ sqldelight { } } } + +// Workaround yarn concurrency issue - https://youtrack.jetbrains.com/issue/KT-43320 +tasks.withType() + .configureEach { + args.addAll( + listOf( + "--mutex", + "file:${file("../build/.yarn-mutex")}", + ), + ) + } diff --git a/core/database/karma.config.d/sqljs-config.js b/core/database/karma.config.d/sqljs-config.js new file mode 100644 index 000000000..ef2be34df --- /dev/null +++ b/core/database/karma.config.d/sqljs-config.js @@ -0,0 +1,30 @@ +const path = require("path"); +const os = require("os"); +const dist = path.resolve("../../node_modules/sql.js/dist/") +const wasm = path.join(dist, "sql-wasm.wasm") + +config.files.push({ + pattern: wasm, + served: true, + watched: false, + included: false, + nocache: false, +}); + +config.proxies["/sql-wasm.wasm"] = `/absolute${wasm}` + +// Adapted from: https://github.com/ryanclark/karma-webpack/issues/498#issuecomment-790040818 +const output = { + path: path.join(os.tmpdir(), '_karma_webpack_') + Math.floor(Math.random() * 1000000), +} +config.set({ + webpack: {...config.webpack, output} +}); +config.files.push({ + pattern: `${output.path}/**/*`, + watched: false, + included: false, +}); + +// TODO: Figure out why on earth this is necessary. Presumably a karma-webpack bug??? +delete config.webpack.optimization; diff --git a/core/database/src/androidMain/kotlin/com/google/samples/apps/nowinandroid/core/database/DriverModule.kt b/core/database/src/androidMain/kotlin/com/google/samples/apps/nowinandroid/core/database/DriverModule.kt index 4e703461d..e5eb5090b 100644 --- a/core/database/src/androidMain/kotlin/com/google/samples/apps/nowinandroid/core/database/DriverModule.kt +++ b/core/database/src/androidMain/kotlin/com/google/samples/apps/nowinandroid/core/database/DriverModule.kt @@ -17,21 +17,33 @@ package com.google.samples.apps.nowinandroid.core.database import android.content.Context +import androidx.sqlite.db.SupportSQLiteDatabase import app.cash.sqldelight.async.coroutines.synchronous import app.cash.sqldelight.db.QueryResult import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.db.SqlSchema import app.cash.sqldelight.driver.android.AndroidSqliteDriver +import me.tatarka.inject.annotations.Component import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Provides -@Inject -actual class DriverModule(private val context: Context) { +@Component +internal actual abstract class DriverModule(private val context: Context) { @Provides actual suspend fun provideDbDriver( schema: SqlSchema>, ): SqlDriver { - return AndroidSqliteDriver(schema.synchronous(), context, "nia-database.db") + val synchronousSchema = schema.synchronous() + return AndroidSqliteDriver( + schema = synchronousSchema, + context = context, + name = "nia-database.db", + callback = object : AndroidSqliteDriver.Callback(synchronousSchema) { + override fun onOpen(db: SupportSQLiteDatabase) { + db.setForeignKeyConstraintsEnabled(true) + } + }, + ) } } diff --git a/core/database/src/androidUnitTest/kotlin/com/google/samples/apps/nowinandroid/core/database/BaseTest.android.kt b/core/database/src/androidUnitTest/kotlin/com/google/samples/apps/nowinandroid/core/database/BaseTest.android.kt index 56e63a063..14e178877 100644 --- a/core/database/src/androidUnitTest/kotlin/com/google/samples/apps/nowinandroid/core/database/BaseTest.android.kt +++ b/core/database/src/androidUnitTest/kotlin/com/google/samples/apps/nowinandroid/core/database/BaseTest.android.kt @@ -18,8 +18,12 @@ package com.google.samples.apps.nowinandroid.core.database import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver +import java.util.Properties actual suspend fun createDriver(): SqlDriver { - return JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY) + return JdbcSqliteDriver( + url = JdbcSqliteDriver.IN_MEMORY, + properties = Properties().apply { put("foreign_keys", "true") }, + ) .also { NiaDatabase.Schema.create(it).await() } } diff --git a/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/DatabaseModule.kt b/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/DatabaseModule.kt index d5bddadc4..7fec4df6f 100644 --- a/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/DatabaseModule.kt +++ b/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/DatabaseModule.kt @@ -22,35 +22,43 @@ import com.google.samples.apps.nowinandroid.core.database.dao.NewsResourceFtsDao import com.google.samples.apps.nowinandroid.core.database.dao.RecentSearchQueryDao import com.google.samples.apps.nowinandroid.core.database.dao.TopicDao import com.google.samples.apps.nowinandroid.core.database.dao.TopicFtsDao +import com.google.samples.apps.nowinandroid.core.di.IODispatcher import kotlinx.coroutines.Dispatchers +import me.tatarka.inject.annotations.Component import me.tatarka.inject.annotations.Provides -internal object DatabaseModule { +@Component +internal abstract class DatabaseModule { @Provides fun providesNiaDatabase(driver: SqlDriver): NiaDatabase = NiaDatabase(driver) @Provides fun providesTopicsDao( database: NiaDatabase, - ): TopicDao = TopicDao(database, Dispatchers.Default) + dispatcher: IODispatcher, + ): TopicDao = TopicDao(database, dispatcher) @Provides fun providesNewsResourceDao( database: NiaDatabase, - ): NewsResourceDao = NewsResourceDao(database, Dispatchers.Default) + dispatcher: IODispatcher, + ): NewsResourceDao = NewsResourceDao(database, dispatcher) @Provides fun providesTopicFtsDao( database: NiaDatabase, - ): TopicFtsDao = TopicFtsDao(database, Dispatchers.Default) + dispatcher: IODispatcher, + ): TopicFtsDao = TopicFtsDao(database, dispatcher) @Provides fun providesNewsResourceFtsDao( database: NiaDatabase, - ): NewsResourceFtsDao = NewsResourceFtsDao(database, Dispatchers.Default) + dispatcher: IODispatcher, + ): NewsResourceFtsDao = NewsResourceFtsDao(database, dispatcher) @Provides fun providesRecentSearchQueryDao( database: NiaDatabase, - ): RecentSearchQueryDao = RecentSearchQueryDao(database, Dispatchers.Default) + dispatcher: IODispatcher, + ): RecentSearchQueryDao = RecentSearchQueryDao(database, dispatcher) } diff --git a/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/DriverModule.kt b/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/DriverModule.kt index aaeaf9b15..4d79ddaf1 100644 --- a/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/DriverModule.kt +++ b/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/DriverModule.kt @@ -21,7 +21,7 @@ import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.db.SqlSchema import me.tatarka.inject.annotations.Provides -expect class DriverModule { +internal expect abstract class DriverModule { @Provides suspend fun provideDbDriver( schema: SqlSchema>, 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 a49e44ca3..73e27ef38 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 @@ -126,8 +126,12 @@ class NewsResourceDao(db: NiaDatabase, private val dispatcher: CoroutineDispatch suspend fun insertOrIgnoreTopicCrossRefEntities( newsResourceTopicCrossReferences: List, ) { - // TODO Consider removing cross references -// query.insertOrIgnoreNewsResourceTopicCrossRefs(newsResourceTopicCrossReferences) + newsResourceTopicCrossReferences.forEach { + query.insertOrIgnoreTopicCrossRefEntitiy( + news_resource_id = it.newsResourceId, + topic_id = it.topicId, + ) + } } /** diff --git a/core/database/src/commonMain/sqldelight/com/google/samples/apps/nowinandroid/core/database/NewsResource.sq b/core/database/src/commonMain/sqldelight/com/google/samples/apps/nowinandroid/core/database/NewsResource.sq index 85cef42cc..73c687c29 100644 --- a/core/database/src/commonMain/sqldelight/com/google/samples/apps/nowinandroid/core/database/NewsResource.sq +++ b/core/database/src/commonMain/sqldelight/com/google/samples/apps/nowinandroid/core/database/NewsResource.sq @@ -59,7 +59,7 @@ header_image_url = excluded.header_image_url, publish_date = excluded.publish_date, type = excluded.type; -insertOrIgnoreTopicCrossRefEntities: +insertOrIgnoreTopicCrossRefEntitiy: INSERT OR IGNORE INTO news_resources_topics (news_resource_id, topic_id) VALUES (?, ?); diff --git a/core/database/src/commonMain/sqldelight/com/google/samples/apps/nowinandroid/core/database/NewsResourceFts.sq b/core/database/src/commonMain/sqldelight/com/google/samples/apps/nowinandroid/core/database/NewsResourceFts.sq index 59795c2e4..d951d917b 100644 --- a/core/database/src/commonMain/sqldelight/com/google/samples/apps/nowinandroid/core/database/NewsResourceFts.sq +++ b/core/database/src/commonMain/sqldelight/com/google/samples/apps/nowinandroid/core/database/NewsResourceFts.sq @@ -2,18 +2,18 @@ CREATE VIRTUAL TABLE news_resource_fts USING FTS4( news_resource_id TEXT NOT NULL, title TEXT NOT NULL, - content TEXT NOT NULL, + content TEXT NOT NULL ); -- Triggers to keep the FTS index up to date. CREATE TRIGGER news_resource_ai AFTER INSERT ON news_resource BEGIN INSERT INTO news_resource_fts (rowid, news_resource_id, title, content) VALUES (new.rowid, new.id, new.title, new.content); END; CREATE TRIGGER news_resource_ad AFTER DELETE ON news_resource BEGIN - INSERT INTO news_resource_fts (news_resource_fts, rowid, news_resource_id, title, content) VALUES ('delete', old.rowid, old.id, old.title, old.content); + DELETE FROM news_resource_fts WHERE news_resource_id = old.id; END; CREATE TRIGGER news_resource_au AFTER UPDATE ON news_resource BEGIN - INSERT INTO news_resource_fts (news_resource_fts, rowid, news_resource_id, title, content) VALUES ('delete', old.rowid, old.id, old.title, old.content); - INSERT INTO news_resource_fts (rowid, news_resource_id, title, content) VALUES (new.rowid, new.id, new.title, new.content); + DELETE FROM news_resource_fts WHERE news_resource_id = old.id; + INSERT INTO news_resource_fts (news_resource_id, title, content) VALUES (new.id, new.title, new.content); END; insert: diff --git a/core/database/src/commonMain/sqldelight/com/google/samples/apps/nowinandroid/core/database/NewsResourceTopicCrossRef.sq b/core/database/src/commonMain/sqldelight/com/google/samples/apps/nowinandroid/core/database/NewsResourceTopicCrossRef.sq index 2100a8a3c..d8f900b9e 100644 --- a/core/database/src/commonMain/sqldelight/com/google/samples/apps/nowinandroid/core/database/NewsResourceTopicCrossRef.sq +++ b/core/database/src/commonMain/sqldelight/com/google/samples/apps/nowinandroid/core/database/NewsResourceTopicCrossRef.sq @@ -6,6 +6,6 @@ CREATE TABLE IF NOT EXISTS news_resources_topics ( FOREIGN KEY (topic_id) REFERENCES topic(id) ON DELETE CASCADE ); -CREATE INDEX idx_news_resource_id ON news_resource_topic(news_resource_id); +CREATE INDEX idx_news_resource_id ON news_resources_topics(news_resource_id); -CREATE INDEX idx_topic_id ON news_resource_topic(topic_id); +CREATE INDEX idx_topic_id ON news_resources_topics(topic_id); diff --git a/core/database/src/commonTest/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceDaoTest.kt b/core/database/src/commonTest/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceDaoTest.kt index 8b873e8cb..998e0d628 100644 --- a/core/database/src/commonTest/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceDaoTest.kt +++ b/core/database/src/commonTest/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceDaoTest.kt @@ -26,24 +26,26 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import kotlinx.datetime.Instant -import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals class NewsResourceDaoTest { - private lateinit var newsResourceDao: NewsResourceDao private lateinit var topicDao: TopicDao - @BeforeTest - fun setup() = runTest { - val db = NiaDatabase(createDriver()) - newsResourceDao = NewsResourceDao(db, Dispatchers.Unconfined) - topicDao = TopicDao(db, Dispatchers.Unconfined) - } + // BeforeTest seems not working properly in Kotlin JS + // UninitializedPropertyAccessException: lateinit property newsResourceDao has not been initialized +// @BeforeTest +// fun setup() = runTest { +// val db = NiaDatabase(createDriver()) +// newsResourceDao = NewsResourceDao(db, Dispatchers.Unconfined) +// topicDao = TopicDao(db, Dispatchers.Unconfined) +// } @Test fun newsResourceDao_fetches_items_by_descending_publish_date() = runTest { + val db = NiaDatabase(createDriver()) + val newsResourceDao = NewsResourceDao(db, Dispatchers.Unconfined) val newsResourceEntities = listOf( testNewsResource( id = "0", @@ -79,6 +81,8 @@ class NewsResourceDaoTest { @Test fun newsResourceDao_filters_items_by_news_ids_by_descending_publish_date() = runTest { + val db = NiaDatabase(createDriver()) + val newsResourceDao = NewsResourceDao(db, Dispatchers.Unconfined) val newsResourceEntities = listOf( testNewsResource( id = "0", @@ -117,6 +121,9 @@ class NewsResourceDaoTest { @Test fun newsResourceDao_filters_items_by_topic_ids_by_descending_publish_date() = runTest { + val db = NiaDatabase(createDriver()) + val newsResourceDao = NewsResourceDao(db, Dispatchers.Unconfined) + val topicDao = TopicDao(db, Dispatchers.Unconfined) val topicEntities = listOf( testTopicEntity( id = "1", @@ -177,6 +184,9 @@ class NewsResourceDaoTest { @Test fun newsResourceDao_filters_items_by_news_and_topic_ids_by_descending_publish_date() = runTest { + val db = NiaDatabase(createDriver()) + val newsResourceDao = NewsResourceDao(db, Dispatchers.Unconfined) + val topicDao = TopicDao(db, Dispatchers.Unconfined) val topicEntities = listOf( testTopicEntity( id = "1", @@ -238,42 +248,43 @@ class NewsResourceDaoTest { } @Test - fun newsResourceDao_deletes_items_by_ids() = - runTest { - val newsResourceEntities = listOf( - testNewsResource( - id = "0", - millisSinceEpoch = 0, - ), - testNewsResource( - id = "1", - millisSinceEpoch = 3, - ), - testNewsResource( - id = "2", - millisSinceEpoch = 1, - ), - testNewsResource( - id = "3", - millisSinceEpoch = 2, - ), - ) - newsResourceDao.upsertNewsResources(newsResourceEntities) + fun newsResourceDao_deletes_items_by_ids() = runTest { + val db = NiaDatabase(createDriver()) + val newsResourceDao = NewsResourceDao(db, Dispatchers.Unconfined) + val newsResourceEntities = listOf( + testNewsResource( + id = "0", + millisSinceEpoch = 0, + ), + testNewsResource( + id = "1", + millisSinceEpoch = 3, + ), + testNewsResource( + id = "2", + millisSinceEpoch = 1, + ), + testNewsResource( + id = "3", + millisSinceEpoch = 2, + ), + ) + newsResourceDao.upsertNewsResources(newsResourceEntities) - val (toDelete, toKeep) = newsResourceEntities.partition { it.id.toInt() % 2 == 0 } + val (toDelete, toKeep) = newsResourceEntities.partition { it.id.toInt() % 2 == 0 } - newsResourceDao.deleteNewsResources( - toDelete.map(NewsResourceEntity::id), - ) + newsResourceDao.deleteNewsResources( + toDelete.map(NewsResourceEntity::id), + ) - assertEquals( - toKeep.map(NewsResourceEntity::id) - .toSet(), - newsResourceDao.getNewsResources().first() - .map { it.entity.id } - .toSet(), - ) - } + assertEquals( + toKeep.map(NewsResourceEntity::id) + .toSet(), + newsResourceDao.getNewsResources().first() + .map { it.entity.id } + .toSet(), + ) + } } private fun testTopicEntity( diff --git a/core/database/src/jsMain/kotlin/com/google/samples/apps/nowinandroid/core/database/DriverModule.kt b/core/database/src/jsMain/kotlin/com/google/samples/apps/nowinandroid/core/database/DriverModule.kt index 4a34bc105..5f1a9c977 100644 --- a/core/database/src/jsMain/kotlin/com/google/samples/apps/nowinandroid/core/database/DriverModule.kt +++ b/core/database/src/jsMain/kotlin/com/google/samples/apps/nowinandroid/core/database/DriverModule.kt @@ -20,10 +20,12 @@ import app.cash.sqldelight.db.QueryResult import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.db.SqlSchema import app.cash.sqldelight.driver.worker.WebWorkerDriver +import me.tatarka.inject.annotations.Component import me.tatarka.inject.annotations.Provides import org.w3c.dom.Worker -actual class DriverModule { +@Component +internal actual abstract class DriverModule { @Provides actual suspend fun provideDbDriver( schema: SqlSchema>, diff --git a/core/database/src/jvmMain/kotlin/com/google/samples/apps/nowinandroid/core/database/DriverModule.kt b/core/database/src/jvmMain/kotlin/com/google/samples/apps/nowinandroid/core/database/DriverModule.kt index 8b8e797dc..220e575d5 100644 --- a/core/database/src/jvmMain/kotlin/com/google/samples/apps/nowinandroid/core/database/DriverModule.kt +++ b/core/database/src/jvmMain/kotlin/com/google/samples/apps/nowinandroid/core/database/DriverModule.kt @@ -20,14 +20,20 @@ import app.cash.sqldelight.db.QueryResult import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.db.SqlSchema import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver +import me.tatarka.inject.annotations.Component import me.tatarka.inject.annotations.Provides +import java.util.Properties -actual class DriverModule { +@Component +internal actual abstract class DriverModule { @Provides actual suspend fun provideDbDriver( schema: SqlSchema>, ): SqlDriver { - return JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY) + return JdbcSqliteDriver( + url = JdbcSqliteDriver.IN_MEMORY, + properties = Properties().apply { put("foreign_keys", "true") }, + ) .also { schema.create(it).await() } } } diff --git a/core/database/src/jvmTest/kotlin/com/google/samples/apps/nowinandroid/core/database/BaseTest.jvm.kt b/core/database/src/jvmTest/kotlin/com/google/samples/apps/nowinandroid/core/database/BaseTest.jvm.kt index a77aa2349..d650bbf6f 100644 --- a/core/database/src/jvmTest/kotlin/com/google/samples/apps/nowinandroid/core/database/BaseTest.jvm.kt +++ b/core/database/src/jvmTest/kotlin/com/google/samples/apps/nowinandroid/core/database/BaseTest.jvm.kt @@ -19,8 +19,12 @@ package com.google.samples.apps.nowinandroid.core.database import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver import com.google.samples.apps.nowinandroid.core.database.NiaDatabase.Companion.Schema +import java.util.Properties actual suspend fun createDriver(): SqlDriver { - return JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY) + return JdbcSqliteDriver( + url = JdbcSqliteDriver.IN_MEMORY, + properties = Properties().apply { put("foreign_keys", "true") }, + ) .also { Schema.create(it).await() } } diff --git a/core/database/src/nativeMain/kotlin/com/google/samples/apps/nowinandroid/core/database/DriverModule.kt b/core/database/src/nativeMain/kotlin/com/google/samples/apps/nowinandroid/core/database/DriverModule.kt index cd54d58d2..3c4fce8ba 100644 --- a/core/database/src/nativeMain/kotlin/com/google/samples/apps/nowinandroid/core/database/DriverModule.kt +++ b/core/database/src/nativeMain/kotlin/com/google/samples/apps/nowinandroid/core/database/DriverModule.kt @@ -21,14 +21,26 @@ import app.cash.sqldelight.db.QueryResult import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.db.SqlSchema import app.cash.sqldelight.driver.native.NativeSqliteDriver +import co.touchlab.sqliter.DatabaseConfiguration +import me.tatarka.inject.annotations.Component import me.tatarka.inject.annotations.Provides -actual class DriverModule { +@Component +internal actual abstract class DriverModule { @Provides actual suspend fun provideDbDriver( schema: SqlSchema>, ): SqlDriver { - return NativeSqliteDriver(schema.synchronous(), "nia-database.db") + val synchronousSchema = schema.synchronous() + return NativeSqliteDriver( + schema = synchronousSchema, + name = "nia-database.db", + onConfiguration = { config: DatabaseConfiguration -> + config.copy( + extendedConfig = DatabaseConfiguration.Extended(foreignKeyConstraints = true), + ) + }, + ) } } diff --git a/core/database/src/nativeTest/kotlin/com/google/samples/apps/nowinandroid/core/database/BaseTest.native.kt b/core/database/src/nativeTest/kotlin/com/google/samples/apps/nowinandroid/core/database/BaseTest.native.kt index d7b6b1c9d..ae6d56d81 100644 --- a/core/database/src/nativeTest/kotlin/com/google/samples/apps/nowinandroid/core/database/BaseTest.native.kt +++ b/core/database/src/nativeTest/kotlin/com/google/samples/apps/nowinandroid/core/database/BaseTest.native.kt @@ -19,8 +19,17 @@ package com.google.samples.apps.nowinandroid.core.database import app.cash.sqldelight.async.coroutines.synchronous import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.native.NativeSqliteDriver +import co.touchlab.sqliter.DatabaseConfiguration import com.google.samples.apps.nowinandroid.core.database.NiaDatabase.Companion.Schema actual suspend fun createDriver(): SqlDriver { - return NativeSqliteDriver(Schema.synchronous(), "nia-database-test.db") + return NativeSqliteDriver( + schema = Schema.synchronous(), + name = "nia-database-test.db", + onConfiguration = { config: DatabaseConfiguration -> + config.copy( + extendedConfig = DatabaseConfiguration.Extended(foreignKeyConstraints = true), + ) + }, + ) } diff --git a/core/database/webpack.config.d/sqljs-config.js b/core/database/webpack.config.d/sqljs-config.js new file mode 100644 index 000000000..03f701073 --- /dev/null +++ b/core/database/webpack.config.d/sqljs-config.js @@ -0,0 +1,16 @@ +config.resolve = { + fallback: { + fs: false, + path: false, + crypto: false, + } +}; + +const CopyWebpackPlugin = require('copy-webpack-plugin'); +config.plugins.push( + new CopyWebpackPlugin({ + patterns: [ + '../../node_modules/sql.js/dist/sql-wasm.wasm' + ] + }) +); diff --git a/core/datastore-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/datastore/test/TestDataStoreModule.kt b/core/datastore-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/datastore/test/TestDataStoreModule.kt index 295b2978a..b4a5cb182 100644 --- a/core/datastore-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/datastore/test/TestDataStoreModule.kt +++ b/core/datastore-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/datastore/test/TestDataStoreModule.kt @@ -21,7 +21,7 @@ import androidx.datastore.core.DataStoreFactory import com.google.samples.apps.nowinandroid.core.datastore.UserPreferences import com.google.samples.apps.nowinandroid.core.datastore.UserPreferencesSerializer import com.google.samples.apps.nowinandroid.core.datastore.di.DataStoreModule -import com.google.samples.apps.nowinandroid.core.network.di.ApplicationScope +import com.google.samples.apps.nowinandroid.core.di.ApplicationScope import dagger.Module import dagger.Provides import dagger.hilt.components.SingletonComponent diff --git a/core/datastore/src/main/kotlin/com/google/samples/apps/nowinandroid/core/datastore/di/DataStoreModule.kt b/core/datastore/src/main/kotlin/com/google/samples/apps/nowinandroid/core/datastore/di/DataStoreModule.kt index 8e0d7d4d8..e54760ead 100644 --- a/core/datastore/src/main/kotlin/com/google/samples/apps/nowinandroid/core/datastore/di/DataStoreModule.kt +++ b/core/datastore/src/main/kotlin/com/google/samples/apps/nowinandroid/core/datastore/di/DataStoreModule.kt @@ -23,9 +23,9 @@ import androidx.datastore.dataStoreFile import com.google.samples.apps.nowinandroid.core.datastore.IntToStringIdsMigration import com.google.samples.apps.nowinandroid.core.datastore.UserPreferences import com.google.samples.apps.nowinandroid.core.datastore.UserPreferencesSerializer +import com.google.samples.apps.nowinandroid.core.di.ApplicationScope import com.google.samples.apps.nowinandroid.core.network.Dispatcher import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO -import com.google.samples.apps.nowinandroid.core.network.di.ApplicationScope import dagger.Module import dagger.Provides import dagger.hilt.InstallIn diff --git a/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/di/TestDispatchersModule.kt b/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/di/TestDispatchersModule.kt index 4f5d15be1..a664d5538 100644 --- a/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/di/TestDispatchersModule.kt +++ b/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/di/TestDispatchersModule.kt @@ -16,10 +16,10 @@ package com.google.samples.apps.nowinandroid.core.testing.di +import com.google.samples.apps.nowinandroid.core.di.DispatchersComponent import com.google.samples.apps.nowinandroid.core.network.Dispatcher import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.Default import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO -import com.google.samples.apps.nowinandroid.core.network.di.DispatchersModule import dagger.Module import dagger.Provides import dagger.hilt.components.SingletonComponent @@ -30,7 +30,7 @@ import kotlinx.coroutines.test.TestDispatcher @Module @TestInstallIn( components = [SingletonComponent::class], - replaces = [DispatchersModule::class], + replaces = [DispatchersComponent::class], ) internal object TestDispatchersModule { @Provides diff --git a/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/repository/TestSearchContentsRepository.kt b/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/repository/TestSearchContentsRepository.kt index 9b6151449..5436cd10f 100644 --- a/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/repository/TestSearchContentsRepository.kt +++ b/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/repository/TestSearchContentsRepository.kt @@ -21,42 +21,36 @@ import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.SearchResult import com.google.samples.apps.nowinandroid.core.model.data.Topic import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.update +import org.jetbrains.annotations.TestOnly class TestSearchContentsRepository : SearchContentsRepository { - private val cachedTopics: MutableList = mutableListOf() - private val cachedNewsResources: MutableList = mutableListOf() + private val cachedTopics = MutableStateFlow(emptyList()) + private val cachedNewsResources = MutableStateFlow(emptyList()) override suspend fun populateFtsData() = Unit - override fun searchContents(searchQuery: String): Flow = flowOf( - SearchResult( - topics = cachedTopics.filter { - searchQuery in it.name || searchQuery in it.shortDescription || searchQuery in it.longDescription - }, - newsResources = cachedNewsResources.filter { - searchQuery in it.content || searchQuery in it.title - }, - ), - ) - - override fun getSearchContentsCount(): Flow = flow { - emit(cachedTopics.size + cachedNewsResources.size) - } - - /** - * Test only method to add the topics to the stored list in memory - */ - fun addTopics(topics: List) { - cachedTopics.addAll(topics) - } - - /** - * Test only method to add the news resources to the stored list in memory - */ - fun addNewsResources(newsResources: List) { - cachedNewsResources.addAll(newsResources) - } + override fun searchContents(searchQuery: String): Flow = + combine(cachedTopics, cachedNewsResources) { topics, news -> + SearchResult( + topics = topics.filter { + searchQuery in it.name || searchQuery in it.shortDescription || searchQuery in it.longDescription + }, + newsResources = news.filter { + searchQuery in it.content || searchQuery in it.title + }, + ) + } + + override fun getSearchContentsCount(): Flow = combine(cachedTopics, cachedNewsResources) { topics, news -> topics.size + news.size } + + @TestOnly + fun addTopics(topics: List) = cachedTopics.update { it + topics } + + @TestOnly + fun addNewsResources(newsResources: List) = + cachedNewsResources.update { it + newsResources } } diff --git a/feature/bookmarks/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt b/feature/bookmarks/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt index 3d684f9d1..40f54e4a7 100644 --- a/feature/bookmarks/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt +++ b/feature/bookmarks/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt @@ -17,6 +17,8 @@ package com.google.samples.apps.nowinandroid.feature.bookmarks import androidx.activity.ComponentActivity +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.filter @@ -30,8 +32,11 @@ import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollToNode +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.testing.TestLifecycleOwner import com.google.samples.apps.nowinandroid.core.testing.data.userNewsResourcesTestData import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState +import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test import kotlin.test.assertEquals @@ -166,4 +171,29 @@ class BookmarksScreenTest { ) .assertExists() } + + @Test + fun feed_whenLifecycleStops_undoBookmarkedStateIsCleared() = runTest { + var undoStateCleared = false + val testLifecycleOwner = TestLifecycleOwner(initialState = Lifecycle.State.STARTED) + + composeTestRule.setContent { + CompositionLocalProvider(LocalLifecycleOwner provides testLifecycleOwner) { + BookmarksScreen( + feedState = NewsFeedUiState.Success(emptyList()), + onShowSnackbar = { _, _ -> false }, + removeFromBookmarks = {}, + onTopicClick = {}, + onNewsResourceViewed = {}, + clearUndoState = { + undoStateCleared = true + }, + ) + } + } + + assertEquals(false, undoStateCleared) + testLifecycleOwner.handleLifecycleEvent(event = Lifecycle.Event.ON_STOP) + assertEquals(true, undoStateCleared) + } } diff --git a/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt b/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt index 5b54db7cd..7c229c5ea 100644 --- a/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt +++ b/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt @@ -42,14 +42,12 @@ import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridS import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -60,7 +58,7 @@ 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.LifecycleEventObserver +import androidx.lifecycle.compose.LifecycleEventEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.DraggableScrollbar @@ -128,15 +126,8 @@ internal fun BookmarksScreen( } } - val lifecycleOwner = LocalLifecycleOwner.current - DisposableEffect(lifecycleOwner) { - val observer = LifecycleEventObserver { _, event -> - if (event == Lifecycle.Event.ON_STOP) { - clearUndoState() - } - } - lifecycleOwner.lifecycle.addObserver(observer) - onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } + LifecycleEventEffect(Lifecycle.Event.ON_STOP) { + clearUndoState() } when (feedState) { diff --git a/feature/search/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchViewModelTest.kt b/feature/search/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchViewModelTest.kt index fc9c20549..da0d5654e 100644 --- a/feature/search/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchViewModelTest.kt +++ b/feature/search/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchViewModelTest.kt @@ -26,6 +26,7 @@ import com.google.samples.apps.nowinandroid.core.testing.data.topicsTestData import com.google.samples.apps.nowinandroid.core.testing.repository.TestRecentSearchRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestSearchContentsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository +import com.google.samples.apps.nowinandroid.core.testing.repository.emptyUserData import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule import com.google.samples.apps.nowinandroid.feature.search.RecentSearchQueriesUiState.Success import com.google.samples.apps.nowinandroid.feature.search.SearchResultUiState.EmptyQuery @@ -71,6 +72,7 @@ class SearchViewModelTest { recentSearchRepository = recentSearchRepository, analyticsHelper = NoOpAnalyticsHelper(), ) + userDataRepository.setUserData(emptyUserData) } @Test @@ -100,8 +102,7 @@ class SearchViewModelTest { searchContentsRepository.addTopics(topicsTestData) val result = viewModel.searchResultUiState.value - // TODO: Figure out to get the latest emitted ui State? The result is emitted as EmptyQuery - // assertIs(result) + assertIs(result) collectJob.cancel() } diff --git a/gradle.properties b/gradle.properties index c0acfeb02..82b326eab 100644 --- a/gradle.properties +++ b/gradle.properties @@ -39,3 +39,6 @@ kotlin.code.style=official # https://developer.android.com/build/releases/gradle-plugin#default-changes android.defaults.buildfeatures.resvalues=false android.defaults.buildfeatures.shaders=false + +# Suppress: The following Kotlin/Native targets cannot be built on this machine and are disabled +kotlin.native.ignoreDisabledTargets=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cd8db06c4..aafac75ef 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,7 +15,7 @@ androidxCoreSplashscreen = "1.0.1" androidxDataStore = "1.0.0" androidxEspresso = "3.5.1" androidxHiltNavigationCompose = "1.0.0" -androidxLifecycle = "2.6.2" +androidxLifecycle = "2.7.0" androidxMacroBenchmark = "1.2.3" androidxMetrics = "1.0.0-alpha04" androidxNavigation = "2.7.7" @@ -98,6 +98,7 @@ androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscree androidx-dataStore-core = { group = "androidx.datastore", name = "datastore", version.ref = "androidxDataStore" } androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "androidxHiltNavigationCompose" } androidx-lifecycle-runtimeCompose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycle" } +androidx-lifecycle-runtimeTesting = { group = "androidx.lifecycle", name = "lifecycle-runtime-testing", version.ref = "androidxLifecycle" } androidx-lifecycle-viewModelCompose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" } androidx-metrics = { group = "androidx.metrics", name = "metrics-performance", version.ref = "androidxMetrics" } androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" } @@ -129,6 +130,7 @@ hilt-ext-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.r hilt-ext-work = { group = "androidx.hilt", name = "hilt-work", version.ref = "hiltExt" } javax-inject = { module = "javax.inject:javax.inject", version = "1" } kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib-jdk8", version.ref = "kotlin" } +kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } kotlinx-coroutines-guava = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-guava", version.ref = "kotlinxCoroutines" } kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" } kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinxDatetime" } diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index 366040fcd..94c5abfdb 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@cashapp/sqldelight-sqljs-worker@2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@cashapp/sqldelight-sqljs-worker/-/sqldelight-sqljs-worker-2.0.1.tgz#81964d4954fb24ba4e5019343ea20fba27a8cac4" + integrity sha512-wzOjAverdz1F8eiAI4fP3WObu1+geXbLSpX03WIXoDzmJ2XS2AByW4x1CpGnGZutX3AfjyqpcXL4wv9ZOH1aKQ== + "@colors/colors@1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" @@ -1789,10 +1794,10 @@ source-map@^0.6.0, source-map@^0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -sql.js@1.6.2: - version "1.6.2" - resolved "https://registry.yarnpkg.com/sql.js/-/sql.js-1.6.2.tgz#7fb70ff68089434826a39b8f5afb2170d682eb3f" - integrity sha512-9iucI5fXQa+Gspeqf/BNB20PxJIn5LhXDt4mjXoFPqXdR+NqtFs15SdKpSIJ6s529aGL9zFR9p2eSCIEiMsNGA== +sql.js@1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/sql.js/-/sql.js-1.8.0.tgz#cb45d957e17a2239662fe2f614c9b678990867a6" + integrity sha512-3HD8pSkZL+5YvYUI8nlvNILs61ALqq34xgmF+BHpqxe68yZIJ1H+sIVIODvni25+CcxHUxDyrTJUL0lE/m7afw== statuses@2.0.1: version "2.0.1"