Browse Source

Add web app for the TodoApp example (#778)

* Prepare the TodoApp example for adding the JavaScript app

* Add the JavaScript app for the TodoApp example

* TodoApp. Update Compose to 0.5.0-build225.
pull/796/head
Arkadii Ivanov 4 years ago committed by GitHub
parent
commit
8f87cda24d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      examples/todoapp/android/src/main/java/example/todo/android/MainActivity.kt
  2. 6
      examples/todoapp/buildSrc/buildSrc/src/main/kotlin/Deps.kt
  3. 9
      examples/todoapp/buildSrc/src/main/kotlin/multiplatform-setup.gradle.kts
  4. 2
      examples/todoapp/common/compose-ui/src/commonMain/kotlin/example/todo/common/ui/TodoMainUi.kt
  5. 6
      examples/todoapp/common/database/build.gradle.kts
  6. 13
      examples/todoapp/common/database/src/androidMain/kotlin/example/todo/common/database/TestDatabaseDriverFactory.kt
  7. 91
      examples/todoapp/common/database/src/commonMain/kotlin/example/todo/common/database/DefaultTodoSharedDatabase.kt
  8. 28
      examples/todoapp/common/database/src/commonMain/kotlin/example/todo/common/database/ReaktiveExt.kt
  9. 6
      examples/todoapp/common/database/src/commonMain/kotlin/example/todo/common/database/TestDatabaseDriverFactory.kt
  10. 105
      examples/todoapp/common/database/src/commonMain/kotlin/example/todo/common/database/TestTodoSharedDatabase.kt
  11. 22
      examples/todoapp/common/database/src/commonMain/kotlin/example/todo/common/database/TodoSharedDatabase.kt
  12. 13
      examples/todoapp/common/database/src/desktopMain/kotlin/example/todo/common/database/TestDatabaseDriverFactory.kt
  13. 26
      examples/todoapp/common/database/src/iosMain/kotlin/example/todo/common/database/TestDatabaseDriverFactory.kt
  14. 10
      examples/todoapp/common/database/src/jsMain/kotlin/example/todo/common/database/TodoDatabaseDriverFactory.kt
  15. 6
      examples/todoapp/common/edit/src/commonMain/kotlin/example/todo/common/edit/integration/TodoEditComponent.kt
  16. 23
      examples/todoapp/common/edit/src/commonMain/kotlin/example/todo/common/edit/integration/TodoEditStoreDatabase.kt
  17. 6
      examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/integration/TodoMainComponent.kt
  18. 23
      examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/integration/TodoMainStoreDatabase.kt
  19. 45
      examples/todoapp/common/main/src/commonTest/kotlin/example/todo/common/main/integration/TodoMainTest.kt
  20. 4
      examples/todoapp/common/root/src/commonMain/kotlin/example/todo/common/root/integration/TodoRootComponent.kt
  21. 4
      examples/todoapp/desktop/src/jvmMain/kotlin/example/todo/desktop/Main.kt
  22. 2
      examples/todoapp/ios/ios/ContentView.swift
  23. 3
      examples/todoapp/settings.gradle.kts
  24. 34
      examples/todoapp/web/build.gradle.kts
  25. 35
      examples/todoapp/web/src/jsMain/kotlin/example/todo/web/App.kt
  26. 174
      examples/todoapp/web/src/jsMain/kotlin/example/todo/web/Components.kt
  27. 115
      examples/todoapp/web/src/jsMain/kotlin/example/todo/web/Crossfade.kt
  28. 96
      examples/todoapp/web/src/jsMain/kotlin/example/todo/web/TodoEditUi.kt
  29. 189
      examples/todoapp/web/src/jsMain/kotlin/example/todo/web/TodoMainUi.kt
  30. 54
      examples/todoapp/web/src/jsMain/kotlin/example/todo/web/TodoRootUi.kt
  31. 26
      examples/todoapp/web/src/jsMain/kotlin/example/todo/web/ValueExt.kt
  32. BIN
      examples/todoapp/web/src/jsMain/resources/MaterialIcons-Regular.ttf
  33. 30
      examples/todoapp/web/src/jsMain/resources/index.html
  34. 13
      examples/todoapp/web/src/jsMain/resources/materialize.min.css
  35. 6
      examples/todoapp/web/src/jsMain/resources/materialize.min.js
  36. 34
      examples/todoapp/web/src/jsMain/resources/styles.css
  37. 9
      examples/todoapp/web/webpack.config.d/fs.js
  38. 12
      examples/todoapp/web/webpack.config.d/wasm.js

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

@ -10,11 +10,11 @@ import com.arkivanov.decompose.extensions.compose.jetbrains.rememberRootComponen
import com.arkivanov.mvikotlin.logging.store.LoggingStoreFactory import com.arkivanov.mvikotlin.logging.store.LoggingStoreFactory
import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory
import com.arkivanov.mvikotlin.timetravel.store.TimeTravelStoreFactory import com.arkivanov.mvikotlin.timetravel.store.TimeTravelStoreFactory
import example.todo.common.database.DefaultTodoSharedDatabase
import example.todo.common.database.TodoDatabaseDriver import example.todo.common.database.TodoDatabaseDriver
import example.todo.common.root.TodoRoot import example.todo.common.root.TodoRoot
import example.todo.common.root.integration.TodoRootComponent import example.todo.common.root.integration.TodoRootComponent
import example.todo.common.ui.TodoRootContent import example.todo.common.ui.TodoRootContent
import example.todo.database.TodoDatabase
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -33,6 +33,6 @@ class MainActivity : AppCompatActivity() {
TodoRootComponent( TodoRootComponent(
componentContext = componentContext, componentContext = componentContext,
storeFactory = LoggingStoreFactory(TimeTravelStoreFactory(DefaultStoreFactory)), storeFactory = LoggingStoreFactory(TimeTravelStoreFactory(DefaultStoreFactory)),
database = TodoDatabase(TodoDatabaseDriver(context = this)) database = DefaultTodoSharedDatabase(TodoDatabaseDriver(context = this))
) )
} }

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

@ -7,12 +7,13 @@ object Deps {
const val gradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$VERSION" const val gradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$VERSION"
const val testCommon = "org.jetbrains.kotlin:kotlin-test-common:$VERSION" const val testCommon = "org.jetbrains.kotlin:kotlin-test-common:$VERSION"
const val testJunit = "org.jetbrains.kotlin:kotlin-test-junit:$VERSION" const val testJunit = "org.jetbrains.kotlin:kotlin-test-junit:$VERSION"
const val testJs = "org.jetbrains.kotlin:kotlin-test-js:$VERSION"
const val testAnnotationsCommon = "org.jetbrains.kotlin:kotlin-test-annotations-common:$VERSION" const val testAnnotationsCommon = "org.jetbrains.kotlin:kotlin-test-annotations-common:$VERSION"
} }
object Compose { object Compose {
// __LATEST_COMPOSE_RELEASE_VERSION__ // __LATEST_COMPOSE_RELEASE_VERSION__
private const val VERSION = "0.4.0" private const val VERSION = "0.5.0-build225"
const val gradlePlugin = "org.jetbrains.compose:compose-gradle-plugin:$VERSION" const val gradlePlugin = "org.jetbrains.compose:compose-gradle-plugin:$VERSION"
} }
} }
@ -37,7 +38,7 @@ object Deps {
object ArkIvanov { object ArkIvanov {
object MVIKotlin { object MVIKotlin {
private const val VERSION = "2.0.2" private const val VERSION = "2.0.3"
const val rx = "com.arkivanov.mvikotlin:rx:$VERSION" const val rx = "com.arkivanov.mvikotlin:rx:$VERSION"
const val mvikotlin = "com.arkivanov.mvikotlin:mvikotlin:$VERSION" const val mvikotlin = "com.arkivanov.mvikotlin:mvikotlin:$VERSION"
const val mvikotlinMain = "com.arkivanov.mvikotlin:mvikotlin-main:$VERSION" const val mvikotlinMain = "com.arkivanov.mvikotlin:mvikotlin-main:$VERSION"
@ -71,6 +72,7 @@ object Deps {
const val androidDriver = "com.squareup.sqldelight:android-driver:$VERSION" const val androidDriver = "com.squareup.sqldelight:android-driver:$VERSION"
const val sqliteDriver = "com.squareup.sqldelight:sqlite-driver:$VERSION" const val sqliteDriver = "com.squareup.sqldelight:sqlite-driver:$VERSION"
const val nativeDriver = "com.squareup.sqldelight:native-driver:$VERSION" const val nativeDriver = "com.squareup.sqldelight:native-driver:$VERSION"
const val sqljsDriver = "com.squareup.sqldelight:sqljs-driver:$VERSION"
} }
} }
} }

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

@ -8,6 +8,10 @@ kotlin {
android() android()
ios() ios()
js(IR) {
browser()
}
sourceSets { sourceSets {
named("commonTest") { named("commonTest") {
dependencies { dependencies {
@ -26,6 +30,11 @@ kotlin {
implementation(Deps.JetBrains.Kotlin.testJunit) implementation(Deps.JetBrains.Kotlin.testJunit)
} }
} }
named("jsTest") {
dependencies {
implementation(Deps.JetBrains.Kotlin.testJs)
}
}
} }
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> { tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {

2
examples/todoapp/common/compose-ui/src/commonMain/kotlin/example/todo/common/ui/TodoMainUi.kt

@ -24,6 +24,7 @@ import androidx.compose.material.icons.filled.Delete
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.onKeyEvent
@ -130,6 +131,7 @@ private fun Item(
} }
} }
@OptIn(ExperimentalComposeUiApi::class)
@Composable @Composable
private fun TodoInput( private fun TodoInput(
text: String, text: String,

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

@ -36,5 +36,11 @@ kotlin {
implementation(Deps.Squareup.SQLDelight.nativeDriver) implementation(Deps.Squareup.SQLDelight.nativeDriver)
} }
} }
jsMain {
dependencies {
implementation(Deps.Squareup.SQLDelight.sqljsDriver)
}
}
} }
} }

13
examples/todoapp/common/database/src/androidMain/kotlin/example/todo/common/database/TestDatabaseDriverFactory.kt

@ -1,13 +0,0 @@
package example.todo.common.database
import com.squareup.sqldelight.db.SqlDriver
import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver
import example.todo.database.TodoDatabase
@Suppress("FunctionName") // FactoryFunction
actual fun TestDatabaseDriver(): SqlDriver {
val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
TodoDatabase.Schema.create(driver)
return driver
}

91
examples/todoapp/common/database/src/commonMain/kotlin/example/todo/common/database/DefaultTodoSharedDatabase.kt

@ -0,0 +1,91 @@
package example.todo.common.database
import com.badoo.reaktive.base.setCancellable
import com.badoo.reaktive.completable.Completable
import com.badoo.reaktive.maybe.Maybe
import com.badoo.reaktive.observable.Observable
import com.badoo.reaktive.observable.autoConnect
import com.badoo.reaktive.observable.firstOrError
import com.badoo.reaktive.observable.map
import com.badoo.reaktive.observable.observable
import com.badoo.reaktive.observable.observeOn
import com.badoo.reaktive.observable.replay
import com.badoo.reaktive.scheduler.ioScheduler
import com.badoo.reaktive.single.Single
import com.badoo.reaktive.single.asCompletable
import com.badoo.reaktive.single.asObservable
import com.badoo.reaktive.single.doOnBeforeSuccess
import com.badoo.reaktive.single.flatMapObservable
import com.badoo.reaktive.single.map
import com.badoo.reaktive.single.mapNotNull
import com.badoo.reaktive.single.observeOn
import com.badoo.reaktive.single.singleOf
import com.squareup.sqldelight.Query
import com.squareup.sqldelight.db.SqlDriver
import example.todo.database.TodoDatabase
class DefaultTodoSharedDatabase(driver: Single<SqlDriver>) : TodoSharedDatabase {
constructor(driver: SqlDriver) : this(singleOf(driver))
private val queries: Single<TodoDatabaseQueries> =
driver
.map { TodoDatabase(it).todoDatabaseQueries }
.asObservable()
.replay()
.autoConnect()
.firstOrError()
override fun observeAll(): Observable<List<TodoItemEntity>> =
query(TodoDatabaseQueries::selectAll)
.observe { it.executeAsList() }
override fun select(id: Long): Maybe<TodoItemEntity> =
query { it.select(id = id) }
.mapNotNull { it.executeAsOneOrNull() }
override fun add(text: String): Completable =
execute { it.add(text = text) }
override fun setText(id: Long, text: String): Completable =
execute { it.setText(id = id, text = text) }
override fun setDone(id: Long, isDone: Boolean): Completable =
execute { it.setDone(id = id, isDone = isDone) }
override fun delete(id: Long): Completable =
execute { it.delete(id = id) }
override fun clear(): Completable =
execute { it.clear() }
private fun <T : Any> query(query: (TodoDatabaseQueries) -> Query<T>): Single<Query<T>> =
queries
.observeOn(ioScheduler)
.map(query)
private fun execute(query: (TodoDatabaseQueries) -> Unit): Completable =
queries
.observeOn(ioScheduler)
.doOnBeforeSuccess(query)
.asCompletable()
private fun <T : Any, R> Single<Query<T>>.observe(get: (Query<T>) -> R): Observable<R> =
flatMapObservable { it.observed() }
.observeOn(ioScheduler)
.map(get)
private fun <T : Any> Query<T>.observed(): Observable<Query<T>> =
observable { emitter ->
val listener =
object : Query.Listener {
override fun queryResultsChanged() {
emitter.onNext(this@observed)
}
}
emitter.onNext(this@observed)
addListener(listener)
emitter.setCancellable { removeListener(listener) }
}
}

28
examples/todoapp/common/database/src/commonMain/kotlin/example/todo/common/database/ReaktiveExt.kt

@ -1,28 +0,0 @@
package example.todo.common.database
import com.badoo.reaktive.base.setCancellable
import com.badoo.reaktive.observable.Observable
import com.badoo.reaktive.observable.map
import com.badoo.reaktive.observable.observable
import com.badoo.reaktive.observable.observeOn
import com.badoo.reaktive.scheduler.ioScheduler
import com.squareup.sqldelight.Query
fun <T : Any, R> Query<T>.asObservable(execute: (Query<T>) -> R): Observable<R> =
asObservable()
.observeOn(ioScheduler)
.map(execute)
fun <T : Any> Query<T>.asObservable(): Observable<Query<T>> =
observable { emitter ->
val listener =
object : Query.Listener {
override fun queryResultsChanged() {
emitter.onNext(this@asObservable)
}
}
emitter.onNext(this@asObservable)
addListener(listener)
emitter.setCancellable { removeListener(listener) }
}

6
examples/todoapp/common/database/src/commonMain/kotlin/example/todo/common/database/TestDatabaseDriverFactory.kt

@ -1,6 +0,0 @@
package example.todo.common.database
import com.squareup.sqldelight.db.SqlDriver
@Suppress("FunctionName")
expect fun TestDatabaseDriver(): SqlDriver

105
examples/todoapp/common/database/src/commonMain/kotlin/example/todo/common/database/TestTodoSharedDatabase.kt

@ -0,0 +1,105 @@
package example.todo.common.database
import com.badoo.reaktive.base.invoke
import com.badoo.reaktive.completable.Completable
import com.badoo.reaktive.completable.completableFromFunction
import com.badoo.reaktive.completable.observeOn
import com.badoo.reaktive.maybe.Maybe
import com.badoo.reaktive.maybe.observeOn
import com.badoo.reaktive.observable.Observable
import com.badoo.reaktive.observable.map
import com.badoo.reaktive.observable.observeOn
import com.badoo.reaktive.scheduler.Scheduler
import com.badoo.reaktive.single.notNull
import com.badoo.reaktive.single.singleFromFunction
import com.badoo.reaktive.subject.behavior.BehaviorSubject
// There were problems when using real database in JS tests, hence the in-memory test implementation
class TestTodoSharedDatabase(
private val scheduler: Scheduler
) : TodoSharedDatabase {
private val itemsSubject = BehaviorSubject<Map<Long, TodoItemEntity>>(emptyMap())
private val itemsObservable = itemsSubject.observeOn(scheduler)
val testing: Testing = Testing()
override fun observeAll(): Observable<List<TodoItemEntity>> =
itemsObservable.map { it.values.toList() }
override fun select(id: Long): Maybe<TodoItemEntity> =
singleFromFunction { testing.select(id = id) }
.notNull()
.observeOn(scheduler)
override fun add(text: String): Completable =
execute { testing.add(text = text) }
override fun setText(id: Long, text: String): Completable =
execute { testing.setText(id = id, text = text) }
override fun setDone(id: Long, isDone: Boolean): Completable =
execute { testing.setDone(id = id, isDone = isDone) }
override fun delete(id: Long): Completable =
execute { testing.delete(id = id) }
override fun clear(): Completable =
execute { testing.clear() }
private fun execute(block: () -> Unit): Completable =
completableFromFunction(block)
.observeOn(scheduler)
inner class Testing {
fun select(id: Long): TodoItemEntity? =
itemsSubject.value[id]
fun selectRequired(id: Long): TodoItemEntity =
requireNotNull(select(id = id))
fun add(text: String) {
updateItems { items ->
val nextId = items.keys.maxOrNull()?.plus(1L) ?: 1L
val item =
TodoItemEntity(
id = nextId,
orderNum = items.size.toLong(),
text = text,
isDone = false
)
items + (nextId to item)
}
}
fun setText(id: Long, text: String) {
updateItem(id = id) { it.copy(text = text) }
}
fun setDone(id: Long, isDone: Boolean) {
updateItem(id = id) { it.copy(isDone = isDone) }
}
fun delete(id: Long) {
updateItems { it - id }
}
fun clear() {
updateItems { emptyMap() }
}
fun getLastInsertId(): Long? =
itemsSubject.value.values.lastOrNull()?.id
private fun updateItems(func: (Map<Long, TodoItemEntity>) -> Map<Long, TodoItemEntity>) {
itemsSubject(func(itemsSubject.value))
}
private fun updateItem(id: Long, func: (TodoItemEntity) -> TodoItemEntity) {
updateItems {
it + (id to it.getValue(id).let(func))
}
}
}
}

22
examples/todoapp/common/database/src/commonMain/kotlin/example/todo/common/database/TodoSharedDatabase.kt

@ -0,0 +1,22 @@
package example.todo.common.database
import com.badoo.reaktive.completable.Completable
import com.badoo.reaktive.maybe.Maybe
import com.badoo.reaktive.observable.Observable
interface TodoSharedDatabase {
fun observeAll(): Observable<List<TodoItemEntity>>
fun select(id: Long): Maybe<TodoItemEntity>
fun add(text: String): Completable
fun setText(id: Long, text: String): Completable
fun setDone(id: Long, isDone: Boolean): Completable
fun delete(id: Long): Completable
fun clear(): Completable
}

13
examples/todoapp/common/database/src/desktopMain/kotlin/example/todo/common/database/TestDatabaseDriverFactory.kt

@ -1,13 +0,0 @@
package example.todo.common.database
import com.squareup.sqldelight.db.SqlDriver
import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver
import example.todo.database.TodoDatabase
@Suppress("FunctionName") // FactoryFunction
actual fun TestDatabaseDriver(): SqlDriver {
val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
TodoDatabase.Schema.create(driver)
return driver
}

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

@ -1,26 +0,0 @@
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
)
)
}

10
examples/todoapp/common/database/src/jsMain/kotlin/example/todo/common/database/TodoDatabaseDriverFactory.kt

@ -0,0 +1,10 @@
package example.todo.common.database
import com.badoo.reaktive.promise.asSingle
import com.badoo.reaktive.single.Single
import com.squareup.sqldelight.db.SqlDriver
import com.squareup.sqldelight.drivers.sqljs.initSqlDriver
import example.todo.database.TodoDatabase
fun todoDatabaseDriver(): Single<SqlDriver> =
initSqlDriver(TodoDatabase.Schema).asSingle()

6
examples/todoapp/common/edit/src/commonMain/kotlin/example/todo/common/edit/integration/TodoEditComponent.kt

@ -6,6 +6,7 @@ import com.arkivanov.decompose.value.operator.map
import com.arkivanov.mvikotlin.core.store.StoreFactory import com.arkivanov.mvikotlin.core.store.StoreFactory
import com.badoo.reaktive.base.Consumer import com.badoo.reaktive.base.Consumer
import com.badoo.reaktive.base.invoke import com.badoo.reaktive.base.invoke
import example.todo.common.database.TodoSharedDatabase
import example.todo.common.edit.TodoEdit import example.todo.common.edit.TodoEdit
import example.todo.common.edit.TodoEdit.Model import example.todo.common.edit.TodoEdit.Model
import example.todo.common.edit.TodoEdit.Output import example.todo.common.edit.TodoEdit.Output
@ -13,12 +14,11 @@ import example.todo.common.edit.store.TodoEditStore.Intent
import example.todo.common.edit.store.TodoEditStoreProvider import example.todo.common.edit.store.TodoEditStoreProvider
import example.todo.common.utils.asValue import example.todo.common.utils.asValue
import example.todo.common.utils.getStore import example.todo.common.utils.getStore
import example.todo.database.TodoDatabase
class TodoEditComponent( class TodoEditComponent(
componentContext: ComponentContext, componentContext: ComponentContext,
storeFactory: StoreFactory, storeFactory: StoreFactory,
database: TodoDatabase, database: TodoSharedDatabase,
itemId: Long, itemId: Long,
private val output: Consumer<Output> private val output: Consumer<Output>
) : TodoEdit, ComponentContext by componentContext { ) : TodoEdit, ComponentContext by componentContext {
@ -27,7 +27,7 @@ class TodoEditComponent(
instanceKeeper.getStore { instanceKeeper.getStore {
TodoEditStoreProvider( TodoEditStoreProvider(
storeFactory = storeFactory, storeFactory = storeFactory,
database = TodoEditStoreDatabase(queries = database.todoDatabaseQueries), database = TodoEditStoreDatabase(database = database),
id = itemId id = itemId
).provide() ).provide()
} }

23
examples/todoapp/common/edit/src/commonMain/kotlin/example/todo/common/edit/integration/TodoEditStoreDatabase.kt

@ -1,29 +1,20 @@
package example.todo.common.edit.integration package example.todo.common.edit.integration
import com.badoo.reaktive.completable.Completable import com.badoo.reaktive.completable.Completable
import com.badoo.reaktive.completable.completableFromFunction
import com.badoo.reaktive.completable.subscribeOn
import com.badoo.reaktive.maybe.Maybe import com.badoo.reaktive.maybe.Maybe
import com.badoo.reaktive.maybe.map import com.badoo.reaktive.maybe.map
import com.badoo.reaktive.maybe.maybeFromFunction
import com.badoo.reaktive.maybe.notNull
import com.badoo.reaktive.maybe.subscribeOn
import com.badoo.reaktive.scheduler.ioScheduler
import com.squareup.sqldelight.Query
import example.todo.common.database.TodoDatabaseQueries
import example.todo.common.database.TodoItemEntity import example.todo.common.database.TodoItemEntity
import example.todo.common.database.TodoSharedDatabase
import example.todo.common.edit.TodoItem import example.todo.common.edit.TodoItem
import example.todo.common.edit.store.TodoEditStoreProvider.Database import example.todo.common.edit.store.TodoEditStoreProvider.Database
internal class TodoEditStoreDatabase( internal class TodoEditStoreDatabase(
private val queries: TodoDatabaseQueries private val database: TodoSharedDatabase
) : Database { ) : Database {
override fun load(id: Long): Maybe<TodoItem> = override fun load(id: Long): Maybe<TodoItem> =
maybeFromFunction { queries.select(id = id) } database
.subscribeOn(ioScheduler) .select(id = id)
.map(Query<TodoItemEntity>::executeAsOne)
.notNull()
.map { it.toItem() } .map { it.toItem() }
private fun TodoItemEntity.toItem(): TodoItem = private fun TodoItemEntity.toItem(): TodoItem =
@ -33,10 +24,8 @@ internal class TodoEditStoreDatabase(
) )
override fun setText(id: Long, text: String): Completable = override fun setText(id: Long, text: String): Completable =
completableFromFunction { queries.setText(id = id, text = text) } database.setText(id = id, text = text)
.subscribeOn(ioScheduler)
override fun setDone(id: Long, isDone: Boolean): Completable = override fun setDone(id: Long, isDone: Boolean): Completable =
completableFromFunction { queries.setDone(id = id, isDone = isDone) } database.setDone(id = id, isDone = isDone)
.subscribeOn(ioScheduler)
} }

6
examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/integration/TodoMainComponent.kt

@ -6,6 +6,7 @@ import com.arkivanov.decompose.value.operator.map
import com.arkivanov.mvikotlin.core.store.StoreFactory import com.arkivanov.mvikotlin.core.store.StoreFactory
import com.badoo.reaktive.base.Consumer import com.badoo.reaktive.base.Consumer
import com.badoo.reaktive.base.invoke import com.badoo.reaktive.base.invoke
import example.todo.common.database.TodoSharedDatabase
import example.todo.common.main.TodoMain import example.todo.common.main.TodoMain
import example.todo.common.main.TodoMain.Model import example.todo.common.main.TodoMain.Model
import example.todo.common.main.TodoMain.Output import example.todo.common.main.TodoMain.Output
@ -13,12 +14,11 @@ import example.todo.common.main.store.TodoMainStore.Intent
import example.todo.common.main.store.TodoMainStoreProvider import example.todo.common.main.store.TodoMainStoreProvider
import example.todo.common.utils.asValue import example.todo.common.utils.asValue
import example.todo.common.utils.getStore import example.todo.common.utils.getStore
import example.todo.database.TodoDatabase
class TodoMainComponent( class TodoMainComponent(
componentContext: ComponentContext, componentContext: ComponentContext,
storeFactory: StoreFactory, storeFactory: StoreFactory,
database: TodoDatabase, database: TodoSharedDatabase,
private val output: Consumer<Output> private val output: Consumer<Output>
) : TodoMain, ComponentContext by componentContext { ) : TodoMain, ComponentContext by componentContext {
@ -26,7 +26,7 @@ class TodoMainComponent(
instanceKeeper.getStore { instanceKeeper.getStore {
TodoMainStoreProvider( TodoMainStoreProvider(
storeFactory = storeFactory, storeFactory = storeFactory,
database = TodoMainStoreDatabase(queries = database.todoDatabaseQueries) database = TodoMainStoreDatabase(database = database)
).provide() ).provide()
} }

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

@ -1,26 +1,20 @@
package example.todo.common.main.integration package example.todo.common.main.integration
import com.badoo.reaktive.completable.Completable import com.badoo.reaktive.completable.Completable
import com.badoo.reaktive.completable.completableFromFunction
import com.badoo.reaktive.completable.subscribeOn
import com.badoo.reaktive.observable.Observable import com.badoo.reaktive.observable.Observable
import com.badoo.reaktive.observable.mapIterable import com.badoo.reaktive.observable.mapIterable
import com.badoo.reaktive.scheduler.ioScheduler
import com.squareup.sqldelight.Query
import example.todo.common.database.TodoDatabaseQueries
import example.todo.common.database.TodoItemEntity import example.todo.common.database.TodoItemEntity
import example.todo.common.database.asObservable import example.todo.common.database.TodoSharedDatabase
import example.todo.common.main.TodoItem import example.todo.common.main.TodoItem
import example.todo.common.main.store.TodoMainStoreProvider import example.todo.common.main.store.TodoMainStoreProvider
internal class TodoMainStoreDatabase( internal class TodoMainStoreDatabase(
private val queries: TodoDatabaseQueries private val database: TodoSharedDatabase
) : TodoMainStoreProvider.Database { ) : TodoMainStoreProvider.Database {
override val updates: Observable<List<TodoItem>> = override val updates: Observable<List<TodoItem>> =
queries database
.selectAll() .observeAll()
.asObservable(Query<TodoItemEntity>::executeAsList)
.mapIterable { it.toItem() } .mapIterable { it.toItem() }
private fun TodoItemEntity.toItem(): TodoItem = private fun TodoItemEntity.toItem(): TodoItem =
@ -32,14 +26,11 @@ internal class TodoMainStoreDatabase(
) )
override fun setDone(id: Long, isDone: Boolean): Completable = override fun setDone(id: Long, isDone: Boolean): Completable =
completableFromFunction { queries.setDone(id = id, isDone = isDone) } database.setDone(id = id, isDone = isDone)
.subscribeOn(ioScheduler)
override fun delete(id: Long): Completable = override fun delete(id: Long): Completable =
completableFromFunction { queries.delete(id = id) } database.delete(id = id)
.subscribeOn(ioScheduler)
override fun add(text: String): Completable = override fun add(text: String): Completable =
completableFromFunction { queries.add(text = text) } database.add(text = text)
.subscribeOn(ioScheduler)
} }

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

@ -8,12 +8,11 @@ import com.badoo.reaktive.subject.publish.PublishSubject
import com.badoo.reaktive.test.observable.assertValue import com.badoo.reaktive.test.observable.assertValue
import com.badoo.reaktive.test.observable.test import com.badoo.reaktive.test.observable.test
import com.badoo.reaktive.test.scheduler.TestScheduler import com.badoo.reaktive.test.scheduler.TestScheduler
import example.todo.common.database.TestDatabaseDriver import example.todo.common.database.TestTodoSharedDatabase
import example.todo.common.database.TodoItemEntity import example.todo.common.database.TodoItemEntity
import example.todo.common.main.TodoItem import example.todo.common.main.TodoItem
import example.todo.common.main.TodoMain.Model import example.todo.common.main.TodoMain.Model
import example.todo.common.main.TodoMain.Output import example.todo.common.main.TodoMain.Output
import example.todo.database.TodoDatabase
import kotlin.test.BeforeTest import kotlin.test.BeforeTest
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -25,11 +24,10 @@ import kotlin.test.assertTrue
class TodoMainTest { class TodoMainTest {
private val lifecycle = LifecycleRegistry() private val lifecycle = LifecycleRegistry()
private val database = TodoDatabase(TestDatabaseDriver()) private val database = TestTodoSharedDatabase(TestScheduler())
private val outputSubject = PublishSubject<Output>() private val outputSubject = PublishSubject<Output>()
private val output = outputSubject.test() private val output = outputSubject.test()
private val databaseTesting = database.testing
private val queries = database.todoDatabaseQueries
private val impl by lazy { private val impl by lazy {
TodoMainComponent( TodoMainComponent(
@ -49,29 +47,29 @@ class TodoMainTest {
io = { TestScheduler() } io = { TestScheduler() }
) )
queries.clear() databaseTesting.clear()
} }
@Test @Test
fun WHEN_item_added_to_database_THEN_item_displayed() { fun WHEN_item_added_to_database_THEN_item_displayed() {
queries.add("Item1") databaseTesting.add("Item1")
assertEquals("Item1", firstItem().text) assertEquals("Item1", firstItem().text)
} }
@Test @Test
fun WHEN_item_deleted_from_database_THEN_item_not_displayed() { fun WHEN_item_deleted_from_database_THEN_item_not_displayed() {
queries.add("Item1") databaseTesting.add("Item1")
val id = lastInsertItem().id val id = lastInsertItem().id
queries.delete(id = id) databaseTesting.delete(id = id)
assertFalse(model.items.any { it.id == id }) assertFalse(model.items.any { it.id == id })
} }
@Test @Test
fun WHEN_item_clicked_THEN_Output_Selected_emitted() { fun WHEN_item_clicked_THEN_Output_Selected_emitted() {
queries.add("Item1") databaseTesting.add("Item1")
val id = firstItem().id val id = firstItem().id
impl.onItemClicked(id = id) impl.onItemClicked(id = id)
@ -81,42 +79,42 @@ class TodoMainTest {
@Test @Test
fun GIVEN_item_isDone_false_WHEN_done_changed_to_true_THEN_item_isDone_true_in_database() { fun GIVEN_item_isDone_false_WHEN_done_changed_to_true_THEN_item_isDone_true_in_database() {
queries.add("Item1") databaseTesting.add("Item1")
val id = firstItem().id val id = firstItem().id
queries.setDone(id = id, isDone = false) databaseTesting.setDone(id = id, isDone = false)
impl.onItemDoneChanged(id = id, isDone = true) impl.onItemDoneChanged(id = id, isDone = true)
assertTrue(queries.select(id = id).executeAsOne().isDone) assertTrue(databaseTesting.selectRequired(id = id).isDone)
} }
@Test @Test
fun GIVEN_item_isDone_true_WHEN_done_changed_to_false_THEN_item_isDone_false_in_database() { fun GIVEN_item_isDone_true_WHEN_done_changed_to_false_THEN_item_isDone_false_in_database() {
queries.add("Item1") databaseTesting.add("Item1")
val id = firstItem().id val id = firstItem().id
queries.setDone(id = id, isDone = true) databaseTesting.setDone(id = id, isDone = true)
impl.onItemDoneChanged(id = id, isDone = false) impl.onItemDoneChanged(id = id, isDone = false)
assertFalse(queries.select(id = id).executeAsOne().isDone) assertFalse(databaseTesting.selectRequired(id = id).isDone)
} }
@Test @Test
fun WHEN_item_delete_clicked_THEN_item_deleted_in_database() { fun WHEN_item_delete_clicked_THEN_item_deleted_in_database() {
queries.add("Item1") databaseTesting.add("Item1")
val id = firstItem().id val id = firstItem().id
impl.onItemDeleteClicked(id = id) impl.onItemDeleteClicked(id = id)
assertNull(queries.select(id = id).executeAsOneOrNull()) assertNull(databaseTesting.select(id = id))
} }
@Test @Test
fun WHEN_item_text_changed_in_database_THEN_item_updated() { fun WHEN_item_text_changed_in_database_THEN_item_updated() {
queries.add("Item1") databaseTesting.add("Item1")
val id = firstItem().id val id = firstItem().id
queries.setText(id = id, text = "New text") databaseTesting.setText(id = id, text = "New text")
assertEquals("New text", firstItem().text) assertEquals("New text", firstItem().text)
} }
@ -139,9 +137,6 @@ class TodoMainTest {
private fun firstItem(): TodoItem = model.items[0] private fun firstItem(): TodoItem = model.items[0]
private fun lastInsertItem(): TodoItemEntity { private fun lastInsertItem(): TodoItemEntity =
val lastInsertId = queries.transactionWithResult<Long> { queries.getLastInsertId().executeAsOne() } databaseTesting.selectRequired(id = requireNotNull(databaseTesting.getLastInsertId()))
return queries.select(id = lastInsertId).executeAsOne()
}
} }

4
examples/todoapp/common/root/src/commonMain/kotlin/example/todo/common/root/integration/TodoRootComponent.kt

@ -10,6 +10,7 @@ import com.arkivanov.decompose.statekeeper.Parcelize
import com.arkivanov.decompose.value.Value import com.arkivanov.decompose.value.Value
import com.arkivanov.mvikotlin.core.store.StoreFactory import com.arkivanov.mvikotlin.core.store.StoreFactory
import com.badoo.reaktive.base.Consumer import com.badoo.reaktive.base.Consumer
import example.todo.common.database.TodoSharedDatabase
import example.todo.common.edit.TodoEdit import example.todo.common.edit.TodoEdit
import example.todo.common.edit.integration.TodoEditComponent import example.todo.common.edit.integration.TodoEditComponent
import example.todo.common.main.TodoMain import example.todo.common.main.TodoMain
@ -17,7 +18,6 @@ import example.todo.common.main.integration.TodoMainComponent
import example.todo.common.root.TodoRoot import example.todo.common.root.TodoRoot
import example.todo.common.root.TodoRoot.Child import example.todo.common.root.TodoRoot.Child
import example.todo.common.utils.Consumer import example.todo.common.utils.Consumer
import example.todo.database.TodoDatabase
class TodoRootComponent internal constructor( class TodoRootComponent internal constructor(
componentContext: ComponentContext, componentContext: ComponentContext,
@ -28,7 +28,7 @@ class TodoRootComponent internal constructor(
constructor( constructor(
componentContext: ComponentContext, componentContext: ComponentContext,
storeFactory: StoreFactory, storeFactory: StoreFactory,
database: TodoDatabase database: TodoSharedDatabase
) : this( ) : this(
componentContext = componentContext, componentContext = componentContext,
todoMain = { childContext, output -> todoMain = { childContext, output ->

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

@ -11,11 +11,11 @@ import com.arkivanov.decompose.extensions.compose.jetbrains.rememberRootComponen
import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory
import com.badoo.reaktive.coroutinesinterop.asScheduler import com.badoo.reaktive.coroutinesinterop.asScheduler
import com.badoo.reaktive.scheduler.overrideSchedulers import com.badoo.reaktive.scheduler.overrideSchedulers
import example.todo.common.database.DefaultTodoSharedDatabase
import example.todo.common.database.TodoDatabaseDriver import example.todo.common.database.TodoDatabaseDriver
import example.todo.common.root.TodoRoot import example.todo.common.root.TodoRoot
import example.todo.common.root.integration.TodoRootComponent import example.todo.common.root.integration.TodoRootComponent
import example.todo.common.ui.TodoRootContent import example.todo.common.ui.TodoRootContent
import example.todo.database.TodoDatabase
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
fun main() { fun main() {
@ -36,5 +36,5 @@ private fun todoRoot(componentContext: ComponentContext): TodoRoot =
TodoRootComponent( TodoRootComponent(
componentContext = componentContext, componentContext = componentContext,
storeFactory = DefaultStoreFactory, storeFactory = DefaultStoreFactory,
database = TodoDatabase(TodoDatabaseDriver()) database = DefaultTodoSharedDatabase(TodoDatabaseDriver())
) )

2
examples/todoapp/ios/ios/ContentView.swift

@ -8,7 +8,7 @@ struct ContentView: View {
TodoRootComponent( TodoRootComponent(
componentContext: $0, componentContext: $0,
storeFactory: DefaultStoreFactory(), storeFactory: DefaultStoreFactory(),
database: TodoDatabaseCompanion().invoke(driver: TodoDatabaseDriverFactoryKt.TodoDatabaseDriver()) database: DefaultTodoSharedDatabase(driver: TodoDatabaseDriverFactoryKt.TodoDatabaseDriver())
) )
} }

3
examples/todoapp/settings.gradle.kts

@ -6,5 +6,6 @@ include(
":common:root", ":common:root",
":common:compose-ui", ":common:compose-ui",
":android", ":android",
":desktop" ":desktop",
":web"
) )

34
examples/todoapp/web/build.gradle.kts

@ -0,0 +1,34 @@
import org.jetbrains.compose.compose
plugins {
kotlin("multiplatform")
id("org.jetbrains.compose")
}
kotlin {
js(IR) {
browser {
useCommonJs()
binaries.executable()
}
}
sourceSets {
named("jsMain") {
dependencies {
implementation(compose.runtime)
implementation(compose.web.widgets)
implementation(project(":common:utils"))
implementation(project(":common:database"))
implementation(project(":common:root"))
implementation(project(":common:main"))
implementation(project(":common:edit"))
implementation(Deps.ArkIvanov.Decompose.decompose)
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlin)
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlinMain)
implementation(npm("copy-webpack-plugin", "9.0.0"))
implementation(npm("@material-ui/icons", "4.11.2"))
}
}
}
}

35
examples/todoapp/web/src/jsMain/kotlin/example/todo/web/App.kt

@ -0,0 +1,35 @@
package example.todo.web
import com.arkivanov.decompose.DefaultComponentContext
import com.arkivanov.decompose.lifecycle.LifecycleRegistry
import com.arkivanov.decompose.lifecycle.resume
import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory
import example.todo.common.database.DefaultTodoSharedDatabase
import example.todo.common.database.todoDatabaseDriver
import example.todo.common.root.integration.TodoRootComponent
import kotlinx.browser.document
import org.jetbrains.compose.web.css.Style
import org.jetbrains.compose.web.renderComposable
import org.jetbrains.compose.web.ui.Styles
import org.w3c.dom.HTMLElement
fun main() {
val rootElement = document.getElementById("root") as HTMLElement
val lifecycle = LifecycleRegistry()
val root =
TodoRootComponent(
componentContext = DefaultComponentContext(lifecycle = lifecycle),
storeFactory = DefaultStoreFactory,
database = DefaultTodoSharedDatabase(todoDatabaseDriver())
)
lifecycle.resume()
renderComposable(root = rootElement) {
Style(Styles)
TodoRootUi(root)
}
}

174
examples/todoapp/web/src/jsMain/kotlin/example/todo/web/Components.kt

@ -0,0 +1,174 @@
package example.todo.web
import androidx.compose.runtime.Composable
import org.jetbrains.compose.common.material.Text
import org.jetbrains.compose.web.attributes.InputType
import org.jetbrains.compose.web.attributes.checked
import org.jetbrains.compose.web.css.AlignItems
import org.jetbrains.compose.web.css.DisplayStyle
import org.jetbrains.compose.web.css.JustifyContent
import org.jetbrains.compose.web.css.alignItems
import org.jetbrains.compose.web.css.display
import org.jetbrains.compose.web.css.height
import org.jetbrains.compose.web.css.justifyContent
import org.jetbrains.compose.web.css.percent
import org.jetbrains.compose.web.css.px
import org.jetbrains.compose.web.css.width
import org.jetbrains.compose.web.dom.A
import org.jetbrains.compose.web.dom.AttrBuilderContext
import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.dom.ElementScope
import org.jetbrains.compose.web.dom.I
import org.jetbrains.compose.web.dom.Input
import org.jetbrains.compose.web.dom.Label
import org.jetbrains.compose.web.dom.Li
import org.jetbrains.compose.web.dom.Nav
import org.jetbrains.compose.web.dom.Span
import org.jetbrains.compose.web.dom.Text
import org.jetbrains.compose.web.dom.TextArea
import org.jetbrains.compose.web.dom.Ul
import org.w3c.dom.HTMLUListElement
@Composable
fun MaterialCheckbox(
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
attrs: AttrBuilderContext<*> = {},
content: @Composable () -> Unit = {}
) {
Div(attrs = attrs) {
Label {
Input(
type = InputType.Checkbox,
attrs = {
classes("filled-in")
if (checked) checked()
onCheckboxInput { onCheckedChange(it.checked) }
}
)
Span {
content()
}
}
}
}
@Composable
fun Card(attrs: AttrBuilderContext<*> = {}, content: @Composable () -> Unit) {
Div(
attrs = {
classes("card")
attrs()
}
) {
content()
}
}
@Composable
fun MaterialTextArea(
id: String,
label: String,
text: String,
onTextChanged: (String) -> Unit,
attrs: AttrBuilderContext<*> = {}
) {
Div(
attrs = {
classes("input-field", "col", "s12")
attrs()
}
) {
TextArea(
value = text,
attrs = {
id("text_area_add_todo")
classes("materialize-textarea")
onTextInput { onTextChanged(it.inputValue) }
style {
width(100.percent)
height(100.percent)
}
}
)
Label(forId = id) {
Text(text = label)
}
}
}
@Composable
fun ImageButton(
onClick: () -> Unit,
iconName: String,
attrs: AttrBuilderContext<*> = {}
) {
A(
attrs = {
classes("waves-effect", "waves-teal", "btn-flat")
style {
width(48.px)
height(48.px)
display(DisplayStyle.Flex)
alignItems(AlignItems.Center)
justifyContent(JustifyContent.Center)
}
this.onClick { onClick() }
attrs()
}
) {
MaterialIcon(name = iconName)
}
}
@Composable
fun MaterialIcon(name: String) {
I(attrs = { classes("material-icons") }) { Text(value = name) }
}
@Composable
fun NavBar(
title: String,
navigationIcon: NavBarIcon? = null
) {
Nav {
Div(attrs = { classes("nav-wrapper") }) {
if (navigationIcon != null) {
Ul(attrs = { classes("left") }) {
NavBarIcon(icon = navigationIcon)
}
}
A(
attrs = {
classes("brand-logo")
style {
property("padding-left", 16.px)
}
}
) {
Text(value = title)
}
}
}
}
@Composable
private fun ElementScope<HTMLUListElement>.NavBarIcon(icon: NavBarIcon) {
Li {
A(
attrs = {
onClick { icon.onClick() }
}
) {
MaterialIcon(name = icon.name)
}
}
}
class NavBarIcon(
val name: String,
val onClick: () -> Unit
)

115
examples/todoapp/web/src/jsMain/kotlin/example/todo/web/Crossfade.kt

@ -0,0 +1,115 @@
package example.todo.web
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import kotlinx.coroutines.delay
import org.jetbrains.compose.web.css.Position
import org.jetbrains.compose.web.css.height
import org.jetbrains.compose.web.css.left
import org.jetbrains.compose.web.css.opacity
import org.jetbrains.compose.web.css.percent
import org.jetbrains.compose.web.css.position
import org.jetbrains.compose.web.css.px
import org.jetbrains.compose.web.css.top
import org.jetbrains.compose.web.css.width
import org.jetbrains.compose.web.dom.AttrBuilderContext
import org.jetbrains.compose.web.dom.Div
import kotlin.js.Date
import kotlin.math.sqrt
@Composable
fun <T : Any> Crossfade(target: T, attrs: AttrBuilderContext<*> = {}, content: @Composable (T) -> Unit) {
val holder = remember { TargetHolder<T>(null) }
val previousTarget: T? = holder.value
if (previousTarget == null) {
holder.value = target
Div(attrs = attrs) {
content(target)
}
return
}
if (previousTarget == target) {
Div(attrs = attrs) {
content(target)
}
return
}
holder.value = target
val animationFactor by animateFloatFactor(key = target, durationMillis = 300L, easing = ::sqrt)
Div(attrs = attrs) {
if (animationFactor < 1F) {
Div(
attrs = {
style {
width(100.percent)
height(100.percent)
position(Position.Absolute)
top(0.px)
left(0.px)
opacity(1F - animationFactor)
}
}
) {
content(previousTarget)
}
}
Div(
attrs = {
style {
width(100.percent)
height(100.percent)
position(Position.Absolute)
top(0.px)
left(0.px)
if (animationFactor < 1F) {
opacity(animationFactor)
}
}
}
) {
content(target)
}
}
}
private class TargetHolder<T : Any>(
var value: T?
)
@Composable
private fun animateFloatFactor(key: Any, durationMillis: Long, easing: Easing = Easing { it }): State<Float> {
val state = remember(key) { mutableStateOf(0F) }
LaunchedEffect(key) {
var date = Date.now()
val startMillis = date
val endMillis = startMillis + durationMillis.toDouble()
while (true) {
date = Date.now()
if (date >= endMillis) {
break
}
state.value = easing.transform(((date - startMillis) / durationMillis.toDouble()).toFloat())
delay(16L)
}
state.value = 1F
}
return state
}
private fun interface Easing {
fun transform(fraction: Float): Float
}

96
examples/todoapp/web/src/jsMain/kotlin/example/todo/web/TodoEditUi.kt

@ -0,0 +1,96 @@
package example.todo.web
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import example.todo.common.edit.TodoEdit
import org.jetbrains.compose.web.css.DisplayStyle
import org.jetbrains.compose.web.css.FlexDirection
import org.jetbrains.compose.web.css.FlexWrap
import org.jetbrains.compose.web.css.JustifyContent
import org.jetbrains.compose.web.css.display
import org.jetbrains.compose.web.css.flexFlow
import org.jetbrains.compose.web.css.height
import org.jetbrains.compose.web.css.justifyContent
import org.jetbrains.compose.web.css.percent
import org.jetbrains.compose.web.css.width
import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.dom.Text
@Composable
fun TodoEditUi(component: TodoEdit) {
val model by component.models.subscribeAsState()
Div(
attrs = {
style {
width(100.percent)
height(100.percent)
display(DisplayStyle.Flex)
flexFlow(FlexDirection.Column, FlexWrap.Nowrap)
}
}
) {
Div(
attrs = {
style {
width(100.percent)
property("flex", "0 1 auto")
}
}
) {
NavBar(
title = "Edit todo",
navigationIcon = NavBarIcon(
name = "arrow_back",
onClick = component::onCloseClicked
)
)
}
Div(
attrs = {
style {
width(100.percent)
property("flex", "1 1 auto")
property("padding", "0px 16px 0px 16px")
display(DisplayStyle.Flex)
flexFlow(FlexDirection.Column, FlexWrap.Nowrap)
}
}
) {
MaterialTextArea(
id = "text_area_edit_todo",
label = "",
text = model.text,
onTextChanged = component::onTextChanged,
attrs = {
style {
width(100.percent)
property("flex", "1 1 auto")
}
}
)
}
Div(
attrs = {
style {
width(100.percent)
property("flex", "0 1 auto")
property("padding-bottom", "16px")
display(DisplayStyle.Flex)
justifyContent(JustifyContent.Center)
}
}
) {
MaterialCheckbox(
checked = model.isDone,
onCheckedChange = component::onDoneChanged,
content = {
Text(value = "Completed")
}
)
}
}
}

189
examples/todoapp/web/src/jsMain/kotlin/example/todo/web/TodoMainUi.kt

@ -0,0 +1,189 @@
package example.todo.web
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import example.todo.common.main.TodoItem
import example.todo.common.main.TodoMain
import org.jetbrains.compose.web.css.AlignItems
import org.jetbrains.compose.web.css.DisplayStyle
import org.jetbrains.compose.web.css.FlexDirection
import org.jetbrains.compose.web.css.FlexWrap
import org.jetbrains.compose.web.css.alignItems
import org.jetbrains.compose.web.css.display
import org.jetbrains.compose.web.css.flexFlow
import org.jetbrains.compose.web.css.height
import org.jetbrains.compose.web.css.margin
import org.jetbrains.compose.web.css.marginLeft
import org.jetbrains.compose.web.css.percent
import org.jetbrains.compose.web.css.px
import org.jetbrains.compose.web.css.width
import org.jetbrains.compose.web.dom.DOMScope
import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.dom.Li
import org.jetbrains.compose.web.dom.Text
import org.jetbrains.compose.web.dom.Ul
import org.w3c.dom.HTMLUListElement
@Composable
fun TodoMainUi(component: TodoMain) {
val model by component.models.subscribeAsState()
Div(
attrs = {
style {
width(100.percent)
height(100.percent)
display(DisplayStyle.Flex)
flexFlow(FlexDirection.Column, FlexWrap.Nowrap)
}
}
) {
Div(
attrs = {
style {
width(100.percent)
property("flex", "0 1 auto")
}
}
) {
NavBar(title = "Todo List")
}
Ul(
attrs = {
style {
width(100.percent)
margin(0.px)
property("flex", "1 1 auto")
property("overflow-y", "scroll")
}
}
) {
model.items.forEach { item ->
Item(
item = item,
onClicked = component::onItemClicked,
onDoneChanged = component::onItemDoneChanged,
onDeleteClicked = component::onItemDeleteClicked
)
}
}
Div(
attrs = {
style {
width(100.percent)
property("flex", "0 1 auto")
}
}
) {
TodoInput(
text = model.text,
onTextChanged = component::onInputTextChanged,
onAddClicked = component::onAddItemClicked
)
}
}
}
@Composable
private fun DOMScope<HTMLUListElement>.Item(
item: TodoItem,
onClicked: (id: Long) -> Unit,
onDoneChanged: (id: Long, isDone: Boolean) -> Unit,
onDeleteClicked: (id: Long) -> Unit
) {
Li(
attrs = {
style {
width(100.percent)
display(DisplayStyle.Flex)
flexFlow(FlexDirection.Row, FlexWrap.Nowrap)
alignItems(AlignItems.Center)
property("padding", "0px 0px 0px 16px")
}
}
) {
MaterialCheckbox(
checked = item.isDone,
onCheckedChange = { onDoneChanged(item.id, !item.isDone) },
attrs = {
style {
property("flex", "0 1 auto")
property("padding-top", 10.px) // Fix for the checkbox not being centered vertically
}
}
)
Div(
attrs = {
style {
height(48.px)
property("flex", "1 1 auto")
property("white-space", "nowrap")
property("text-overflow", "ellipsis")
property("overflow", "hidden")
display(DisplayStyle.Flex)
alignItems(AlignItems.Center)
}
onClick { onClicked(item.id) }
}
) {
Text(value = item.text)
}
ImageButton(
onClick = { onDeleteClicked(item.id) },
iconName = "delete",
attrs = {
style {
property("flex", "0 1 auto")
marginLeft(8.px)
}
}
)
}
}
@Composable
private fun TodoInput(
text: String,
onTextChanged: (String) -> Unit,
onAddClicked: () -> Unit
) {
Div(
attrs = {
style {
width(100.percent)
display(DisplayStyle.Flex)
flexFlow(FlexDirection.Row, FlexWrap.Nowrap)
alignItems(AlignItems.Center)
}
}
) {
MaterialTextArea(
id = "text_area_add_todo",
label = "Add todo",
text = text,
onTextChanged = onTextChanged,
attrs = {
style {
property("flex", "1 1 auto")
margin(16.px)
}
}
)
ImageButton(
onClick = onAddClicked,
iconName = "add",
attrs = {
style {
property("flex", "0 1 auto")
property("margin", "0px 16px 0px 0px")
}
}
)
}
}

54
examples/todoapp/web/src/jsMain/kotlin/example/todo/web/TodoRootUi.kt

@ -0,0 +1,54 @@
package example.todo.web
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import example.todo.common.root.TodoRoot
import org.jetbrains.compose.web.css.Position
import org.jetbrains.compose.web.css.auto
import org.jetbrains.compose.web.css.bottom
import org.jetbrains.compose.web.css.height
import org.jetbrains.compose.web.css.left
import org.jetbrains.compose.web.css.percent
import org.jetbrains.compose.web.css.position
import org.jetbrains.compose.web.css.px
import org.jetbrains.compose.web.css.right
import org.jetbrains.compose.web.css.top
import org.jetbrains.compose.web.css.width
@Composable
fun TodoRootUi(component: TodoRoot) {
Card(
attrs = {
style {
position(Position.Absolute)
height(700.px)
property("max-width", 640.px)
top(0.px)
bottom(0.px)
left(0.px)
right(0.px)
property("margin", auto)
}
}
) {
val routerState by component.routerState.subscribeAsState()
Crossfade(
target = routerState.activeChild.instance,
attrs = {
style {
width(100.percent)
height(100.percent)
position(Position.Relative)
left(0.px)
top(0.px)
}
}
) { child ->
when (child) {
is TodoRoot.Child.Main -> TodoMainUi(child.component)
is TodoRoot.Child.Edit -> TodoEditUi(child.component)
}
}
}
}

26
examples/todoapp/web/src/jsMain/kotlin/example/todo/web/ValueExt.kt

@ -0,0 +1,26 @@
package example.todo.web
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import com.arkivanov.decompose.value.Value
import com.arkivanov.decompose.value.ValueObserver
@Composable
fun <T : Any> Value<T>.subscribeAsState(): State<T> {
val state = remember(this) { mutableStateOf(value) }
DisposableEffect(this) {
val observer: ValueObserver<T> = { state.value = it }
subscribe(observer)
onDispose {
unsubscribe(observer)
}
}
return state
}

BIN
examples/todoapp/web/src/jsMain/resources/MaterialIcons-Regular.ttf

Binary file not shown.

30
examples/todoapp/web/src/jsMain/resources/index.html

@ -0,0 +1,30 @@
<!--
~ Copyright 2021 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Todo</title>
<link href="styles.css" rel="stylesheet" type="text/css"/>
<link href="materialize.min.css" media="screen,projection" rel="stylesheet" type="text/css"/>
</head>
<body>
<div id="root"></div>
<script src="web.js"></script>
<script src="materialize.min.js" type="text/javascript"></script>
</body>
</html>

13
examples/todoapp/web/src/jsMain/resources/materialize.min.css vendored

File diff suppressed because one or more lines are too long

6
examples/todoapp/web/src/jsMain/resources/materialize.min.js vendored

File diff suppressed because one or more lines are too long

34
examples/todoapp/web/src/jsMain/resources/styles.css

@ -0,0 +1,34 @@
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: local('Material Icons'),
local('MaterialIcons-Regular'),
url(MaterialIcons-Regular.ttf) format('truetype');
}
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px; /* Preferred icon size */
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
/* Support for all WebKit browsers. */
-webkit-font-smoothing: antialiased;
/* Support for Safari and Chrome. */
text-rendering: optimizeLegibility;
/* Support for Firefox. */
-moz-osx-font-smoothing: grayscale;
/* Support for IE. */
font-feature-settings: 'liga';
}

9
examples/todoapp/web/webpack.config.d/fs.js

@ -0,0 +1,9 @@
// As per https://cashapp.github.io/sqldelight/js_sqlite/
config.resolve = {
fallback: {
fs: false,
path: false,
crypto: false
}
}

12
examples/todoapp/web/webpack.config.d/wasm.js

@ -0,0 +1,12 @@
// As per https://cashapp.github.io/sqldelight/js_sqlite/
var CopyWebpackPlugin = require('copy-webpack-plugin');
config.plugins.push(
new CopyWebpackPlugin(
{
patterns: [
{from: '../../node_modules/sql.js/dist/sql-wasm.wasm', to: '../../../web/build/distributions'}
]
}
)
);
Loading…
Cancel
Save