Make state defined in KMM shared module easy to observe within Jetpack Compose and SwiftUI code with near zero
boilerplate by implementation of StateHolder
interface on class providing state to UI.
flowchart RL
subgraph KMM Shared Module
StateHolder
end
subgraph iOS/Android
View
end
StateHolder -- state --> View
View -- event --> StateHolder
Define a ViewModel
, Store
or whatever implementing a StateHolder
interface inside your shared module. Use it in
the platform.
class SimpleViewModel : KmmViewModel(), StateHolder by StateHolder() {
var message by stateful("Hello World")
private set
fun updateMessage() = viewModelScope.launch {
delay(500)
message = "Hello k-state"
}
}
Add kstate-core
dependency to the commonMain
source set.
val commonMain by getting {
dependencies {
implementation("com.jstarczewski.kstate:kstate-core:0.0.3")
}
}
Depending on the architecture consider exporting the library.
Add kstate.generate
gradle plugin to proper gradle module to generate Swift wrappers.
plugins {
id("com.jstarczewski.kstate.generate").version("0.0.3")
}
Configure wrappers by kstate.generate
DSL syntax.
swiftTemplates {
outputDir = "../ios/ios/StateHolder"
sharedModuleName = "common"
coreLibraryExported = false
}
Generate templates for iOS app by executing following gradle task.
./gradlew generateSwiftTemplates
Link generated files and add them to VCS.
After generation the plugin is no longer needed and can be safely removed from the project configuration.
Documentation is available here.
Samples are available as an example app on GitHub.
- Make your class in KMM shared module a
StateHolder
by implementingStateHolder
interface via interface delegation pattern withStateHolder()
function. - Use
StateHolder
DSL functions to declareStateful
in sharedStateHolder
withstateful
delegate.
class SimpleViewModel : KmmViewModel(), StateHolder by StateHolder() {
var message by stateful("Hello World")
private set
fun updateMessage() = viewModelScope.launch {
delay(500)
message = "Hello k-state"
}
}
State changes in @Composable
functions are reflected because shared State
on Android platform is exposed as
Compose MutableState
@Composable
fun SimpleScreen() {
val viewModel: SimpleViewModel = viewModel { SimpleViewModel() }
Box(modifier = Modifier.fillMaxSize()) {
Greeting(
modifier = Modifier
.align(Alignment.Center)
.clickable { viewModel.updateMessage() },
text = viewModel.message
)
}
}
Apply ObservedStateHolder
property wrapper to SimpleViewModel
to automatically wire state change observation.
ObservedStateHolder
is a utility property wrapper that will be generated
during library setup process.
struct SimpleView: View {
@ObservedStateHolder var viewModel = SimpleViewModel()
var body: some View {
Text(viewModel.message)
.onTapGesture {
viewModel.updateMessage()
}
}
}
Depending on whether the for sake of the project architecture the kstate-core
library should be
exported or not
additional templates setup may be needed.
The default behaviour is that the kstate-core
is not exported and no further configuration is needed, but for
exported kstate-core
where the export example is contained withing the following snippet of build.gradle.kts
file.
kotlin {
android()
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach {
it.binaries.framework {
baseName = "common"
export(project(":feature:saveable"))
export(project(":feature:obtainable"))
export("com.jstarczewski.kstate:kstate-core:0.0.3")
}
}
sourceSets {
val commonTest by getting
val commonMain by getting {
dependencies {
implementation(kotlin("test"))
api(project(":feature:saveable"))
api(project(":feature:obtainable"))
api("com.jstarczewski.kstate:kstate-core:0.0.3")
}
}
// Rest of build.gradle.kts file....
Templates need extra configuration to work. Add the following coreLibraryExported
flag set to true
to swiftTempaltes
config.
plugins {
id("com.jstarczewski.kstate.generate").version("0.0.3")
}
swiftTemplates {
outputDir = "../ios/ios/StateHolder"
sharedModuleName = "common"
// On default it is set to false
coreLibraryExported = true
}
After successfully applying the plugin configuration, generate Swift wrappers. Files should appear in outputDir
specified in
configuration
block.
./gradlew generateSwiftTemplates
To link generated files with your project, from XCode File
menu click Add files
and create group with sources.
When the files are linked, run the iOS
app to check whether everything builds. Maybe there are additional changes
needed to be done in the files, but
the vision is that after linking generated sources iOS app should build without anny issues.
The generation process is a one time operation. After successfully generating everything feel free to add it to source
control and remove kstate-generate
plugin