Skip to content

Commit

Permalink
Replace CFlows with StateFlows using SKIE
Browse files Browse the repository at this point in the history
  • Loading branch information
marcantoinefortier committed Dec 8, 2023
1 parent 8baf0d7 commit ac1b05d
Show file tree
Hide file tree
Showing 24 changed files with 271 additions and 175 deletions.
1 change: 1 addition & 0 deletions androidApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
24 changes: 0 additions & 24 deletions androidApp/src/main/java/com/mirego/kmp/boilerplate/Greeting.kt

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
@@ -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 <reified VM : ViewModel> lifecycleViewModel(crossinline initializer: () -> VM): VM {
val factory = viewModelFactory {
initializer {
LifecycleViewModel(vm = initializer())
}
}
return viewModel<LifecycleViewModel<VM>>(factory = factory).vm
}

/**
* Wraps our common ViewModel into an androidx.lifecycle.ViewModel() to cancel work when cleared.
*/
class LifecycleViewModel<VM : ViewModel>(val vm: VM) : androidx.lifecycle.ViewModel() {
override fun onCleared() = vm.cancel()
}
Original file line number Diff line number Diff line change
@@ -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()
)
}
}
3 changes: 2 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -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
Expand Down
13 changes: 10 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -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" }
Expand All @@ -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" }
Expand All @@ -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]

18 changes: 7 additions & 11 deletions ios/iosApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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 = "<group>"; };
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 = "<group>"; };
7555FF82242A565900829871 /* ExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleView.swift; sourceTree = "<group>"; };
7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
BC5700EA2B1A94D200525C22 /* PreviewContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewContext.swift; sourceTree = "<group>"; };
BC83B465276E4F080053E064 /* FlowUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowUtils.swift; sourceTree = "<group>"; };
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 = "<group>"; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -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 = "<group>";
};
BC83B464276E4EF80053E064 /* Utils */ = {
BCB66E1E2B2177530025AC5F /* Views */ = {
isa = PBXGroup;
children = (
BC83B465276E4F080053E064 /* FlowUtils.swift */,
7555FF82242A565900829871 /* ExampleView.swift */,
);
path = Utils;
path = Views;
sourceTree = "<group>";
};
C8C629BFDC2144230B71E3BC /* Frameworks */ = {
Expand Down Expand Up @@ -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;
};
Expand Down
17 changes: 0 additions & 17 deletions ios/iosApp/GreetingView.swift

This file was deleted.

6 changes: 4 additions & 2 deletions ios/iosApp/Preview Content/PreviewContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import Shared
import SwiftUI

struct PreviewContext<Content>: View where Content: View {
let content: @MainActor () -> Content
let content: @MainActor (ViewModelFactory) -> Content

let viewModelFactory = ViewModelFactory()

var body: some View {
content()
content(viewModelFactory)
}
}
21 changes: 0 additions & 21 deletions ios/iosApp/Utils/FlowUtils.swift

This file was deleted.

37 changes: 37 additions & 0 deletions ios/iosApp/Views/ExampleView.swift
Original file line number Diff line number Diff line change
@@ -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()
)
}
}
15 changes: 10 additions & 5 deletions ios/iosApp/iOSApp.swift
Original file line number Diff line number Diff line change
@@ -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()
)
}
}
}
Loading

0 comments on commit ac1b05d

Please sign in to comment.