@ -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,28 @@
|
||||
An example of Kotlin Multiplatform todo app with shared Jetpack Compose UI. |
||||
|
||||
Supported targets: `Android` and `JVM`. |
||||
|
||||
Libraries used: |
||||
- Jetpack Compose - shared UI |
||||
- [Decompose](https://github.com/arkivanov/Decompose) - navigation and lifecycle |
||||
- [MVIKotlin](https://github.com/arkivanov/MVIKotlin) - presentation and business logic |
||||
- [Reaktive](https://github.com/badoo/Reaktive) - background processing and data transformation |
||||
- [SQLDelight](https://github.com/cashapp/sqldelight) - data storage |
||||
|
||||
There are multiple common modules: |
||||
- `utils` - just some useful helpers |
||||
- `database` - SQLDelight database definition |
||||
- `main` - displays a list of todo items and a text field |
||||
- `edit` - accepts an item id and allows editing |
||||
- `root` - navigates between `main` and `edit` screens |
||||
|
||||
The `root` module is integrated into both Android and Desktop apps. |
||||
|
||||
Features: |
||||
- 99% of the code is shared: data, business logic, presentation, navigation and UI |
||||
- View state is preserved when navigating between screens, Android configuration change, etc. |
||||
- Model-View-Intent (aka MVI) architectural pattern |
||||
|
||||
To run the desktop application execute the following command: `./gradlew desktop:run`. |
||||
|
||||
To run the Android application you will need to open the 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,48 @@
|
||||
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,61 @@
|
||||
object Deps { |
||||
|
||||
object JetBrains { |
||||
object Kotlin { |
||||
private const val VERSION = "1.4.0" |
||||
const val gradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$VERSION" |
||||
const val testCommon = "org.jetbrains.kotlin:kotlin-test-common:$VERSION" |
||||
const val testJunit = "org.jetbrains.kotlin:kotlin-test-junit:$VERSION" |
||||
const val testAnnotationsCommon = "org.jetbrains.kotlin:kotlin-test-annotations-common:$VERSION" |
||||
} |
||||
|
||||
object Compose { |
||||
private const val VERSION = "0.1.0-dev97" |
||||
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 reaktiveTesting = "com.badoo.reaktive:reaktive-testing:$VERSION" |
||||
const val utils = "com.badoo.reaktive:utils:$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,63 @@
|
||||
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) |
||||
} |
||||
} |
||||
|
||||
named("commonTest") { |
||||
dependencies { |
||||
implementation(Deps.JetBrains.Kotlin.testCommon) |
||||
implementation(Deps.JetBrains.Kotlin.testAnnotationsCommon) |
||||
} |
||||
} |
||||
|
||||
named("androidTest") { |
||||
dependencies { |
||||
implementation(Deps.JetBrains.Kotlin.testJunit) |
||||
} |
||||
} |
||||
named("desktopTest") { |
||||
dependencies { |
||||
implementation(Deps.JetBrains.Kotlin.testJunit) |
||||
} |
||||
} |
||||
} |
||||
|
||||
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,33 @@
|
||||
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) |
||||
implementation(Deps.Squareup.SQLDelight.sqliteDriver) |
||||
} |
||||
} |
||||
|
||||
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,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 |
||||
actual fun TestDatabaseDriver(): SqlDriver { |
||||
val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY) |
||||
TodoDatabase.Schema.create(driver) |
||||
|
||||
return driver |
||||
} |
@ -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,6 @@
|
||||
package example.todo.common.database |
||||
|
||||
import com.squareup.sqldelight.db.SqlDriver |
||||
|
||||
@Suppress("FunctionName") |
||||
expect fun TestDatabaseDriver(): SqlDriver |
@ -0,0 +1,36 @@
|
||||
CREATE TABLE IF NOT EXISTS 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; |
||||
|
||||
delete: |
||||
DELETE FROM TodoItemEntity |
||||
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 |
||||
actual fun TestDatabaseDriver(): SqlDriver { |
||||
val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY) |
||||
TodoDatabase.Schema.create(driver) |
||||
|
||||
return driver |
||||
} |
@ -0,0 +1,15 @@
|
||||
package example.todo.common.database |
||||
|
||||
import com.squareup.sqldelight.db.SqlDriver |
||||
import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver |
||||
import example.todo.database.TodoDatabase |
||||
import java.io.File |
||||
|
||||
@Suppress("FunctionName") // FactoryFunction |
||||
fun TodoDatabaseDriver(): SqlDriver { |
||||
val databasePath = File(System.getProperty("java.io.tmpdir"), "ComposeTodoDatabase.db") |
||||
val driver = JdbcSqliteDriver(url = "jdbc:sqlite:${databasePath.absolutePath}") |
||||
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,37 @@
|
||||
package example.todo.common.edit.integration |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.getValue |
||||
import com.arkivanov.decompose.ComponentContext |
||||
import example.todo.common.edit.TodoEdit |
||||
import example.todo.common.edit.TodoEdit.Dependencies |
||||
import example.todo.common.edit.store.TodoEditStoreProvider |
||||
import example.todo.common.edit.ui.TodoEditUi |
||||
import example.todo.common.utils.composeState |
||||
import example.todo.common.utils.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 |
||||
|
||||
TodoEditUi( |
||||
state = state, |
||||
output = editOutput, |
||||
intents = store::accept |
||||
) |
||||
} |
||||
} |
@ -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,60 @@
|
||||
package example.todo.common.edit.ui |
||||
|
||||
import androidx.compose.foundation.Icon |
||||
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.Checkbox |
||||
import androidx.compose.material.IconButton |
||||
import androidx.compose.material.TextField |
||||
import androidx.compose.material.TopAppBar |
||||
import androidx.compose.material.icons.Icons |
||||
import androidx.compose.material.icons.filled.ArrowBack |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Alignment |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.unit.dp |
||||
import com.badoo.reaktive.base.Consumer |
||||
import example.todo.common.edit.TodoEdit.Output |
||||
import example.todo.common.edit.store.TodoEditStore.Intent |
||||
import example.todo.common.edit.store.TodoEditStore.State |
||||
|
||||
@Composable |
||||
internal fun TodoEditUi( |
||||
state: State, |
||||
output: Consumer<Output>, |
||||
intents: (Intent) -> Unit |
||||
) { |
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) { |
||||
TopAppBar( |
||||
title = { Text("Edit todo") }, |
||||
navigationIcon = { |
||||
IconButton(onClick = { output.onNext(Output.Finished) }) { |
||||
Icon(Icons.Default.ArrowBack) |
||||
} |
||||
} |
||||
) |
||||
|
||||
TextField( |
||||
value = state.text, |
||||
modifier = Modifier.weight(1F).fillMaxWidth().padding(8.dp), |
||||
label = { Text("Todo text") }, |
||||
onValueChange = { intents(Intent.SetText(text = it)) } |
||||
) |
||||
|
||||
Row(modifier = Modifier.padding(8.dp)) { |
||||
Text(text = "Completed") |
||||
|
||||
Spacer(modifier = Modifier.width(8.dp)) |
||||
|
||||
Checkbox( |
||||
checked = state.isDone, |
||||
onCheckedChange = { intents(Intent.SetDone(isDone = it)) } |
||||
) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,27 @@
|
||||
plugins { |
||||
id("multiplatform-setup") |
||||
id("multiplatform-compose-setup") |
||||
} |
||||
|
||||
kotlin { |
||||
sourceSets { |
||||
named("commonMain") { |
||||
dependencies { |
||||
implementation(project(":common:utils")) |
||||
implementation(project(":common:database")) |
||||
implementation(Deps.ArkIvanov.Decompose.decompose) |
||||
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlin) |
||||
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlinExtensionsReaktive) |
||||
implementation(Deps.Badoo.Reaktive.reaktive) |
||||
} |
||||
} |
||||
|
||||
named("commonTest") { |
||||
dependencies { |
||||
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlinMain) |
||||
implementation(Deps.Badoo.Reaktive.reaktiveTesting) |
||||
implementation(Deps.Badoo.Reaktive.utils) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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,49 @@
|
||||
package example.todo.common.main.integration |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.getValue |
||||
import com.arkivanov.decompose.ComponentContext |
||||
import example.todo.common.main.TodoMain |
||||
import example.todo.common.main.TodoMain.Dependencies |
||||
import example.todo.common.main.TodoMain.Output |
||||
import example.todo.common.main.store.TodoMainStore.Intent |
||||
import example.todo.common.main.store.TodoMainStore.State |
||||
import example.todo.common.main.store.TodoMainStoreProvider |
||||
import example.todo.common.main.ui.TodoMainUi |
||||
import example.todo.common.utils.composeState |
||||
import example.todo.common.utils.getStore |
||||
|
||||
internal class TodoMainImpl( |
||||
componentContext: ComponentContext, |
||||
dependencies: Dependencies |
||||
) : TodoMain, ComponentContext by componentContext, Dependencies by dependencies { |
||||
|
||||
private val store = |
||||
instanceKeeper.getStore { |
||||
TodoMainStoreProvider( |
||||
storeFactory = storeFactory, |
||||
database = TodoMainStoreDatabase(queries = database.todoDatabaseQueries) |
||||
).provide() |
||||
} |
||||
|
||||
internal val state: State get() = store.state |
||||
|
||||
@Composable |
||||
override fun invoke() { |
||||
val state by store.composeState |
||||
|
||||
TodoMainUi( |
||||
state = state, |
||||
output = mainOutput, |
||||
intents = store::accept |
||||
) |
||||
} |
||||
|
||||
internal fun onIntent(intent: Intent) { |
||||
store.accept(intent) |
||||
} |
||||
|
||||
internal fun onOutput(output: Output) { |
||||
mainOutput.onNext(output) |
||||
} |
||||
} |
@ -0,0 +1,50 @@
|
||||
package example.todo.common.main.integration |
||||
|
||||
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.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.main.store.TodoItem |
||||
import example.todo.common.main.store.TodoMainStoreProvider |
||||
|
||||
internal class TodoMainStoreDatabase( |
||||
private val queries: TodoDatabaseQueries |
||||
) : TodoMainStoreProvider.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 = |
||||
completableFromFunction { queries.setDone(id = id, isDone = isDone) } |
||||
.subscribeOn(ioScheduler) |
||||
|
||||
override fun delete(id: Long): Completable = |
||||
completableFromFunction { queries.delete(id = id) } |
||||
.subscribeOn(ioScheduler) |
||||
|
||||
override fun add(text: String): Completable = |
||||
completableFromFunction { |
||||
queries.transactionWithResult { |
||||
queries.add(text = text) |
||||
val lastId = queries.getLastInsertId().executeAsOne() |
||||
queries.select(id = lastId).executeAsOne() |
||||
} |
||||
}.subscribeOn(ioScheduler) |
||||
} |
@ -0,0 +1,8 @@
|
||||
package example.todo.common.main.store |
||||
|
||||
internal data class TodoItem( |
||||
val id: Long = 0L, |
||||
val order: Long = 0L, |
||||
val text: String = "", |
||||
val isDone: Boolean = false |
||||
) |
@ -0,0 +1,20 @@
|
||||
package example.todo.common.main.store |
||||
|
||||
import com.arkivanov.mvikotlin.core.store.Store |
||||
import example.todo.common.main.store.TodoMainStore.Intent |
||||
import example.todo.common.main.store.TodoMainStore.State |
||||
|
||||
internal interface TodoMainStore : Store<Intent, State, Nothing> { |
||||
|
||||
sealed class Intent { |
||||
data class SetItemDone(val id: Long, val isDone: Boolean) : Intent() |
||||
data class DeleteItem(val id: Long) : Intent() |
||||
data class SetText(val text: String) : Intent() |
||||
object AddItem : Intent() |
||||
} |
||||
|
||||
data class State( |
||||
val items: List<TodoItem> = emptyList(), |
||||
val text: String = "" |
||||
) |
||||
} |
@ -0,0 +1,106 @@
|
||||
package example.todo.common.main.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.main.store.TodoMainStore.Intent |
||||
import example.todo.common.main.store.TodoMainStore.State |
||||
|
||||
internal class TodoMainStoreProvider( |
||||
private val storeFactory: StoreFactory, |
||||
private val database: Database |
||||
) { |
||||
|
||||
fun provide(): TodoMainStore = |
||||
object : TodoMainStore, 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 ItemDoneChanged(val id: Long, val isDone: Boolean) : Result() |
||||
data class ItemDeleted(val id: Long) : Result() |
||||
data class TextChanged(val text: String) : 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.SetItemDone -> setItemDone(id = intent.id, isDone = intent.isDone) |
||||
is Intent.DeleteItem -> deleteItem(id = intent.id) |
||||
is Intent.SetText -> dispatch(Result.TextChanged(text = intent.text)) |
||||
is Intent.AddItem -> addItem(state = getState()) |
||||
} |
||||
|
||||
private fun setItemDone(id: Long, isDone: Boolean) { |
||||
dispatch(Result.ItemDoneChanged(id = id, isDone = isDone)) |
||||
database.setDone(id = id, isDone = isDone).subscribeScoped() |
||||
} |
||||
|
||||
private fun deleteItem(id: Long) { |
||||
dispatch(Result.ItemDeleted(id = id)) |
||||
database.delete(id = id).subscribeScoped() |
||||
} |
||||
|
||||
private fun addItem(state: State) { |
||||
if (state.text.isNotEmpty()) { |
||||
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.ItemsLoaded -> copy(items = result.items.sorted()) |
||||
is Result.ItemDoneChanged -> update(id = result.id) { copy(isDone = result.isDone) } |
||||
is Result.ItemDeleted -> copy(items = items.filterNot { it.id == result.id }) |
||||
is Result.TextChanged -> copy(text = result.text) |
||||
} |
||||
|
||||
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 |
||||
|
||||
fun delete(id: Long): Completable |
||||
|
||||
fun add(text: String): Completable |
||||
} |
||||
} |
@ -0,0 +1,119 @@
|
||||
package example.todo.common.main.ui |
||||
|
||||
import androidx.compose.foundation.Icon |
||||
import androidx.compose.foundation.Text |
||||
import androidx.compose.foundation.clickable |
||||
import androidx.compose.foundation.layout.Box |
||||
import androidx.compose.foundation.layout.Column |
||||
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.Button |
||||
import androidx.compose.material.Checkbox |
||||
import androidx.compose.material.Divider |
||||
import androidx.compose.material.IconButton |
||||
import androidx.compose.material.OutlinedTextField |
||||
import androidx.compose.material.TopAppBar |
||||
import androidx.compose.material.icons.Icons |
||||
import androidx.compose.material.icons.filled.Delete |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Alignment |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.input.key.ExperimentalKeyInput |
||||
import androidx.compose.ui.input.key.Key |
||||
import androidx.compose.ui.input.key.keyInputFilter |
||||
import androidx.compose.ui.text.AnnotatedString |
||||
import androidx.compose.ui.text.style.TextOverflow |
||||
import androidx.compose.ui.unit.dp |
||||
import com.badoo.reaktive.base.Consumer |
||||
import example.todo.common.main.TodoMain.Output |
||||
import example.todo.common.main.store.TodoItem |
||||
import example.todo.common.main.store.TodoMainStore.Intent |
||||
import example.todo.common.main.store.TodoMainStore.State |
||||
import example.todo.common.utils.onKeyUp |
||||
|
||||
@Composable |
||||
internal fun TodoMainUi( |
||||
state: State, |
||||
output: Consumer<Output>, |
||||
intents: (Intent) -> Unit |
||||
) { |
||||
Column { |
||||
TopAppBar(title = { Text(text = "Todo List") }) |
||||
|
||||
Box(Modifier.weight(1F)) { |
||||
TodoList( |
||||
items = state.items, |
||||
onItemClicked = { output.onNext(Output.Selected(id = it)) }, |
||||
onDoneChanged = { id, isDone -> intents(Intent.SetItemDone(id = id, isDone = isDone)) }, |
||||
onDeleteItemClicked = { intents(Intent.DeleteItem(id = it)) } |
||||
) |
||||
} |
||||
|
||||
TodoInput( |
||||
text = state.text, |
||||
onAddClicked = { intents(Intent.AddItem) }, |
||||
onTextChanged = { intents(Intent.SetText(text = it)) } |
||||
) |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
private fun TodoList( |
||||
items: List<TodoItem>, |
||||
onItemClicked: (id: Long) -> Unit, |
||||
onDoneChanged: (id: Long, isDone: Boolean) -> Unit, |
||||
onDeleteItemClicked: (id: Long) -> Unit |
||||
) { |
||||
LazyColumnFor(items = items) { item -> |
||||
Row(modifier = Modifier.clickable(onClick = { onItemClicked(item.id) })) { |
||||
Spacer(modifier = Modifier.width(8.dp)) |
||||
|
||||
Checkbox( |
||||
checked = item.isDone, |
||||
modifier = Modifier.align(Alignment.CenterVertically), |
||||
onCheckedChange = { onDoneChanged(item.id, it) } |
||||
) |
||||
|
||||
Spacer(modifier = Modifier.width(8.dp)) |
||||
|
||||
Text( |
||||
text = AnnotatedString(item.text), |
||||
modifier = Modifier.weight(1F).align(Alignment.CenterVertically), |
||||
maxLines = 1, |
||||
overflow = TextOverflow.Ellipsis |
||||
) |
||||
|
||||
Spacer(modifier = Modifier.width(8.dp)) |
||||
|
||||
IconButton(onClick = { onDeleteItemClicked(item.id) }) { |
||||
Icon(Icons.Default.Delete) |
||||
} |
||||
} |
||||
|
||||
Divider() |
||||
} |
||||
} |
||||
|
||||
@OptIn(ExperimentalKeyInput::class) |
||||
@Composable |
||||
private fun TodoInput( |
||||
text: String, |
||||
onTextChanged: (String) -> Unit, |
||||
onAddClicked: () -> Unit |
||||
) { |
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(8.dp)) { |
||||
OutlinedTextField( |
||||
value = text, |
||||
modifier = Modifier.weight(weight = 1F).keyInputFilter(onKeyUp(Key.Enter, onAddClicked)), |
||||
onValueChange = onTextChanged, |
||||
label = { Text(text = "Add a todo") } |
||||
) |
||||
|
||||
Button(modifier = Modifier.padding(start = 8.dp), onClick = onAddClicked) { |
||||
Text(text = "+") |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,148 @@
|
||||
package example.todo.common.main.integration |
||||
|
||||
import com.arkivanov.decompose.DefaultComponentContext |
||||
import com.arkivanov.decompose.lifecycle.LifecycleRegistry |
||||
import com.arkivanov.mvikotlin.core.store.StoreFactory |
||||
import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory |
||||
import com.badoo.reaktive.base.Consumer |
||||
import com.badoo.reaktive.scheduler.overrideSchedulers |
||||
import com.badoo.reaktive.subject.publish.PublishSubject |
||||
import com.badoo.reaktive.test.observable.assertValue |
||||
import com.badoo.reaktive.test.observable.test |
||||
import com.badoo.reaktive.test.scheduler.TestScheduler |
||||
import example.todo.common.database.TestDatabaseDriver |
||||
import example.todo.common.database.TodoItemEntity |
||||
import example.todo.common.main.TodoMain.Dependencies |
||||
import example.todo.common.main.TodoMain.Output |
||||
import example.todo.common.main.store.TodoItem |
||||
import example.todo.common.main.store.TodoMainStore.Intent |
||||
import example.todo.database.TodoDatabase |
||||
import kotlin.test.BeforeTest |
||||
import kotlin.test.Test |
||||
import kotlin.test.assertEquals |
||||
import kotlin.test.assertFalse |
||||
import kotlin.test.assertNull |
||||
import kotlin.test.assertTrue |
||||
|
||||
@Suppress("TestFunctionName") |
||||
class TodoMainTest { |
||||
|
||||
private val lifecycle = LifecycleRegistry() |
||||
private val database = TodoDatabase(TestDatabaseDriver()) |
||||
private val outputSubject = PublishSubject<Output>() |
||||
private val output = outputSubject.test() |
||||
|
||||
private val queries = database.todoDatabaseQueries |
||||
|
||||
private val impl by lazy { |
||||
TodoMainImpl( |
||||
componentContext = DefaultComponentContext(lifecycle = lifecycle), |
||||
dependencies = object : Dependencies { |
||||
override val storeFactory: StoreFactory = DefaultStoreFactory |
||||
override val database: TodoDatabase = this@TodoMainTest.database |
||||
override val mainOutput: Consumer<Output> = outputSubject |
||||
} |
||||
) |
||||
} |
||||
|
||||
@BeforeTest |
||||
fun before() { |
||||
overrideSchedulers( |
||||
main = { TestScheduler() }, |
||||
io = { TestScheduler() } |
||||
) |
||||
} |
||||
|
||||
@Test |
||||
fun WHEN_item_added_to_database_THEN_item_displayed() { |
||||
queries.add("Item1") |
||||
|
||||
assertEquals("Item1", firstItem().text) |
||||
} |
||||
|
||||
@Test |
||||
fun WHEN_item_deleted_from_database_THEN_item_not_displayed() { |
||||
queries.add("Item1") |
||||
val id = lastInsertItem().id |
||||
|
||||
queries.delete(id = id) |
||||
|
||||
assertFalse(impl.state.items.any { it.id == id }) |
||||
} |
||||
|
||||
@Test |
||||
fun WHEN_item_selected_THEN_Output_Selected_emitted() { |
||||
queries.add("Item1") |
||||
val id = firstItem().id |
||||
|
||||
impl.onOutput(Output.Selected(id = id)) |
||||
|
||||
output.assertValue(Output.Selected(id = id)) |
||||
} |
||||
|
||||
@Test |
||||
fun GIVEN_item_isDone_false_WHEN_done_changed_to_true_THEN_item_isDone_true_in_database() { |
||||
queries.add("Item1") |
||||
val id = firstItem().id |
||||
queries.setDone(id = id, isDone = false) |
||||
|
||||
impl.onIntent(Intent.SetItemDone(id = id, isDone = true)) |
||||
|
||||
assertTrue(queries.select(id = id).executeAsOne().isDone) |
||||
} |
||||
|
||||
@Test |
||||
fun GIVEN_item_isDone_true_WHEN_done_changed_to_false_THEN_item_isDone_false_in_database() { |
||||
queries.add("Item1") |
||||
val id = firstItem().id |
||||
queries.setDone(id = id, isDone = true) |
||||
|
||||
impl.onIntent(Intent.SetItemDone(id = id, isDone = false)) |
||||
|
||||
assertFalse(queries.select(id = id).executeAsOne().isDone) |
||||
} |
||||
|
||||
@Test |
||||
fun WHEN_delete_clicked_THEN_item_deleted_in_database() { |
||||
queries.add("Item1") |
||||
val id = firstItem().id |
||||
|
||||
impl.onIntent(Intent.DeleteItem(id = id)) |
||||
|
||||
assertNull(queries.select(id = id).executeAsOneOrNull()) |
||||
} |
||||
|
||||
@Test |
||||
fun WHEN_item_text_changed_in_database_THEN_item_updated() { |
||||
queries.add("Item1") |
||||
val id = firstItem().id |
||||
|
||||
queries.setText(id = id, text = "New text") |
||||
|
||||
assertEquals("New text", firstItem().text) |
||||
} |
||||
|
||||
@Test |
||||
fun WHEN_text_changed_THEN_text_updated() { |
||||
impl.onIntent(Intent.SetText(text = "Item text")) |
||||
|
||||
assertEquals("Item text", impl.state.text) |
||||
} |
||||
|
||||
@Test |
||||
fun GIVEN_text_entered_WHEN_add_clicked_THEN_item_added_in_database() { |
||||
impl.onIntent(Intent.SetText(text = "Item text")) |
||||
|
||||
impl.onIntent(Intent.AddItem) |
||||
|
||||
assertEquals("Item text", lastInsertItem().text) |
||||
} |
||||
|
||||
private fun firstItem(): TodoItem = impl.state.items[0] |
||||
|
||||
private fun lastInsertItem(): TodoItemEntity { |
||||
val lastInsertId = queries.getLastInsertId().executeAsOne() |
||||
|
||||
return queries.select(id = lastInsertId).executeAsOne() |
||||
} |
||||
} |
@ -0,0 +1,39 @@
|
||||
package example.todo.common.main.store |
||||
|
||||
import com.badoo.reaktive.completable.Completable |
||||
import com.badoo.reaktive.completable.completableFromFunction |
||||
import com.badoo.reaktive.observable.Observable |
||||
import com.badoo.reaktive.subject.behavior.BehaviorSubject |
||||
|
||||
internal class TestTodoMainStoreDatabase : TodoMainStoreProvider.Database { |
||||
|
||||
private val subject = BehaviorSubject<List<TodoItem>>(emptyList()) |
||||
|
||||
var items: List<TodoItem> |
||||
get() = subject.value |
||||
set(value) { |
||||
subject.onNext(value) |
||||
} |
||||
|
||||
override val updates: Observable<List<TodoItem>> = subject |
||||
|
||||
override fun setDone(id: Long, isDone: Boolean): Completable = |
||||
completableFromFunction { |
||||
update(id = id) { copy(isDone = isDone) } |
||||
} |
||||
|
||||
override fun delete(id: Long): Completable = |
||||
completableFromFunction { |
||||
this.items = items.filterNot { it.id == id } |
||||
} |
||||
|
||||
override fun add(text: String): Completable = |
||||
completableFromFunction { |
||||
val id = items.maxBy(TodoItem::id)?.id?.inc() ?: 1L |
||||
this.items += TodoItem(id = id, order = id, text = text) |
||||
} |
||||
|
||||
private fun update(id: Long, func: TodoItem.() -> TodoItem) { |
||||
items = items.map { if (it.id == id) it.func() else it } |
||||
} |
||||
} |
@ -0,0 +1,134 @@
|
||||
package example.todo.common.main.store |
||||
|
||||
import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory |
||||
import com.badoo.reaktive.scheduler.overrideSchedulers |
||||
import com.badoo.reaktive.test.scheduler.TestScheduler |
||||
import example.todo.common.main.store.TodoMainStore.Intent |
||||
import kotlin.test.BeforeTest |
||||
import kotlin.test.Test |
||||
import kotlin.test.assertEquals |
||||
import kotlin.test.assertFalse |
||||
import kotlin.test.assertTrue |
||||
|
||||
@Suppress("TestFunctionName") |
||||
class TodoMainStoreTest { |
||||
|
||||
private val database = TestTodoMainStoreDatabase() |
||||
private val provider = TodoMainStoreProvider(storeFactory = DefaultStoreFactory, database = database) |
||||
|
||||
@BeforeTest |
||||
fun before() { |
||||
overrideSchedulers(main = { TestScheduler() }) |
||||
} |
||||
|
||||
@Test |
||||
fun GIVEN_items_in_database_WHEN_created_THEN_loads_items_from_database() { |
||||
val item1 = TodoItem(id = 1L, order = 2L, text = "item1") |
||||
val item2 = TodoItem(id = 2L, order = 1L, text = "item2", isDone = true) |
||||
val item3 = TodoItem(id = 3L, order = 3L, text = "item3") |
||||
database.items = listOf(item1, item2, item3) |
||||
|
||||
val store = provider.provide() |
||||
|
||||
assertEquals(listOf(item3, item1, item2), store.state.items) |
||||
} |
||||
|
||||
@Test |
||||
fun WHEN_items_changed_in_database_THEN_contains_new_items() { |
||||
database.items = listOf(TodoItem()) |
||||
val store = provider.provide() |
||||
|
||||
val item1 = TodoItem(id = 1L, order = 2L, text = "item1") |
||||
val item2 = TodoItem(id = 2L, order = 1L, text = "item2", isDone = true) |
||||
val item3 = TodoItem(id = 3L, order = 3L, text = "item3") |
||||
database.items = listOf(item1, item2, item3) |
||||
|
||||
assertEquals(listOf(item3, item1, item2), store.state.items) |
||||
} |
||||
|
||||
@Test |
||||
fun WHEN_Intent_SetItemDone_THEN_done_changed_in_state() { |
||||
val item1 = TodoItem(id = 1L, text = "item1") |
||||
val item2 = TodoItem(id = 2L, text = "item2", isDone = false) |
||||
database.items = listOf(item1, item2) |
||||
val store = provider.provide() |
||||
|
||||
store.accept(Intent.SetItemDone(id = 2L, isDone = true)) |
||||
|
||||
assertTrue(store.state.items.first { it.id == 2L }.isDone) |
||||
} |
||||
|
||||
@Test |
||||
fun WHEN_Intent_SetItemDone_THEN_done_changed_in_database() { |
||||
val item1 = TodoItem(id = 1L, text = "item1") |
||||
val item2 = TodoItem(id = 2L, text = "item2", isDone = false) |
||||
database.items = listOf(item1, item2) |
||||
val store = provider.provide() |
||||
|
||||
store.accept(Intent.SetItemDone(id = 2L, isDone = true)) |
||||
|
||||
assertTrue(database.items.first { it.id == 2L }.isDone) |
||||
} |
||||
|
||||
@Test |
||||
fun WHEN_Intent_DeleteItem_THEN_item_deleted_in_state() { |
||||
val item1 = TodoItem(id = 1L, text = "item1") |
||||
val item2 = TodoItem(id = 2L, text = "item2") |
||||
database.items = listOf(item1, item2) |
||||
val store = provider.provide() |
||||
|
||||
store.accept(Intent.DeleteItem(id = 2L)) |
||||
|
||||
assertFalse(store.state.items.any { it.id == 2L }) |
||||
} |
||||
|
||||
@Test |
||||
fun WHEN_Intent_DeleteItem_THEN_item_deleted_in_database() { |
||||
val item1 = TodoItem(id = 1L, text = "item1") |
||||
val item2 = TodoItem(id = 2L, text = "item2") |
||||
database.items = listOf(item1, item2) |
||||
val store = provider.provide() |
||||
|
||||
store.accept(Intent.DeleteItem(id = 2L)) |
||||
|
||||
assertFalse(database.items.any { it.id == 2L }) |
||||
} |
||||
|
||||
@Test |
||||
fun WHEN_Intent_SetText_WHEN_text_changed_in_state() { |
||||
val store = provider.provide() |
||||
|
||||
store.accept(Intent.SetText(text = "Item text")) |
||||
|
||||
assertEquals("Item text", store.state.text) |
||||
} |
||||
|
||||
@Test |
||||
fun GIVEN_text_entered_WHEN_Intent_AddItem_THEN_item_added_in_database() { |
||||
val store = provider.provide() |
||||
store.accept(Intent.SetText(text = "Item text")) |
||||
|
||||
store.accept(Intent.AddItem) |
||||
|
||||
assertTrue(database.items.any { it.text == "Item text" }) |
||||
} |
||||
|
||||
@Test |
||||
fun GIVEN_text_entered_WHEN_Intent_AddItem_THEN_text_cleared_in_state() { |
||||
val store = provider.provide() |
||||
store.accept(Intent.SetText(text = "Item text")) |
||||
|
||||
store.accept(Intent.AddItem) |
||||
|
||||
assertEquals("", store.state.text) |
||||
} |
||||
|
||||
@Test |
||||
fun GIVEN_no_text_entered_WHEN_Intent_AddItem_THEN_item_not_added() { |
||||
val store = provider.provide() |
||||
|
||||
store.accept(Intent.AddItem) |
||||
|
||||
assertEquals(0, store.state.items.size) |
||||
} |
||||
} |
@ -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,79 @@
|
||||
package example.todo.common.root.integration |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
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.children |
||||
|
||||
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() { |
||||
router.state.children { child, configuration -> |
||||
Crossfade(currentChild = child, currentKey = configuration) { currentChild -> |
||||
currentChild() |
||||
} |
||||
} |
||||
} |
||||
|
||||
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,99 @@
|
||||
/* |
||||
* Copied from Decompose |
||||
*/ |
||||
|
||||
package example.todo.common.utils |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.Providers |
||||
import androidx.compose.runtime.onDispose |
||||
import androidx.compose.runtime.remember |
||||
import androidx.compose.runtime.savedinstancestate.UiSavedStateRegistry |
||||
import androidx.compose.runtime.savedinstancestate.UiSavedStateRegistryAmbient |
||||
import com.arkivanov.decompose.RouterState |
||||
import com.arkivanov.decompose.statekeeper.Parcelable |
||||
import com.arkivanov.decompose.value.Value |
||||
|
||||
private typealias SavedState = Map<String, List<Any?>> |
||||
|
||||
@Composable |
||||
fun <C : Parcelable, T : Any> Value<RouterState<C, T>>.children(render: @Composable() (child: T, configuration: C) -> Unit) { |
||||
val parentRegistry: UiSavedStateRegistry? = UiSavedStateRegistryAmbient.current |
||||
val children = remember { Children<C>() } |
||||
|
||||
if (parentRegistry != null) { |
||||
onDispose { |
||||
children.inactive.entries.forEach { (key, value) -> |
||||
parentRegistry.unregisterProvider(key, value.provider) |
||||
} |
||||
children.active?.also { |
||||
parentRegistry.unregisterProvider(it.key, it.provider) |
||||
} |
||||
} |
||||
} |
||||
|
||||
invoke { state -> |
||||
val activeChildConfiguration = state.activeChild.configuration |
||||
|
||||
val currentChild: ActiveChild<C>? = children.active |
||||
if ((currentChild != null) && state.backStack.any { it.configuration === currentChild.configuration }) { |
||||
parentRegistry?.unregisterProvider(currentChild.key, currentChild.provider) |
||||
val inactiveChild = InactiveChild(configuration = currentChild.configuration, savedState = currentChild.provider()) |
||||
children.inactive[currentChild.key] = inactiveChild |
||||
parentRegistry?.registerProvider(currentChild.key, inactiveChild.provider) |
||||
} |
||||
|
||||
val activeChildRegistry: UiSavedStateRegistry |
||||
|
||||
if (currentChild?.configuration === activeChildConfiguration) { |
||||
activeChildRegistry = currentChild.registry |
||||
} else { |
||||
val key = activeChildConfiguration.toString() |
||||
|
||||
val savedChild: InactiveChild<C>? = children.inactive.remove(key) |
||||
if (savedChild != null) { |
||||
parentRegistry?.unregisterProvider(key, savedChild.provider) |
||||
} |
||||
@Suppress("UNCHECKED_CAST") |
||||
val savedState: SavedState? = savedChild?.savedState ?: parentRegistry?.consumeRestored(key) as SavedState? |
||||
|
||||
activeChildRegistry = UiSavedStateRegistry(savedState) { true } |
||||
|
||||
val newActiveChild = ActiveChild(configuration = activeChildConfiguration, key = key, registry = activeChildRegistry) |
||||
children.active = newActiveChild |
||||
parentRegistry?.registerProvider(key, newActiveChild.provider) |
||||
} |
||||
|
||||
children.inactive.entries.removeAll { (key, value) -> |
||||
val remove = state.backStack.none { it.configuration === value.configuration } |
||||
if (remove) { |
||||
parentRegistry?.unregisterProvider(key, value.provider) |
||||
} |
||||
remove |
||||
} |
||||
|
||||
Providers(UiSavedStateRegistryAmbient provides activeChildRegistry) { |
||||
render(state.activeChild.component, activeChildConfiguration) |
||||
} |
||||
} |
||||
} |
||||
|
||||
private class Children<C : Parcelable> { |
||||
val inactive: MutableMap<String, InactiveChild<C>> = HashMap() |
||||
var active: ActiveChild<C>? = null |
||||
} |
||||
|
||||
private class ActiveChild<out C : Parcelable>( |
||||
val configuration: C, |
||||
val key: String, |
||||
val registry: UiSavedStateRegistry |
||||
) { |
||||
val provider: () -> SavedState = registry::performSave |
||||
} |
||||
|
||||
private class InactiveChild<out C : Parcelable>( |
||||
val configuration: C, |
||||
val savedState: SavedState |
||||
) { |
||||
val provider: () -> SavedState = ::savedState |
||||
} |
@ -0,0 +1,16 @@
|
||||
package example.todo.common.utils |
||||
|
||||
import androidx.compose.ui.input.key.ExperimentalKeyInput |
||||
import androidx.compose.ui.input.key.Key |
||||
import androidx.compose.ui.input.key.KeyEvent |
||||
|
||||
@OptIn(ExperimentalKeyInput::class) |
||||
fun onKeyUp(key: Key, onEvent: () -> Unit): (KeyEvent) -> Boolean = |
||||
{ keyEvent -> |
||||
if (keyEvent.key == key) { |
||||
onEvent() |
||||
true |
||||
} else { |
||||
false |
||||
} |
||||
} |
@ -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,35 @@
|
||||
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,23 @@
|
||||
# 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 |
||||
org.gradle.parallel=true |
||||
org.gradle.caching=true |
@ -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 |