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 @@
-
-
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..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,18 +11,18 @@ 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"
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"
-coroutines = "1.6.4"
+coroutines = "1.7.1"
kotlinx-datetime = "0.4.0"
ktor = "2.1.1"
@@ -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",
diff --git a/ios/KaMPKitiOS.xcodeproj/project.pbxproj b/ios/KaMPKitiOS.xcodeproj/project.pbxproj
index 765a0360..9d39915c 100644
--- a/ios/KaMPKitiOS.xcodeproj/project.pbxproj
+++ b/ios/KaMPKitiOS.xcodeproj/project.pbxproj
@@ -3,13 +3,15 @@
archiveVersion = 1;
classes = {
};
- objectVersion = 51;
+ objectVersion = 52;
objects = {
/* Begin PBXBuildFile section */
+ 3C73D04A2A335103003E6929 /* BreedsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C73D0492A335103003E6929 /* BreedsViewModel.swift */; };
+ 3CD290EC2A251417004C7AD1 /* KMPNativeCoroutinesCombine in Frameworks */ = {isa = PBXBuildFile; productRef = 3CD290EB2A251417004C7AD1 /* KMPNativeCoroutinesCombine */; };
+ 3CD290EE2A251417004C7AD1 /* KMPNativeCoroutinesCore in Frameworks */ = {isa = PBXBuildFile; productRef = 3CD290ED2A251417004C7AD1 /* KMPNativeCoroutinesCore */; };
3DFF917C64A18A83DA010EE1 /* Pods_KaMPKitiOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B859F3FB23133D22AB9DD835 /* Pods_KaMPKitiOS.framework */; };
- 461C74AA2788F5F3004B1FFC /* CombineAdapters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 461C74A92788F5F3004B1FFC /* CombineAdapters.swift */; };
- 46A5B5EF26AF54F7002EFEAA /* BreedListScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46A5B5EE26AF54F7002EFEAA /* BreedListScreen.swift */; };
+ 46A5B5EF26AF54F7002EFEAA /* BreedsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46A5B5EE26AF54F7002EFEAA /* BreedsScreen.swift */; };
46A5B60826B04921002EFEAA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 46A5B60626B04920002EFEAA /* Main.storyboard */; };
46B5284D249C5CF400A7725D /* Koin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46B5284C249C5CF400A7725D /* Koin.swift */; };
F1465F0123AA94BF0055F7C3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1465F0023AA94BF0055F7C3 /* AppDelegate.swift */; };
@@ -39,8 +41,8 @@
/* Begin PBXFileReference section */
1DFCC00C8DAA719770A18D1A /* Pods-KaMPKitiOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-KaMPKitiOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-KaMPKitiOS/Pods-KaMPKitiOS.release.xcconfig"; sourceTree = ""; };
2A1ED6A4A2A53F5F75C58E5F /* Pods-KaMPKitiOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-KaMPKitiOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-KaMPKitiOS/Pods-KaMPKitiOS.release.xcconfig"; sourceTree = ""; };
- 461C74A92788F5F3004B1FFC /* CombineAdapters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineAdapters.swift; sourceTree = ""; };
- 46A5B5EE26AF54F7002EFEAA /* BreedListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreedListScreen.swift; sourceTree = ""; };
+ 3C73D0492A335103003E6929 /* BreedsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreedsViewModel.swift; sourceTree = ""; };
+ 46A5B5EE26AF54F7002EFEAA /* BreedsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreedsScreen.swift; sourceTree = ""; };
46A5B60726B04920002EFEAA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
46B5284C249C5CF400A7725D /* Koin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Koin.swift; sourceTree = ""; };
B859F3FB23133D22AB9DD835 /* Pods_KaMPKitiOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_KaMPKitiOS.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -65,6 +67,8 @@
buildActionMask = 2147483647;
files = (
3DFF917C64A18A83DA010EE1 /* Pods_KaMPKitiOS.framework in Frameworks */,
+ 3CD290EC2A251417004C7AD1 /* KMPNativeCoroutinesCombine in Frameworks */,
+ 3CD290EE2A251417004C7AD1 /* KMPNativeCoroutinesCore in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -85,6 +89,15 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
+ 3C73D0482A335061003E6929 /* Breeds */ = {
+ isa = PBXGroup;
+ children = (
+ 46A5B5EE26AF54F7002EFEAA /* BreedsScreen.swift */,
+ 3C73D0492A335103003E6929 /* BreedsViewModel.swift */,
+ );
+ path = Breeds;
+ sourceTree = "";
+ };
6278498AD96A4D949D39BF44 /* Frameworks */ = {
isa = PBXGroup;
children = (
@@ -129,14 +142,13 @@
F1465EFF23AA94BF0055F7C3 /* KaMPKitiOS */ = {
isa = PBXGroup;
children = (
+ 3C73D0482A335061003E6929 /* Breeds */,
F1465F0023AA94BF0055F7C3 /* AppDelegate.swift */,
46A5B60626B04920002EFEAA /* Main.storyboard */,
46B5284C249C5CF400A7725D /* Koin.swift */,
F1465F0923AA94BF0055F7C3 /* Assets.xcassets */,
F1465F0B23AA94BF0055F7C3 /* LaunchScreen.storyboard */,
F1465F0E23AA94BF0055F7C3 /* Info.plist */,
- 46A5B5EE26AF54F7002EFEAA /* BreedListScreen.swift */,
- 461C74A92788F5F3004B1FFC /* CombineAdapters.swift */,
);
path = KaMPKitiOS;
sourceTree = "";
@@ -178,6 +190,10 @@
dependencies = (
);
name = KaMPKitiOS;
+ packageProductDependencies = (
+ 3CD290EB2A251417004C7AD1 /* KMPNativeCoroutinesCombine */,
+ 3CD290ED2A251417004C7AD1 /* KMPNativeCoroutinesCore */,
+ );
productName = KaMPKitiOS;
productReference = F1465EFD23AA94BF0055F7C3 /* KaMPKitiOS.app */;
productType = "com.apple.product-type.application";
@@ -250,6 +266,9 @@
Base,
);
mainGroup = F1465EF423AA94BF0055F7C3;
+ packageReferences = (
+ 3CD290EA2A251417004C7AD1 /* XCRemoteSwiftPackageReference "KMP-NativeCoroutines" */,
+ );
productRefGroup = F1465EFE23AA94BF0055F7C3 /* Products */;
projectDirPath = "";
projectRoot = "";
@@ -354,8 +373,8 @@
buildActionMask = 2147483647;
files = (
46B5284D249C5CF400A7725D /* Koin.swift in Sources */,
- 461C74AA2788F5F3004B1FFC /* CombineAdapters.swift in Sources */,
- 46A5B5EF26AF54F7002EFEAA /* BreedListScreen.swift in Sources */,
+ 3C73D04A2A335103003E6929 /* BreedsViewModel.swift in Sources */,
+ 46A5B5EF26AF54F7002EFEAA /* BreedsScreen.swift in Sources */,
F1465F0123AA94BF0055F7C3 /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -542,6 +561,7 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 8UD86646U9;
INFOPLIST_FILE = KaMPKitiOS/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -570,6 +590,7 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 8UD86646U9;
INFOPLIST_FILE = KaMPKitiOS/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -709,6 +730,30 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
+
+/* Begin XCRemoteSwiftPackageReference section */
+ 3CD290EA2A251417004C7AD1 /* XCRemoteSwiftPackageReference "KMP-NativeCoroutines" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/rickclephas/KMP-NativeCoroutines.git";
+ requirement = {
+ kind = exactVersion;
+ version = "1.0.0-ALPHA-9";
+ };
+ };
+/* End XCRemoteSwiftPackageReference section */
+
+/* Begin XCSwiftPackageProductDependency section */
+ 3CD290EB2A251417004C7AD1 /* KMPNativeCoroutinesCombine */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 3CD290EA2A251417004C7AD1 /* XCRemoteSwiftPackageReference "KMP-NativeCoroutines" */;
+ productName = KMPNativeCoroutinesCombine;
+ };
+ 3CD290ED2A251417004C7AD1 /* KMPNativeCoroutinesCore */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 3CD290EA2A251417004C7AD1 /* XCRemoteSwiftPackageReference "KMP-NativeCoroutines" */;
+ productName = KMPNativeCoroutinesCore;
+ };
+/* End XCSwiftPackageProductDependency section */
};
rootObject = F1465EF523AA94BF0055F7C3 /* Project object */;
}
diff --git a/ios/KaMPKitiOS/AppDelegate.swift b/ios/KaMPKitiOS/AppDelegate.swift
index 25924195..139d7889 100644
--- a/ios/KaMPKitiOS/AppDelegate.swift
+++ b/ios/KaMPKitiOS/AppDelegate.swift
@@ -22,7 +22,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
startKoin()
- let viewController = UIHostingController(rootView: BreedListScreen())
+ let viewController = UIHostingController(rootView: BreedsScreen())
self.window = UIWindow(frame: UIScreen.main.bounds)
self.window?.rootViewController = viewController
diff --git a/ios/KaMPKitiOS/BreedListScreen.swift b/ios/KaMPKitiOS/BreedListScreen.swift
deleted file mode 100644
index 8cda285d..00000000
--- a/ios/KaMPKitiOS/BreedListScreen.swift
+++ /dev/null
@@ -1,146 +0,0 @@
-//
-// BreedListView.swift
-// KaMPKitiOS
-//
-// Created by Russell Wolf on 7/26/21.
-// Copyright © 2021 Touchlab. All rights reserved.
-//
-
-import Combine
-import SwiftUI
-import shared
-
-private let log = koin.loggerWithTag(tag: "ViewController")
-
-class ObservableBreedModel: ObservableObject {
- private var viewModel: BreedCallbackViewModel?
-
- @Published
- var loading = false
-
- @Published
- var breeds: [Breed]?
-
- @Published
- var error: String?
-
- private var cancellables = [AnyCancellable]()
-
- 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
-
- 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)"})
- }
- }.store(in: &cancellables)
-
- self.viewModel = viewModel
- }
-
- func deactivate() {
- cancellables.forEach { $0.cancel() }
- cancellables.removeAll()
-
- viewModel?.clear()
- viewModel = nil
- }
-
- func onBreedFavorite(_ breed: Breed) {
- viewModel?.updateBreedFavorite(breed: breed)
- }
-
- func refresh() {
- viewModel?.refreshBreeds()
- }
-}
-
-struct BreedListScreen: View {
- @StateObject
- var observableModel = ObservableBreedModel()
-
- var body: some View {
- BreedListContent(
- loading: observableModel.loading,
- breeds: observableModel.breeds,
- error: observableModel.error,
- onBreedFavorite: { observableModel.onBreedFavorite($0) },
- refresh: { observableModel.refresh() }
- )
- .onAppear(perform: {
- observableModel.activate()
- })
- .onDisappear(perform: {
- observableModel.deactivate()
- })
- }
-}
-
-struct BreedListContent: View {
- var loading: Bool
- var breeds: [Breed]?
- var error: String?
- var onBreedFavorite: (Breed) -> Void
- var refresh: () -> Void
-
- var body: some View {
- ZStack {
- VStack {
- if let breeds = breeds {
- List(breeds, id: \.id) { breed in
- BreedRowView(breed: breed) {
- onBreedFavorite(breed)
- }
- }
- }
- if let error = error {
- Text(error)
- .foregroundColor(.red)
- }
- Button("Refresh") {
- refresh()
- }
- }
- if loading { Text("Loading...") }
- }
- }
-}
-
-struct BreedRowView: View {
- var breed: Breed
- var onTap: () -> Void
-
- var body: some View {
- Button(action: onTap) {
- HStack {
- Text(breed.name)
- .padding(4.0)
- Spacer()
- Image(systemName: (!breed.favorite) ? "heart" : "heart.fill")
- .padding(4.0)
- }
- }
- }
-}
-
-struct BreedListScreen_Previews: PreviewProvider {
- static var previews: some View {
- BreedListContent(
- loading: false,
- breeds: [
- Breed(id: 0, name: "appenzeller", favorite: false),
- Breed(id: 1, name: "australian", favorite: true)
- ],
- error: nil,
- onBreedFavorite: { _ in },
- refresh: {}
- )
- }
-}
diff --git a/ios/KaMPKitiOS/Breeds/BreedsScreen.swift b/ios/KaMPKitiOS/Breeds/BreedsScreen.swift
new file mode 100644
index 00000000..1b0c8e33
--- /dev/null
+++ b/ios/KaMPKitiOS/Breeds/BreedsScreen.swift
@@ -0,0 +1,90 @@
+//
+// BreedListView.swift
+// KaMPKitiOS
+//
+// Created by Russell Wolf on 7/26/21.
+// Copyright © 2021 Touchlab. All rights reserved.
+//
+
+import SwiftUI
+import shared
+
+struct BreedsScreen: View {
+ @StateObject
+ var viewModel = BreedsViewModel()
+
+ var body: some View {
+ BreedsContent(
+ state: viewModel.state,
+ onBreedFavorite: { viewModel.onBreedFavorite($0) },
+ refresh: { viewModel.refresh() }
+ )
+ .onAppear(perform: {
+ viewModel.subscribeState()
+ })
+ .onDisappear(perform: {
+ viewModel.unsubscribeState()
+ })
+ }
+}
+
+struct BreedsContent: View {
+ var state: BreedViewState
+ var onBreedFavorite: (Breed) -> Void
+ var refresh: () -> Void
+
+ var body: some View {
+ ZStack {
+ VStack {
+ List(state.breeds, id: \.id) { breed in
+ BreedRowView(breed: breed) {
+ onBreedFavorite(breed)
+ }
+ }
+ if let error = state.error {
+ Text(error)
+ .foregroundColor(.red)
+ }
+ Button("Refresh") {
+ refresh()
+ }
+ }
+ if state.isLoading { Text("Loading...") }
+ }
+ }
+}
+
+struct BreedRowView: View {
+ var breed: Breed
+ var onTap: () -> Void
+
+ var body: some View {
+ Button(action: onTap) {
+ HStack {
+ Text(breed.name)
+ .padding(4.0)
+ Spacer()
+ Image(systemName: (!breed.favorite) ? "heart" : "heart.fill")
+ .padding(4.0)
+ }
+ }
+ }
+}
+
+struct BreedsScreen_Previews: PreviewProvider {
+ static var previews: some View {
+ BreedsContent(
+ state: BreedViewState(
+ breeds: [
+ Breed(id: 0, name: "appenzeller", favorite: false),
+ Breed(id: 1, name: "australian", favorite: true)
+ ],
+ error: nil,
+ isLoading: false,
+ isEmpty: false
+ ),
+ onBreedFavorite: { _ in },
+ refresh: {}
+ )
+ }
+}
diff --git a/ios/KaMPKitiOS/Breeds/BreedsViewModel.swift b/ios/KaMPKitiOS/Breeds/BreedsViewModel.swift
new file mode 100644
index 00000000..c4ea2b2e
--- /dev/null
+++ b/ios/KaMPKitiOS/Breeds/BreedsViewModel.swift
@@ -0,0 +1,45 @@
+//
+// BreedsViewModel.swift
+// KaMPKitiOS
+//
+// Created by Bartłomiej Pedryc on 09/06/2023.
+// Copyright © 2023 Touchlab. All rights reserved.
+//
+
+import Combine
+import Foundation
+import shared
+import KMPNativeCoroutinesCombine
+
+class BreedsViewModel: ObservableObject {
+
+ @Published var state: BreedViewState = BreedViewState.companion.default()
+
+ private var viewModelDelegate: BreedViewModelDelegate = KotlinDependencies.shared.getBreedViewModel()
+ private var cancellables = [AnyCancellable]()
+
+ deinit {
+ viewModelDelegate.clear()
+ }
+
+ func subscribeState() {
+ createPublisher(for: viewModelDelegate.breedStateFlow)
+ .sink { _ in } receiveValue: { [weak self] (breedState: BreedViewState) in
+ self?.state = breedState
+ }
+ .store(in: &cancellables)
+ }
+
+ func unsubscribeState() {
+ cancellables.forEach { $0.cancel() }
+ cancellables.removeAll()
+ }
+
+ func onBreedFavorite(_ breed: Breed) {
+ viewModelDelegate.updateBreedFavorite(breed: breed)
+ }
+
+ func refresh() {
+ viewModelDelegate.refreshBreeds()
+ }
+}
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/ios/Podfile.lock b/ios/Podfile.lock
index 0d481a6a..711a5871 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -20,4 +20,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: d5a73f50a47bad1893e4fbf8978f1bef946ebdf6
-COCOAPODS: 1.11.3
+COCOAPODS: 1.12.1
diff --git a/ios/Pods/Manifest.lock b/ios/Pods/Manifest.lock
index 0d481a6a..711a5871 100644
--- a/ios/Pods/Manifest.lock
+++ b/ios/Pods/Manifest.lock
@@ -20,4 +20,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: d5a73f50a47bad1893e4fbf8978f1bef946ebdf6
-COCOAPODS: 1.11.3
+COCOAPODS: 1.12.1
diff --git a/ios/Pods/Pods.xcodeproj/project.pbxproj b/ios/Pods/Pods.xcodeproj/project.pbxproj
index a510df3e..79a804f9 100644
--- a/ios/Pods/Pods.xcodeproj/project.pbxproj
+++ b/ios/Pods/Pods.xcodeproj/project.pbxproj
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
- objectVersion = 51;
+ objectVersion = 52;
objects = {
/* Begin PBXAggregateTarget section */
@@ -261,7 +261,7 @@
LastUpgradeCheck = 1300;
};
buildConfigurationList = 46EB2E00000030 /* Build configuration list for PBXProject "Pods" */;
- compatibilityVersion = "Xcode 10.0";
+ compatibilityVersion = "Xcode 11.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
diff --git a/ios/Pods/Target Support Files/Pods-KaMPKitiOS/Pods-KaMPKitiOS-Info.plist b/ios/Pods/Target Support Files/Pods-KaMPKitiOS/Pods-KaMPKitiOS-Info.plist
index 2243fe6e..19cf209d 100644
--- a/ios/Pods/Target Support Files/Pods-KaMPKitiOS/Pods-KaMPKitiOS-Info.plist
+++ b/ios/Pods/Target Support Files/Pods-KaMPKitiOS/Pods-KaMPKitiOS-Info.plist
@@ -3,7 +3,7 @@
CFBundleDevelopmentRegion
- en
+ ${PODS_DEVELOPMENT_LANGUAGE}
CFBundleExecutable
${EXECUTABLE_NAME}
CFBundleIdentifier
diff --git a/ios/Pods/Target Support Files/Pods-KaMPKitiOS/Pods-KaMPKitiOS-frameworks.sh b/ios/Pods/Target Support Files/Pods-KaMPKitiOS/Pods-KaMPKitiOS-frameworks.sh
index 333d767c..1e57ae2b 100755
--- a/ios/Pods/Target Support Files/Pods-KaMPKitiOS/Pods-KaMPKitiOS-frameworks.sh
+++ b/ios/Pods/Target Support Files/Pods-KaMPKitiOS/Pods-KaMPKitiOS-frameworks.sh
@@ -41,7 +41,7 @@ install_framework()
if [ -L "${source}" ]; then
echo "Symlinked..."
- source="$(readlink "${source}")"
+ source="$(readlink -f "${source}")"
fi
if [ -d "${source}/${BCSYMBOLMAP_DIR}" ]; then
diff --git a/ios/Pods/Target Support Files/SwiftLint/SwiftLint.debug.xcconfig b/ios/Pods/Target Support Files/SwiftLint/SwiftLint.debug.xcconfig
index 003a1f43..5238df58 100644
--- a/ios/Pods/Target Support Files/SwiftLint/SwiftLint.debug.xcconfig
+++ b/ios/Pods/Target Support Files/SwiftLint/SwiftLint.debug.xcconfig
@@ -3,6 +3,7 @@ CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SwiftLint
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
PODS_BUILD_DIR = ${BUILD_DIR}
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
+PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
PODS_ROOT = ${SRCROOT}
PODS_TARGET_SRCROOT = ${PODS_ROOT}/SwiftLint
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
diff --git a/ios/Pods/Target Support Files/SwiftLint/SwiftLint.release.xcconfig b/ios/Pods/Target Support Files/SwiftLint/SwiftLint.release.xcconfig
index 003a1f43..5238df58 100644
--- a/ios/Pods/Target Support Files/SwiftLint/SwiftLint.release.xcconfig
+++ b/ios/Pods/Target Support Files/SwiftLint/SwiftLint.release.xcconfig
@@ -3,6 +3,7 @@ CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SwiftLint
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
PODS_BUILD_DIR = ${BUILD_DIR}
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
+PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
PODS_ROOT = ${SRCROOT}
PODS_TARGET_SRCROOT = ${PODS_ROOT}/SwiftLint
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
diff --git a/ios/Pods/Target Support Files/shared/shared.debug.xcconfig b/ios/Pods/Target Support Files/shared/shared.debug.xcconfig
index 49484481..ee6cdedd 100644
--- a/ios/Pods/Target Support Files/shared/shared.debug.xcconfig
+++ b/ios/Pods/Target Support Files/shared/shared.debug.xcconfig
@@ -6,6 +6,7 @@ KOTLIN_PROJECT_PATH = :shared
OTHER_LDFLAGS = $(inherited) -l"c++"
PODS_BUILD_DIR = ${BUILD_DIR}
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
+PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
PODS_ROOT = ${SRCROOT}
PODS_TARGET_SRCROOT = ${PODS_ROOT}/../../shared
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
diff --git a/ios/Pods/Target Support Files/shared/shared.release.xcconfig b/ios/Pods/Target Support Files/shared/shared.release.xcconfig
index 49484481..ee6cdedd 100644
--- a/ios/Pods/Target Support Files/shared/shared.release.xcconfig
+++ b/ios/Pods/Target Support Files/shared/shared.release.xcconfig
@@ -6,6 +6,7 @@ KOTLIN_PROJECT_PATH = :shared
OTHER_LDFLAGS = $(inherited) -l"c++"
PODS_BUILD_DIR = ${BUILD_DIR}
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
+PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
PODS_ROOT = ${SRCROOT}
PODS_TARGET_SRCROOT = ${PODS_ROOT}/../../shared
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts
index 1a5abe8c..78ab5442 100644
--- a/shared/build.gradle.kts
+++ b/shared/build.gradle.kts
@@ -6,13 +6,14 @@ plugins {
kotlin("plugin.serialization")
id("com.android.library")
id("com.squareup.sqldelight")
+ id("com.google.devtools.ksp") version "1.8.21-1.0.11"
+ id("com.rickclephas.kmp.nativecoroutines") version "1.0.0-ALPHA-9"
}
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 {
@@ -42,6 +43,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..27567f07 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
@@ -9,16 +10,19 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
+import kotlin.native.ObjCName
+@ObjCName("BreedViewModelDelegate")
class BreedViewModel(
private val breedRepository: BreedRepository,
log: Logger
) : ViewModel() {
- private val log = log.withTag("BreedCommonViewModel")
+ private val log = log.withTag("BreedViewModel")
private val mutableBreedState: MutableStateFlow =
MutableStateFlow(BreedViewState(isLoading = true))
+ @NativeCoroutinesState
val breedState: StateFlow = mutableBreedState
init {
@@ -51,7 +55,7 @@ class BreedViewModel(
}
BreedViewState(
isLoading = false,
- breeds = breeds.takeIf { it.isNotEmpty() },
+ breeds = breeds,
error = errorMessage.takeIf { breeds.isEmpty() },
isEmpty = breeds.isEmpty() && errorMessage == null
)
@@ -82,7 +86,7 @@ class BreedViewModel(
private fun handleBreedError(throwable: Throwable) {
log.e(throwable) { "Error downloading breed list" }
mutableBreedState.update {
- if (it.breeds.isNullOrEmpty()) {
+ if (it.breeds.isEmpty()) {
BreedViewState(error = "Unable to refresh breed list")
} else {
// Just let it fail silently if we have a cache
@@ -93,8 +97,14 @@ class BreedViewModel(
}
data class BreedViewState(
- val breeds: List? = null,
+ val breeds: List = emptyList(),
val error: String? = null,
val isLoading: Boolean = false,
val isEmpty: Boolean = false
-)
+) {
+ companion object {
+ // This method lets you use the default constructor values in Swift. When accessing the
+ // constructor directly, they will not work there and would need to be provided explicitly.
+ fun default() = BreedViewState()
+ }
+}
diff --git a/shared/src/commonTest/kotlin/co/touchlab/kampkit/BreedViewModelTest.kt b/shared/src/commonTest/kotlin/co/touchlab/kampkit/BreedViewModelTest.kt
index 7914dc28..38654cad 100644
--- a/shared/src/commonTest/kotlin/co/touchlab/kampkit/BreedViewModelTest.kt
+++ b/shared/src/commonTest/kotlin/co/touchlab/kampkit/BreedViewModelTest.kt
@@ -50,7 +50,7 @@ class BreedViewModelTest {
private val breedViewStateSuccessFavorite = BreedViewState(
breeds = listOf(appenzeller, australianLike)
)
- private val breedNames = breedViewStateSuccessNoFavorite.breeds?.map { it.name }.orEmpty()
+ private val breedNames = breedViewStateSuccessNoFavorite.breeds.map { it.name }
}
@BeforeTest
@@ -106,7 +106,7 @@ class BreedViewModelTest {
viewModel.refreshBreeds().join()
// id is 5 here because it incremented twice when trying to insert duplicate breeds
assertEquals(
- BreedViewState(breedViewStateSuccessFavorite.breeds?.plus(Breed(5, "extra", false))),
+ BreedViewState(breedViewStateSuccessFavorite.breeds.plus(Breed(5, "extra", false))),
awaitItemPrecededBy(breedViewStateSuccessFavorite.copy(isLoading = true))
)
}
@@ -126,7 +126,7 @@ class BreedViewModelTest {
viewModel.breedState.test {
// id is 5 here because it incremented twice when trying to insert duplicate breeds
assertEquals(
- BreedViewState(breedViewStateSuccessFavorite.breeds?.plus(Breed(5, "extra", false))),
+ BreedViewState(breedViewStateSuccessFavorite.breeds.plus(Breed(5, "extra", false))),
awaitItemPrecededBy(BreedViewState(isLoading = true), breedViewStateSuccessFavorite)
)
}
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..82ae4d7c 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("BreedViewModel")) }
}
// 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..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,9 +1,8 @@
package co.touchlab.kampkit.models
-import co.touchlab.kampkit.FlowAdapter
+import com.rickclephas.kmp.nativecoroutines.NativeCoroutineScope
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
@@ -12,6 +11,7 @@ import kotlinx.coroutines.flow.Flow
*/
actual abstract class ViewModel {
+ @NativeCoroutineScope
actual val viewModelScope = MainScope()
/**
@@ -30,16 +30,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()
-}