Browse Source

Add iOS target and app for todoapp example (#166)

pull/210/head
Arkadii Ivanov 3 years ago committed by GitHub
parent
commit
497fd017fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      examples/todoapp/android/build.gradle.kts
  2. 3
      examples/todoapp/android/src/main/java/example/todo/android/MainActivity.kt
  3. 14
      examples/todoapp/buildSrc/buildSrc/src/main/kotlin/Deps.kt
  4. 26
      examples/todoapp/buildSrc/src/main/kotlin/android-setup.gradle.kts
  5. 22
      examples/todoapp/buildSrc/src/main/kotlin/multiplatform-compose-setup.gradle.kts
  6. 31
      examples/todoapp/buildSrc/src/main/kotlin/multiplatform-setup.gradle.kts
  7. 18
      examples/todoapp/common/compose-ui/build.gradle.kts
  8. 2
      examples/todoapp/common/compose-ui/src/androidMain/AndroidManifest.xml
  9. 2
      examples/todoapp/common/compose-ui/src/androidMain/kotlin/example/todo/common/ui/Scrollbars.kt
  10. 20
      examples/todoapp/common/compose-ui/src/commonMain/kotlin/example/todo/common/ui/Crossfade.kt
  11. 2
      examples/todoapp/common/compose-ui/src/commonMain/kotlin/example/todo/common/ui/Scrollbars.kt
  12. 2
      examples/todoapp/common/compose-ui/src/commonMain/kotlin/example/todo/common/ui/ShortcutHandler.kt
  13. 27
      examples/todoapp/common/compose-ui/src/commonMain/kotlin/example/todo/common/ui/TodoEditUi.kt
  14. 37
      examples/todoapp/common/compose-ui/src/commonMain/kotlin/example/todo/common/ui/TodoMainUi.kt
  15. 16
      examples/todoapp/common/compose-ui/src/commonMain/kotlin/example/todo/common/ui/TodoRootUi.kt
  16. 2
      examples/todoapp/common/compose-ui/src/desktopMain/kotlin/example/todo/common/ui/Scrollbars.kt
  17. 7
      examples/todoapp/common/database/build.gradle.kts
  18. 3
      examples/todoapp/common/database/src/commonMain/sqldelight/example/todo/common/database/TodoDatabase.sq
  19. 26
      examples/todoapp/common/database/src/iosMain/kotlin/example/todo/common/database/TestDatabaseDriverFactory.kt
  20. 9
      examples/todoapp/common/database/src/iosMain/kotlin/example/todo/common/database/TodoDatabaseDriverFactory.kt
  21. 2
      examples/todoapp/common/edit/build.gradle.kts
  22. 17
      examples/todoapp/common/edit/src/commonMain/kotlin/example/todo/common/edit/TodoEdit.kt
  23. 2
      examples/todoapp/common/edit/src/commonMain/kotlin/example/todo/common/edit/TodoItem.kt
  24. 12
      examples/todoapp/common/edit/src/commonMain/kotlin/example/todo/common/edit/integration/Mappers.kt
  25. 29
      examples/todoapp/common/edit/src/commonMain/kotlin/example/todo/common/edit/integration/TodoEditImpl.kt
  26. 8
      examples/todoapp/common/main/build.gradle.kts
  27. 4
      examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/TodoItem.kt
  28. 21
      examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/TodoMain.kt
  29. 12
      examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/integration/Mappers.kt
  30. 36
      examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/integration/TodoMainImpl.kt
  31. 11
      examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/integration/TodoMainStoreDatabase.kt
  32. 1
      examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/store/TodoMainStore.kt
  33. 1
      examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/store/TodoMainStoreProvider.kt
  34. 38
      examples/todoapp/common/main/src/commonTest/kotlin/example/todo/common/main/integration/TodoMainTest.kt
  35. 1
      examples/todoapp/common/main/src/commonTest/kotlin/example/todo/common/main/store/TestTodoMainStoreDatabase.kt
  36. 1
      examples/todoapp/common/main/src/commonTest/kotlin/example/todo/common/main/store/TodoMainStoreTest.kt
  37. 80
      examples/todoapp/common/root/build.gradle.kts
  38. 14
      examples/todoapp/common/root/src/commonMain/kotlin/example/todo/common/root/TodoRoot.kt
  39. 26
      examples/todoapp/common/root/src/commonMain/kotlin/example/todo/common/root/integration/TodoRootImpl.kt
  40. 6
      examples/todoapp/common/utils/build.gradle.kts
  41. 24
      examples/todoapp/common/utils/src/commonMain/kotlin/example/todo/common/utils/Binder.kt
  42. 9
      examples/todoapp/common/utils/src/commonMain/kotlin/example/todo/common/utils/Component.kt
  43. 16
      examples/todoapp/common/utils/src/commonMain/kotlin/example/todo/common/utils/CrossfadeExt.kt
  44. 31
      examples/todoapp/common/utils/src/commonMain/kotlin/example/todo/common/utils/StoreExt.kt
  45. 1
      examples/todoapp/desktop/build.gradle.kts
  46. 3
      examples/todoapp/desktop/src/jvmMain/kotlin/example/todo/desktop/Main.kt
  47. 1
      examples/todoapp/gradle.properties
  48. 18
      examples/todoapp/ios/.gitignore
  49. 418
      examples/todoapp/ios/TodoApp.xcodeproj/project.pbxproj
  50. 7
      examples/todoapp/ios/TodoApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  51. 8
      examples/todoapp/ios/TodoApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  52. 11
      examples/todoapp/ios/ios/Assets.xcassets/AccentColor.colorset/Contents.json
  53. 98
      examples/todoapp/ios/ios/Assets.xcassets/AppIcon.appiconset/Contents.json
  54. 6
      examples/todoapp/ios/ios/Assets.xcassets/Contents.json
  55. 19
      examples/todoapp/ios/ios/ComponentHolder.swift
  56. 31
      examples/todoapp/ios/ios/ContentView.swift
  57. 67
      examples/todoapp/ios/ios/EditView.swift
  58. 50
      examples/todoapp/ios/ios/Info.plist
  59. 28
      examples/todoapp/ios/ios/InputView.swift
  60. 39
      examples/todoapp/ios/ios/ListView.swift
  61. 68
      examples/todoapp/ios/ios/MainView.swift
  62. 5
      examples/todoapp/ios/ios/MutableStateBuilder.swift
  63. 22
      examples/todoapp/ios/ios/ObservableValue.swift
  64. 6
      examples/todoapp/ios/ios/Preview Content/Preview Assets.xcassets/Contents.json
  65. 43
      examples/todoapp/ios/ios/RootView.swift
  66. 13
      examples/todoapp/ios/ios/SimpleRouterState.swift
  67. 10
      examples/todoapp/ios/ios/TodoApp.swift
  68. 1
      examples/todoapp/settings.gradle.kts

2
examples/todoapp/android/build.gradle.kts

@ -30,6 +30,7 @@ dependencies {
implementation(project(":common:database"))
implementation(project(":common:utils"))
implementation(project(":common:root"))
implementation(project(":common:compose-ui"))
implementation(compose.material)
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlin)
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlinMain)
@ -37,4 +38,5 @@ dependencies {
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlinTimeTravel)
implementation(Deps.ArkIvanov.Decompose.decompose)
implementation(Deps.ArkIvanov.Decompose.extensionsCompose)
implementation(Deps.AndroidX.AppCompat.appCompat)
}

3
examples/todoapp/android/src/main/java/example/todo/android/MainActivity.kt

@ -13,6 +13,7 @@ import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory
import com.arkivanov.mvikotlin.timetravel.store.TimeTravelStoreFactory
import example.todo.common.database.TodoDatabaseDriver
import example.todo.common.root.TodoRoot
import example.todo.common.ui.TodoRootContent
import example.todo.database.TodoDatabase
class MainActivity : AppCompatActivity() {
@ -22,7 +23,7 @@ class MainActivity : AppCompatActivity() {
setContent {
ComposeAppTheme {
Surface(color = MaterialTheme.colors.background) {
rootComponent(::todoRoot).invoke()
TodoRootContent(rootComponent(::todoRoot))
}
}
}

14
examples/todoapp/buildSrc/buildSrc/src/main/kotlin/Deps.kt

@ -25,19 +25,30 @@ object Deps {
}
}
object AndroidX {
object AppCompat {
const val appCompat = "androidx.appcompat:appcompat:1.1.0"
}
}
object ArkIvanov {
object MVIKotlin {
private const val VERSION = "2.0.0"
const val rx = "com.arkivanov.mvikotlin:rx:$VERSION"
const val mvikotlin = "com.arkivanov.mvikotlin:mvikotlin:$VERSION"
const val mvikotlinMain = "com.arkivanov.mvikotlin:mvikotlin-main:$VERSION"
const val mvikotlinMainIosX64 = "com.arkivanov.mvikotlin:mvikotlin-main-iosx64:$VERSION"
const val mvikotlinMainIosArm64 = "com.arkivanov.mvikotlin:mvikotlin-main-iosarm64:$VERSION"
const val mvikotlinLogging = "com.arkivanov.mvikotlin:mvikotlin-logging:$VERSION"
const val mvikotlinTimeTravel = "com.arkivanov.mvikotlin:mvikotlin-timetravel:$VERSION"
const val mvikotlinExtensionsReaktive = "com.arkivanov.mvikotlin:mvikotlin-extensions-reaktive:$VERSION"
}
object Decompose {
private const val VERSION = "0.1.1"
private const val VERSION = "0.1.3"
const val decompose = "com.arkivanov.decompose:decompose:$VERSION"
const val decomposeIosX64 = "com.arkivanov.decompose:decompose-iosx64:$VERSION"
const val decomposeIosArm64 = "com.arkivanov.decompose:decompose-iosarm64:$VERSION"
const val extensionsCompose = "com.arkivanov.decompose:extensions-compose-jetbrains:$VERSION"
}
}
@ -59,6 +70,7 @@ object Deps {
const val gradlePlugin = "com.squareup.sqldelight:gradle-plugin:$VERSION"
const val androidDriver = "com.squareup.sqldelight:android-driver:$VERSION"
const val sqliteDriver = "com.squareup.sqldelight:sqlite-driver:$VERSION"
const val nativeDriver = "com.squareup.sqldelight:native-driver:$VERSION"
}
}
}

26
examples/todoapp/buildSrc/src/main/kotlin/android-setup.gradle.kts

@ -0,0 +1,26 @@
import gradle.kotlin.dsl.accessors._2e8a70bdda5e56ec477a6ff432ddf9d7.android
plugins {
id("com.android.library")
}
android {
compileSdkVersion(30)
defaultConfig {
minSdkVersion(23)
targetSdkVersion(30)
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
sourceSets {
named("main") {
manifest.srcFile("src/androidMain/AndroidManifest.xml")
res.srcDirs("src/androidMain/res")
}
}
}

22
examples/todoapp/buildSrc/src/main/kotlin/multiplatform-compose-setup.gradle.kts

@ -1,29 +1,39 @@
import org.jetbrains.compose.compose
plugins {
id("com.android.library")
id("kotlin-multiplatform")
id("org.jetbrains.compose")
}
kotlin {
jvm("desktop")
android()
sourceSets {
named("commonMain") {
dependencies {
api(compose.runtime)
api(compose.foundation)
api(compose.material)
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material)
}
}
named("androidMain") {
dependencies {
api("androidx.appcompat:appcompat:1.1.0")
api("androidx.core:core-ktx:1.3.1")
implementation("androidx.appcompat:appcompat:1.1.0")
implementation("androidx.core:core-ktx:1.3.1")
}
}
named("desktopMain") {
dependencies {
api(compose.desktop.common)
implementation(compose.desktop.common)
}
}
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions.jvmTarget = "1.8"
}
}

31
examples/todoapp/buildSrc/src/main/kotlin/multiplatform-setup.gradle.kts

@ -6,17 +6,9 @@ plugins {
kotlin {
jvm("desktop")
android()
ios()
sourceSets {
named("commonMain") {
dependencies {
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlin)
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlinExtensionsReaktive)
implementation(Deps.ArkIvanov.Decompose.decompose)
implementation(Deps.Badoo.Reaktive.reaktive)
}
}
named("commonTest") {
dependencies {
implementation(Deps.JetBrains.Kotlin.testCommon)
@ -40,24 +32,3 @@ kotlin {
kotlinOptions.jvmTarget = "1.8"
}
}
android {
compileSdkVersion(30)
defaultConfig {
minSdkVersion(23)
targetSdkVersion(30)
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
sourceSets {
named("main") {
manifest.srcFile("src/androidMain/AndroidManifest.xml")
res.srcDirs("src/androidMain/res")
}
}
}

18
examples/todoapp/common/compose-ui/build.gradle.kts

@ -0,0 +1,18 @@
plugins {
id("multiplatform-compose-setup")
id("android-setup")
}
kotlin {
sourceSets {
named("commonMain") {
dependencies {
implementation(project(":common:main"))
implementation(project(":common:edit"))
implementation(project(":common:root"))
implementation(Deps.ArkIvanov.Decompose.decompose)
implementation(Deps.ArkIvanov.Decompose.extensionsCompose)
}
}
}
}

2
examples/todoapp/common/compose-ui/src/androidMain/AndroidManifest.xml

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="example.todo.common.ui"/>

2
examples/todoapp/common/utils/src/androidMain/kotlin/example/todo/common/utils/compose/Scrollbars.kt → examples/todoapp/common/compose-ui/src/androidMain/kotlin/example/todo/common/ui/Scrollbars.kt

@ -1,4 +1,4 @@
package example.todo.common.utils.compose
package example.todo.common.ui
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable

20
examples/todoapp/common/compose-ui/src/commonMain/kotlin/example/todo/common/ui/Crossfade.kt

@ -0,0 +1,20 @@
package example.todo.common.ui
import androidx.compose.runtime.Composable
fun <T, K> crossfade(): @Composable (currentChild: T, currentKey: K, children: @Composable (T, K) -> Unit) -> Unit =
{ currentChild: T, currentKey: K, children: @Composable (T, K) -> Unit ->
KeyedCrossfade(currentChild, currentKey, children)
}
@Composable
private fun <T, K> KeyedCrossfade(currentChild: T, currentKey: K, children: @Composable (T, K) -> Unit) {
androidx.compose.animation.Crossfade(current = ChildWrapper(currentChild, currentKey)) {
children(it.child, it.key)
}
}
private class ChildWrapper<out T, out C>(val child: T, val key: C) {
override fun equals(other: Any?): Boolean = key == (other as? ChildWrapper<*, *>)?.key
override fun hashCode(): Int = key.hashCode()
}

2
examples/todoapp/common/utils/src/commonMain/kotlin/example/todo/common/utils/compose/Scrollbars.kt → examples/todoapp/common/compose-ui/src/commonMain/kotlin/example/todo/common/ui/Scrollbars.kt

@ -1,4 +1,4 @@
package example.todo.common.utils.compose
package example.todo.common.ui
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable

2
examples/todoapp/common/utils/src/commonMain/kotlin/example/todo/common/utils/ShortcutHandler.kt → examples/todoapp/common/compose-ui/src/commonMain/kotlin/example/todo/common/ui/ShortcutHandler.kt

@ -1,4 +1,4 @@
package example.todo.common.utils
package example.todo.common.ui
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEvent

27
examples/todoapp/common/edit/src/commonMain/kotlin/example/todo/common/edit/ui/TodoEditUi.kt → examples/todoapp/common/compose-ui/src/commonMain/kotlin/example/todo/common/ui/TodoEditUi.kt

@ -1,4 +1,4 @@
package example.todo.common.edit.ui
package example.todo.common.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -15,35 +15,32 @@ import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.badoo.reaktive.base.Consumer
import example.todo.common.edit.TodoEdit.Output
import example.todo.common.edit.store.TodoEditStore.Intent
import example.todo.common.edit.store.TodoEditStore.State
import com.arkivanov.decompose.extensions.compose.jetbrains.asState
import example.todo.common.edit.TodoEdit
@Composable
internal fun TodoEditUi(
state: State,
output: Consumer<Output>,
intents: (Intent) -> Unit
) {
fun TodoEditContent(component: TodoEdit) {
val model by component.models.asState()
Column(horizontalAlignment = Alignment.CenterHorizontally) {
TopAppBar(
title = { Text("Edit todo") },
navigationIcon = {
IconButton(onClick = { output.onNext(Output.Finished) }) {
IconButton(onClick = component::onCloseClicked) {
Icon(Icons.Default.ArrowBack)
}
}
)
TextField(
value = state.text,
value = model.text,
modifier = Modifier.weight(1F).fillMaxWidth().padding(8.dp),
label = { Text("Todo text") },
onValueChange = { intents(Intent.SetText(text = it)) }
onValueChange = component::onTextChanged
)
Row(modifier = Modifier.padding(8.dp)) {
@ -52,8 +49,8 @@ internal fun TodoEditUi(
Spacer(modifier = Modifier.width(8.dp))
Checkbox(
checked = state.isDone,
onCheckedChange = { intents(Intent.SetDone(isDone = it)) }
checked = model.isDone,
onCheckedChange = component::onDoneChanged
)
}
}

37
examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/ui/TodoMainUi.kt → examples/todoapp/common/compose-ui/src/commonMain/kotlin/example/todo/common/ui/TodoMainUi.kt

@ -1,4 +1,4 @@
package example.todo.common.main.ui
package example.todo.common.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
@ -21,6 +21,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.Key
@ -28,38 +29,30 @@ import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.badoo.reaktive.base.Consumer
import example.todo.common.main.TodoMain.Output
import example.todo.common.main.store.TodoItem
import example.todo.common.main.store.TodoMainStore.Intent
import example.todo.common.main.store.TodoMainStore.State
import example.todo.common.utils.compose.MARGIN_SCROLLBAR
import example.todo.common.utils.compose.VerticalScrollbar
import example.todo.common.utils.compose.rememberScrollbarAdapter
import example.todo.common.utils.onKeyUp
import com.arkivanov.decompose.extensions.compose.jetbrains.asState
import example.todo.common.main.TodoItem
import example.todo.common.main.TodoMain
@Composable
internal fun TodoMainUi(
state: State,
output: Consumer<Output>,
intents: (Intent) -> Unit
) {
fun TodoMainContent(component: TodoMain) {
val model by component.models.asState()
Column {
TopAppBar(title = { Text(text = "Todo List") })
Box(Modifier.weight(1F)) {
TodoList(
items = state.items,
onItemClicked = { output.onNext(Output.Selected(id = it)) },
onDoneChanged = { id, isDone -> intents(Intent.SetItemDone(id = id, isDone = isDone)) },
onDeleteItemClicked = { intents(Intent.DeleteItem(id = it)) }
items = model.items,
onItemClicked = component::onItemClicked,
onDoneChanged = component::onItemDoneChanged,
onDeleteItemClicked = component::onItemDeleteClicked
)
}
TodoInput(
text = state.text,
onAddClicked = { intents(Intent.AddItem) },
onTextChanged = { intents(Intent.SetText(text = it)) }
text = model.text,
onAddClicked = component::onAddItemClicked,
onTextChanged = component::onInputTextChanged
)
}
}

16
examples/todoapp/common/compose-ui/src/commonMain/kotlin/example/todo/common/ui/TodoRootUi.kt

@ -0,0 +1,16 @@
package example.todo.common.ui
import androidx.compose.runtime.Composable
import com.arkivanov.decompose.extensions.compose.jetbrains.Children
import example.todo.common.root.TodoRoot
import example.todo.common.root.TodoRoot.Child
@Composable
fun TodoRootContent(component: TodoRoot) {
Children(routerState = component.routerState, animation = crossfade()) { child, _ ->
when (child) {
is Child.Main -> TodoMainContent(child.component)
is Child.Edit -> TodoEditContent(child.component)
}
}
}

2
examples/todoapp/common/utils/src/desktopMain/kotlin/example/todo/common/utils/compose/Scrollbars.kt → examples/todoapp/common/compose-ui/src/desktopMain/kotlin/example/todo/common/ui/Scrollbars.kt

@ -1,4 +1,4 @@
package example.todo.common.utils.compose
package example.todo.common.ui
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.lazy.LazyListState

7
examples/todoapp/common/database/build.gradle.kts

@ -1,5 +1,6 @@
plugins {
id("multiplatform-setup")
id("android-setup")
id("com.squareup.sqldelight")
}
@ -29,5 +30,11 @@ kotlin {
implementation(Deps.Squareup.SQLDelight.sqliteDriver)
}
}
iosMain {
dependencies {
implementation(Deps.Squareup.SQLDelight.nativeDriver)
}
}
}
}

3
examples/todoapp/common/database/src/commonMain/sqldelight/example/todo/common/database/TodoDatabase.sq

@ -34,3 +34,6 @@ WHERE id = :id;
getLastInsertId:
SELECT last_insert_rowid();
clear:
DELETE FROM TodoItemEntity;

26
examples/todoapp/common/database/src/iosMain/kotlin/example/todo/common/database/TestDatabaseDriverFactory.kt

@ -0,0 +1,26 @@
package example.todo.common.database
import co.touchlab.sqliter.DatabaseConfiguration
import com.squareup.sqldelight.db.SqlDriver
import com.squareup.sqldelight.drivers.native.NativeSqliteDriver
import com.squareup.sqldelight.drivers.native.wrapConnection
import example.todo.database.TodoDatabase
@Suppress("FunctionName") // Factory function
actual fun TestDatabaseDriver(): SqlDriver {
val schema = TodoDatabase.Schema
return NativeSqliteDriver(
DatabaseConfiguration(
name = ":memory:",
version = schema.version,
create = { wrapConnection(it, schema::create) },
upgrade = { connection, oldVersion, newVersion ->
wrapConnection(connection) {
schema.migrate(it, oldVersion, newVersion)
}
},
inMemory = true
)
)
}

9
examples/todoapp/common/database/src/iosMain/kotlin/example/todo/common/database/TodoDatabaseDriverFactory.kt

@ -0,0 +1,9 @@
package example.todo.common.database
import com.squareup.sqldelight.db.SqlDriver
import com.squareup.sqldelight.drivers.native.NativeSqliteDriver
import example.todo.database.TodoDatabase
@Suppress("FunctionName") // Factory function
fun TodoDatabaseDriver(): SqlDriver =
NativeSqliteDriver(TodoDatabase.Schema, "TodoDatabase.db")

2
examples/todoapp/common/edit/build.gradle.kts

@ -1,6 +1,6 @@
plugins {
id("multiplatform-setup")
id("multiplatform-compose-setup")
id("android-setup")
}
kotlin {

17
examples/todoapp/common/edit/src/commonMain/kotlin/example/todo/common/edit/TodoEdit.kt

@ -1,14 +1,27 @@
package example.todo.common.edit
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.value.Value
import com.arkivanov.mvikotlin.core.store.StoreFactory
import com.badoo.reaktive.base.Consumer
import example.todo.common.edit.TodoEdit.Dependencies
import example.todo.common.edit.integration.TodoEditImpl
import example.todo.common.utils.Component
import example.todo.database.TodoDatabase
interface TodoEdit : Component {
interface TodoEdit {
val models: Value<Model>
fun onTextChanged(text: String)
fun onDoneChanged(isDone: Boolean)
fun onCloseClicked()
data class Model(
val text: String,
val isDone: Boolean
)
interface Dependencies {
val storeFactory: StoreFactory

2
examples/todoapp/common/edit/src/commonMain/kotlin/example/todo/common/edit/TodoItem.kt

@ -1,6 +1,6 @@
package example.todo.common.edit
data class TodoItem(
internal data class TodoItem(
val text: String,
val isDone: Boolean
)

12
examples/todoapp/common/edit/src/commonMain/kotlin/example/todo/common/edit/integration/Mappers.kt

@ -0,0 +1,12 @@
package example.todo.common.edit.integration
import example.todo.common.edit.TodoEdit.Model
import example.todo.common.edit.store.TodoEditStore.State
internal val stateToModel: (State) -> Model =
{
Model(
text = it.text,
isDone = it.isDone
)
}

29
examples/todoapp/common/edit/src/commonMain/kotlin/example/todo/common/edit/integration/TodoEditImpl.kt

@ -1,13 +1,15 @@
package example.todo.common.edit.integration
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.value.Value
import com.arkivanov.decompose.value.operator.map
import example.todo.common.edit.TodoEdit
import example.todo.common.edit.TodoEdit.Dependencies
import example.todo.common.edit.TodoEdit.Model
import example.todo.common.edit.TodoEdit.Output
import example.todo.common.edit.store.TodoEditStore.Intent
import example.todo.common.edit.store.TodoEditStoreProvider
import example.todo.common.edit.ui.TodoEditUi
import example.todo.common.utils.composeState
import example.todo.common.utils.asValue
import example.todo.common.utils.getStore
internal class TodoEditImpl(
@ -24,14 +26,17 @@ internal class TodoEditImpl(
).provide()
}
@Composable
override fun invoke() {
val state by store.composeState
override val models: Value<Model> = store.asValue().map(stateToModel)
TodoEditUi(
state = state,
output = editOutput,
intents = store::accept
)
override fun onTextChanged(text: String) {
store.accept(Intent.SetText(text = text))
}
override fun onDoneChanged(isDone: Boolean) {
store.accept(Intent.SetDone(isDone = isDone))
}
override fun onCloseClicked() {
editOutput.onNext(Output.Finished)
}
}

8
examples/todoapp/common/main/build.gradle.kts

@ -1,6 +1,8 @@
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
plugins {
id("multiplatform-setup")
id("multiplatform-compose-setup")
id("android-setup")
}
kotlin {
@ -24,4 +26,8 @@ kotlin {
}
}
}
targets.getByName<KotlinNativeTarget>("iosX64").compilations.forEach {
it.kotlinOptions.freeCompilerArgs += arrayOf("-linker-options", "-lsqlite3")
}
}

4
examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/store/TodoItem.kt → examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/TodoItem.kt

@ -1,6 +1,6 @@
package example.todo.common.main.store
package example.todo.common.main
internal data class TodoItem(
data class TodoItem(
val id: Long = 0L,
val order: Long = 0L,
val text: String = "",

21
examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/TodoMain.kt

@ -1,14 +1,31 @@
package example.todo.common.main
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.value.Value
import com.arkivanov.mvikotlin.core.store.StoreFactory
import com.badoo.reaktive.base.Consumer
import example.todo.common.main.TodoMain.Dependencies
import example.todo.common.main.integration.TodoMainImpl
import example.todo.common.utils.Component
import example.todo.database.TodoDatabase
interface TodoMain : Component {
interface TodoMain {
val models: Value<Model>
fun onItemClicked(id: Long)
fun onItemDoneChanged(id: Long, isDone: Boolean)
fun onItemDeleteClicked(id: Long)
fun onInputTextChanged(text: String)
fun onAddItemClicked()
data class Model(
val items: List<TodoItem>,
val text: String
)
interface Dependencies {
val storeFactory: StoreFactory

12
examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/integration/Mappers.kt

@ -0,0 +1,12 @@
package example.todo.common.main.integration
import example.todo.common.main.TodoMain.Model
import example.todo.common.main.store.TodoMainStore.State
internal val stateToModel: (State) -> Model =
{
Model(
items = it.items,
text = it.text
)
}

36
examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/integration/TodoMainImpl.kt

@ -1,16 +1,15 @@
package example.todo.common.main.integration
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.value.Value
import com.arkivanov.decompose.value.operator.map
import example.todo.common.main.TodoMain
import example.todo.common.main.TodoMain.Dependencies
import example.todo.common.main.TodoMain.Model
import example.todo.common.main.TodoMain.Output
import example.todo.common.main.store.TodoMainStore.Intent
import example.todo.common.main.store.TodoMainStore.State
import example.todo.common.main.store.TodoMainStoreProvider
import example.todo.common.main.ui.TodoMainUi
import example.todo.common.utils.composeState
import example.todo.common.utils.asValue
import example.todo.common.utils.getStore
internal class TodoMainImpl(
@ -26,24 +25,25 @@ internal class TodoMainImpl(
).provide()
}
internal val state: State get() = store.state
override val models: Value<Model> = store.asValue().map(stateToModel)
@Composable
override fun invoke() {
val state by store.composeState
override fun onItemClicked(id: Long) {
mainOutput.onNext(Output.Selected(id = id))
}
override fun onItemDoneChanged(id: Long, isDone: Boolean) {
store.accept(Intent.SetItemDone(id = id, isDone = isDone))
}
TodoMainUi(
state = state,
output = mainOutput,
intents = store::accept
)
override fun onItemDeleteClicked(id: Long) {
store.accept(Intent.DeleteItem(id = id))
}
internal fun onIntent(intent: Intent) {
store.accept(intent)
override fun onInputTextChanged(text: String) {
store.accept(Intent.SetText(text = text))
}
internal fun onOutput(output: Output) {
mainOutput.onNext(output)
override fun onAddItemClicked() {
store.accept(Intent.AddItem)
}
}

11
examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/integration/TodoMainStoreDatabase.kt

@ -10,7 +10,7 @@ import com.squareup.sqldelight.Query
import example.todo.common.database.TodoDatabaseQueries
import example.todo.common.database.TodoItemEntity
import example.todo.common.database.asObservable
import example.todo.common.main.store.TodoItem
import example.todo.common.main.TodoItem
import example.todo.common.main.store.TodoMainStoreProvider
internal class TodoMainStoreDatabase(
@ -40,11 +40,6 @@ internal class TodoMainStoreDatabase(
.subscribeOn(ioScheduler)
override fun add(text: String): Completable =
completableFromFunction {
queries.transactionWithResult {
queries.add(text = text)
val lastId = queries.getLastInsertId().executeAsOne()
queries.select(id = lastId).executeAsOne()
}
}.subscribeOn(ioScheduler)
completableFromFunction { queries.add(text = text) }
.subscribeOn(ioScheduler)
}

1
examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/store/TodoMainStore.kt

@ -1,6 +1,7 @@
package example.todo.common.main.store
import com.arkivanov.mvikotlin.core.store.Store
import example.todo.common.main.TodoItem
import example.todo.common.main.store.TodoMainStore.Intent
import example.todo.common.main.store.TodoMainStore.State

1
examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/store/TodoMainStoreProvider.kt

@ -10,6 +10,7 @@ import com.badoo.reaktive.observable.Observable
import com.badoo.reaktive.observable.map
import com.badoo.reaktive.observable.observeOn
import com.badoo.reaktive.scheduler.mainScheduler
import example.todo.common.main.TodoItem
import example.todo.common.main.store.TodoMainStore.Intent
import example.todo.common.main.store.TodoMainStore.State

38
examples/todoapp/common/main/src/commonTest/kotlin/example/todo/common/main/integration/TodoMainTest.kt

@ -12,10 +12,10 @@ import com.badoo.reaktive.test.observable.test
import com.badoo.reaktive.test.scheduler.TestScheduler
import example.todo.common.database.TestDatabaseDriver
import example.todo.common.database.TodoItemEntity
import example.todo.common.main.TodoItem
import example.todo.common.main.TodoMain.Dependencies
import example.todo.common.main.TodoMain.Model
import example.todo.common.main.TodoMain.Output
import example.todo.common.main.store.TodoItem
import example.todo.common.main.store.TodoMainStore.Intent
import example.todo.database.TodoDatabase
import kotlin.test.BeforeTest
import kotlin.test.Test
@ -45,12 +45,16 @@ class TodoMainTest {
)
}
private val model: Model get() = impl.models.value
@BeforeTest
fun before() {
overrideSchedulers(
main = { TestScheduler() },
io = { TestScheduler() }
)
queries.clear()
}
@Test
@ -67,15 +71,15 @@ class TodoMainTest {
queries.delete(id = id)
assertFalse(impl.state.items.any { it.id == id })
assertFalse(model.items.any { it.id == id })
}
@Test
fun WHEN_item_selected_THEN_Output_Selected_emitted() {
fun WHEN_item_clicked_THEN_Output_Selected_emitted() {
queries.add("Item1")
val id = firstItem().id
impl.onOutput(Output.Selected(id = id))
impl.onItemClicked(id = id)
output.assertValue(Output.Selected(id = id))
}
@ -86,7 +90,7 @@ class TodoMainTest {
val id = firstItem().id
queries.setDone(id = id, isDone = false)
impl.onIntent(Intent.SetItemDone(id = id, isDone = true))
impl.onItemDoneChanged(id = id, isDone = true)
assertTrue(queries.select(id = id).executeAsOne().isDone)
}
@ -97,17 +101,17 @@ class TodoMainTest {
val id = firstItem().id
queries.setDone(id = id, isDone = true)
impl.onIntent(Intent.SetItemDone(id = id, isDone = false))
impl.onItemDoneChanged(id = id, isDone = false)
assertFalse(queries.select(id = id).executeAsOne().isDone)
}
@Test
fun WHEN_delete_clicked_THEN_item_deleted_in_database() {
fun WHEN_item_delete_clicked_THEN_item_deleted_in_database() {
queries.add("Item1")
val id = firstItem().id
impl.onIntent(Intent.DeleteItem(id = id))
impl.onItemDeleteClicked(id = id)
assertNull(queries.select(id = id).executeAsOneOrNull())
}
@ -123,25 +127,25 @@ class TodoMainTest {
}
@Test
fun WHEN_text_changed_THEN_text_updated() {
impl.onIntent(Intent.SetText(text = "Item text"))
fun WHEN_input_text_changed_THEN_text_updated() {
impl.onInputTextChanged(text = "Item text")
assertEquals("Item text", impl.state.text)
assertEquals("Item text", model.text)
}
@Test
fun GIVEN_text_entered_WHEN_add_clicked_THEN_item_added_in_database() {
impl.onIntent(Intent.SetText(text = "Item text"))
fun GIVEN_input_text_entered_WHEN_add_item_clicked_THEN_item_added_in_database() {
impl.onInputTextChanged(text = "Item text")
impl.onIntent(Intent.AddItem)
impl.onAddItemClicked()
assertEquals("Item text", lastInsertItem().text)
}
private fun firstItem(): TodoItem = impl.state.items[0]
private fun firstItem(): TodoItem = model.items[0]
private fun lastInsertItem(): TodoItemEntity {
val lastInsertId = queries.getLastInsertId().executeAsOne()
val lastInsertId = queries.transactionWithResult<Long> { queries.getLastInsertId().executeAsOne() }
return queries.select(id = lastInsertId).executeAsOne()
}

1
examples/todoapp/common/main/src/commonTest/kotlin/example/todo/common/main/store/TestTodoMainStoreDatabase.kt

@ -4,6 +4,7 @@ import com.badoo.reaktive.completable.Completable
import com.badoo.reaktive.completable.completableFromFunction
import com.badoo.reaktive.observable.Observable
import com.badoo.reaktive.subject.behavior.BehaviorSubject
import example.todo.common.main.TodoItem
internal class TestTodoMainStoreDatabase : TodoMainStoreProvider.Database {

1
examples/todoapp/common/main/src/commonTest/kotlin/example/todo/common/main/store/TodoMainStoreTest.kt

@ -3,6 +3,7 @@ package example.todo.common.main.store
import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory
import com.badoo.reaktive.scheduler.overrideSchedulers
import com.badoo.reaktive.test.scheduler.TestScheduler
import example.todo.common.main.TodoItem
import example.todo.common.main.store.TodoMainStore.Intent
import kotlin.test.BeforeTest
import kotlin.test.Test

80
examples/todoapp/common/root/build.gradle.kts

@ -1,14 +1,38 @@
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
plugins {
id("multiplatform-setup")
id("multiplatform-compose-setup")
id("kotlin-android-extensions")
}
androidExtensions {
features = setOf("parcelize")
id("android-setup")
id("kotlin-parcelize")
}
kotlin {
ios {
binaries {
framework {
baseName = "Todo"
linkerOpts.add("-lsqlite3")
export(project(":common:database"))
export(project(":common:main"))
export(project(":common:edit"))
when (val target = this.compilation.target.name) {
"iosX64" -> {
export(Deps.ArkIvanov.Decompose.decomposeIosX64)
export(Deps.ArkIvanov.MVIKotlin.mvikotlinMainIosX64)
}
"iosArm64" -> {
export(Deps.ArkIvanov.Decompose.decomposeIosArm64)
export(Deps.ArkIvanov.MVIKotlin.mvikotlinMainIosArm64)
}
else -> error("Unsupported target: $target")
}
}
}
}
sourceSets {
named("commonMain") {
dependencies {
@ -16,10 +40,52 @@ kotlin {
implementation(project(":common:database"))
implementation(project(":common:main"))
implementation(project(":common:edit"))
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlin)
implementation(Deps.ArkIvanov.Decompose.decompose)
implementation(Deps.ArkIvanov.Decompose.extensionsCompose)
implementation(Deps.Badoo.Reaktive.reaktive)
}
}
}
sourceSets {
named("iosMain") {
dependencies {
api(project(":common:database"))
api(project(":common:main"))
api(project(":common:edit"))
}
}
named("iosX64Main") {
dependencies {
api(Deps.ArkIvanov.Decompose.decomposeIosX64)
api(Deps.ArkIvanov.MVIKotlin.mvikotlinMainIosX64)
}
}
named("iosArm64Main") {
dependencies {
api(Deps.ArkIvanov.Decompose.decomposeIosArm64)
api(Deps.ArkIvanov.MVIKotlin.mvikotlinMainIosArm64)
}
}
}
}
fun getIosTarget(): String {
val sdkName = System.getenv("SDK_NAME") ?: "iphonesimulator"
return if (sdkName.startsWith("iphoneos")) "iosArm64" else "iosX64"
}
val packForXcode by tasks.creating(Sync::class) {
group = "build"
val mode = System.getenv("CONFIGURATION") ?: "DEBUG"
val targetName = getIosTarget()
val framework = kotlin.targets.getByName<KotlinNativeTarget>(targetName).binaries.getFramework(mode)
inputs.property("mode", mode)
dependsOn(framework.linkTask)
val targetDir = File(buildDir, "xcode-frameworks")
from(framework.outputDirectory)
into(targetDir)
}

14
examples/todoapp/common/root/src/commonMain/kotlin/example/todo/common/root/TodoRoot.kt

@ -1,13 +1,23 @@
package example.todo.common.root
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.RouterState
import com.arkivanov.decompose.value.Value
import com.arkivanov.mvikotlin.core.store.StoreFactory
import example.todo.common.edit.TodoEdit
import example.todo.common.main.TodoMain
import example.todo.common.root.TodoRoot.Dependencies
import example.todo.common.root.integration.TodoRootImpl
import example.todo.common.utils.Component
import example.todo.database.TodoDatabase
interface TodoRoot : Component {
interface TodoRoot {
val routerState: Value<RouterState<*, Child>>
sealed class Child {
data class Main(val component: TodoMain) : Child()
data class Edit(val component: TodoEdit) : Child()
}
interface Dependencies {
val storeFactory: StoreFactory

26
examples/todoapp/common/root/src/commonMain/kotlin/example/todo/common/root/integration/TodoRootImpl.kt

@ -1,19 +1,18 @@
package example.todo.common.root.integration
import androidx.compose.runtime.Composable
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.jetbrains.children
import com.arkivanov.decompose.RouterState
import com.arkivanov.decompose.router
import com.arkivanov.decompose.statekeeper.Parcelable
import com.arkivanov.decompose.statekeeper.Parcelize
import com.arkivanov.decompose.value.Value
import com.badoo.reaktive.base.Consumer
import example.todo.common.edit.TodoEdit
import example.todo.common.main.TodoMain
import example.todo.common.root.TodoRoot
import example.todo.common.root.TodoRoot.Child
import example.todo.common.root.TodoRoot.Dependencies
import example.todo.common.utils.Component
import example.todo.common.utils.Consumer
import example.todo.common.utils.Crossfade
internal class TodoRootImpl(
componentContext: ComponentContext,
@ -21,16 +20,18 @@ internal class TodoRootImpl(
) : TodoRoot, ComponentContext by componentContext, Dependencies by dependencies {
private val router =
router<Configuration, Component>(
router<Configuration, Child>(
initialConfiguration = Configuration.Main,
handleBackButton = true,
componentFactory = ::createChild
)
private fun createChild(configuration: Configuration, componentContext: ComponentContext): Component =
override val routerState: Value<RouterState<*, Child>> = router.state
private fun createChild(configuration: Configuration, componentContext: ComponentContext): Child =
when (configuration) {
is Configuration.Main -> todoMain(componentContext)
is Configuration.Edit -> todoEdit(componentContext, itemId = configuration.itemId)
is Configuration.Main -> Child.Main(todoMain(componentContext))
is Configuration.Edit -> Child.Edit(todoEdit(componentContext, itemId = configuration.itemId))
}
private fun todoMain(componentContext: ComponentContext): TodoMain =
@ -60,15 +61,6 @@ internal class TodoRootImpl(
is TodoEdit.Output.Finished -> router.pop()
}
@Composable
override fun invoke() {
router.state.children { child, configuration ->
Crossfade(currentChild = child, currentKey = configuration) { currentChild ->
currentChild()
}
}
}
private sealed class Configuration : Parcelable {
@Parcelize
object Main : Configuration()

6
examples/todoapp/common/utils/build.gradle.kts

@ -1,16 +1,16 @@
plugins {
id("multiplatform-setup")
id("multiplatform-compose-setup")
id("android-setup")
}
kotlin {
sourceSets {
named("commonMain") {
dependencies {
implementation(Deps.ArkIvanov.MVIKotlin.rx)
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlin)
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlinExtensionsReaktive)
implementation(Deps.ArkIvanov.Decompose.decompose)
implementation(Deps.ArkIvanov.Decompose.extensionsCompose)
implementation(Deps.Badoo.Reaktive.reaktive)
}
}
}

24
examples/todoapp/common/utils/src/commonMain/kotlin/example/todo/common/utils/Binder.kt

@ -1,24 +0,0 @@
package example.todo.common.utils
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.lifecycle.Lifecycle
import com.arkivanov.decompose.lifecycle.subscribe
import com.arkivanov.mvikotlin.core.binder.Binder
import com.arkivanov.mvikotlin.core.binder.BinderLifecycleMode
import com.arkivanov.mvikotlin.extensions.reaktive.BindingsBuilder
import com.arkivanov.mvikotlin.extensions.reaktive.bind
fun bind(lifecycle: Lifecycle, mode: BinderLifecycleMode, builder: BindingsBuilder.() -> Unit): Binder {
val binder = bind(builder)
when (mode) {
BinderLifecycleMode.CREATE_DESTROY -> lifecycle.subscribe(onCreate = { binder.start() }, onDestroy = { binder.stop() })
BinderLifecycleMode.START_STOP -> lifecycle.subscribe(onStart = { binder.start() }, onStop = { binder.stop() })
BinderLifecycleMode.RESUME_PAUSE -> lifecycle.subscribe(onResume = { binder.start() }, onPause = { binder.stop() })
}.let {}
return binder
}
fun ComponentContext.bind(mode: BinderLifecycleMode, builder: BindingsBuilder.() -> Unit): Binder =
bind(lifecycle, mode, builder)

9
examples/todoapp/common/utils/src/commonMain/kotlin/example/todo/common/utils/Component.kt

@ -1,9 +0,0 @@
package example.todo.common.utils
import androidx.compose.runtime.Composable
interface Component {
@Composable
operator fun invoke()
}

16
examples/todoapp/common/utils/src/commonMain/kotlin/example/todo/common/utils/CrossfadeExt.kt

@ -1,16 +0,0 @@
package example.todo.common.utils
import androidx.compose.animation.Crossfade
import androidx.compose.runtime.Composable
@Composable
fun <T> Crossfade(currentChild: T, currentKey: Any, children: @Composable() (T) -> Unit) {
Crossfade(current = ChildWrapper(currentChild, currentKey)) {
children(it.child)
}
}
private class ChildWrapper<out T>(val child: T, val key: Any) {
override fun equals(other: Any?): Boolean = key == (other as? ChildWrapper<*>)?.key
override fun hashCode(): Int = key.hashCode()
}

31
examples/todoapp/common/utils/src/commonMain/kotlin/example/todo/common/utils/StoreExt.kt

@ -1,20 +1,23 @@
package example.todo.common.utils
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.onDispose
import androidx.compose.runtime.remember
import com.arkivanov.decompose.value.Value
import com.arkivanov.decompose.value.ValueObserver
import com.arkivanov.mvikotlin.core.store.Store
import com.arkivanov.mvikotlin.extensions.reaktive.states
import com.badoo.reaktive.observable.subscribe
import com.arkivanov.mvikotlin.rx.Disposable
@Composable
val <T : Any> Store<*, T, *>.composeState: State<T>
get() {
val composeState = remember(this) { mutableStateOf(state) }
val disposable = remember(this) { states.subscribe(onNext = { composeState.value = it }) }
onDispose(disposable::dispose)
fun <T : Any> Store<*, T, *>.asValue(): Value<T> =
object : Value<T>() {
override val value: T get() = state
private var disposables = emptyMap<ValueObserver<T>, Disposable>()
return composeState
override fun subscribe(observer: ValueObserver<T>) {
val disposable = states(com.arkivanov.mvikotlin.rx.observer(onNext = observer))
this.disposables += observer to disposable
}
override fun unsubscribe(observer: ValueObserver<T>) {
val disposable = disposables[observer] ?: return
this.disposables -= observer
disposable.dispose()
}
}

1
examples/todoapp/desktop/build.gradle.kts

@ -18,6 +18,7 @@ kotlin {
implementation(project(":common:utils"))
implementation(project(":common:database"))
implementation(project(":common:root"))
implementation(project(":common:compose-ui"))
implementation(Deps.ArkIvanov.Decompose.decompose)
implementation(Deps.ArkIvanov.Decompose.extensionsCompose)
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlin)

3
examples/todoapp/desktop/src/jvmMain/kotlin/example/todo/desktop/Main.kt

@ -15,6 +15,7 @@ import com.badoo.reaktive.coroutinesinterop.asScheduler
import com.badoo.reaktive.scheduler.overrideSchedulers
import example.todo.common.database.TodoDatabaseDriver
import example.todo.common.root.TodoRoot
import example.todo.common.ui.TodoRootContent
import example.todo.database.TodoDatabase
import kotlinx.coroutines.Dispatchers
@ -28,7 +29,7 @@ fun main() {
Surface(modifier = Modifier.fillMaxSize()) {
MaterialTheme {
DesktopTheme {
rootComponent(factory = ::todoRoot).invoke()
TodoRootContent(rootComponent(factory = ::todoRoot))
}
}
}

1
examples/todoapp/gradle.properties

@ -21,3 +21,4 @@ android.enableJetifier=true
kotlin.code.style=official
org.gradle.parallel=true
org.gradle.caching=true
kotlin.native.disableCompilerDaemon=true

18
examples/todoapp/ios/.gitignore vendored

@ -0,0 +1,18 @@
DerivedData/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata/
*.moved-aside
*.xccheckout
*.xcscmblueprint
*.hmap
*.ipa
*.dSYM.zip
*.dSYM
Pods

418
examples/todoapp/ios/TodoApp.xcodeproj/project.pbxproj

@ -0,0 +1,418 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objects = {
/* Begin PBXBuildFile section */
1F00F38D257599D800CFAF97 /* TodoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F00F38C257599D800CFAF97 /* TodoApp.swift */; };
1F00F38F257599D800CFAF97 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F00F38E257599D800CFAF97 /* ContentView.swift */; };
1F00F391257599DA00CFAF97 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1F00F390257599DA00CFAF97 /* Assets.xcassets */; };
1F00F394257599DA00CFAF97 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1F00F393257599DA00CFAF97 /* Preview Assets.xcassets */; };
1F00F3A425759FEC00CFAF97 /* Todo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F00F3A325759FEC00CFAF97 /* Todo.framework */; };
1F00F3A525759FEC00CFAF97 /* Todo.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 1F00F3A325759FEC00CFAF97 /* Todo.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
1F00F3A82575A16400CFAF97 /* ObservableValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F00F3A72575A16400CFAF97 /* ObservableValue.swift */; };
1F00F3AA2575A71000CFAF97 /* MutableStateBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F00F3A92575A71000CFAF97 /* MutableStateBuilder.swift */; };
1F00F3AC2575AA4500CFAF97 /* ListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F00F3AB2575AA4500CFAF97 /* ListView.swift */; };
1F00F3AE2575AC6A00CFAF97 /* InputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F00F3AD2575AC6A00CFAF97 /* InputView.swift */; };
1F00F3B02575ADB500CFAF97 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F00F3AF2575ADB500CFAF97 /* MainView.swift */; };
1F00F3B22575B07700CFAF97 /* EditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F00F3B12575B07700CFAF97 /* EditView.swift */; };
1F00F3B42575B18200CFAF97 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F00F3B32575B18200CFAF97 /* RootView.swift */; };
1F00F3B62575B41900CFAF97 /* SimpleRouterState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F00F3B52575B41900CFAF97 /* SimpleRouterState.swift */; };
1F00F3B82575B4F800CFAF97 /* ComponentHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F00F3B72575B4F800CFAF97 /* ComponentHolder.swift */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
1F00F3A625759FEC00CFAF97 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
1F00F3A525759FEC00CFAF97 /* Todo.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1F00F389257599D800CFAF97 /* TodoApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TodoApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
1F00F38C257599D800CFAF97 /* TodoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoApp.swift; sourceTree = "<group>"; };
1F00F38E257599D800CFAF97 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
1F00F390257599DA00CFAF97 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
1F00F393257599DA00CFAF97 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
1F00F395257599DA00CFAF97 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
1F00F3A325759FEC00CFAF97 /* Todo.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Todo.framework; path = "../common/root/build/xcode-frameworks/Todo.framework"; sourceTree = "<group>"; };
1F00F3A72575A16400CFAF97 /* ObservableValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableValue.swift; sourceTree = "<group>"; };
1F00F3A92575A71000CFAF97 /* MutableStateBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutableStateBuilder.swift; sourceTree = "<group>"; };
1F00F3AB2575AA4500CFAF97 /* ListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListView.swift; sourceTree = "<group>"; };
1F00F3AD2575AC6A00CFAF97 /* InputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputView.swift; sourceTree = "<group>"; };
1F00F3AF2575ADB500CFAF97 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; };
1F00F3B12575B07700CFAF97 /* EditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditView.swift; sourceTree = "<group>"; };
1F00F3B32575B18200CFAF97 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = "<group>"; };
1F00F3B52575B41900CFAF97 /* SimpleRouterState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleRouterState.swift; sourceTree = "<group>"; };
1F00F3B72575B4F800CFAF97 /* ComponentHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComponentHolder.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
1F00F386257599D800CFAF97 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
1F00F3A425759FEC00CFAF97 /* Todo.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
1F00F380257599D800CFAF97 = {
isa = PBXGroup;
children = (
1F00F38B257599D800CFAF97 /* ios */,
1F00F38A257599D800CFAF97 /* Products */,
1F00F3A225759FEC00CFAF97 /* Frameworks */,
);
sourceTree = "<group>";
};
1F00F38A257599D800CFAF97 /* Products */ = {
isa = PBXGroup;
children = (
1F00F389257599D800CFAF97 /* TodoApp.app */,
);
name = Products;
sourceTree = "<group>";
};
1F00F38B257599D800CFAF97 /* ios */ = {
isa = PBXGroup;
children = (
1F00F38C257599D800CFAF97 /* TodoApp.swift */,
1F00F38E257599D800CFAF97 /* ContentView.swift */,
1F00F390257599DA00CFAF97 /* Assets.xcassets */,
1F00F395257599DA00CFAF97 /* Info.plist */,
1F00F392257599DA00CFAF97 /* Preview Content */,
1F00F3A72575A16400CFAF97 /* ObservableValue.swift */,
1F00F3A92575A71000CFAF97 /* MutableStateBuilder.swift */,
1F00F3AB2575AA4500CFAF97 /* ListView.swift */,
1F00F3AD2575AC6A00CFAF97 /* InputView.swift */,
1F00F3AF2575ADB500CFAF97 /* MainView.swift */,
1F00F3B12575B07700CFAF97 /* EditView.swift */,
1F00F3B32575B18200CFAF97 /* RootView.swift */,
1F00F3B52575B41900CFAF97 /* SimpleRouterState.swift */,
1F00F3B72575B4F800CFAF97 /* ComponentHolder.swift */,
);
path = ios;
sourceTree = "<group>";
};
1F00F392257599DA00CFAF97 /* Preview Content */ = {
isa = PBXGroup;
children = (
1F00F393257599DA00CFAF97 /* Preview Assets.xcassets */,
);
path = "Preview Content";
sourceTree = "<group>";
};
1F00F3A225759FEC00CFAF97 /* Frameworks */ = {
isa = PBXGroup;
children = (
1F00F3A325759FEC00CFAF97 /* Todo.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
1F00F388257599D800CFAF97 /* TodoApp */ = {
isa = PBXNativeTarget;
buildConfigurationList = 1F00F398257599DA00CFAF97 /* Build configuration list for PBXNativeTarget "TodoApp" */;
buildPhases = (
1F00F39D25759BB300CFAF97 /* ShellScript */,
1F00F385257599D800CFAF97 /* Sources */,
1F00F386257599D800CFAF97 /* Frameworks */,
1F00F387257599D800CFAF97 /* Resources */,
1F00F3A625759FEC00CFAF97 /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = TodoApp;
productName = ios;
productReference = 1F00F389257599D800CFAF97 /* TodoApp.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
1F00F381257599D800CFAF97 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1220;
LastUpgradeCheck = 1220;
TargetAttributes = {
1F00F388257599D800CFAF97 = {
CreatedOnToolsVersion = 12.2;
};
};
};
buildConfigurationList = 1F00F384257599D800CFAF97 /* Build configuration list for PBXProject "TodoApp" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 1F00F380257599D800CFAF97;
productRefGroup = 1F00F38A257599D800CFAF97 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
1F00F388257599D800CFAF97 /* TodoApp */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
1F00F387257599D800CFAF97 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
1F00F394257599DA00CFAF97 /* Preview Assets.xcassets in Resources */,
1F00F391257599DA00CFAF97 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
1F00F39D25759BB300CFAF97 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "cd $SRCROOT/..\n./gradlew :common:root:packForXCode -PXCODE_CONFIGURATION=${CONFIGURATION}\ncd $SRCROOT\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
1F00F385257599D800CFAF97 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
1F00F38F257599D800CFAF97 /* ContentView.swift in Sources */,
1F00F3B02575ADB500CFAF97 /* MainView.swift in Sources */,
1F00F3B22575B07700CFAF97 /* EditView.swift in Sources */,
1F00F38D257599D800CFAF97 /* TodoApp.swift in Sources */,
1F00F3AC2575AA4500CFAF97 /* ListView.swift in Sources */,
1F00F3B82575B4F800CFAF97 /* ComponentHolder.swift in Sources */,
1F00F3B42575B18200CFAF97 /* RootView.swift in Sources */,
1F00F3B62575B41900CFAF97 /* SimpleRouterState.swift in Sources */,
1F00F3AE2575AC6A00CFAF97 /* InputView.swift in Sources */,
1F00F3AA2575A71000CFAF97 /* MutableStateBuilder.swift in Sources */,
1F00F3A82575A16400CFAF97 /* ObservableValue.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
1F00F396257599DA00CFAF97 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.2;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
1F00F397257599DA00CFAF97 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.2;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
1F00F399257599DA00CFAF97 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"ios/Preview Content\"";
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = "$SRCROOT/../common/root/build/xcode-frameworks";
INFOPLIST_FILE = ios/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = org.jetbrains.todoapp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
1F00F39A257599DA00CFAF97 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"ios/Preview Content\"";
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = "$SRCROOT/../common/root/build/xcode-frameworks";
INFOPLIST_FILE = ios/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = org.jetbrains.todoapp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
1F00F384257599D800CFAF97 /* Build configuration list for PBXProject "TodoApp" */ = {
isa = XCConfigurationList;
buildConfigurations = (
1F00F396257599DA00CFAF97 /* Debug */,
1F00F397257599DA00CFAF97 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
1F00F398257599DA00CFAF97 /* Build configuration list for PBXNativeTarget "TodoApp" */ = {
isa = XCConfigurationList;
buildConfigurations = (
1F00F399257599DA00CFAF97 /* Debug */,
1F00F39A257599DA00CFAF97 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 1F00F381257599D800CFAF97 /* Project object */;
}

7
examples/todoapp/ios/TodoApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata generated

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:/Users/arkadiiivanov/dev/compose-jb/examples/todoapp/ios/TodoApp.xcodeproj">
</FileRef>
</Workspace>

8
examples/todoapp/ios/TodoApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

11
examples/todoapp/ios/ios/Assets.xcassets/AccentColor.colorset/Contents.json

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

98
examples/todoapp/ios/ios/Assets.xcassets/AppIcon.appiconset/Contents.json

@ -0,0 +1,98 @@
{
"images" : [
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

6
examples/todoapp/ios/ios/Assets.xcassets/Contents.json

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

19
examples/todoapp/ios/ios/ComponentHolder.swift

@ -0,0 +1,19 @@
import Todo
class ComponentHolder<T> {
let lifecycle: LifecycleRegistry
let component: T
init(factory: (ComponentContext) -> T) {
let lifecycle = LifecycleRegistryKt.LifecycleRegistry()
let component = factory(DefaultComponentContext(lifecycle: lifecycle))
self.lifecycle = lifecycle
self.component = component
lifecycle.onCreate()
}
deinit {
lifecycle.onDestroy()
}
}

31
examples/todoapp/ios/ios/ContentView.swift

@ -0,0 +1,31 @@
import SwiftUI
import Todo
struct ContentView: View {
@State
private var componentHolder =
ComponentHolder {
TodoRootKt.TodoRoot(
componentContext: $0,
dependencies: RootDependencies()
)
}
var body: some View {
RootView(componentHolder.component)
.onAppear { LifecycleRegistryExtKt.resume(self.componentHolder.lifecycle) }
.onDisappear { LifecycleRegistryExtKt.stop(self.componentHolder.lifecycle) }
}
}
private class RootDependencies: TodoRootDependencies {
let database: TodoDatabase = TodoDatabaseCompanion().invoke(driver: TodoDatabaseDriverFactoryKt.TodoDatabaseDriver())
let storeFactory: MvikotlinStoreFactory = DefaultStoreFactory()
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

67
examples/todoapp/ios/ios/EditView.swift

@ -0,0 +1,67 @@
import SwiftUI
import Todo
struct EditView: View {
private let component: TodoEdit
@ObservedObject
private var models: ObservableValue<TodoEditModel>
init(_ component: TodoEdit) {
self.component = component
self.models = ObservableValue(component.models)
}
var body: some View {
let model = models.value
let binding = Binding(get: { model.text }, set: component.onTextChanged)
return NavigationView {
VStack{
TextEditor(text: binding)
.frame(maxHeight: .infinity)
.padding(8)
HStack {
Text("Completed")
Image(systemName: model.isDone ? "checkmark.square" : "square")
.onTapGesture { self.component.onDoneChanged(isDone: !model.isDone) }
}.padding(8)
}
.navigationBarTitle("Edit todo", displayMode: .inline)
.navigationBarItems(
leading: Button(action: { withAnimation { component.onCloseClicked() } } ) {
HStack {
Image(systemName: "chevron.left")
Text("Close")
}
}
)
}
}
}
struct EditView_Previews: PreviewProvider {
static var previews: some View {
EditView(StubTodoEdit())
}
class StubTodoEdit: TodoEdit {
let models: Value<TodoEditModel> =
valueOf(
TodoEditModel(
text: "Text",
isDone: true
)
)
func onCloseClicked() {
}
func onDoneChanged(isDone: Bool) {
}
func onTextChanged(text: String) {
}
}
}

50
examples/todoapp/ios/ios/Info.plist

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchScreen</key>
<dict/>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

28
examples/todoapp/ios/ios/InputView.swift

@ -0,0 +1,28 @@
import SwiftUI
import Todo
struct InputView: View {
var textBinding: Binding<String>
var onAddClicked: () -> Void
var body: some View {
HStack {
TextField("Todo text", text: self.textBinding)
.textFieldStyle(RoundedBorderTextFieldStyle())
.edgesIgnoringSafeArea(Edge.Set.bottom)
Button(action: self.onAddClicked) {
Image(systemName: "plus")
}.frame(minWidth: 36, minHeight: 36)
}.padding(8)
}
}
struct InputView_Previews: PreviewProvider {
static var previews: some View {
InputView(
textBinding: Binding(get: { "Text" }, set: {_ in }),
onAddClicked: {}
)
}
}

39
examples/todoapp/ios/ios/ListView.swift

@ -0,0 +1,39 @@
import SwiftUI
import Todo
struct ListView: View {
var items: [TodoItem]
var onItemClicked: (_ id: Int64) -> Void
var onDoneChanged: (_ id: Int64, _ isDone: Bool) -> Void
var body: some View {
List(self.items) { item in
HStack {
Text(item.text)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
.background(Color.white)
.onTapGesture { withAnimation { self.onItemClicked(item.id) } }
Image(systemName: item.isDone ? "checkmark.square" : "square")
.onTapGesture { self.onDoneChanged(item.id, !item.isDone) }
}
}.listStyle(PlainListStyle())
}
}
struct ListView_Previews: PreviewProvider {
static var previews: some View {
ListView(
items: [
TodoItem(id: 1, order: 1, text: "Item 1", isDone: false),
TodoItem(id: 2, order: 2, text: "Item 2", isDone: true),
TodoItem(id: 3, order: 3, text: "Item 3", isDone: false)
],
onItemClicked: {_ in },
onDoneChanged: {_,_ in }
)
}
}
extension TodoItem : Identifiable {
}

68
examples/todoapp/ios/ios/MainView.swift

@ -0,0 +1,68 @@
import SwiftUI
import Todo
struct MainView: View {
private let component: TodoMain
@ObservedObject
private var models: ObservableValue<TodoMainModel>
init(_ component: TodoMain) {
self.component = component
self.models = ObservableValue(component.models)
}
var body: some View {
let model = models.value
return NavigationView {
VStack {
ListView(
items: model.items,
onItemClicked: component.onItemClicked,
onDoneChanged: component.onItemDoneChanged
)
InputView(
textBinding: Binding(get: { model.text }, set: component.onInputTextChanged),
onAddClicked: component.onAddItemClicked
)
}.navigationBarTitle("Todo Sample", displayMode: .inline)
}
}
}
struct MainView_Previews: PreviewProvider {
static var previews: some View {
MainView(StubTodoMain())
}
class StubTodoMain: TodoMain {
let models: Value<TodoMainModel> =
valueOf(
TodoMainModel(
items: [
TodoItem(id: 1, order: 1, text: "Item 1", isDone: false),
TodoItem(id: 2, order: 2, text: "Item 2", isDone: true),
TodoItem(id: 3, order: 3, text: "Item 3", isDone: false)
],
text: "Text"
)
)
func onAddItemClicked() {
}
func onInputTextChanged(text: String) {
}
func onItemClicked(id: Int64) {
}
func onItemDeleteClicked(id: Int64) {
}
func onItemDoneChanged(id: Int64, isDone: Bool) {
}
}
}

5
examples/todoapp/ios/ios/MutableStateBuilder.swift

@ -0,0 +1,5 @@
import Todo
func valueOf<T: AnyObject>(_ value: T) -> Value<T> {
return MutableValueBuilderKt.MutableValue(initialValue: value) as! MutableValue<T>
}

22
examples/todoapp/ios/ios/ObservableValue.swift

@ -0,0 +1,22 @@
import Todo
public class ObservableValue<T : AnyObject> : ObservableObject {
private let observableValue: Value<T>
@Published
var value: T
private var observer: ((T) -> Void)?
init(_ value: Value<T>) {
self.observableValue = value
self.value = observableValue.value
self.observer = { [weak self] value in self?.value = value }
observableValue.subscribe(observer: observer!)
}
deinit {
self.observableValue.unsubscribe(observer: self.observer!)
}
}

6
examples/todoapp/ios/ios/Preview Content/Preview Assets.xcassets/Contents.json

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

43
examples/todoapp/ios/ios/RootView.swift

@ -0,0 +1,43 @@
import SwiftUI
import Todo
struct RootView: View {
@ObservedObject
private var routerStates: ObservableValue<RouterState<AnyObject, TodoRootChild>>
init(_ component: TodoRoot) {
self.routerStates = ObservableValue(component.routerState)
}
var body: some View {
let child = self.routerStates.value.activeChild.component
switch child {
case let main as TodoRootChild.Main:
MainView(main.component)
case let edit as TodoRootChild.Edit:
EditView(edit.component)
.transition(
.asymmetric(
insertion: AnyTransition.move(edge: .trailing),
removal: AnyTransition.move(edge: .trailing)
)
)
.animation(.easeInOut)
default: EmptyView()
}
}
}
struct RootView_Previews: PreviewProvider {
static var previews: some View {
RootView(StubTodoRoot())
}
class StubTodoRoot : TodoRoot {
let routerState: Value<RouterState<AnyObject, TodoRootChild>> =
simpleRouterState(TodoRootChild.Main(component: MainView_Previews.StubTodoMain()))
}
}

13
examples/todoapp/ios/ios/SimpleRouterState.swift

@ -0,0 +1,13 @@
import Todo
func simpleRouterState<T : AnyObject>(_ child: T) -> Value<RouterState<AnyObject, T>> {
return valueOf(
RouterState(
activeChild: RouterStateEntryCreated(
configuration: "config" as AnyObject,
component: child
),
backStack: []
)
)
}

10
examples/todoapp/ios/ios/TodoApp.swift

@ -0,0 +1,10 @@
import SwiftUI
@main
struct TodoApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}

1
examples/todoapp/settings.gradle.kts

@ -4,6 +4,7 @@ include(
":common:main",
":common:edit",
":common:root",
":common:compose-ui",
":android",
":desktop"
)

Loading…
Cancel
Save