@ -0,0 +1,15 @@
|
||||
*.iml |
||||
.gradle |
||||
/local.properties |
||||
/.idea |
||||
/.idea/caches |
||||
/.idea/libraries |
||||
/.idea/modules.xml |
||||
/.idea/workspace.xml |
||||
/.idea/navEditor.xml |
||||
/.idea/assetWizardSettings.xml |
||||
.DS_Store |
||||
/build |
||||
/captures |
||||
.externalNativeBuild |
||||
.cxx |
@ -0,0 +1,4 @@
|
||||
An example of todo app based on Jetpack Compose UI library (desktop and android) and Decompose navigation library. |
||||
|
||||
To run desktop application execute in terminal: ./gradlew desktop:run |
||||
To run android application you will need to open project in Intellij IDEA or Android Studio and run "android" configuration |
@ -0,0 +1,39 @@
|
||||
import org.jetbrains.compose.compose |
||||
|
||||
plugins { |
||||
id("com.android.application") |
||||
kotlin("android") |
||||
id("org.jetbrains.compose") |
||||
} |
||||
|
||||
android { |
||||
compileSdkVersion(30) |
||||
|
||||
defaultConfig { |
||||
minSdkVersion(23) |
||||
targetSdkVersion(30) |
||||
versionCode = 1 |
||||
versionName = "1.0" |
||||
} |
||||
|
||||
compileOptions { |
||||
sourceCompatibility = JavaVersion.VERSION_1_8 |
||||
targetCompatibility = JavaVersion.VERSION_1_8 |
||||
} |
||||
|
||||
packagingOptions { |
||||
exclude("META-INF/*") |
||||
} |
||||
} |
||||
|
||||
dependencies { |
||||
implementation(project(":common:database")) |
||||
implementation(project(":common:utils")) |
||||
implementation(project(":common:root")) |
||||
implementation(compose.material) |
||||
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlin) |
||||
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlinMain) |
||||
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlinLogging) |
||||
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlinTimeTravel) |
||||
implementation(Deps.ArkIvanov.Decompose.decompose) |
||||
} |
@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" |
||||
package="example.todo.android"> |
||||
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> |
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> |
||||
<uses-permission android:name="android.permission.INTERNET"/> |
||||
|
||||
<application |
||||
android:allowBackup="true" |
||||
android:icon="@mipmap/ic_launcher" |
||||
android:label="@string/app_name" |
||||
android:name=".App" |
||||
android:roundIcon="@mipmap/ic_launcher_round" |
||||
android:supportsRtl="true" |
||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar"> |
||||
<activity |
||||
android:name="example.todo.android.MainActivity" |
||||
android:label="@string/app_name"> |
||||
<intent-filter> |
||||
<action android:name="android.intent.action.MAIN" /> |
||||
<category android:name="android.intent.category.LAUNCHER" /> |
||||
</intent-filter> |
||||
</activity> |
||||
</application> |
||||
|
||||
</manifest> |
@ -0,0 +1,13 @@
|
||||
package example.todo.android |
||||
|
||||
import android.app.Application |
||||
import com.arkivanov.mvikotlin.timetravel.server.TimeTravelServer |
||||
|
||||
class App : Application() { |
||||
|
||||
override fun onCreate() { |
||||
super.onCreate() |
||||
|
||||
TimeTravelServer().start() |
||||
} |
||||
} |
@ -0,0 +1,8 @@
|
||||
package example.todo.android |
||||
|
||||
import androidx.compose.ui.graphics.Color |
||||
|
||||
val purple200 = Color(0xFFBB86FC) |
||||
val purple500 = Color(0xFF6200EE) |
||||
val purple700 = Color(0xFF3700B3) |
||||
val teal200 = Color(0xFF03DAC5) |
@ -0,0 +1,63 @@
|
||||
/* |
||||
* Copyright 2020 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. |
||||
*/ |
||||
package example.todo.android |
||||
|
||||
import android.os.Bundle |
||||
import androidx.appcompat.app.AppCompatActivity |
||||
import androidx.compose.material.MaterialTheme |
||||
import androidx.compose.material.Surface |
||||
import androidx.compose.ui.platform.setContent |
||||
import com.arkivanov.decompose.DefaultComponentContext |
||||
import com.arkivanov.decompose.backpressed.toBackPressedDispatched |
||||
import com.arkivanov.decompose.instancekeeper.toInstanceKeeper |
||||
import com.arkivanov.decompose.lifecycle.asDecomposeLifecycle |
||||
import com.arkivanov.decompose.statekeeper.toStateKeeper |
||||
import com.arkivanov.mvikotlin.core.store.StoreFactory |
||||
import com.arkivanov.mvikotlin.logging.store.LoggingStoreFactory |
||||
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.database.TodoDatabase |
||||
|
||||
class MainActivity : AppCompatActivity() { |
||||
override fun onCreate(savedInstanceState: Bundle?) { |
||||
super.onCreate(savedInstanceState) |
||||
|
||||
val todoRoot = |
||||
TodoRoot( |
||||
componentContext = DefaultComponentContext( |
||||
lifecycle = lifecycle.asDecomposeLifecycle(), |
||||
stateKeeper = savedStateRegistry.toStateKeeper(), |
||||
instanceKeeper = viewModelStore.toInstanceKeeper(), |
||||
backPressedDispatcher = onBackPressedDispatcher.toBackPressedDispatched(lifecycle) |
||||
), |
||||
dependencies = object : TodoRoot.Dependencies { |
||||
// You can play with time travel using IDEA plugin: https://arkivanov.github.io/MVIKotlin/time_travel.html |
||||
override val storeFactory: StoreFactory = LoggingStoreFactory(TimeTravelStoreFactory(DefaultStoreFactory)) |
||||
override val database: TodoDatabase = TodoDatabase(TodoDatabaseDriver(this@MainActivity)) |
||||
} |
||||
) |
||||
|
||||
setContent { |
||||
ComposeAppTheme { |
||||
Surface(color = MaterialTheme.colors.background) { |
||||
todoRoot() |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,11 @@
|
||||
package example.todo.android |
||||
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape |
||||
import androidx.compose.material.Shapes |
||||
import androidx.compose.ui.unit.dp |
||||
|
||||
val shapes = Shapes( |
||||
small = RoundedCornerShape(4.dp), |
||||
medium = RoundedCornerShape(4.dp), |
||||
large = RoundedCornerShape(0.dp) |
||||
) |
@ -0,0 +1,35 @@
|
||||
package example.todo.android |
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme |
||||
import androidx.compose.material.MaterialTheme |
||||
import androidx.compose.material.darkColors |
||||
import androidx.compose.material.lightColors |
||||
import androidx.compose.runtime.Composable |
||||
|
||||
private val DarkColorPalette = darkColors( |
||||
primary = purple200, |
||||
primaryVariant = purple700, |
||||
secondary = teal200 |
||||
) |
||||
|
||||
private val LightColorPalette = lightColors( |
||||
primary = purple500, |
||||
primaryVariant = purple700, |
||||
secondary = teal200 |
||||
) |
||||
|
||||
@Composable |
||||
fun ComposeAppTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) { |
||||
val colors = if (darkTheme) { |
||||
DarkColorPalette |
||||
} else { |
||||
LightColorPalette |
||||
} |
||||
|
||||
MaterialTheme( |
||||
colors = colors, |
||||
typography = typography, |
||||
shapes = shapes, |
||||
content = content |
||||
) |
||||
} |
@ -0,0 +1,16 @@
|
||||
package example.todo.android |
||||
|
||||
import androidx.compose.material.Typography |
||||
import androidx.compose.ui.text.TextStyle |
||||
import androidx.compose.ui.text.font.FontFamily |
||||
import androidx.compose.ui.text.font.FontWeight |
||||
import androidx.compose.ui.unit.sp |
||||
|
||||
// Set of Material typography styles to start with |
||||
val typography = Typography( |
||||
body1 = TextStyle( |
||||
fontFamily = FontFamily.Default, |
||||
fontWeight = FontWeight.Normal, |
||||
fontSize = 16.sp |
||||
) |
||||
) |
@ -0,0 +1,34 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
||||
xmlns:aapt="http://schemas.android.com/aapt" |
||||
android:width="108dp" |
||||
android:height="108dp" |
||||
android:viewportWidth="108" |
||||
android:viewportHeight="108"> |
||||
<path |
||||
android:fillType="evenOdd" |
||||
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z" |
||||
android:strokeWidth="1" |
||||
android:strokeColor="#00000000"> |
||||
<aapt:attr name="android:fillColor"> |
||||
<gradient |
||||
android:endX="78.5885" |
||||
android:endY="90.9159" |
||||
android:startX="48.7653" |
||||
android:startY="61.0927" |
||||
android:type="linear"> |
||||
<item |
||||
android:color="#44000000" |
||||
android:offset="0.0" /> |
||||
<item |
||||
android:color="#00000000" |
||||
android:offset="1.0" /> |
||||
</gradient> |
||||
</aapt:attr> |
||||
</path> |
||||
<path |
||||
android:fillColor="#FFFFFF" |
||||
android:fillType="nonZero" |
||||
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z" |
||||
android:strokeWidth="1" |
||||
android:strokeColor="#00000000" /> |
||||
</vector> |
@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:width="108dp" |
||||
android:height="108dp" |
||||
android:viewportWidth="108" |
||||
android:viewportHeight="108"> |
||||
<path |
||||
android:fillColor="#008577" |
||||
android:pathData="M0,0h108v108h-108z" /> |
||||
<path |
||||
android:fillColor="#00000000" |
||||
android:pathData="M9,0L9,108" |
||||
android:strokeWidth="0.8" |
||||
android:strokeColor="#33FFFFFF" /> |
||||
<path |
||||
android:fillColor="#00000000" |
||||
android:pathData="M19,0L19,108" |
||||
android:strokeWidth="0.8" |
||||
android:strokeColor="#33FFFFFF" /> |
||||
<path |
||||
android:fillColor="#00000000" |
||||
android:pathData="M29,0L29,108" |
||||
android:strokeWidth="0.8" |
||||
android:strokeColor="#33FFFFFF" /> |
||||
<path |
||||
android:fillColor="#00000000" |
||||
android:pathData="M39,0L39,108" |
||||
android:strokeWidth="0.8" |
||||
android:strokeColor="#33FFFFFF" /> |
||||
<path |
||||
android:fillColor="#00000000" |
||||
android:pathData="M49,0L49,108" |
||||
android:strokeWidth="0.8" |
||||
android:strokeColor="#33FFFFFF" /> |
||||
<path |
||||
android:fillColor="#00000000" |
||||
android:pathData="M59,0L59,108" |
||||
android:strokeWidth="0.8" |
||||
android:strokeColor="#33FFFFFF" /> |
||||
<path |
||||
android:fillColor="#00000000" |
||||
android:pathData="M69,0L69,108" |
||||
android:strokeWidth="0.8" |
||||
android:strokeColor="#33FFFFFF" /> |
||||
<path |
||||
android:fillColor="#00000000" |
||||
android:pathData="M79,0L79,108" |
||||
android:strokeWidth="0.8" |
||||
android:strokeColor="#33FFFFFF" /> |
||||
<path |
||||
android:fillColor="#00000000" |
||||
android:pathData="M89,0L89,108" |
||||
android:strokeWidth="0.8" |
||||
android:strokeColor="#33FFFFFF" /> |
||||
<path |
||||
android:fillColor="#00000000" |
||||
android:pathData="M99,0L99,108" |
||||
android:strokeWidth="0.8" |
||||
android:strokeColor="#33FFFFFF" /> |
||||
<path |
||||
android:fillColor="#00000000" |
||||
android:pathData="M0,9L108,9" |
||||
android:strokeWidth="0.8" |
||||
android:strokeColor="#33FFFFFF" /> |
||||
<path |
||||
android:fillColor="#00000000" |
||||
android:pathData="M0,19L108,19" |
||||
android:strokeWidth="0.8" |
||||
android:strokeColor="#33FFFFFF" /> |
||||
<path |
||||
android:fillColor="#00000000" |
||||
android:pathData="M0,29L108,29" |
||||
android:strokeWidth="0.8" |
||||
android:strokeColor="#33FFFFFF" /> |
||||
<path |
||||
android:fillColor="#00000000" |
||||
android:pathData="M0,39L108,39" |
||||
android:strokeWidth="0.8" |
||||
android:strokeColor="#33FFFFFF" /> |
||||
<path |
||||
android:fillColor="#00000000" |
||||
android:pathData="M0,49L108,49" |
||||
android:strokeWidth="0.8" |
||||
android:strokeColor="#33FFFFFF" /> |
||||
<path |
||||
android:fillColor="#00000000" |
||||
android:pathData="M0,59L108,59" |
||||
android:strokeWidth="0.8" |
||||
android:strokeColor="#33FFFFFF" /> |
||||
<path |
||||
android:fillColor="#00000000" |
||||
android:pathData="M0,69L108,69" |
||||
android:strokeWidth="0.8" |
||||
android:strokeColor="#33FFFFFF" /> |
||||
<path |
||||
android:fillColor="#00000000" |
||||
android:pathData="M0,79L108,79" |
||||
android:strokeWidth="0.8" |
||||
android:strokeColor="#33FFFFFF" /> |
||||
<path |
||||
android:fillColor="#00000000" |
||||
android:pathData="M0,89L108,89" |
||||
android:strokeWidth="0.8" |
||||
android:strokeColor="#33FFFFFF" /> |
||||
<path |
||||
android:fillColor="#00000000" |
||||
android:pathData="M0,99L108,99" |
||||
android:strokeWidth="0.8" |
||||
android:strokeColor="#33FFFFFF" /> |
||||
<path |
||||
android:fillColor="#00000000" |
||||
android:pathData="M19,29L89,29" |
||||
android:strokeWidth="0.8" |
||||
android:strokeColor="#33FFFFFF" /> |
||||
<path |
||||
android:fillColor="#00000000" |
||||
android:pathData="M19,39L89,39" |
||||
android:strokeWidth="0.8" |
||||
android:strokeColor="#33FFFFFF" /> |
||||
<path |
||||
android:fillColor="#00000000" |
||||
android:pathData="M19,49L89,49" |
||||
android:strokeWidth="0.8" |
||||
android:strokeColor="#33FFFFFF" /> |
||||
<path |
||||
android:fillColor="#00000000" |
||||
android:pathData="M19,59L89,59" |
||||
android:strokeWidth="0.8" |
||||
android:strokeColor="#33FFFFFF" /> |
||||
<path |
||||
android:fillColor="#00000000" |
||||
android:pathData="M19,69L89,69" |
||||
android:strokeWidth="0.8" |
||||
android:strokeColor="#33FFFFFF" /> |
||||
<path |
||||
android:fillColor="#00000000" |
||||
android:pathData="M19,79L89,79" |
||||
android:strokeWidth="0.8" |
||||
android:strokeColor="#33FFFFFF" /> |
||||
<path |
||||
android:fillColor="#00000000" |
||||
android:pathData="M29,19L29,89" |
||||
android:strokeWidth="0.8" |
||||
android:strokeColor="#33FFFFFF" /> |
||||
<path |
||||
android:fillColor="#00000000" |
||||
android:pathData="M39,19L39,89" |
||||
android:strokeWidth="0.8" |
||||
android:strokeColor="#33FFFFFF" /> |
||||
<path |
||||
android:fillColor="#00000000" |
||||
android:pathData="M49,19L49,89" |
||||
android:strokeWidth="0.8" |
||||
android:strokeColor="#33FFFFFF" /> |
||||
<path |
||||
android:fillColor="#00000000" |
||||
android:pathData="M59,19L59,89" |
||||
android:strokeWidth="0.8" |
||||
android:strokeColor="#33FFFFFF" /> |
||||
<path |
||||
android:fillColor="#00000000" |
||||
android:pathData="M69,19L69,89" |
||||
android:strokeWidth="0.8" |
||||
android:strokeColor="#33FFFFFF" /> |
||||
<path |
||||
android:fillColor="#00000000" |
||||
android:pathData="M79,19L79,89" |
||||
android:strokeWidth="0.8" |
||||
android:strokeColor="#33FFFFFF" /> |
||||
</vector> |
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
<background android:drawable="@drawable/ic_launcher_background" /> |
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" /> |
||||
</adaptive-icon> |
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
<background android:drawable="@drawable/ic_launcher_background" /> |
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" /> |
||||
</adaptive-icon> |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 6.7 KiB |
After Width: | Height: | Size: 6.2 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 8.9 KiB |
After Width: | Height: | Size: 15 KiB |
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<resources> |
||||
<string name="app_name">Todo</string> |
||||
</resources> |
@ -0,0 +1,14 @@
|
||||
plugins { |
||||
`kotlin-dsl` |
||||
} |
||||
|
||||
allprojects { |
||||
repositories { |
||||
google() |
||||
jcenter() |
||||
mavenLocal() |
||||
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") |
||||
maven("https://dl.bintray.com/arkivanov/maven") |
||||
maven("https://dl.bintray.com/badoo/maven") |
||||
} |
||||
} |
@ -0,0 +1,36 @@
|
||||
plugins { |
||||
`kotlin-dsl` |
||||
`kotlin-dsl-precompiled-script-plugins` |
||||
} |
||||
|
||||
repositories { |
||||
google() |
||||
jcenter() |
||||
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") |
||||
} |
||||
|
||||
buildscript { |
||||
repositories { |
||||
google() |
||||
jcenter() |
||||
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") |
||||
} |
||||
|
||||
dependencies { |
||||
classpath(Deps.JetBrains.Compose.gradlePlugin) |
||||
classpath(Deps.JetBrains.Kotlin.gradlePlugin) |
||||
classpath(Deps.Android.Tools.Build.gradlePlugin) |
||||
} |
||||
} |
||||
|
||||
dependencies { |
||||
implementation(Deps.JetBrains.Compose.gradlePlugin) |
||||
implementation(Deps.JetBrains.Kotlin.gradlePlugin) |
||||
implementation(Deps.Android.Tools.Build.gradlePlugin) |
||||
implementation(Deps.Squareup.SQLDelight.gradlePlugin) |
||||
} |
||||
|
||||
kotlin { |
||||
// Add Deps to compilation, so it will become available in main project |
||||
sourceSets.getByName("main").kotlin.srcDir("buildSrc/src/main/kotlin") |
||||
} |
@ -0,0 +1,7 @@
|
||||
plugins { |
||||
`kotlin-dsl` |
||||
} |
||||
|
||||
repositories { |
||||
jcenter() |
||||
} |
@ -0,0 +1,56 @@
|
||||
object Deps { |
||||
|
||||
object JetBrains { |
||||
object Kotlin { |
||||
private const val VERSION = "1.4.0" |
||||
const val gradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$VERSION" |
||||
} |
||||
|
||||
object Compose { |
||||
private const val VERSION = "0.1.0-build63" |
||||
const val gradlePlugin = "org.jetbrains.compose:compose-gradle-plugin:$VERSION" |
||||
} |
||||
} |
||||
|
||||
object Android { |
||||
object Tools { |
||||
object Build { |
||||
const val gradlePlugin = "com.android.tools.build:gradle:4.0.1" |
||||
} |
||||
} |
||||
} |
||||
|
||||
object ArkIvanov { |
||||
object MVIKotlin { |
||||
private const val VERSION = "2.0.0" |
||||
const val mvikotlin = "com.arkivanov.mvikotlin:mvikotlin:$VERSION" |
||||
const val mvikotlinMain = "com.arkivanov.mvikotlin:mvikotlin-main:$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.0.10" |
||||
const val decompose = "com.arkivanov.decompose:decompose:$VERSION" |
||||
} |
||||
} |
||||
|
||||
object Badoo { |
||||
object Reaktive { |
||||
private const val VERSION = "1.1.18" |
||||
const val reaktive = "com.badoo.reaktive:reaktive:$VERSION" |
||||
const val coroutinesInterop = "com.badoo.reaktive:coroutines-interop:$VERSION" |
||||
} |
||||
} |
||||
|
||||
object Squareup { |
||||
object SQLDelight { |
||||
private const val VERSION = "1.4.4" |
||||
|
||||
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" |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,29 @@
|
||||
import org.jetbrains.compose.compose |
||||
|
||||
plugins { |
||||
id("kotlin-multiplatform") |
||||
id("org.jetbrains.compose") |
||||
} |
||||
|
||||
kotlin { |
||||
sourceSets { |
||||
named("commonMain") { |
||||
dependencies { |
||||
api(compose.runtime) |
||||
api(compose.foundation) |
||||
api(compose.material) |
||||
} |
||||
} |
||||
named("androidMain") { |
||||
dependencies { |
||||
api("androidx.appcompat:appcompat:1.1.0") |
||||
api("androidx.core:core-ktx:1.3.1") |
||||
} |
||||
} |
||||
named("desktopMain") { |
||||
dependencies { |
||||
api(compose.desktop.common) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,45 @@
|
||||
plugins { |
||||
id("com.android.library") |
||||
id("kotlin-multiplatform") |
||||
} |
||||
|
||||
kotlin { |
||||
jvm("desktop") |
||||
android() |
||||
|
||||
sourceSets { |
||||
named("commonMain") { |
||||
dependencies { |
||||
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlin) |
||||
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlinExtensionsReaktive) |
||||
implementation(Deps.ArkIvanov.Decompose.decompose) |
||||
implementation(Deps.Badoo.Reaktive.reaktive) |
||||
} |
||||
} |
||||
} |
||||
|
||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> { |
||||
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") |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,19 @@
|
||||
plugins { |
||||
id("multiplatform-setup") |
||||
id("multiplatform-compose-setup") |
||||
} |
||||
|
||||
kotlin { |
||||
sourceSets { |
||||
named("commonMain") { |
||||
dependencies { |
||||
implementation(project(":common:utils")) |
||||
implementation(project(":common:database")) |
||||
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlin) |
||||
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlinExtensionsReaktive) |
||||
implementation(Deps.ArkIvanov.Decompose.decompose) |
||||
implementation(Deps.Badoo.Reaktive.reaktive) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<manifest package="example.todo.common.add"/> |
@ -0,0 +1,20 @@
|
||||
package example.todo.common.add |
||||
|
||||
import com.arkivanov.decompose.ComponentContext |
||||
import com.arkivanov.mvikotlin.core.store.StoreFactory |
||||
import example.todo.common.add.TodoAdd.Dependencies |
||||
import example.todo.common.add.integration.TodoAddImpl |
||||
import example.todo.common.utils.Component |
||||
import example.todo.database.TodoDatabase |
||||
|
||||
interface TodoAdd : Component { |
||||
|
||||
interface Dependencies { |
||||
val storeFactory: StoreFactory |
||||
val database: TodoDatabase |
||||
} |
||||
} |
||||
|
||||
@Suppress("FunctionName") // Factory function |
||||
fun TodoAdd(componentContext: ComponentContext, dependencies: Dependencies): TodoAdd = |
||||
TodoAddImpl(componentContext, dependencies) |
@ -0,0 +1,59 @@
|
||||
package example.todo.common.add.integration |
||||
|
||||
import androidx.compose.foundation.Text |
||||
import androidx.compose.foundation.layout.Row |
||||
import androidx.compose.foundation.layout.padding |
||||
import androidx.compose.material.Button |
||||
import androidx.compose.material.OutlinedTextField |
||||
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.arkivanov.decompose.ComponentContext |
||||
import example.todo.common.add.TodoAdd |
||||
import example.todo.common.add.TodoAdd.Dependencies |
||||
import example.todo.common.add.store.TodoAddStore.Intent |
||||
import example.todo.common.add.store.TodoAddStoreProvider |
||||
import example.todo.common.utils.composeState |
||||
import example.todo.common.utils.getStore |
||||
|
||||
internal class TodoAddImpl( |
||||
componentContext: ComponentContext, |
||||
dependencies: Dependencies |
||||
) : TodoAdd, ComponentContext by componentContext, Dependencies by dependencies { |
||||
|
||||
private val store = |
||||
instanceKeeper.getStore { |
||||
TodoAddStoreProvider( |
||||
storeFactory = storeFactory, |
||||
database = TodoAddStoreDatabase(queries = database.todoDatabaseQueries) |
||||
).provide() |
||||
} |
||||
|
||||
@Composable |
||||
override fun invoke() { |
||||
val state by store.composeState |
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(8.dp)) { |
||||
OutlinedTextField( |
||||
value = state.text, |
||||
modifier = Modifier.weight(weight = 1F), |
||||
onValueChange = ::onTextChanged, |
||||
label = { Text(text = "Add a todo") } |
||||
) |
||||
|
||||
Button(modifier = Modifier.padding(start = 8.dp), onClick = ::onAddClicked) { |
||||
Text(text = "+") |
||||
} |
||||
} |
||||
} |
||||
|
||||
private fun onTextChanged(text: String) { |
||||
store.accept(Intent.SetText(text = text)) |
||||
} |
||||
|
||||
private fun onAddClicked() { |
||||
store.accept(Intent.Add) |
||||
} |
||||
} |
@ -0,0 +1,23 @@
|
||||
package example.todo.common.add.integration |
||||
|
||||
import com.badoo.reaktive.completable.Completable |
||||
import com.badoo.reaktive.completable.completableFromFunction |
||||
import com.badoo.reaktive.completable.subscribeOn |
||||
import com.badoo.reaktive.scheduler.ioScheduler |
||||
import example.todo.common.add.store.TodoAddStoreProvider.Database |
||||
import example.todo.common.database.TodoDatabaseQueries |
||||
|
||||
internal class TodoAddStoreDatabase( |
||||
private val queries: TodoDatabaseQueries |
||||
) : Database { |
||||
|
||||
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) |
||||
} |
@ -0,0 +1,17 @@
|
||||
package example.todo.common.add.store |
||||
|
||||
import com.arkivanov.mvikotlin.core.store.Store |
||||
import example.todo.common.add.store.TodoAddStore.Intent |
||||
import example.todo.common.add.store.TodoAddStore.State |
||||
|
||||
internal interface TodoAddStore : Store<Intent, State, Nothing> { |
||||
|
||||
sealed class Intent { |
||||
data class SetText(val text: String) : Intent() |
||||
object Add : Intent() |
||||
} |
||||
|
||||
data class State( |
||||
val text: String = "" |
||||
) |
||||
} |
@ -0,0 +1,51 @@
|
||||
package example.todo.common.add.store |
||||
|
||||
import com.arkivanov.mvikotlin.core.store.Reducer |
||||
import com.arkivanov.mvikotlin.core.store.Store |
||||
import com.arkivanov.mvikotlin.core.store.StoreFactory |
||||
import com.arkivanov.mvikotlin.extensions.reaktive.ReaktiveExecutor |
||||
import com.badoo.reaktive.completable.Completable |
||||
import example.todo.common.add.store.TodoAddStore.Intent |
||||
import example.todo.common.add.store.TodoAddStore.State |
||||
|
||||
internal class TodoAddStoreProvider( |
||||
private val storeFactory: StoreFactory, |
||||
private val database: Database |
||||
) { |
||||
|
||||
fun provide(): TodoAddStore = |
||||
object : TodoAddStore, Store<Intent, State, Nothing> by storeFactory.create( |
||||
name = "TodoAddStore", |
||||
initialState = State(), |
||||
executorFactory = ::ExecutorImpl, |
||||
reducer = ReducerImpl |
||||
) {} |
||||
|
||||
private sealed class Result { |
||||
data class TextChanged(val text: String) : Result() |
||||
} |
||||
|
||||
private inner class ExecutorImpl : ReaktiveExecutor<Intent, Nothing, State, Result, Nothing>() { |
||||
override fun executeIntent(intent: Intent, getState: () -> State): Unit = |
||||
when (intent) { |
||||
is Intent.SetText -> dispatch(Result.TextChanged(text = intent.text)) |
||||
is Intent.Add -> add(state = getState()) |
||||
} |
||||
|
||||
private fun add(state: State) { |
||||
dispatch(Result.TextChanged(text = "")) |
||||
database.add(text = state.text).subscribeScoped() |
||||
} |
||||
} |
||||
|
||||
private object ReducerImpl : Reducer<State, Result> { |
||||
override fun State.reduce(result: Result): State = |
||||
when (result) { |
||||
is Result.TextChanged -> copy(text = result.text) |
||||
} |
||||
} |
||||
|
||||
interface Database { |
||||
fun add(text: String): Completable |
||||
} |
||||
} |
@ -0,0 +1,32 @@
|
||||
plugins { |
||||
id("multiplatform-setup") |
||||
id("com.squareup.sqldelight") |
||||
} |
||||
|
||||
sqldelight { |
||||
database("TodoDatabase") { |
||||
packageName = "example.todo.database" |
||||
} |
||||
} |
||||
|
||||
kotlin { |
||||
sourceSets { |
||||
commonMain { |
||||
dependencies { |
||||
implementation(Deps.Badoo.Reaktive.reaktive) |
||||
} |
||||
} |
||||
|
||||
androidMain { |
||||
dependencies { |
||||
implementation(Deps.Squareup.SQLDelight.androidDriver) |
||||
} |
||||
} |
||||
|
||||
desktopMain { |
||||
dependencies { |
||||
implementation(Deps.Squareup.SQLDelight.sqliteDriver) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<manifest package="example.todo.common.database"/> |
@ -0,0 +1,14 @@
|
||||
package example.todo.common.database |
||||
|
||||
import android.content.Context |
||||
import com.squareup.sqldelight.android.AndroidSqliteDriver |
||||
import com.squareup.sqldelight.db.SqlDriver |
||||
import example.todo.database.TodoDatabase |
||||
|
||||
@Suppress("FunctionName") // FactoryFunction |
||||
fun TodoDatabaseDriver(context: Context): SqlDriver = |
||||
AndroidSqliteDriver( |
||||
schema = TodoDatabase.Schema, |
||||
context = context, |
||||
name = "TodoDatabase.db" |
||||
) |
@ -0,0 +1,28 @@
|
||||
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) } |
||||
} |
@ -0,0 +1,32 @@
|
||||
CREATE TABLE TodoItemEntity ( |
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
||||
orderNum INTEGER NOT NULL, |
||||
text TEXT NOT NULL, |
||||
isDone INTEGER AS Boolean NOT NULL DEFAULT 0 |
||||
); |
||||
|
||||
selectAll: |
||||
SELECT * |
||||
FROM TodoItemEntity; |
||||
|
||||
select: |
||||
SELECT * |
||||
FROM TodoItemEntity |
||||
WHERE id = :id; |
||||
|
||||
add: |
||||
INSERT INTO TodoItemEntity (orderNum, text) |
||||
VALUES ((CASE (SELECT COUNT(*) FROM TodoItemEntity) WHEN 0 THEN 1 ELSE (SELECT MAX(orderNum)+1 FROM TodoItemEntity) END), :text); |
||||
|
||||
setText: |
||||
UPDATE TodoItemEntity |
||||
SET text = :text |
||||
WHERE id = :id; |
||||
|
||||
setDone: |
||||
UPDATE TodoItemEntity |
||||
SET isDone = :isDone |
||||
WHERE id = :id; |
||||
|
||||
getLastInsertId: |
||||
SELECT last_insert_rowid(); |
@ -0,0 +1,13 @@
|
||||
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 |
||||
fun TodoDatabaseDriver(): SqlDriver { |
||||
val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY) |
||||
TodoDatabase.Schema.create(driver) |
||||
|
||||
return driver |
||||
} |
@ -0,0 +1,19 @@
|
||||
plugins { |
||||
id("multiplatform-setup") |
||||
id("multiplatform-compose-setup") |
||||
} |
||||
|
||||
kotlin { |
||||
sourceSets { |
||||
named("commonMain") { |
||||
dependencies { |
||||
implementation(project(":common:utils")) |
||||
implementation(project(":common:database")) |
||||
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlin) |
||||
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlinExtensionsReaktive) |
||||
implementation(Deps.ArkIvanov.Decompose.decompose) |
||||
implementation(Deps.Badoo.Reaktive.reaktive) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<manifest package="example.todo.common.edit"/> |
@ -0,0 +1,27 @@
|
||||
package example.todo.common.edit |
||||
|
||||
import com.arkivanov.decompose.ComponentContext |
||||
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 Dependencies { |
||||
val storeFactory: StoreFactory |
||||
val database: TodoDatabase |
||||
val itemId: Long |
||||
val editOutput: Consumer<Output> |
||||
} |
||||
|
||||
sealed class Output { |
||||
object Finished : Output() |
||||
} |
||||
} |
||||
|
||||
@Suppress("FunctionName") // Factory function |
||||
fun TodoEdit(componentContext: ComponentContext, dependencies: Dependencies): TodoEdit = |
||||
TodoEditImpl(componentContext, dependencies) |
@ -0,0 +1,6 @@
|
||||
package example.todo.common.edit |
||||
|
||||
data class TodoItem( |
||||
val text: String, |
||||
val isDone: Boolean |
||||
) |
@ -0,0 +1,86 @@
|
||||
package example.todo.common.edit.integration |
||||
|
||||
import androidx.compose.foundation.Text |
||||
import androidx.compose.foundation.layout.Column |
||||
import androidx.compose.foundation.layout.Row |
||||
import androidx.compose.foundation.layout.Spacer |
||||
import androidx.compose.foundation.layout.fillMaxWidth |
||||
import androidx.compose.foundation.layout.padding |
||||
import androidx.compose.foundation.layout.width |
||||
import androidx.compose.material.Button |
||||
import androidx.compose.material.Checkbox |
||||
import androidx.compose.material.TextField |
||||
import androidx.compose.material.TopAppBar |
||||
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.arkivanov.decompose.ComponentContext |
||||
import example.todo.common.edit.TodoEdit |
||||
import example.todo.common.edit.TodoEdit.Dependencies |
||||
import example.todo.common.edit.store.TodoEditStore.Intent |
||||
import example.todo.common.edit.store.TodoEditStoreProvider |
||||
import example.todo.common.utils.composeState |
||||
import example.todo.common.utils.getStore |
||||
|
||||
internal class TodoEditImpl( |
||||
componentContext: ComponentContext, |
||||
dependencies: Dependencies |
||||
) : TodoEdit, ComponentContext by componentContext, Dependencies by dependencies { |
||||
|
||||
private val store = |
||||
instanceKeeper.getStore { |
||||
TodoEditStoreProvider( |
||||
storeFactory = storeFactory, |
||||
database = TodoEditStoreDatabase(queries = database.todoDatabaseQueries), |
||||
id = itemId |
||||
).provide() |
||||
} |
||||
|
||||
@Composable |
||||
override fun invoke() { |
||||
val state by store.composeState |
||||
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) { |
||||
TopAppBar( |
||||
title = { Text("Edit todo") }, |
||||
navigationIcon = { |
||||
Button(onClick = ::onFinished) { |
||||
Text(text = "<") |
||||
} |
||||
} |
||||
) |
||||
|
||||
TextField( |
||||
value = state.text, |
||||
modifier = Modifier.weight(1F).fillMaxWidth().padding(8.dp), |
||||
label = { Text("Todo text") }, |
||||
onValueChange = ::onTextChanged |
||||
) |
||||
|
||||
Row(modifier = Modifier.padding(8.dp)) { |
||||
Text(text = "Completed") |
||||
|
||||
Spacer(modifier = Modifier.width(8.dp)) |
||||
|
||||
Checkbox( |
||||
checked = state.isDone, |
||||
onCheckedChange = ::onDoneChanged |
||||
) |
||||
} |
||||
} |
||||
} |
||||
|
||||
private fun onTextChanged(text: String) { |
||||
store.accept(Intent.SetText(text = text)) |
||||
} |
||||
|
||||
private fun onDoneChanged(isDone: Boolean) { |
||||
store.accept(Intent.SetDone(isDone = isDone)) |
||||
} |
||||
|
||||
private fun onFinished() { |
||||
editOutput.onNext(TodoEdit.Output.Finished) |
||||
} |
||||
} |
@ -0,0 +1,42 @@
|
||||
package example.todo.common.edit.integration |
||||
|
||||
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.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.edit.TodoItem |
||||
import example.todo.common.edit.store.TodoEditStoreProvider.Database |
||||
|
||||
internal class TodoEditStoreDatabase( |
||||
private val queries: TodoDatabaseQueries |
||||
) : Database { |
||||
|
||||
override fun load(id: Long): Maybe<TodoItem> = |
||||
maybeFromFunction { queries.select(id = id) } |
||||
.subscribeOn(ioScheduler) |
||||
.map(Query<TodoItemEntity>::executeAsOne) |
||||
.notNull() |
||||
.map { it.toItem() } |
||||
|
||||
private fun TodoItemEntity.toItem(): TodoItem = |
||||
TodoItem( |
||||
text = text, |
||||
isDone = isDone |
||||
) |
||||
|
||||
override fun setText(id: Long, text: String): Completable = |
||||
completableFromFunction { queries.setText(id = id, text = text) } |
||||
.subscribeOn(ioScheduler) |
||||
|
||||
override fun setDone(id: Long, isDone: Boolean): Completable = |
||||
completableFromFunction { queries.setDone(id = id, isDone = isDone) } |
||||
.subscribeOn(ioScheduler) |
||||
} |
@ -0,0 +1,24 @@
|
||||
package example.todo.common.edit.store |
||||
|
||||
import com.arkivanov.mvikotlin.core.store.Store |
||||
import example.todo.common.edit.TodoItem |
||||
import example.todo.common.edit.store.TodoEditStore.Intent |
||||
import example.todo.common.edit.store.TodoEditStore.Label |
||||
import example.todo.common.edit.store.TodoEditStore.State |
||||
|
||||
internal interface TodoEditStore : Store<Intent, State, Label> { |
||||
|
||||
sealed class Intent { |
||||
data class SetText(val text: String) : Intent() |
||||
data class SetDone(val isDone: Boolean) : Intent() |
||||
} |
||||
|
||||
data class State( |
||||
val text: String = "", |
||||
val isDone: Boolean = false |
||||
) |
||||
|
||||
sealed class Label { |
||||
data class Changed(val item: TodoItem) : Label() |
||||
} |
||||
} |
@ -0,0 +1,83 @@
|
||||
package example.todo.common.edit.store |
||||
|
||||
import com.arkivanov.mvikotlin.core.store.Reducer |
||||
import com.arkivanov.mvikotlin.core.store.SimpleBootstrapper |
||||
import com.arkivanov.mvikotlin.core.store.Store |
||||
import com.arkivanov.mvikotlin.core.store.StoreFactory |
||||
import com.arkivanov.mvikotlin.extensions.reaktive.ReaktiveExecutor |
||||
import com.badoo.reaktive.completable.Completable |
||||
import com.badoo.reaktive.maybe.Maybe |
||||
import com.badoo.reaktive.maybe.map |
||||
import com.badoo.reaktive.maybe.observeOn |
||||
import com.badoo.reaktive.scheduler.mainScheduler |
||||
import example.todo.common.edit.TodoItem |
||||
import example.todo.common.edit.store.TodoEditStore.Intent |
||||
import example.todo.common.edit.store.TodoEditStore.Label |
||||
import example.todo.common.edit.store.TodoEditStore.State |
||||
|
||||
internal class TodoEditStoreProvider( |
||||
private val storeFactory: StoreFactory, |
||||
private val database: Database, |
||||
private val id: Long |
||||
) { |
||||
|
||||
fun provide(): TodoEditStore = |
||||
object : TodoEditStore, Store<Intent, State, Label> by storeFactory.create( |
||||
name = "EditStore", |
||||
initialState = State(), |
||||
bootstrapper = SimpleBootstrapper(Unit), |
||||
executorFactory = ::ExecutorImpl, |
||||
reducer = ReducerImpl |
||||
) {} |
||||
|
||||
private sealed class Result { |
||||
data class Loaded(val item: TodoItem) : Result() |
||||
data class TextChanged(val text: String) : Result() |
||||
data class DoneChanged(val isDone: Boolean) : Result() |
||||
} |
||||
|
||||
private inner class ExecutorImpl : ReaktiveExecutor<Intent, Unit, State, Result, Label>() { |
||||
override fun executeAction(action: Unit, getState: () -> State) { |
||||
database |
||||
.load(id = id) |
||||
.map(Result::Loaded) |
||||
.observeOn(mainScheduler) |
||||
.subscribeScoped(onSuccess = ::dispatch) |
||||
} |
||||
|
||||
override fun executeIntent(intent: Intent, getState: () -> State) = |
||||
when (intent) { |
||||
is Intent.SetText -> setText(text = intent.text, state = getState()) |
||||
is Intent.SetDone -> setDone(isDone = intent.isDone, state = getState()) |
||||
} |
||||
|
||||
private fun setText(text: String, state: State) { |
||||
dispatch(Result.TextChanged(text = text)) |
||||
publish(Label.Changed(TodoItem(text = text, isDone = state.isDone))) |
||||
database.setText(id = id, text = text).subscribeScoped() |
||||
} |
||||
|
||||
private fun setDone(isDone: Boolean, state: State) { |
||||
dispatch(Result.DoneChanged(isDone = isDone)) |
||||
publish(Label.Changed(TodoItem(text = state.text, isDone = isDone))) |
||||
database.setDone(id = id, isDone = isDone).subscribeScoped() |
||||
} |
||||
} |
||||
|
||||
private object ReducerImpl : Reducer<State, Result> { |
||||
override fun State.reduce(result: Result): State = |
||||
when (result) { |
||||
is Result.Loaded -> copy(text = result.item.text, isDone = result.item.isDone) |
||||
is Result.TextChanged -> copy(text = result.text) |
||||
is Result.DoneChanged -> copy(isDone = result.isDone) |
||||
} |
||||
} |
||||
|
||||
interface Database { |
||||
fun load(id: Long): Maybe<TodoItem> |
||||
|
||||
fun setText(id: Long, text: String): Completable |
||||
|
||||
fun setDone(id: Long, isDone: Boolean): Completable |
||||
} |
||||
} |
@ -0,0 +1,19 @@
|
||||
plugins { |
||||
id("multiplatform-setup") |
||||
id("multiplatform-compose-setup") |
||||
} |
||||
|
||||
kotlin { |
||||
sourceSets { |
||||
named("commonMain") { |
||||
dependencies { |
||||
implementation(project(":common:utils")) |
||||
implementation(project(":common:database")) |
||||
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlin) |
||||
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlinExtensionsReaktive) |
||||
implementation(Deps.ArkIvanov.Decompose.decompose) |
||||
implementation(Deps.Badoo.Reaktive.reaktive) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<manifest package="example.todo.common.list"/> |
@ -0,0 +1,26 @@
|
||||
package example.todo.common.list |
||||
|
||||
import com.arkivanov.decompose.ComponentContext |
||||
import com.arkivanov.mvikotlin.core.store.StoreFactory |
||||
import com.badoo.reaktive.base.Consumer |
||||
import example.todo.common.list.TodoList.Dependencies |
||||
import example.todo.common.list.integration.TodoListImpl |
||||
import example.todo.common.utils.Component |
||||
import example.todo.database.TodoDatabase |
||||
|
||||
interface TodoList : Component { |
||||
|
||||
interface Dependencies { |
||||
val storeFactory: StoreFactory |
||||
val database: TodoDatabase |
||||
val listOutput: Consumer<Output> |
||||
} |
||||
|
||||
sealed class Output { |
||||
data class ItemSelected(val id: Long) : Output() |
||||
} |
||||
} |
||||
|
||||
@Suppress("FunctionName") // Factory function |
||||
fun TodoList(componentContext: ComponentContext, dependencies: Dependencies): TodoList = |
||||
TodoListImpl(componentContext, dependencies) |
@ -0,0 +1,72 @@
|
||||
package example.todo.common.list.integration |
||||
|
||||
import androidx.compose.foundation.Text |
||||
import androidx.compose.foundation.clickable |
||||
import androidx.compose.foundation.layout.Row |
||||
import androidx.compose.foundation.layout.Spacer |
||||
import androidx.compose.foundation.layout.padding |
||||
import androidx.compose.foundation.layout.width |
||||
import androidx.compose.foundation.lazy.LazyColumnFor |
||||
import androidx.compose.material.Checkbox |
||||
import androidx.compose.material.Divider |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.getValue |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.text.AnnotatedString |
||||
import androidx.compose.ui.text.style.TextOverflow |
||||
import androidx.compose.ui.unit.dp |
||||
import com.arkivanov.decompose.ComponentContext |
||||
import example.todo.common.list.TodoList |
||||
import example.todo.common.list.TodoList.Dependencies |
||||
import example.todo.common.list.TodoList.Output |
||||
import example.todo.common.list.store.TodoListStore.Intent |
||||
import example.todo.common.list.store.TodoListStoreProvider |
||||
import example.todo.common.utils.composeState |
||||
import example.todo.common.utils.getStore |
||||
|
||||
internal class TodoListImpl( |
||||
componentContext: ComponentContext, |
||||
dependencies: Dependencies |
||||
) : TodoList, ComponentContext by componentContext, Dependencies by dependencies { |
||||
|
||||
private val store = |
||||
instanceKeeper.getStore { |
||||
TodoListStoreProvider( |
||||
storeFactory = storeFactory, |
||||
database = TodoListStoreDatabase(queries = database.todoDatabaseQueries) |
||||
).provide() |
||||
} |
||||
|
||||
@Composable |
||||
override fun invoke() { |
||||
val state by store.composeState |
||||
|
||||
LazyColumnFor(items = state.items) { item -> |
||||
Row(modifier = Modifier.clickable(onClick = { onItemClicked(id = item.id) }).padding(8.dp)) { |
||||
Text( |
||||
text = AnnotatedString(item.text), |
||||
modifier = Modifier.weight(1F), |
||||
maxLines = 1, |
||||
overflow = TextOverflow.Ellipsis |
||||
) |
||||
|
||||
Spacer(modifier = Modifier.width(8.dp)) |
||||
|
||||
Checkbox( |
||||
checked = item.isDone, |
||||
onCheckedChange = { onDoneChanged(id = item.id, isDone = it) } |
||||
) |
||||
} |
||||
|
||||
Divider() |
||||
} |
||||
} |
||||
|
||||
private fun onItemClicked(id: Long) { |
||||
listOutput.onNext(Output.ItemSelected(id = id)) |
||||
} |
||||
|
||||
private fun onDoneChanged(id: Long, isDone: Boolean) { |
||||
store.accept(Intent.SetDone(id = id, isDone = isDone)) |
||||
} |
||||
} |
@ -0,0 +1,37 @@
|
||||
package example.todo.common.list.integration |
||||
|
||||
import com.badoo.reaktive.completable.Completable |
||||
import com.badoo.reaktive.completable.completable |
||||
import com.badoo.reaktive.completable.subscribeOn |
||||
import com.badoo.reaktive.observable.Observable |
||||
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.asObservable |
||||
import example.todo.common.list.store.TodoItem |
||||
import example.todo.common.list.store.TodoListStoreProvider.Database |
||||
|
||||
internal class TodoListStoreDatabase( |
||||
private val queries: TodoDatabaseQueries |
||||
) : Database { |
||||
|
||||
override val updates: Observable<List<TodoItem>> = |
||||
queries |
||||
.selectAll() |
||||
.asObservable(Query<TodoItemEntity>::executeAsList) |
||||
.mapIterable { it.toItem() } |
||||
|
||||
private fun TodoItemEntity.toItem(): TodoItem = |
||||
TodoItem( |
||||
id = id, |
||||
order = orderNum, |
||||
text = text, |
||||
isDone = isDone |
||||
) |
||||
|
||||
override fun setDone(id: Long, isDone: Boolean): Completable = |
||||
completable { queries.setDone(id = id, isDone = isDone) } |
||||
.subscribeOn(ioScheduler) |
||||
} |
@ -0,0 +1,8 @@
|
||||
package example.todo.common.list.store |
||||
|
||||
internal data class TodoItem( |
||||
val id: Long = 0L, |
||||
val order: Long = 0L, |
||||
val text: String = "", |
||||
val isDone: Boolean = false |
||||
) |
@ -0,0 +1,16 @@
|
||||
package example.todo.common.list.store |
||||
|
||||
import com.arkivanov.mvikotlin.core.store.Store |
||||
import example.todo.common.list.store.TodoListStore.Intent |
||||
import example.todo.common.list.store.TodoListStore.State |
||||
|
||||
internal interface TodoListStore : Store<Intent, State, Nothing> { |
||||
|
||||
sealed class Intent { |
||||
data class SetDone(val id: Long, val isDone: Boolean) : Intent() |
||||
} |
||||
|
||||
data class State( |
||||
val items: List<TodoItem> = emptyList() |
||||
) |
||||
} |
@ -0,0 +1,83 @@
|
||||
package example.todo.common.list.store |
||||
|
||||
import com.arkivanov.mvikotlin.core.store.Reducer |
||||
import com.arkivanov.mvikotlin.core.store.SimpleBootstrapper |
||||
import com.arkivanov.mvikotlin.core.store.Store |
||||
import com.arkivanov.mvikotlin.core.store.StoreFactory |
||||
import com.arkivanov.mvikotlin.extensions.reaktive.ReaktiveExecutor |
||||
import com.badoo.reaktive.completable.Completable |
||||
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.list.store.TodoListStore.Intent |
||||
import example.todo.common.list.store.TodoListStore.State |
||||
|
||||
internal class TodoListStoreProvider( |
||||
private val storeFactory: StoreFactory, |
||||
private val database: Database |
||||
) { |
||||
|
||||
fun provide(): TodoListStore = |
||||
object : TodoListStore, Store<Intent, State, Nothing> by storeFactory.create( |
||||
name = "TodoListStore", |
||||
initialState = State(), |
||||
bootstrapper = SimpleBootstrapper(Unit), |
||||
executorFactory = ::ExecutorImpl, |
||||
reducer = ReducerImpl |
||||
) {} |
||||
|
||||
private sealed class Result { |
||||
data class ItemsLoaded(val items: List<TodoItem>) : Result() |
||||
data class DoneChanged(val id: Long, val isDone: Boolean) : Result() |
||||
} |
||||
|
||||
private inner class ExecutorImpl : ReaktiveExecutor<Intent, Unit, State, Result, Nothing>() { |
||||
override fun executeAction(action: Unit, getState: () -> State) { |
||||
database |
||||
.updates |
||||
.observeOn(mainScheduler) |
||||
.map(Result::ItemsLoaded) |
||||
.subscribeScoped(onNext = ::dispatch) |
||||
} |
||||
|
||||
override fun executeIntent(intent: Intent, getState: () -> State): Unit = |
||||
when (intent) { |
||||
is Intent.SetDone -> setDone(id = intent.id, isDone = intent.isDone) |
||||
} |
||||
|
||||
private fun setDone(id: Long, isDone: Boolean) { |
||||
dispatch(Result.DoneChanged(id = id, isDone = isDone)) |
||||
database.setDone(id = id, isDone = isDone).subscribeScoped() |
||||
} |
||||
} |
||||
|
||||
private object ReducerImpl : Reducer<State, Result> { |
||||
override fun State.reduce(result: Result): State = |
||||
when (result) { |
||||
is Result.ItemsLoaded -> copy(items = result.items.sorted()) |
||||
is Result.DoneChanged -> update(id = result.id) { copy(isDone = result.isDone) } |
||||
} |
||||
|
||||
private inline fun State.update(id: Long, func: TodoItem.() -> TodoItem): State { |
||||
val item = items.find { it.id == id } ?: return this |
||||
|
||||
return put(item.func()) |
||||
} |
||||
|
||||
private fun State.put(item: TodoItem): State { |
||||
val oldItems = items.associateByTo(mutableMapOf(), TodoItem::id) |
||||
val oldItem: TodoItem? = oldItems.put(item.id, item) |
||||
|
||||
return copy(items = if (oldItem?.order == item.order) oldItems.values.toList() else oldItems.values.sorted()) |
||||
} |
||||
|
||||
private fun Iterable<TodoItem>.sorted(): List<TodoItem> = sortedByDescending(TodoItem::order) |
||||
} |
||||
|
||||
interface Database { |
||||
val updates: Observable<List<TodoItem>> |
||||
|
||||
fun setDone(id: Long, isDone: Boolean): Completable |
||||
} |
||||
} |
@ -0,0 +1,19 @@
|
||||
plugins { |
||||
id("multiplatform-setup") |
||||
id("multiplatform-compose-setup") |
||||
} |
||||
|
||||
kotlin { |
||||
sourceSets { |
||||
named("commonMain") { |
||||
dependencies { |
||||
implementation(project(":common:utils")) |
||||
implementation(project(":common:database")) |
||||
implementation(project(":common:list")) |
||||
implementation(project(":common:add")) |
||||
implementation(Deps.ArkIvanov.Decompose.decompose) |
||||
implementation(Deps.Badoo.Reaktive.reaktive) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<manifest package="example.todo.common.main"/> |
@ -0,0 +1,26 @@
|
||||
package example.todo.common.main |
||||
|
||||
import com.arkivanov.decompose.ComponentContext |
||||
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 Dependencies { |
||||
val storeFactory: StoreFactory |
||||
val database: TodoDatabase |
||||
val mainOutput: Consumer<Output> |
||||
} |
||||
|
||||
sealed class Output { |
||||
data class Selected(val id: Long) : Output() |
||||
} |
||||
} |
||||
|
||||
@Suppress("FunctionName") // Factory function |
||||
fun TodoMain(componentContext: ComponentContext, dependencies: Dependencies): TodoMain = |
||||
TodoMainImpl(componentContext, dependencies) |
@ -0,0 +1,11 @@
|
||||
package example.todo.common.main.integration |
||||
|
||||
import example.todo.common.list.TodoList |
||||
import example.todo.common.main.TodoMain.Output |
||||
|
||||
internal val listOutputToOutput: TodoList.Output.() -> Output? = |
||||
{ |
||||
when (this) { |
||||
is TodoList.Output.ItemSelected -> Output.Selected(id = id) |
||||
} |
||||
} |
@ -0,0 +1,59 @@
|
||||
package example.todo.common.main.integration |
||||
|
||||
import androidx.compose.foundation.Box |
||||
import androidx.compose.foundation.Text |
||||
import androidx.compose.foundation.layout.Column |
||||
import androidx.compose.material.TopAppBar |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Modifier |
||||
import com.arkivanov.decompose.ComponentContext |
||||
import com.arkivanov.decompose.child |
||||
import com.arkivanov.mvikotlin.core.binder.BinderLifecycleMode |
||||
import com.badoo.reaktive.base.Consumer |
||||
import com.badoo.reaktive.observable.mapNotNull |
||||
import com.badoo.reaktive.subject.publish.PublishSubject |
||||
import example.todo.common.add.TodoAdd |
||||
import example.todo.common.list.TodoList |
||||
import example.todo.common.main.TodoMain |
||||
import example.todo.common.main.TodoMain.Dependencies |
||||
import example.todo.common.utils.bind |
||||
|
||||
internal class TodoMainImpl( |
||||
componentContext: ComponentContext, |
||||
dependencies: Dependencies |
||||
) : TodoMain, ComponentContext by componentContext, Dependencies by dependencies { |
||||
|
||||
private val listOutput = PublishSubject<TodoList.Output>() |
||||
|
||||
private val todoList = |
||||
TodoList( |
||||
componentContext = child(key = "TodoList"), |
||||
dependencies = object : TodoList.Dependencies, Dependencies by dependencies { |
||||
override val listOutput: Consumer<TodoList.Output> = this@TodoMainImpl.listOutput |
||||
} |
||||
) |
||||
|
||||
private val todoAdd = |
||||
TodoAdd( |
||||
componentContext = child(key = "TodoAdd"), |
||||
dependencies = object : TodoAdd.Dependencies, Dependencies by dependencies {} |
||||
) |
||||
|
||||
init { |
||||
bind(BinderLifecycleMode.START_STOP) { |
||||
listOutput.mapNotNull(listOutputToOutput) bindTo mainOutput |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
override fun invoke() { |
||||
Column { |
||||
TopAppBar(title = { Text(text = "Todo List") }) |
||||
|
||||
Box(Modifier.weight(1F)) { |
||||
todoList() |
||||
} |
||||
todoAdd() |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,24 @@
|
||||
plugins { |
||||
id("multiplatform-setup") |
||||
id("multiplatform-compose-setup") |
||||
id("kotlin-android-extensions") |
||||
} |
||||
|
||||
androidExtensions { |
||||
features = setOf("parcelize") |
||||
} |
||||
|
||||
kotlin { |
||||
sourceSets { |
||||
named("commonMain") { |
||||
dependencies { |
||||
implementation(project(":common:utils")) |
||||
implementation(project(":common:database")) |
||||
implementation(project(":common:main")) |
||||
implementation(project(":common:edit")) |
||||
implementation(Deps.ArkIvanov.Decompose.decompose) |
||||
implementation(Deps.Badoo.Reaktive.reaktive) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<manifest package="example.todo.common.root"/> |
@ -0,0 +1,20 @@
|
||||
package example.todo.common.root |
||||
|
||||
import com.arkivanov.decompose.ComponentContext |
||||
import com.arkivanov.mvikotlin.core.store.StoreFactory |
||||
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 Dependencies { |
||||
val storeFactory: StoreFactory |
||||
val database: TodoDatabase |
||||
} |
||||
} |
||||
|
||||
@Suppress("FunctionName") // Factory function |
||||
fun TodoRoot(componentContext: ComponentContext, dependencies: Dependencies): TodoRoot = |
||||
TodoRootImpl(componentContext, dependencies) |
@ -0,0 +1,81 @@
|
||||
package example.todo.common.root.integration |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.getValue |
||||
import com.arkivanov.decompose.ComponentContext |
||||
import com.arkivanov.decompose.router |
||||
import com.arkivanov.decompose.statekeeper.Parcelable |
||||
import com.arkivanov.decompose.statekeeper.Parcelize |
||||
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.Dependencies |
||||
import example.todo.common.utils.Component |
||||
import example.todo.common.utils.Consumer |
||||
import example.todo.common.utils.Crossfade |
||||
import example.todo.common.utils.asState |
||||
|
||||
internal class TodoRootImpl( |
||||
componentContext: ComponentContext, |
||||
dependencies: Dependencies |
||||
) : TodoRoot, ComponentContext by componentContext, Dependencies by dependencies { |
||||
|
||||
private val router = |
||||
router<Configuration, Component>( |
||||
initialConfiguration = Configuration.Main, |
||||
handleBackButton = true, |
||||
componentFactory = ::createChild |
||||
) |
||||
|
||||
private fun createChild(configuration: Configuration, componentContext: ComponentContext): Component = |
||||
when (configuration) { |
||||
is Configuration.Main -> todoMain(componentContext) |
||||
is Configuration.Edit -> todoEdit(componentContext, itemId = configuration.itemId) |
||||
} |
||||
|
||||
private fun todoMain(componentContext: ComponentContext): TodoMain = |
||||
TodoMain( |
||||
componentContext = componentContext, |
||||
dependencies = object : TodoMain.Dependencies, Dependencies by this { |
||||
override val mainOutput: Consumer<TodoMain.Output> = Consumer(::onMainOutput) |
||||
} |
||||
) |
||||
|
||||
private fun todoEdit(componentContext: ComponentContext, itemId: Long): TodoEdit = |
||||
TodoEdit( |
||||
componentContext = componentContext, |
||||
dependencies = object : TodoEdit.Dependencies, Dependencies by this { |
||||
override val itemId: Long = itemId |
||||
override val editOutput: Consumer<TodoEdit.Output> = Consumer(::onEditOutput) |
||||
} |
||||
) |
||||
|
||||
private fun onMainOutput(output: TodoMain.Output): Unit = |
||||
when (output) { |
||||
is TodoMain.Output.Selected -> router.push(Configuration.Edit(itemId = output.id)) |
||||
} |
||||
|
||||
private fun onEditOutput(output: TodoEdit.Output): Unit = |
||||
when (output) { |
||||
is TodoEdit.Output.Finished -> router.pop() |
||||
} |
||||
|
||||
@Composable |
||||
override fun invoke() { |
||||
val routerState by router.state.asState() |
||||
val activeChild = routerState.activeChild |
||||
|
||||
Crossfade(currentChild = activeChild.component, currentKey = activeChild.configuration) { child -> |
||||
child.invoke() |
||||
} |
||||
} |
||||
|
||||
private sealed class Configuration : Parcelable { |
||||
@Parcelize |
||||
object Main : Configuration() |
||||
|
||||
@Parcelize |
||||
data class Edit(val itemId: Long) : Configuration() |
||||
} |
||||
} |
@ -0,0 +1,16 @@
|
||||
plugins { |
||||
id("multiplatform-setup") |
||||
id("multiplatform-compose-setup") |
||||
} |
||||
|
||||
kotlin { |
||||
sourceSets { |
||||
named("commonMain") { |
||||
dependencies { |
||||
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlin) |
||||
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlinExtensionsReaktive) |
||||
implementation(Deps.ArkIvanov.Decompose.decompose) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<manifest package="example.todo.common.list"/> |
@ -0,0 +1,24 @@
|
||||
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) |
@ -0,0 +1,9 @@
|
||||
package example.todo.common.utils |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
|
||||
interface Component { |
||||
|
||||
@Composable |
||||
operator fun invoke() |
||||
} |
@ -0,0 +1,16 @@
|
||||
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() |
||||
} |
@ -0,0 +1,20 @@
|
||||
package example.todo.common.utils |
||||
|
||||
import com.arkivanov.decompose.instancekeeper.InstanceKeeper |
||||
import com.arkivanov.decompose.instancekeeper.getOrCreate |
||||
import com.arkivanov.mvikotlin.core.store.Store |
||||
|
||||
fun <T : Store<*, *, *>> InstanceKeeper.getStore(key: Any, factory: () -> T): T = |
||||
getOrCreate(key) { StoreHolder(factory()) } |
||||
.store |
||||
|
||||
inline fun <reified T : Store<*, *, *>> InstanceKeeper.getStore(noinline factory: () -> T): T = |
||||
getStore(T::class, factory) |
||||
|
||||
private class StoreHolder<T : Store<*, *, *>>( |
||||
val store: T |
||||
) : InstanceKeeper.Instance { |
||||
override fun onDestroy() { |
||||
store.dispose() |
||||
} |
||||
} |
@ -0,0 +1,11 @@
|
||||
package example.todo.common.utils |
||||
|
||||
import com.badoo.reaktive.base.Consumer |
||||
|
||||
@Suppress("FunctionName") // Factory function |
||||
inline fun <T> Consumer(crossinline block: (T) -> Unit): Consumer<T> = |
||||
object : Consumer<T> { |
||||
override fun onNext(value: T) { |
||||
block(value) |
||||
} |
||||
} |
@ -0,0 +1,20 @@
|
||||
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.mvikotlin.core.store.Store |
||||
import com.arkivanov.mvikotlin.extensions.reaktive.states |
||||
import com.badoo.reaktive.observable.subscribe |
||||
|
||||
@Composable |
||||
val <T : Any> Store<*, T, *>.composeState: State<T> |
||||
get() { |
||||
val composeState = remember(this) { mutableStateOf(state) } |
||||
val disposable = states.subscribe(onNext = { composeState.value = it }) |
||||
onDispose(disposable::dispose) |
||||
|
||||
return composeState |
||||
} |
@ -0,0 +1,34 @@
|
||||
/* |
||||
* Copied from Decompose |
||||
*/ |
||||
|
||||
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 |
||||
|
||||
@Composable |
||||
fun <T : Any> Value<T>.asState(): State<T> { |
||||
val composeState = remember(this) { mutableStateOf(value) } |
||||
|
||||
val observer = |
||||
remember(this) { |
||||
val observer: ValueObserver<T> = { composeState.value = it } |
||||
subscribe(observer) |
||||
observer |
||||
} |
||||
|
||||
onDispose { unsubscribe(observer) } |
||||
|
||||
return composeState |
||||
} |
||||
|
||||
@Composable |
||||
operator fun <T : Any> Value<T>.invoke(render: @Composable() (T) -> Unit) { |
||||
render(asState().value) |
||||
} |
@ -0,0 +1,24 @@
|
||||
import org.jetbrains.compose.compose |
||||
|
||||
plugins { |
||||
kotlin("jvm") |
||||
id("org.jetbrains.compose") |
||||
java |
||||
application |
||||
} |
||||
|
||||
dependencies { |
||||
implementation(compose.desktop.all) |
||||
implementation(project(":common:utils")) |
||||
implementation(project(":common:database")) |
||||
implementation(project(":common:root")) |
||||
implementation(Deps.ArkIvanov.Decompose.decompose) |
||||
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlin) |
||||
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlinMain) |
||||
implementation(Deps.Badoo.Reaktive.reaktive) |
||||
implementation(Deps.Badoo.Reaktive.coroutinesInterop) |
||||
} |
||||
|
||||
application { |
||||
mainClassName = "example.todo.desktop.MainKt" |
||||
} |
@ -0,0 +1,50 @@
|
||||
/* |
||||
* Copyright 2020 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. |
||||
*/ |
||||
package example.todo.desktop |
||||
|
||||
import androidx.compose.desktop.AppWindow |
||||
import androidx.compose.foundation.layout.fillMaxSize |
||||
import androidx.compose.material.Surface |
||||
import androidx.compose.ui.Modifier |
||||
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 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.database.TodoDatabase |
||||
import kotlinx.coroutines.Dispatchers |
||||
|
||||
fun main() { |
||||
overrideSchedulers(main = Dispatchers.Main::asScheduler) |
||||
|
||||
val lifecycle = LifecycleRegistry() |
||||
lifecycle.resume() |
||||
|
||||
AppWindow("Todo").show { |
||||
Surface(modifier = Modifier.fillMaxSize()) { |
||||
TodoRoot( |
||||
componentContext = DefaultComponentContext(lifecycle), |
||||
dependencies = object : TodoRoot.Dependencies { |
||||
override val storeFactory = DefaultStoreFactory |
||||
override val database = TodoDatabase(TodoDatabaseDriver()) |
||||
} |
||||
).invoke() |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,21 @@
|
||||
# Project-wide Gradle settings. |
||||
# IDE (e.g. Android Studio) users: |
||||
# Gradle settings configured through the IDE *will override* |
||||
# any settings specified in this file. |
||||
# For more details on how to configure your build environment visit |
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html |
||||
# Specifies the JVM arguments used for the daemon process. |
||||
# The setting is particularly useful for tweaking memory settings. |
||||
org.gradle.jvmargs=-Xmx2048m |
||||
# When configured, Gradle will run in incubating parallel mode. |
||||
# This option should only be used with decoupled projects. More details, visit |
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects |
||||
# org.gradle.parallel=true |
||||
# AndroidX package structure to make it clearer which packages are bundled with the |
||||
# Android operating system, and which are packaged with your app"s APK |
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn |
||||
android.useAndroidX=true |
||||
# Automatically convert third-party libraries to use AndroidX |
||||
android.enableJetifier=true |
||||
# Kotlin code style for this project: "official" or "obsolete": |
||||
kotlin.code.style=official |
@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME |
||||
distributionPath=wrapper/dists |
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip |
||||
zipStoreBase=GRADLE_USER_HOME |
||||
zipStorePath=wrapper/dists |
@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env sh |
||||
|
||||
# |
||||
# Copyright 2015 the original author or authors. |
||||
# |
||||
# 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 |
||||
# |
||||
# https://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. |
||||
# |
||||
|
||||
############################################################################## |
||||
## |
||||
## Gradle start up script for UN*X |
||||
## |
||||
############################################################################## |
||||
|
||||
# Attempt to set APP_HOME |
||||
# Resolve links: $0 may be a link |
||||
PRG="$0" |
||||
# Need this for relative symlinks. |
||||
while [ -h "$PRG" ] ; do |
||||
ls=`ls -ld "$PRG"` |
||||
link=`expr "$ls" : '.*-> \(.*\)$'` |
||||
if expr "$link" : '/.*' > /dev/null; then |
||||
PRG="$link" |
||||
else |
||||
PRG=`dirname "$PRG"`"/$link" |
||||
fi |
||||
done |
||||
SAVED="`pwd`" |
||||
cd "`dirname \"$PRG\"`/" >/dev/null |
||||
APP_HOME="`pwd -P`" |
||||
cd "$SAVED" >/dev/null |
||||
|
||||
APP_NAME="Gradle" |
||||
APP_BASE_NAME=`basename "$0"` |
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. |
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' |
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value. |
||||
MAX_FD="maximum" |
||||
|
||||
warn () { |
||||
echo "$*" |
||||
} |
||||
|
||||
die () { |
||||
echo |
||||
echo "$*" |
||||
echo |
||||
exit 1 |
||||
} |
||||
|
||||
# OS specific support (must be 'true' or 'false'). |
||||
cygwin=false |
||||
msys=false |
||||
darwin=false |
||||
nonstop=false |
||||
case "`uname`" in |
||||
CYGWIN* ) |
||||
cygwin=true |
||||
;; |
||||
Darwin* ) |
||||
darwin=true |
||||
;; |
||||
MINGW* ) |
||||
msys=true |
||||
;; |
||||
NONSTOP* ) |
||||
nonstop=true |
||||
;; |
||||
esac |
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar |
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM. |
||||
if [ -n "$JAVA_HOME" ] ; then |
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then |
||||
# IBM's JDK on AIX uses strange locations for the executables |
||||
JAVACMD="$JAVA_HOME/jre/sh/java" |
||||
else |
||||
JAVACMD="$JAVA_HOME/bin/java" |
||||
fi |
||||
if [ ! -x "$JAVACMD" ] ; then |
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME |
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the |
||||
location of your Java installation." |
||||
fi |
||||
else |
||||
JAVACMD="java" |
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. |
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the |
||||
location of your Java installation." |
||||
fi |
||||
|
||||
# Increase the maximum file descriptors if we can. |
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then |
||||
MAX_FD_LIMIT=`ulimit -H -n` |
||||
if [ $? -eq 0 ] ; then |
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then |
||||
MAX_FD="$MAX_FD_LIMIT" |
||||
fi |
||||
ulimit -n $MAX_FD |
||||
if [ $? -ne 0 ] ; then |
||||
warn "Could not set maximum file descriptor limit: $MAX_FD" |
||||
fi |
||||
else |
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" |
||||
fi |
||||
fi |
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock |
||||
if $darwin; then |
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" |
||||
fi |
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java |
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then |
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"` |
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` |
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"` |
||||
|
||||
# We build the pattern for arguments to be converted via cygpath |
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` |
||||
SEP="" |
||||
for dir in $ROOTDIRSRAW ; do |
||||
ROOTDIRS="$ROOTDIRS$SEP$dir" |
||||
SEP="|" |
||||
done |
||||
OURCYGPATTERN="(^($ROOTDIRS))" |
||||
# Add a user-defined pattern to the cygpath arguments |
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then |
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" |
||||
fi |
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh |
||||
i=0 |
||||
for arg in "$@" ; do |
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` |
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option |
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition |
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` |
||||
else |
||||
eval `echo args$i`="\"$arg\"" |
||||
fi |
||||
i=`expr $i + 1` |
||||
done |
||||
case $i in |
||||
0) set -- ;; |
||||
1) set -- "$args0" ;; |
||||
2) set -- "$args0" "$args1" ;; |
||||
3) set -- "$args0" "$args1" "$args2" ;; |
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;; |
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; |
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; |
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; |
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; |
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; |
||||
esac |
||||
fi |
||||
|
||||
# Escape application args |
||||
save () { |
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done |
||||
echo " " |
||||
} |
||||
APP_ARGS=`save "$@"` |
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules |
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" |
||||
|
||||
exec "$JAVACMD" "$@" |
@ -0,0 +1,104 @@
|
||||
@rem |
||||
@rem Copyright 2015 the original author or authors. |
||||
@rem |
||||
@rem Licensed under the Apache License, Version 2.0 (the "License"); |
||||
@rem you may not use this file except in compliance with the License. |
||||
@rem You may obtain a copy of the License at |
||||
@rem |
||||
@rem https://www.apache.org/licenses/LICENSE-2.0 |
||||
@rem |
||||
@rem Unless required by applicable law or agreed to in writing, software |
||||
@rem distributed under the License is distributed on an "AS IS" BASIS, |
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
@rem See the License for the specific language governing permissions and |
||||
@rem limitations under the License. |
||||
@rem |
||||
|
||||
@if "%DEBUG%" == "" @echo off |
||||
@rem ########################################################################## |
||||
@rem |
||||
@rem Gradle startup script for Windows |
||||
@rem |
||||
@rem ########################################################################## |
||||
|
||||
@rem Set local scope for the variables with windows NT shell |
||||
if "%OS%"=="Windows_NT" setlocal |
||||
|
||||
set DIRNAME=%~dp0 |
||||
if "%DIRNAME%" == "" set DIRNAME=. |
||||
set APP_BASE_NAME=%~n0 |
||||
set APP_HOME=%DIRNAME% |
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter. |
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi |
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. |
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" |
||||
|
||||
@rem Find java.exe |
||||
if defined JAVA_HOME goto findJavaFromJavaHome |
||||
|
||||
set JAVA_EXE=java.exe |
||||
%JAVA_EXE% -version >NUL 2>&1 |
||||
if "%ERRORLEVEL%" == "0" goto init |
||||
|
||||
echo. |
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. |
||||
echo. |
||||
echo Please set the JAVA_HOME variable in your environment to match the |
||||
echo location of your Java installation. |
||||
|
||||
goto fail |
||||
|
||||
:findJavaFromJavaHome |
||||
set JAVA_HOME=%JAVA_HOME:"=% |
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe |
||||
|
||||
if exist "%JAVA_EXE%" goto init |
||||
|
||||
echo. |
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% |
||||
echo. |
||||
echo Please set the JAVA_HOME variable in your environment to match the |
||||
echo location of your Java installation. |
||||
|
||||
goto fail |
||||
|
||||
:init |
||||
@rem Get command-line arguments, handling Windows variants |
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args |
||||
|
||||
:win9xME_args |
||||
@rem Slurp the command line arguments. |
||||
set CMD_LINE_ARGS= |
||||
set _SKIP=2 |
||||
|
||||
:win9xME_args_slurp |
||||
if "x%~1" == "x" goto execute |
||||
|
||||
set CMD_LINE_ARGS=%* |
||||
|
||||
:execute |
||||
@rem Setup the command line |
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar |
||||
|
||||
|
||||
@rem Execute Gradle |
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% |
||||
|
||||
:end |
||||
@rem End local scope for the variables with windows NT shell |
||||
if "%ERRORLEVEL%"=="0" goto mainEnd |
||||
|
||||
:fail |
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of |
||||
rem the _cmd.exe /c_ return code! |
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 |
||||
exit /b 1 |
||||
|
||||
:mainEnd |
||||
if "%OS%"=="Windows_NT" endlocal |
||||
|
||||
:omega |