From ac1b05dcbf54106ee261f076719db3b50617da2f Mon Sep 17 00:00:00 2001 From: Marc-Antoine Fortier Date: Wed, 6 Dec 2023 22:52:09 -0500 Subject: [PATCH 1/2] Replace CFlows with StateFlows using SKIE --- androidApp/build.gradle.kts | 1 + .../com/mirego/kmp/boilerplate/Greeting.kt | 24 -------- .../mirego/kmp/boilerplate/MainActivity.kt | 13 ++++- .../boilerplate/previews/PreviewContext.kt | 7 ++- .../viewmodels/LifecycleViewModel.kt | 28 +++++++++ .../kmp/boilerplate/views/ExampleView.kt | 26 +++++++++ build.gradle.kts | 3 +- gradle.properties | 2 +- gradle/libs.versions.toml | 13 ++++- ios/iosApp.xcodeproj/project.pbxproj | 18 +++--- ios/iosApp/GreetingView.swift | 17 ------ .../Preview Content/PreviewContext.swift | 6 +- ios/iosApp/Utils/FlowUtils.swift | 21 ------- ios/iosApp/Views/ExampleView.swift | 37 ++++++++++++ ios/iosApp/iOSApp.swift | 15 +++-- shared/build.gradle.kts | 32 ++++++++++ .../com/mirego/kmp/boilerplate/utils/CFlow.kt | 7 --- .../com/mirego/kmp/boilerplate/Greeting.kt | 35 ----------- .../kmp/boilerplate/platform/Platform.kt | 6 +- .../com/mirego/kmp/boilerplate/utils/CFlow.kt | 7 --- .../kmp/boilerplate/viewmodels/ViewModel.kt | 12 ++++ .../viewmodels/ViewModelFactory.kt | 24 ++++++++ .../viewmodels/example/ExampleViewModel.kt | 58 +++++++++++++++++++ .../com/mirego/kmp/boilerplate/utils/CFlow.kt | 34 ----------- 24 files changed, 271 insertions(+), 175 deletions(-) delete mode 100644 androidApp/src/main/java/com/mirego/kmp/boilerplate/Greeting.kt create mode 100644 androidApp/src/main/java/com/mirego/kmp/boilerplate/viewmodels/LifecycleViewModel.kt create mode 100644 androidApp/src/main/java/com/mirego/kmp/boilerplate/views/ExampleView.kt delete mode 100644 ios/iosApp/GreetingView.swift delete mode 100644 ios/iosApp/Utils/FlowUtils.swift create mode 100644 ios/iosApp/Views/ExampleView.swift delete mode 100644 shared/src/androidMain/kotlin/com/mirego/kmp/boilerplate/utils/CFlow.kt delete mode 100644 shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/Greeting.kt delete mode 100644 shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/utils/CFlow.kt create mode 100644 shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodels/ViewModel.kt create mode 100644 shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodels/ViewModelFactory.kt create mode 100644 shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodels/example/ExampleViewModel.kt delete mode 100644 shared/src/iosMain/kotlin/com/mirego/kmp/boilerplate/utils/CFlow.kt diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index b5b7c2e..77ffaa5 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -59,5 +59,6 @@ dependencies { implementation(libs.androidx.compose.ui.tooling) implementation(libs.androidx.compose.material) + implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.startup.runtime) } diff --git a/androidApp/src/main/java/com/mirego/kmp/boilerplate/Greeting.kt b/androidApp/src/main/java/com/mirego/kmp/boilerplate/Greeting.kt deleted file mode 100644 index ca9de6e..0000000 --- a/androidApp/src/main/java/com/mirego/kmp/boilerplate/Greeting.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.mirego.kmp.boilerplate - -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.tooling.preview.Preview -import com.mirego.kmp.boilerplate.previews.PreviewContext -import kotlinx.coroutines.flow.Flow - -@Composable -fun Greeting(textFlow: Flow) { - val text: String by textFlow.collectAsState("initial") - - Text(text = text) -} - -@Preview(showSystemUi = true) -@Composable -fun PreviewGreeting() { - PreviewContext { - Greeting(Greeting().greeting()) - } -} diff --git a/androidApp/src/main/java/com/mirego/kmp/boilerplate/MainActivity.kt b/androidApp/src/main/java/com/mirego/kmp/boilerplate/MainActivity.kt index bf86377..f3f0d7f 100644 --- a/androidApp/src/main/java/com/mirego/kmp/boilerplate/MainActivity.kt +++ b/androidApp/src/main/java/com/mirego/kmp/boilerplate/MainActivity.kt @@ -3,13 +3,22 @@ package com.mirego.kmp.boilerplate import android.os.Bundle import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity - +import com.mirego.kmp.boilerplate.viewmodels.ViewModelFactory +import com.mirego.kmp.boilerplate.viewmodels.lifecycleViewModel +import com.mirego.kmp.boilerplate.views.ExampleView class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + val viewModelFactory = ViewModelFactory() + setContent { - Greeting(textFlow = Greeting().greeting()) + ExampleView( + viewModel = lifecycleViewModel { + viewModelFactory.example() + } + ) } } } diff --git a/androidApp/src/main/java/com/mirego/kmp/boilerplate/previews/PreviewContext.kt b/androidApp/src/main/java/com/mirego/kmp/boilerplate/previews/PreviewContext.kt index 6fb5c53..1dc48d8 100644 --- a/androidApp/src/main/java/com/mirego/kmp/boilerplate/previews/PreviewContext.kt +++ b/androidApp/src/main/java/com/mirego/kmp/boilerplate/previews/PreviewContext.kt @@ -4,12 +4,15 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext import androidx.startup.AppInitializer import com.mirego.kmp.boilerplate.platform.AppContextInitializer +import com.mirego.kmp.boilerplate.viewmodels.ViewModelFactory @Composable -fun PreviewContext(content: @Composable () -> Unit) { +fun PreviewContext(content: @Composable (ViewModelFactory) -> Unit) { // @Composable previews do not call AppInitializer. We must initialize our components manually. AppInitializer.getInstance(LocalContext.current) .initializeComponent(AppContextInitializer::class.java) - content() + val viewModelFactory = ViewModelFactory() + + content(viewModelFactory) } diff --git a/androidApp/src/main/java/com/mirego/kmp/boilerplate/viewmodels/LifecycleViewModel.kt b/androidApp/src/main/java/com/mirego/kmp/boilerplate/viewmodels/LifecycleViewModel.kt new file mode 100644 index 0000000..a187e97 --- /dev/null +++ b/androidApp/src/main/java/com/mirego/kmp/boilerplate/viewmodels/LifecycleViewModel.kt @@ -0,0 +1,28 @@ +package com.mirego.kmp.boilerplate.viewmodels + +import androidx.compose.runtime.Composable +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory + + +/** + * Convenience viewModel builder which creates the common ViewModel using the provided initializer + * and wraps it into an androidx.lifecycle.ViewModel() for proper cancellation. + */ +@Composable +inline fun lifecycleViewModel(crossinline initializer: () -> VM): VM { + val factory = viewModelFactory { + initializer { + LifecycleViewModel(vm = initializer()) + } + } + return viewModel>(factory = factory).vm +} + +/** + * Wraps our common ViewModel into an androidx.lifecycle.ViewModel() to cancel work when cleared. + */ +class LifecycleViewModel(val vm: VM) : androidx.lifecycle.ViewModel() { + override fun onCleared() = vm.cancel() +} diff --git a/androidApp/src/main/java/com/mirego/kmp/boilerplate/views/ExampleView.kt b/androidApp/src/main/java/com/mirego/kmp/boilerplate/views/ExampleView.kt new file mode 100644 index 0000000..5982081 --- /dev/null +++ b/androidApp/src/main/java/com/mirego/kmp/boilerplate/views/ExampleView.kt @@ -0,0 +1,26 @@ +package com.mirego.kmp.boilerplate.views + +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.mirego.kmp.boilerplate.previews.PreviewContext +import com.mirego.kmp.boilerplate.viewmodels.example.ExampleViewModel + +@Composable +fun ExampleView(viewModel: ExampleViewModel) { + val state: ExampleViewModel.State by viewModel.state.collectAsStateWithLifecycle() + + Text(text = state.greeting) +} + +@Preview(showSystemUi = true) +@Composable +fun PreviewExampleView() { + PreviewContext { viewModelFactory -> + ExampleView( + viewModel = viewModelFactory.example() + ) + } +} diff --git a/build.gradle.kts b/build.gradle.kts index 9018ab6..f2d43b2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,8 +5,9 @@ plugins { alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.multiplatform) apply false alias(libs.plugins.kotlin.native.cocoapods) apply false - alias(libs.plugins.serialization) apply false alias(libs.plugins.ktlint) apply false + alias(libs.plugins.serialization) apply false + alias(libs.plugins.skie) apply false alias(libs.plugins.owasp.dependencycheck) } diff --git a/gradle.properties b/gradle.properties index 0fecfef..46f8aa7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ #Gradle org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" -org.gradle.configuration-cache=true +org.gradle.configuration-cache=false #Kotlin kotlin.code.style=official #Android diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 43296ac..8f4095e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,15 +1,17 @@ [versions] androidComposeCompiler = "1.5.6" androidGradlePlugin = "8.2.0" -androidxStartup = "1.1.1" androidxActivityCompose = "1.8.1" androidxAppcompat = "1.6.1" androidxComposeBom = "2023.10.01" +androidxLifecycle = "2.6.2" +androidxStartup = "1.1.1" konnectivity = "0.3.0" kotlin = "1.9.21" kotlinxCoroutines = "1.7.3" -kotlinxSerialization = "1.6.0" -ktlint = "11.6.1" +kotlinxSerialization = "1.6.2" +ktlint = "12.0.2" +skie = "0.5.6" [libraries] androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidxActivityCompose" } @@ -18,11 +20,15 @@ androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", versi androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-compose-material = { group = "androidx.compose.material", name = "material" } +androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidxLifecycle" } +androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidxLifecycle" } +androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" } androidx-startup-runtime = { module = "androidx.startup:startup-runtime", version.ref = "androidxStartup" } konnectivity = { module = "com.mirego:konnectivity", version.ref = "konnectivity" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" } +skie-configuration-annotations = { module = "co.touchlab.skie:configuration-annotations", version.ref = "skie" } [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } @@ -33,6 +39,7 @@ kotlin-native-cocoapods = { id = "org.jetbrains.kotlin.native.cocoapods", versio ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } owasp-dependencycheck = { id = "org.owasp.dependencycheck", version = "8.4.2" } serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +skie = { id = "co.touchlab.skie", version.ref = "skie" } [bundles] diff --git a/ios/iosApp.xcodeproj/project.pbxproj b/ios/iosApp.xcodeproj/project.pbxproj index 04df674..a008d9f 100644 --- a/ios/iosApp.xcodeproj/project.pbxproj +++ b/ios/iosApp.xcodeproj/project.pbxproj @@ -10,10 +10,9 @@ 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; }; 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; }; 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; }; - 7555FF83242A565900829871 /* GreetingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* GreetingView.swift */; }; + 7555FF83242A565900829871 /* ExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ExampleView.swift */; }; 9B8ACFDB4E332DFCA8B97CBB /* Pods_iosApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E4E1328B104D05A50A097EE /* Pods_iosApp.framework */; }; BC5700EB2B1A94D200525C22 /* PreviewContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC5700EA2B1A94D200525C22 /* PreviewContext.swift */; }; - BC83B466276E4F080053E064 /* FlowUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC83B465276E4F080053E064 /* FlowUtils.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -23,10 +22,9 @@ 308A8A1989CCC0B3DD133EE0 /* Pods-iosApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.debug.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.debug.xcconfig"; sourceTree = ""; }; 4E4E1328B104D05A50A097EE /* Pods_iosApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iosApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7555FF7B242A565900829871 /* iosApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iosApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 7555FF82242A565900829871 /* GreetingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GreetingView.swift; sourceTree = ""; }; + 7555FF82242A565900829871 /* ExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleView.swift; sourceTree = ""; }; 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; BC5700EA2B1A94D200525C22 /* PreviewContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewContext.swift; sourceTree = ""; }; - BC83B465276E4F080053E064 /* FlowUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowUtils.swift; sourceTree = ""; }; E232C917135C2C1E3BC8748A /* Pods-iosApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.release.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -75,19 +73,18 @@ 7555FF8C242A565B00829871 /* Info.plist */, 058557BA273AAA24004C7B11 /* Assets.xcassets */, 2152FB032600AC8F00CF470E /* iOSApp.swift */, - 7555FF82242A565900829871 /* GreetingView.swift */, - BC83B464276E4EF80053E064 /* Utils */, + BCB66E1E2B2177530025AC5F /* Views */, 058557D7273AAEEB004C7B11 /* Preview Content */, ); path = iosApp; sourceTree = ""; }; - BC83B464276E4EF80053E064 /* Utils */ = { + BCB66E1E2B2177530025AC5F /* Views */ = { isa = PBXGroup; children = ( - BC83B465276E4F080053E064 /* FlowUtils.swift */, + 7555FF82242A565900829871 /* ExampleView.swift */, ); - path = Utils; + path = Views; sourceTree = ""; }; C8C629BFDC2144230B71E3BC /* Frameworks */ = { @@ -242,9 +239,8 @@ buildActionMask = 2147483647; files = ( 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */, - 7555FF83242A565900829871 /* GreetingView.swift in Sources */, + 7555FF83242A565900829871 /* ExampleView.swift in Sources */, BC5700EB2B1A94D200525C22 /* PreviewContext.swift in Sources */, - BC83B466276E4F080053E064 /* FlowUtils.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/iosApp/GreetingView.swift b/ios/iosApp/GreetingView.swift deleted file mode 100644 index 621f275..0000000 --- a/ios/iosApp/GreetingView.swift +++ /dev/null @@ -1,17 +0,0 @@ -import Shared -import SwiftUI - -struct GreetingView: View { - - @ObservedObject var greet = ObservableFlowWrapper(Greeting().greeting(), initial: "initial") - - var body: some View { - Text("\(greet.value)") - } -} - -#Preview { - PreviewContext { - GreetingView() - } -} diff --git a/ios/iosApp/Preview Content/PreviewContext.swift b/ios/iosApp/Preview Content/PreviewContext.swift index 663fc3a..9eda52f 100644 --- a/ios/iosApp/Preview Content/PreviewContext.swift +++ b/ios/iosApp/Preview Content/PreviewContext.swift @@ -2,9 +2,11 @@ import Shared import SwiftUI struct PreviewContext: View where Content: View { - let content: @MainActor () -> Content + let content: @MainActor (ViewModelFactory) -> Content + + let viewModelFactory = ViewModelFactory() var body: some View { - content() + content(viewModelFactory) } } diff --git a/ios/iosApp/Utils/FlowUtils.swift b/ios/iosApp/Utils/FlowUtils.swift deleted file mode 100644 index f6fe5fb..0000000 --- a/ios/iosApp/Utils/FlowUtils.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation -import Shared - -public class ObservableFlowWrapper: ObservableObject { - - @Published public private(set) var value: T - - private var watcher: Closeable? - - public init(_ flow: CFlow, initial value: T) { - self.value = value - - watcher = flow.watch { [weak self] t in - self?.value = t - } - } - - deinit { - watcher?.close() - } -} diff --git a/ios/iosApp/Views/ExampleView.swift b/ios/iosApp/Views/ExampleView.swift new file mode 100644 index 0000000..c69d359 --- /dev/null +++ b/ios/iosApp/Views/ExampleView.swift @@ -0,0 +1,37 @@ +import Shared +import SwiftUI + +struct ExampleView: View { + + private let viewModel: ExampleViewModel + + @State private var state: ExampleViewModelState + + init(viewModel: ExampleViewModel) { + self.viewModel = viewModel + self.state = viewModel.state.value + } + + var body: some View { + VStack { + Text(state.greeting) + } + .task { + await withTaskCancellationHandler { + for await state in viewModel.state { + self.state = state + } + } onCancel: { + viewModel.cancel() + } + } + } +} + +#Preview { + PreviewContext { viewModelFactory in + ExampleView( + viewModel: viewModelFactory.example() + ) + } +} diff --git a/ios/iosApp/iOSApp.swift b/ios/iosApp/iOSApp.swift index 947b0aa..e2638fb 100644 --- a/ios/iosApp/iOSApp.swift +++ b/ios/iosApp/iOSApp.swift @@ -1,10 +1,15 @@ +import Shared import SwiftUI @main struct IOSApp: App { - var body: some Scene { - WindowGroup { - GreetingView() - } - } + let viewModelFactory = ViewModelFactory() + + var body: some Scene { + WindowGroup { + ExampleView( + viewModel: viewModelFactory.example() + ) + } + } } diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 2966fd6..5df0cf5 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -1,5 +1,11 @@ @file:Suppress("UNUSED_VARIABLE") +import co.touchlab.skie.configuration.DefaultArgumentInterop +import co.touchlab.skie.configuration.EnumInterop +import co.touchlab.skie.configuration.ExperimentalFeatures +import co.touchlab.skie.configuration.FlowInterop +import co.touchlab.skie.configuration.SealedInterop +import co.touchlab.skie.configuration.SuspendInterop import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi plugins { @@ -8,6 +14,7 @@ plugins { alias(libs.plugins.serialization) alias(libs.plugins.android.library) alias(libs.plugins.ktlint) + alias(libs.plugins.skie) } version = project.property("versionName") as String @@ -48,6 +55,7 @@ kotlin { dependencies { implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.serialization.json) + implementation(libs.skie.configuration.annotations) api(libs.konnectivity) } } @@ -62,6 +70,8 @@ kotlin { androidMain { dependencies { implementation(libs.androidx.startup.runtime) + api(libs.androidx.lifecycle.runtime.ktx) + api(libs.androidx.lifecycle.viewmodel.compose) } } } @@ -99,3 +109,25 @@ ktlint { exclude { element -> element.file.path.contains("generated/") } } } + +skie { + analytics { + enabled.set(false) + } + features { + group { + DefaultArgumentInterop.Enabled(false) + EnumInterop.Enabled(false) + ExperimentalFeatures.Enabled(false) + FlowInterop.Enabled(false) + SealedInterop.Enabled(false) + SuspendInterop.Enabled(false) + } + group("com.mirego.kmp.boilerplate.viewmodels") { + EnumInterop.Enabled(true) + FlowInterop.Enabled(true) + SealedInterop.Enabled(true) + SuspendInterop.Enabled(true) + } + } +} diff --git a/shared/src/androidMain/kotlin/com/mirego/kmp/boilerplate/utils/CFlow.kt b/shared/src/androidMain/kotlin/com/mirego/kmp/boilerplate/utils/CFlow.kt deleted file mode 100644 index 1bfe07e..0000000 --- a/shared/src/androidMain/kotlin/com/mirego/kmp/boilerplate/utils/CFlow.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.mirego.kmp.boilerplate.utils - -import kotlinx.coroutines.flow.Flow - -actual class CFlow internal constructor(origin: Flow) : Flow by origin - -actual fun Flow.wrap(): CFlow = CFlow(this) diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/Greeting.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/Greeting.kt deleted file mode 100644 index 5ce8ea0..0000000 --- a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/Greeting.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.mirego.kmp.boilerplate - -import com.mirego.kmp.boilerplate.platform.Platform -import com.mirego.kmp.boilerplate.utils.CFlow -import com.mirego.kmp.boilerplate.utils.wrap -import com.mirego.konnectivity.Konnectivity -import com.mirego.konnectivity.NetworkState -import kotlinx.coroutines.flow.map - -class Greeting { - private val platform = Platform() - private val konnectivity = Konnectivity() - - private val greetingText = buildString { - appendLine("Hello! 👋") - appendLine(platform.system) - appendLine(platform.locale) - appendLine(platform.version) - appendLine() - } - - fun greeting(): CFlow = konnectivity.networkState - .map { networkState -> - greetingText + networkState.asGreetingInfo() - } - .wrap() - - private fun NetworkState.asGreetingInfo(): String = "By the way, you're " + when (this) { - NetworkState.Unreachable -> "offline. 🔌" - is NetworkState.Reachable -> when (metered) { - true -> "online, but your connection is metered. 📶" - else -> "online! 🛜" - } - } -} diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/platform/Platform.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/platform/Platform.kt index aeea0f5..3a31bd9 100644 --- a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/platform/Platform.kt +++ b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/platform/Platform.kt @@ -10,15 +10,15 @@ interface Platform { data class Locale( val languageCode: String, - val regionCode: String? + val regionCode: String?, ) data class System( val name: String, - val version: String + val version: String, ) data class Version( val name: String, - val code: Int + val code: Int, ) diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/utils/CFlow.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/utils/CFlow.kt deleted file mode 100644 index fe4d8d3..0000000 --- a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/utils/CFlow.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.mirego.kmp.boilerplate.utils - -import kotlinx.coroutines.flow.Flow - -expect class CFlow : Flow - -expect fun Flow.wrap(): CFlow diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodels/ViewModel.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodels/ViewModel.kt new file mode 100644 index 0000000..c90d3e5 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodels/ViewModel.kt @@ -0,0 +1,12 @@ +package com.mirego.kmp.boilerplate.viewmodels + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel + +interface ViewModel { + fun cancel() +} + +abstract class BaseViewModel(private val coroutineScope: CoroutineScope) : ViewModel { + final override fun cancel() = coroutineScope.cancel() +} diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodels/ViewModelFactory.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodels/ViewModelFactory.kt new file mode 100644 index 0000000..5c28b93 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodels/ViewModelFactory.kt @@ -0,0 +1,24 @@ +package com.mirego.kmp.boilerplate.viewmodels + +import com.mirego.kmp.boilerplate.platform.Platform +import com.mirego.kmp.boilerplate.viewmodels.example.ExampleViewModel +import com.mirego.kmp.boilerplate.viewmodels.example.ExampleViewModelImpl +import com.mirego.konnectivity.Konnectivity +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob + +class ViewModelFactory { + private val platform = Platform() + private val konnectivity = Konnectivity() + + private val defaultCoroutineScope: CoroutineScope + get() = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob()) + + fun example(): ExampleViewModel = + ExampleViewModelImpl( + defaultCoroutineScope, + platform, + konnectivity, + ) +} diff --git a/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodels/example/ExampleViewModel.kt b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodels/example/ExampleViewModel.kt new file mode 100644 index 0000000..5a450cb --- /dev/null +++ b/shared/src/commonMain/kotlin/com/mirego/kmp/boilerplate/viewmodels/example/ExampleViewModel.kt @@ -0,0 +1,58 @@ +package com.mirego.kmp.boilerplate.viewmodels.example + +import com.mirego.kmp.boilerplate.platform.Platform +import com.mirego.kmp.boilerplate.viewmodels.BaseViewModel +import com.mirego.kmp.boilerplate.viewmodels.ViewModel +import com.mirego.konnectivity.Konnectivity +import com.mirego.konnectivity.NetworkState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlin.time.Duration.Companion.seconds + +interface ExampleViewModel : ViewModel { + val state: StateFlow + + data class State( + val greeting: String, + ) +} + +class ExampleViewModelImpl( + coroutineScope: CoroutineScope, + platform: Platform, + konnectivity: Konnectivity, +) : BaseViewModel(coroutineScope), ExampleViewModel { + private val greetingText = + buildString { + appendLine("Hello! 👋") + appendLine(platform.system) + appendLine(platform.locale) + appendLine(platform.version) + appendLine() + } + + override val state: StateFlow = + konnectivity.networkState + .map { networkState -> + ExampleViewModel.State(greeting = greetingText + networkState.asGreetingInfo()) + } + .stateIn( + coroutineScope, + SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds), + ExampleViewModel.State(greeting = greetingText), + ) + + private fun NetworkState.asGreetingInfo(): String = + "By the way, you're " + + when (this) { + NetworkState.Unreachable -> "offline. 🔌" + is NetworkState.Reachable -> + when (metered) { + true -> "online, but your connection is metered. 📶" + else -> "online! 🛜" + } + } +} diff --git a/shared/src/iosMain/kotlin/com/mirego/kmp/boilerplate/utils/CFlow.kt b/shared/src/iosMain/kotlin/com/mirego/kmp/boilerplate/utils/CFlow.kt deleted file mode 100644 index 30d013c..0000000 --- a/shared/src/iosMain/kotlin/com/mirego/kmp/boilerplate/utils/CFlow.kt +++ /dev/null @@ -1,34 +0,0 @@ -@file:Suppress("unused") - -package com.mirego.kmp.boilerplate.utils - -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach - -fun interface Closeable { - fun close() -} - -actual class CFlow internal constructor( - private val origin: Flow, - private val dispatcher: CoroutineDispatcher = Dispatchers.Main -) : Flow by origin { - - fun watch(block: (T) -> Unit): Closeable { - val job = Job() - val scope = CoroutineScope(dispatcher + job) - - onEach { - block(it) - }.launchIn(scope) - - return Closeable { job.cancel() } - } -} - -actual fun Flow.wrap(): CFlow = CFlow(this) From a099eef0af8493e3214e7d572996326e80c01447 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Fortier Date: Mon, 8 Jan 2024 14:26:15 -0500 Subject: [PATCH 2/2] Update SKIE to 0.6.1 --- gradle/libs.versions.toml | 6 +++--- shared/build.gradle.kts | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8f4095e..73dcb26 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] androidComposeCompiler = "1.5.6" -androidGradlePlugin = "8.2.0" -androidxActivityCompose = "1.8.1" +androidGradlePlugin = "8.2.1" +androidxActivityCompose = "1.8.2" androidxAppcompat = "1.6.1" androidxComposeBom = "2023.10.01" androidxLifecycle = "2.6.2" @@ -11,7 +11,7 @@ kotlin = "1.9.21" kotlinxCoroutines = "1.7.3" kotlinxSerialization = "1.6.2" ktlint = "12.0.2" -skie = "0.5.6" +skie = "0.6.1" [libraries] androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidxActivityCompose" } diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 5df0cf5..e76e4b6 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -4,7 +4,9 @@ import co.touchlab.skie.configuration.DefaultArgumentInterop import co.touchlab.skie.configuration.EnumInterop import co.touchlab.skie.configuration.ExperimentalFeatures import co.touchlab.skie.configuration.FlowInterop +import co.touchlab.skie.configuration.FunctionInterop import co.touchlab.skie.configuration.SealedInterop +import co.touchlab.skie.configuration.SuppressSkieWarning import co.touchlab.skie.configuration.SuspendInterop import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi @@ -118,13 +120,19 @@ skie { group { DefaultArgumentInterop.Enabled(false) EnumInterop.Enabled(false) + EnumInterop.LegacyCaseName(true) ExperimentalFeatures.Enabled(false) FlowInterop.Enabled(false) + FunctionInterop.FileScopeConversion.Enabled(false) + FunctionInterop.LegacyName(true) SealedInterop.Enabled(false) + SealedInterop.ExportEntireHierarchy(false) + SuppressSkieWarning.NameCollision(false) SuspendInterop.Enabled(false) } group("com.mirego.kmp.boilerplate.viewmodels") { EnumInterop.Enabled(true) + EnumInterop.LegacyCaseName(false) FlowInterop.Enabled(true) SealedInterop.Enabled(true) SuspendInterop.Enabled(true)