From b5bce8feee856b6d4aef660f743ebd80948601c5 Mon Sep 17 00:00:00 2001 From: bpedryc Date: Wed, 31 May 2023 08:57:02 +0200 Subject: [PATCH 01/12] Make use of latest collectAsStateWithLifecycle() API --- .../co/touchlab/kampkit/android/ui/Composables.kt | 14 ++------------ gradle/libs.versions.toml | 4 +++- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/app/src/main/kotlin/co/touchlab/kampkit/android/ui/Composables.kt b/app/src/main/kotlin/co/touchlab/kampkit/android/ui/Composables.kt index fcc9c770..b9337244 100644 --- a/app/src/main/kotlin/co/touchlab/kampkit/android/ui/Composables.kt +++ b/app/src/main/kotlin/co/touchlab/kampkit/android/ui/Composables.kt @@ -1,6 +1,5 @@ package co.touchlab.kampkit.android.ui -import android.annotation.SuppressLint import androidx.compose.animation.Crossfade import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.TweenSpec @@ -21,17 +20,14 @@ import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.compose.collectAsStateWithLifecycle import co.touchlab.kampkit.android.R import co.touchlab.kampkit.db.Breed import co.touchlab.kampkit.models.BreedViewModel @@ -45,13 +41,7 @@ fun MainScreen( viewModel: BreedViewModel, log: Logger ) { - val lifecycleOwner = LocalLifecycleOwner.current - val lifecycleAwareDogsFlow = remember(viewModel.breedState, lifecycleOwner) { - viewModel.breedState.flowWithLifecycle(lifecycleOwner.lifecycle) - } - - @SuppressLint("StateFlowValueCalledInComposition") // False positive lint check when used inside collectAsState() - val dogsState by lifecycleAwareDogsFlow.collectAsState(viewModel.breedState.value) + val dogsState by viewModel.breedState.collectAsStateWithLifecycle() MainScreenContent( dogsState = dogsState, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 29824116..8c20c853 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,7 +18,7 @@ android-desugaring = "1.1.8" # Don't bump to 1.2.x until AGP is 7.3.x androidx-core = "1.9.0" androidx-test-junit = "1.1.3" androidx-activity-compose = "1.5.1" -androidx-lifecycle = "2.5.1" +androidx-lifecycle = "2.6.0" junit = "4.13.2" @@ -42,6 +42,7 @@ android-desugaring = { module = "com.android.tools:desugar_jdk_libs", version.re androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" } androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" } +androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle"} androidx-test-junit = { module = "androidx.test.ext:junit-ktx", version.ref = "androidx-test-junit" } compose-compiler = { module = "androidx.compose.compiler:compiler", version.ref = "composeCompiler" } @@ -99,6 +100,7 @@ app-ui = [ "androidx-core", "androidx-lifecycle-runtime", "androidx-lifecycle-viewmodel", + "androidx-lifecycle-runtime-compose", "compose-ui", "compose-tooling", "compose-foundation", From bb4267ceb13df3642bc06a4079341a4c3c99b3fb Mon Sep 17 00:00:00 2001 From: bpedryc Date: Wed, 31 May 2023 09:45:14 +0200 Subject: [PATCH 02/12] Implement KMP-Native-Coroutines in BreedViewModel --- ios/KaMPKitiOS/BreedListScreen.swift | 25 +++++++----- ios/KaMPKitiOS/CombineAdapters.swift | 40 ------------------- shared/build.gradle.kts | 3 ++ .../touchlab/kampkit/models/BreedViewModel.kt | 2 + .../kampkit/BreedCallbackViewModel.kt | 26 ------------ .../co/touchlab/kampkit/CoroutineAdapters.kt | 35 ---------------- .../kampkit/KermitExceptionHandler.kt | 9 ----- .../kotlin/co/touchlab/kampkit/KoinIOS.kt | 5 ++- .../co/touchlab/kampkit/models/ViewModel.kt | 15 ------- 9 files changed, 22 insertions(+), 138 deletions(-) delete mode 100644 ios/KaMPKitiOS/CombineAdapters.swift delete mode 100644 shared/src/iosMain/kotlin/co/touchlab/kampkit/BreedCallbackViewModel.kt delete mode 100644 shared/src/iosMain/kotlin/co/touchlab/kampkit/CoroutineAdapters.kt delete mode 100644 shared/src/iosMain/kotlin/co/touchlab/kampkit/KermitExceptionHandler.kt diff --git a/ios/KaMPKitiOS/BreedListScreen.swift b/ios/KaMPKitiOS/BreedListScreen.swift index 8cda285d..41220be2 100644 --- a/ios/KaMPKitiOS/BreedListScreen.swift +++ b/ios/KaMPKitiOS/BreedListScreen.swift @@ -9,11 +9,12 @@ import Combine import SwiftUI import shared +import KMPNativeCoroutinesCombine private let log = koin.loggerWithTag(tag: "ViewController") class ObservableBreedModel: ObservableObject { - private var viewModel: BreedCallbackViewModel? + private var viewModel: BreedViewModel? @Published var loading = false @@ -29,18 +30,20 @@ class ObservableBreedModel: ObservableObject { func activate() { let viewModel = KotlinDependencies.shared.getBreedViewModel() - doPublish(viewModel.breeds) { [weak self] dogsState in - self?.loading = dogsState.isLoading - self?.breeds = dogsState.breeds - self?.error = dogsState.error + createPublisher(for: viewModel.breedStateFlow) + .sink { _ in } receiveValue: { [weak self] (breedState: BreedViewState) in + self?.loading = breedState.isLoading + self?.breeds = breedState.breeds + self?.error = breedState.error - if let breeds = dogsState.breeds { - log.d(message: {"View updating with \(breeds.count) breeds"}) - } - if let errorMessage = dogsState.error { - log.e(message: {"Displaying error: \(errorMessage)"}) + if let breeds = breedState.breeds { + log.d(message: {"View updating with \(breeds.count) breeds"}) + } + if let errorMessage = breedState.error { + log.e(message: {"Displaying error: \(errorMessage)"}) + } } - }.store(in: &cancellables) + .store(in: &cancellables) self.viewModel = viewModel } diff --git a/ios/KaMPKitiOS/CombineAdapters.swift b/ios/KaMPKitiOS/CombineAdapters.swift deleted file mode 100644 index 53435445..00000000 --- a/ios/KaMPKitiOS/CombineAdapters.swift +++ /dev/null @@ -1,40 +0,0 @@ -import Combine -import shared - -/// Create a Combine publisher from the supplied `FlowAdapter`. Use this in contexts where more transformation will be -/// done on the Swift side before the value is bound to UI -func createPublisher(_ flowAdapter: FlowAdapter) -> AnyPublisher { - return Deferred>> { - let subject = PassthroughSubject() - let canceller = flowAdapter.subscribe( - onEach: { item in subject.send(item) }, - onComplete: { subject.send(completion: .finished) }, - onThrow: { error in subject.send(completion: .failure(KotlinError(error))) } - ) - return subject.handleEvents(receiveCancel: { canceller.cancel() }) - }.eraseToAnyPublisher() -} - -/// Prepare the supplied `FlowAdapter` to be bound to UI. The `onEach` callback will be called from `DispatchQueue.main` -/// on every new emission. -/// -/// Note that this calls `assertNoFailure()` internally so you should handle errors upstream to avoid crashes. -func doPublish(_ flowAdapter: FlowAdapter, onEach: @escaping (T) -> Void) -> Cancellable { - return createPublisher(flowAdapter) - .assertNoFailure() - .compactMap { $0 } - .receive(on: DispatchQueue.main) - .sink { onEach($0) } -} - -/// Wraps a `KotlinThrowable` in a `LocalizedError` which can be used as a Combine error type -class KotlinError: LocalizedError { - let throwable: KotlinThrowable - - init(_ throwable: KotlinThrowable) { - self.throwable = throwable - } - var errorDescription: String? { - throwable.message - } -} diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 1a5abe8c..bcdfedd8 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -6,6 +6,8 @@ plugins { kotlin("plugin.serialization") id("com.android.library") id("com.squareup.sqldelight") + id("com.google.devtools.ksp") version "1.8.0-1.0.9" + id("com.rickclephas.kmp.nativecoroutines") version "1.0.0-ALPHA-4" } android { @@ -42,6 +44,7 @@ kotlin { optIn("kotlin.RequiresOptIn") optIn("kotlinx.coroutines.ExperimentalCoroutinesApi") optIn("kotlin.time.ExperimentalTime") + optIn("kotlin.experimental.ExperimentalObjCName") } } diff --git a/shared/src/commonMain/kotlin/co/touchlab/kampkit/models/BreedViewModel.kt b/shared/src/commonMain/kotlin/co/touchlab/kampkit/models/BreedViewModel.kt index a61748dd..8f8ea556 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/kampkit/models/BreedViewModel.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/kampkit/models/BreedViewModel.kt @@ -2,6 +2,7 @@ package co.touchlab.kampkit.models import co.touchlab.kampkit.db.Breed import co.touchlab.kermit.Logger +import com.rickclephas.kmp.nativecoroutines.NativeCoroutinesState import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -19,6 +20,7 @@ class BreedViewModel( private val mutableBreedState: MutableStateFlow = MutableStateFlow(BreedViewState(isLoading = true)) + @NativeCoroutinesState val breedState: StateFlow = mutableBreedState init { diff --git a/shared/src/iosMain/kotlin/co/touchlab/kampkit/BreedCallbackViewModel.kt b/shared/src/iosMain/kotlin/co/touchlab/kampkit/BreedCallbackViewModel.kt deleted file mode 100644 index 2246ed15..00000000 --- a/shared/src/iosMain/kotlin/co/touchlab/kampkit/BreedCallbackViewModel.kt +++ /dev/null @@ -1,26 +0,0 @@ -package co.touchlab.kampkit - -import co.touchlab.kampkit.db.Breed -import co.touchlab.kampkit.models.BreedRepository -import co.touchlab.kampkit.models.BreedViewModel -import co.touchlab.kampkit.models.CallbackViewModel -import co.touchlab.kermit.Logger - -@Suppress("Unused") // Members are called from Swift -class BreedCallbackViewModel( - breedRepository: BreedRepository, - log: Logger -) : CallbackViewModel() { - - override val viewModel = BreedViewModel(breedRepository, log) - - val breeds = viewModel.breedState.asCallbacks() - - fun refreshBreeds() { - viewModel.refreshBreeds() - } - - fun updateBreedFavorite(breed: Breed) { - viewModel.updateBreedFavorite(breed) - } -} diff --git a/shared/src/iosMain/kotlin/co/touchlab/kampkit/CoroutineAdapters.kt b/shared/src/iosMain/kotlin/co/touchlab/kampkit/CoroutineAdapters.kt deleted file mode 100644 index 0cbdb4e2..00000000 --- a/shared/src/iosMain/kotlin/co/touchlab/kampkit/CoroutineAdapters.kt +++ /dev/null @@ -1,35 +0,0 @@ -package co.touchlab.kampkit - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.flow.onEach - -class FlowAdapter( - private val scope: CoroutineScope, - private val flow: Flow -) { - fun subscribe( - onEach: (item: T) -> Unit, - onComplete: () -> Unit, - onThrow: (error: Throwable) -> Unit - ): Canceller = JobCanceller( - flow.onEach { onEach(it) } - .catch { onThrow(it) } - .onCompletion { onComplete() } - .launchIn(scope) - ) -} - -interface Canceller { - fun cancel() -} - -private class JobCanceller(private val job: Job) : Canceller { - override fun cancel() { - job.cancel() - } -} diff --git a/shared/src/iosMain/kotlin/co/touchlab/kampkit/KermitExceptionHandler.kt b/shared/src/iosMain/kotlin/co/touchlab/kampkit/KermitExceptionHandler.kt deleted file mode 100644 index e13f9ed7..00000000 --- a/shared/src/iosMain/kotlin/co/touchlab/kampkit/KermitExceptionHandler.kt +++ /dev/null @@ -1,9 +0,0 @@ -package co.touchlab.kampkit - -import co.touchlab.kermit.Logger -import kotlinx.coroutines.CoroutineExceptionHandler - -fun kermitExceptionHandler(log: Logger) = CoroutineExceptionHandler { _, throwable -> - throwable.printStackTrace() - log.e(throwable = throwable) { "Error in MainScope" } -} diff --git a/shared/src/iosMain/kotlin/co/touchlab/kampkit/KoinIOS.kt b/shared/src/iosMain/kotlin/co/touchlab/kampkit/KoinIOS.kt index 17fbbaa8..10c9a89c 100644 --- a/shared/src/iosMain/kotlin/co/touchlab/kampkit/KoinIOS.kt +++ b/shared/src/iosMain/kotlin/co/touchlab/kampkit/KoinIOS.kt @@ -1,6 +1,7 @@ package co.touchlab.kampkit import co.touchlab.kampkit.db.KaMPKitDb +import co.touchlab.kampkit.models.BreedViewModel import co.touchlab.kermit.Logger import com.russhwolf.settings.NSUserDefaultsSettings import com.russhwolf.settings.Settings @@ -31,7 +32,7 @@ actual val platformModule = module { single { Darwin.create() } - single { BreedCallbackViewModel(get(), getWith("BreedCallbackViewModel")) } + single { BreedViewModel(get(), getWith("BreedCallbackViewModel")) } } // Access from Swift to create a logger @@ -41,5 +42,5 @@ fun Koin.loggerWithTag(tag: String) = @Suppress("unused") // Called from Swift object KotlinDependencies : KoinComponent { - fun getBreedViewModel() = getKoin().get() + fun getBreedViewModel() = getKoin().get() } diff --git a/shared/src/iosMain/kotlin/co/touchlab/kampkit/models/ViewModel.kt b/shared/src/iosMain/kotlin/co/touchlab/kampkit/models/ViewModel.kt index 7ae32085..1b94b1c3 100644 --- a/shared/src/iosMain/kotlin/co/touchlab/kampkit/models/ViewModel.kt +++ b/shared/src/iosMain/kotlin/co/touchlab/kampkit/models/ViewModel.kt @@ -1,9 +1,7 @@ package co.touchlab.kampkit.models -import co.touchlab.kampkit.FlowAdapter import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.Flow /** * Base class that provides a Kotlin/Native equivalent to the AndroidX `ViewModel`. In particular, this provides @@ -30,16 +28,3 @@ actual abstract class ViewModel { viewModelScope.cancel() } } - -abstract class CallbackViewModel { - protected abstract val viewModel: ViewModel - - /** - * Create a [FlowAdapter] from this [Flow] to make it easier to interact with from Swift. - */ - fun Flow.asCallbacks() = - FlowAdapter(viewModel.viewModelScope, this) - - @Suppress("Unused") // Called from Swift - fun clear() = viewModel.clear() -} From 6b2b7ff106ea29010931e7a5875949da53d69b94 Mon Sep 17 00:00:00 2001 From: bpedryc Date: Wed, 31 May 2023 11:07:09 +0200 Subject: [PATCH 03/12] Remove deprecated targetSdk property --- shared/build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index bcdfedd8..91834dd4 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -14,7 +14,6 @@ android { compileSdk = libs.versions.compileSdk.get().toInt() defaultConfig { minSdk = libs.versions.minSdk.get().toInt() - targetSdk = libs.versions.targetSdk.get().toInt() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } testOptions { From 7b6fcf64e7388905d68dde38af9d0c79829490aa Mon Sep 17 00:00:00 2001 From: bpedryc Date: Wed, 31 May 2023 11:07:51 +0200 Subject: [PATCH 04/12] Add NativeCoroutineScope for iOS to subscribe on main thread --- .../src/iosMain/kotlin/co/touchlab/kampkit/models/ViewModel.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shared/src/iosMain/kotlin/co/touchlab/kampkit/models/ViewModel.kt b/shared/src/iosMain/kotlin/co/touchlab/kampkit/models/ViewModel.kt index 1b94b1c3..296b6316 100644 --- a/shared/src/iosMain/kotlin/co/touchlab/kampkit/models/ViewModel.kt +++ b/shared/src/iosMain/kotlin/co/touchlab/kampkit/models/ViewModel.kt @@ -1,5 +1,6 @@ package co.touchlab.kampkit.models +import com.rickclephas.kmp.nativecoroutines.NativeCoroutineScope import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel @@ -10,6 +11,7 @@ import kotlinx.coroutines.cancel */ actual abstract class ViewModel { + @NativeCoroutineScope actual val viewModelScope = MainScope() /** From 7b1abd251357b96ffca954c4d9d5b36bd0703085 Mon Sep 17 00:00:00 2001 From: bpedryc Date: Wed, 31 May 2023 11:27:23 +0200 Subject: [PATCH 05/12] Bump Kotlin version to 1.8.21 --- gradle/libs.versions.toml | 8 ++++---- shared/build.gradle.kts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8c20c853..e301cfb7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -kotlin = "1.8.0" +kotlin = "1.8.21" ## SDK Versions minSdk = "21" @@ -11,8 +11,8 @@ android-gradle-plugin = "7.4.1" ktlint-gradle = "11.2.0" gradle-versions = "0.42.0" -compose = "1.4.0-alpha03" -composeCompiler = "1.4.0-dev-k1.8.0-33c0ad36f83" +compose = "1.4.3" +composeCompiler = "1.4.7" android-desugaring = "1.1.8" # Don't bump to 1.2.x until AGP is 7.3.x androidx-core = "1.9.0" @@ -22,7 +22,7 @@ androidx-lifecycle = "2.6.0" junit = "4.13.2" -coroutines = "1.6.4" +coroutines = "1.7.1" kotlinx-datetime = "0.4.0" ktor = "2.1.1" diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 91834dd4..78ab5442 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -6,8 +6,8 @@ plugins { kotlin("plugin.serialization") id("com.android.library") id("com.squareup.sqldelight") - id("com.google.devtools.ksp") version "1.8.0-1.0.9" - id("com.rickclephas.kmp.nativecoroutines") version "1.0.0-ALPHA-4" + id("com.google.devtools.ksp") version "1.8.21-1.0.11" + id("com.rickclephas.kmp.nativecoroutines") version "1.0.0-ALPHA-9" } android { From b3b912a639796284177593538e03cd8ccffccf93 Mon Sep 17 00:00:00 2001 From: bpedryc Date: Wed, 31 May 2023 11:28:51 +0200 Subject: [PATCH 06/12] Update XCode project files --- .idea/codeStyles/Project.xml | 2 - ios/KaMPKitiOS.xcodeproj/project.pbxproj | 43 ++++++++++++++++--- ios/Podfile.lock | 2 +- ios/Pods/Manifest.lock | 2 +- ios/Pods/Pods.xcodeproj/project.pbxproj | 4 +- .../Pods-KaMPKitiOS-Info.plist | 2 +- .../Pods-KaMPKitiOS-frameworks.sh | 2 +- .../SwiftLint/SwiftLint.debug.xcconfig | 1 + .../SwiftLint/SwiftLint.release.xcconfig | 1 + .../shared/shared.debug.xcconfig | 1 + .../shared/shared.release.xcconfig | 1 + 11 files changed, 48 insertions(+), 13 deletions(-) diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 7829f3cf..aca9a1ab 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -6,8 +6,6 @@ -