@ -1,15 +0,0 @@ |
|||||||
*.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 |
|
@ -1,23 +0,0 @@ |
|||||||
<component name="ProjectRunConfigurationManager"> |
|
||||||
<configuration default="false" name="browser" type="GradleRunConfiguration" factoryName="Gradle"> |
|
||||||
<ExternalSystemSettings> |
|
||||||
<option name="executionName" /> |
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" /> |
|
||||||
<option name="externalSystemIdString" value="GRADLE" /> |
|
||||||
<option name="scriptParameters" value="" /> |
|
||||||
<option name="taskDescriptions"> |
|
||||||
<list /> |
|
||||||
</option> |
|
||||||
<option name="taskNames"> |
|
||||||
<list> |
|
||||||
<option value=":web:jsBrowserDevelopmentRun" /> |
|
||||||
</list> |
|
||||||
</option> |
|
||||||
<option name="vmOptions" /> |
|
||||||
</ExternalSystemSettings> |
|
||||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess> |
|
||||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess> |
|
||||||
<DebugAllEnabled>false</DebugAllEnabled> |
|
||||||
<method v="2" /> |
|
||||||
</configuration> |
|
||||||
</component> |
|
@ -1,23 +0,0 @@ |
|||||||
<component name="ProjectRunConfigurationManager"> |
|
||||||
<configuration default="false" name="desktop" type="GradleRunConfiguration" factoryName="Gradle"> |
|
||||||
<ExternalSystemSettings> |
|
||||||
<option name="executionName" /> |
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" /> |
|
||||||
<option name="externalSystemIdString" value="GRADLE" /> |
|
||||||
<option name="scriptParameters" value="" /> |
|
||||||
<option name="taskDescriptions"> |
|
||||||
<list /> |
|
||||||
</option> |
|
||||||
<option name="taskNames"> |
|
||||||
<list> |
|
||||||
<option value=":desktop:run" /> |
|
||||||
</list> |
|
||||||
</option> |
|
||||||
<option name="vmOptions" /> |
|
||||||
</ExternalSystemSettings> |
|
||||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess> |
|
||||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess> |
|
||||||
<DebugAllEnabled>false</DebugAllEnabled> |
|
||||||
<method v="2" /> |
|
||||||
</configuration> |
|
||||||
</component> |
|
@ -1,61 +0,0 @@ |
|||||||
An example of Kotlin Multiplatform todo app with shared Android/Desktop Compose UI and SwiftUI (not Compose) iOS. |
|
||||||
|
|
||||||
This example supports the following targets: |
|
||||||
- `Android` (Compose) |
|
||||||
- `Desktop/JVM` (Compose) |
|
||||||
- `Web/JavaScript` (Compose) |
|
||||||
- `iOS` (SwiftUI, not Compose) |
|
||||||
|
|
||||||
Libraries used: |
|
||||||
- Compose Multiplatform - 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 modules: |
|
||||||
- `:common:utils` - just some useful helpers |
|
||||||
- `:common:database` - SQLDelight database definition |
|
||||||
- `:common:main` - displays a list of todo items and a text field |
|
||||||
- `:common:edit` - accepts an item id and allows editing |
|
||||||
- `:common:root` - navigates between `main` and `edit` screens |
|
||||||
- `:common:compose-ui` - Shared Compose UI for Android and Desktop |
|
||||||
- `:android` - Android application |
|
||||||
- `:desktop` - Desktop application |
|
||||||
- `:web` - Web browser application + Compose HTML Library |
|
||||||
- `ios` - iOS Xcode project |
|
||||||
|
|
||||||
The root module is integrated into Android, Desktop and iOS (non-Compose) 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 |
|
||||||
- Pluggable UI - Compose UI for Android, Desktop and Web, SwiftUI (not Compose) for iOS |
|
||||||
|
|
||||||
### Running desktop application |
|
||||||
* To run, launch command: `./gradlew :desktop:run` |
|
||||||
* Or choose **desktop** configuration in IDE and run it. |
|
||||||
![desktop-run-configuration.png](screenshots/desktop-run-configuration.png) |
|
||||||
|
|
||||||
#### Building native desktop distribution |
|
||||||
``` |
|
||||||
./gradlew :desktop:packageDistributionForCurrentOS |
|
||||||
# outputs are written to desktop/build/compose/binaries |
|
||||||
``` |
|
||||||
|
|
||||||
### Running Android application |
|
||||||
|
|
||||||
Open project in Intellij IDEA or Android Studio and run "android" configuration. |
|
||||||
|
|
||||||
### Running Web browser application |
|
||||||
|
|
||||||
* To run, launch command: `./gradlew :web:jsBrowserDevelopmentRun` |
|
||||||
* Or choose **browser** configuration in IDE and run it. |
|
||||||
![browser-run-configuration.png](screenshots/browser-run-configuration.png) |
|
||||||
|
|
||||||
### Running iOS application |
|
||||||
|
|
||||||
Open and build the Xcode project located in `ios` folder. |
|
||||||
|
|
||||||
![Desktop](screenshots/todo.png) |
|
@ -1,44 +0,0 @@ |
|||||||
plugins { |
|
||||||
id("com.android.application") |
|
||||||
kotlin("android") |
|
||||||
id("org.jetbrains.compose") |
|
||||||
} |
|
||||||
|
|
||||||
android { |
|
||||||
compileSdk = 33 |
|
||||||
|
|
||||||
defaultConfig { |
|
||||||
minSdk = 26 |
|
||||||
targetSdk = 33 |
|
||||||
versionCode = 1 |
|
||||||
versionName = "1.0" |
|
||||||
} |
|
||||||
|
|
||||||
compileOptions { |
|
||||||
sourceCompatibility = JavaVersion.VERSION_11 |
|
||||||
targetCompatibility = JavaVersion.VERSION_11 |
|
||||||
} |
|
||||||
|
|
||||||
packagingOptions { |
|
||||||
exclude("META-INF/*") |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
dependencies { |
|
||||||
implementation(project(":common:database")) |
|
||||||
implementation(project(":common:utils")) |
|
||||||
implementation(project(":common:root")) |
|
||||||
implementation(project(":common:compose-ui")) |
|
||||||
implementation(compose.material) |
|
||||||
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlin) |
|
||||||
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlinMain) |
|
||||||
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlinLogging) |
|
||||||
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlinTimeTravel) |
|
||||||
implementation(Deps.ArkIvanov.Decompose.decompose) |
|
||||||
implementation(Deps.ArkIvanov.Decompose.extensionsCompose) |
|
||||||
implementation(Deps.AndroidX.AppCompat.appCompat) |
|
||||||
implementation(Deps.AndroidX.Activity.activityCompose) |
|
||||||
|
|
||||||
// Workaround for https://github.com/JetBrains/compose-jb/issues/2340 |
|
||||||
implementation("androidx.compose.material:material:${Deps.JetpackComposeWorkaround.VERSION}") |
|
||||||
} |
|
@ -1,28 +0,0 @@ |
|||||||
<?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" |
|
||||||
android:exported="true"> |
|
||||||
<intent-filter> |
|
||||||
<action android:name="android.intent.action.MAIN" /> |
|
||||||
<category android:name="android.intent.category.LAUNCHER" /> |
|
||||||
</intent-filter> |
|
||||||
</activity> |
|
||||||
</application> |
|
||||||
|
|
||||||
</manifest> |
|
@ -1,13 +0,0 @@ |
|||||||
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() |
|
||||||
} |
|
||||||
} |
|
@ -1,8 +0,0 @@ |
|||||||
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) |
|
@ -1,39 +0,0 @@ |
|||||||
package example.todo.android |
|
||||||
|
|
||||||
import android.os.Bundle |
|
||||||
import androidx.activity.compose.setContent |
|
||||||
import androidx.appcompat.app.AppCompatActivity |
|
||||||
import androidx.compose.material.MaterialTheme |
|
||||||
import androidx.compose.material.Surface |
|
||||||
import com.arkivanov.decompose.ComponentContext |
|
||||||
import com.arkivanov.decompose.defaultComponentContext |
|
||||||
import com.arkivanov.mvikotlin.logging.store.LoggingStoreFactory |
|
||||||
import com.arkivanov.mvikotlin.timetravel.store.TimeTravelStoreFactory |
|
||||||
import example.todo.common.database.DefaultTodoSharedDatabase |
|
||||||
import example.todo.common.database.TodoDatabaseDriver |
|
||||||
import example.todo.common.root.TodoRoot |
|
||||||
import example.todo.common.root.integration.TodoRootComponent |
|
||||||
import example.todo.common.ui.TodoRootContent |
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() { |
|
||||||
override fun onCreate(savedInstanceState: Bundle?) { |
|
||||||
super.onCreate(savedInstanceState) |
|
||||||
|
|
||||||
val root = todoRoot(defaultComponentContext()) |
|
||||||
|
|
||||||
setContent { |
|
||||||
ComposeAppTheme { |
|
||||||
Surface(color = MaterialTheme.colors.background) { |
|
||||||
TodoRootContent(root) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private fun todoRoot(componentContext: ComponentContext): TodoRoot = |
|
||||||
TodoRootComponent( |
|
||||||
componentContext = componentContext, |
|
||||||
storeFactory = LoggingStoreFactory(TimeTravelStoreFactory()), |
|
||||||
database = DefaultTodoSharedDatabase(TodoDatabaseDriver(context = this)) |
|
||||||
) |
|
||||||
} |
|
@ -1,11 +0,0 @@ |
|||||||
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) |
|
||||||
) |
|
@ -1,35 +0,0 @@ |
|||||||
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 |
|
||||||
) |
|
||||||
} |
|
@ -1,16 +0,0 @@ |
|||||||
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 |
|
||||||
) |
|
||||||
) |
|
@ -1,34 +0,0 @@ |
|||||||
<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> |
|
@ -1,170 +0,0 @@ |
|||||||
<?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> |
|
@ -1,5 +0,0 @@ |
|||||||
<?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> |
|
@ -1,5 +0,0 @@ |
|||||||
<?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> |
|
Before Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 8.9 KiB |
Before Width: | Height: | Size: 15 KiB |
@ -1,4 +0,0 @@ |
|||||||
<?xml version="1.0" encoding="utf-8"?> |
|
||||||
<resources> |
|
||||||
<string name="app_name">Todo</string> |
|
||||||
</resources> |
|
@ -1,19 +0,0 @@ |
|||||||
plugins { |
|
||||||
`kotlin-dsl` |
|
||||||
} |
|
||||||
|
|
||||||
allprojects { |
|
||||||
repositories { |
|
||||||
google() |
|
||||||
mavenCentral() |
|
||||||
mavenLocal() |
|
||||||
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") |
|
||||||
} |
|
||||||
|
|
||||||
afterEvaluate { |
|
||||||
// Workaround for https://youtrack.jetbrains.com/issue/KT-52776 |
|
||||||
rootProject.extensions.findByType<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension>()?.apply { |
|
||||||
versions.webpackCli.version = "4.10.0" |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,24 +0,0 @@ |
|||||||
plugins { |
|
||||||
`kotlin-dsl` |
|
||||||
} |
|
||||||
|
|
||||||
initDeps(project) |
|
||||||
|
|
||||||
repositories { |
|
||||||
mavenLocal() |
|
||||||
google() |
|
||||||
mavenCentral() |
|
||||||
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") |
|
||||||
} |
|
||||||
|
|
||||||
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") |
|
||||||
} |
|
@ -1,12 +0,0 @@ |
|||||||
plugins { |
|
||||||
`kotlin-dsl` |
|
||||||
} |
|
||||||
|
|
||||||
repositories { |
|
||||||
mavenCentral() |
|
||||||
} |
|
||||||
|
|
||||||
dependencies { |
|
||||||
//todo workaround to build iOS Arm64 simulator: |
|
||||||
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10") |
|
||||||
} |
|
@ -1,101 +0,0 @@ |
|||||||
// We store Kotlin and Compose versions in gradle.properties to |
|
||||||
// be able to override them on CI. |
|
||||||
// You probably won't need this, so you can get rid of `project` in this file. |
|
||||||
import org.gradle.api.Project |
|
||||||
|
|
||||||
lateinit var properties: Map<String, *> |
|
||||||
|
|
||||||
fun initDeps(project: Project) { |
|
||||||
properties = project.properties |
|
||||||
} |
|
||||||
|
|
||||||
object Deps { |
|
||||||
object JetpackComposeWorkaround { |
|
||||||
// Workaround for https://github.com/JetBrains/compose-jb/issues/2340 |
|
||||||
val VERSION: String = "1.4.0-rc01" |
|
||||||
} |
|
||||||
|
|
||||||
object JetBrains { |
|
||||||
object Kotlin { |
|
||||||
private val VERSION get() = properties["kotlin.version"] |
|
||||||
val gradlePlugin get() = "org.jetbrains.kotlin:kotlin-gradle-plugin:$VERSION" |
|
||||||
val testCommon get() = "org.jetbrains.kotlin:kotlin-test-common:$VERSION" |
|
||||||
val testJunit get() = "org.jetbrains.kotlin:kotlin-test-junit:$VERSION" |
|
||||||
val testJs get() = "org.jetbrains.kotlin:kotlin-test-js:$VERSION" |
|
||||||
val testAnnotationsCommon get() = "org.jetbrains.kotlin:kotlin-test-annotations-common:$VERSION" |
|
||||||
} |
|
||||||
|
|
||||||
object Coroutines { |
|
||||||
private val VERSION get() = "1.6.4" |
|
||||||
val swing get() = "org.jetbrains.kotlinx:kotlinx-coroutines-swing:$VERSION" |
|
||||||
} |
|
||||||
|
|
||||||
object Compose { |
|
||||||
private val VERSION get() = properties["compose.version"] |
|
||||||
val gradlePlugin get() = "org.jetbrains.compose:compose-gradle-plugin:$VERSION" |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
object Android { |
|
||||||
object Tools { |
|
||||||
object Build { |
|
||||||
const val gradlePlugin = "com.android.tools.build:gradle:7.2.0" |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
object AndroidX { |
|
||||||
object AppCompat { |
|
||||||
const val appCompat = "androidx.appcompat:appcompat:1.3.0" |
|
||||||
} |
|
||||||
|
|
||||||
object Activity { |
|
||||||
const val activityCompose = "androidx.activity:activity-compose:1.3.0" |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
object ArkIvanov { |
|
||||||
object MVIKotlin { |
|
||||||
private const val VERSION = "3.0.0" |
|
||||||
const val rx = "com.arkivanov.mvikotlin:rx:$VERSION" |
|
||||||
const val mvikotlin = "com.arkivanov.mvikotlin:mvikotlin:$VERSION" |
|
||||||
const val mvikotlinMain = "com.arkivanov.mvikotlin:mvikotlin-main:$VERSION" |
|
||||||
const val 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 = "1.0.0-alpha-05" |
|
||||||
const val decompose = "com.arkivanov.decompose:decompose:$VERSION" |
|
||||||
const val extensionsCompose = "com.arkivanov.decompose:extensions-compose-jetbrains:$VERSION" |
|
||||||
} |
|
||||||
|
|
||||||
object Essenty { |
|
||||||
private const val VERSION = "0.6.0" |
|
||||||
const val lifecycle = "com.arkivanov.essenty:lifecycle:$VERSION" |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
object Badoo { |
|
||||||
object Reaktive { |
|
||||||
private const val VERSION = "1.2.1" |
|
||||||
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.5.5" |
|
||||||
|
|
||||||
const val gradlePlugin = "com.squareup.sqldelight:gradle-plugin:$VERSION" |
|
||||||
const val androidDriver = "com.squareup.sqldelight:android-driver:$VERSION" |
|
||||||
const val sqliteDriver = "com.squareup.sqldelight:sqlite-driver:$VERSION" |
|
||||||
const val nativeDriver = "com.squareup.sqldelight:native-driver:$VERSION" |
|
||||||
const val sqljsDriver = "com.squareup.sqldelight:sqljs-driver:$VERSION" |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,17 +0,0 @@ |
|||||||
import org.jetbrains.kotlin.gradle.dsl.KotlinTargetContainerWithNativeShortcuts |
|
||||||
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget |
|
||||||
|
|
||||||
fun KotlinTargetContainerWithNativeShortcuts.iosWorkaroundSupportArm64Simulator( |
|
||||||
configure: KotlinNativeTarget.() -> Unit |
|
||||||
) { |
|
||||||
val isBuildToSimulator = System.getenv("SDK_NAME")?.startsWith("iphonesimulator") ?: false |
|
||||||
val isArm64Target = System.getenv("NATIVE_ARCH") == "arm64" |
|
||||||
|
|
||||||
if (isBuildToSimulator && isArm64Target) { |
|
||||||
//workaround: |
|
||||||
iosSimulatorArm64(name = "ios", configure = configure) |
|
||||||
} else { |
|
||||||
//default behavior: |
|
||||||
ios(configure = configure) |
|
||||||
} |
|
||||||
} |
|
@ -1,3 +0,0 @@ |
|||||||
# TODO can we get rid of duplication with root gradle.properties? |
|
||||||
kotlin.version=1.8.20 |
|
||||||
compose.version=1.4.0 |
|
@ -1,26 +0,0 @@ |
|||||||
plugins { |
|
||||||
id("com.android.library") |
|
||||||
} |
|
||||||
|
|
||||||
initDeps(project) |
|
||||||
|
|
||||||
android { |
|
||||||
compileSdk = 33 |
|
||||||
|
|
||||||
defaultConfig { |
|
||||||
minSdk = 23 |
|
||||||
targetSdk = 33 |
|
||||||
} |
|
||||||
|
|
||||||
compileOptions { |
|
||||||
sourceCompatibility = JavaVersion.VERSION_11 |
|
||||||
targetCompatibility = JavaVersion.VERSION_11 |
|
||||||
} |
|
||||||
|
|
||||||
sourceSets { |
|
||||||
named("main") { |
|
||||||
manifest.srcFile("src/androidMain/AndroidManifest.xml") |
|
||||||
res.srcDirs("src/androidMain/res") |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,39 +0,0 @@ |
|||||||
plugins { |
|
||||||
id("com.android.library") |
|
||||||
id("kotlin-multiplatform") |
|
||||||
id("org.jetbrains.compose") |
|
||||||
} |
|
||||||
|
|
||||||
initDeps(project) |
|
||||||
|
|
||||||
kotlin { |
|
||||||
jvm("desktop") |
|
||||||
android() |
|
||||||
|
|
||||||
sourceSets { |
|
||||||
named("commonMain") { |
|
||||||
dependencies { |
|
||||||
implementation(compose.runtime) |
|
||||||
implementation(compose.foundation) |
|
||||||
implementation(compose.material) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
named("androidMain") { |
|
||||||
dependencies { |
|
||||||
implementation("androidx.appcompat:appcompat:1.3.0") |
|
||||||
implementation("androidx.core:core-ktx:1.3.1") |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
named("desktopMain") { |
|
||||||
dependencies { |
|
||||||
implementation(compose.desktop.common) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> { |
|
||||||
kotlinOptions.jvmTarget = "11" |
|
||||||
} |
|
||||||
} |
|
@ -1,75 +0,0 @@ |
|||||||
plugins { |
|
||||||
id("com.android.library") |
|
||||||
id("kotlin-multiplatform") |
|
||||||
} |
|
||||||
|
|
||||||
initDeps(project) |
|
||||||
|
|
||||||
kotlin { |
|
||||||
jvm("desktop") |
|
||||||
android() |
|
||||||
iosX64() |
|
||||||
iosArm64() |
|
||||||
iosSimulatorArm64() |
|
||||||
|
|
||||||
js(IR) { |
|
||||||
browser() |
|
||||||
} |
|
||||||
|
|
||||||
sourceSets { |
|
||||||
create("iosMain") { |
|
||||||
dependsOn(getByName("commonMain")) |
|
||||||
} |
|
||||||
create("iosTest") { |
|
||||||
dependsOn(getByName("commonTest")) |
|
||||||
} |
|
||||||
|
|
||||||
getByName("iosX64Main") { |
|
||||||
dependsOn(getByName("iosMain")) |
|
||||||
} |
|
||||||
getByName("iosX64Test") { |
|
||||||
dependsOn(getByName("iosTest")) |
|
||||||
} |
|
||||||
|
|
||||||
getByName("iosArm64Main") { |
|
||||||
dependsOn(getByName("iosMain")) |
|
||||||
} |
|
||||||
getByName("iosArm64Test") { |
|
||||||
dependsOn(getByName("iosTest")) |
|
||||||
} |
|
||||||
|
|
||||||
getByName("iosSimulatorArm64Main") { |
|
||||||
dependsOn(getByName("iosMain")) |
|
||||||
} |
|
||||||
getByName("iosSimulatorArm64Test") { |
|
||||||
dependsOn(getByName("iosTest")) |
|
||||||
} |
|
||||||
|
|
||||||
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) |
|
||||||
} |
|
||||||
} |
|
||||||
named("jsTest") { |
|
||||||
dependencies { |
|
||||||
implementation(Deps.JetBrains.Kotlin.testJs) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> { |
|
||||||
kotlinOptions.jvmTarget = "11" |
|
||||||
} |
|
||||||
} |
|
@ -1,25 +0,0 @@ |
|||||||
plugins { |
|
||||||
id("multiplatform-compose-setup") |
|
||||||
id("android-setup") |
|
||||||
} |
|
||||||
|
|
||||||
kotlin { |
|
||||||
sourceSets { |
|
||||||
named("commonMain") { |
|
||||||
dependencies { |
|
||||||
implementation(project(":common:main")) |
|
||||||
implementation(project(":common:edit")) |
|
||||||
implementation(project(":common:root")) |
|
||||||
implementation(Deps.ArkIvanov.Decompose.decompose) |
|
||||||
implementation(Deps.ArkIvanov.Decompose.extensionsCompose) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
named("androidMain") { |
|
||||||
dependencies { |
|
||||||
// Workaround for https://github.com/JetBrains/compose-jb/issues/2340 |
|
||||||
implementation("androidx.compose.foundation:foundation:${Deps.JetpackComposeWorkaround.VERSION}") |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,2 +0,0 @@ |
|||||||
<?xml version="1.0" encoding="utf-8"?> |
|
||||||
<manifest package="example.todo.common.ui"/> |
|
@ -1,26 +0,0 @@ |
|||||||
package example.todo.common.ui |
|
||||||
|
|
||||||
import androidx.compose.foundation.lazy.LazyListState |
|
||||||
import androidx.compose.runtime.Composable |
|
||||||
import androidx.compose.ui.Modifier |
|
||||||
import androidx.compose.ui.unit.Dp |
|
||||||
import androidx.compose.ui.unit.dp |
|
||||||
|
|
||||||
actual val MARGIN_SCROLLBAR: Dp = 0.dp |
|
||||||
|
|
||||||
actual interface ScrollbarAdapter |
|
||||||
|
|
||||||
@Composable |
|
||||||
actual fun rememberScrollbarAdapter( |
|
||||||
scrollState: LazyListState, |
|
||||||
itemCount: Int, |
|
||||||
averageItemSize: Dp |
|
||||||
): ScrollbarAdapter = |
|
||||||
object : ScrollbarAdapter {} |
|
||||||
|
|
||||||
@Composable |
|
||||||
actual fun VerticalScrollbar( |
|
||||||
modifier: Modifier, |
|
||||||
adapter: ScrollbarAdapter |
|
||||||
) { |
|
||||||
} |
|
@ -1,23 +0,0 @@ |
|||||||
package example.todo.common.ui |
|
||||||
|
|
||||||
import androidx.compose.foundation.lazy.LazyListState |
|
||||||
import androidx.compose.runtime.Composable |
|
||||||
import androidx.compose.ui.Modifier |
|
||||||
import androidx.compose.ui.unit.Dp |
|
||||||
|
|
||||||
expect val MARGIN_SCROLLBAR: Dp |
|
||||||
|
|
||||||
expect interface ScrollbarAdapter |
|
||||||
|
|
||||||
@Composable |
|
||||||
expect fun rememberScrollbarAdapter( |
|
||||||
scrollState: LazyListState, |
|
||||||
itemCount: Int, |
|
||||||
averageItemSize: Dp |
|
||||||
): ScrollbarAdapter |
|
||||||
|
|
||||||
@Composable |
|
||||||
expect fun VerticalScrollbar( |
|
||||||
modifier: Modifier, |
|
||||||
adapter: ScrollbarAdapter |
|
||||||
) |
|
@ -1,15 +0,0 @@ |
|||||||
package example.todo.common.ui |
|
||||||
|
|
||||||
import androidx.compose.ui.input.key.Key |
|
||||||
import androidx.compose.ui.input.key.KeyEvent |
|
||||||
import androidx.compose.ui.input.key.key |
|
||||||
|
|
||||||
fun onKeyUp(key: Key, onEvent: () -> Unit): (KeyEvent) -> Boolean = |
|
||||||
{ keyEvent -> |
|
||||||
if (keyEvent.key == key) { |
|
||||||
onEvent() |
|
||||||
true |
|
||||||
} else { |
|
||||||
false |
|
||||||
} |
|
||||||
} |
|
@ -1,60 +0,0 @@ |
|||||||
package example.todo.common.ui |
|
||||||
|
|
||||||
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.Icon |
|
||||||
import androidx.compose.material.IconButton |
|
||||||
import androidx.compose.material.Text |
|
||||||
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.runtime.getValue |
|
||||||
import androidx.compose.ui.Alignment |
|
||||||
import androidx.compose.ui.Modifier |
|
||||||
import androidx.compose.ui.unit.dp |
|
||||||
import com.arkivanov.decompose.extensions.compose.jetbrains.subscribeAsState |
|
||||||
import example.todo.common.edit.TodoEdit |
|
||||||
|
|
||||||
@Composable |
|
||||||
fun TodoEditContent(component: TodoEdit) { |
|
||||||
val model by component.models.subscribeAsState() |
|
||||||
|
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) { |
|
||||||
TopAppBar( |
|
||||||
title = { Text("Edit todo") }, |
|
||||||
navigationIcon = { |
|
||||||
IconButton(onClick = component::onCloseClicked) { |
|
||||||
Icon( |
|
||||||
imageVector = Icons.Default.ArrowBack, |
|
||||||
contentDescription = null |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
) |
|
||||||
|
|
||||||
TextField( |
|
||||||
value = model.text, |
|
||||||
modifier = Modifier.weight(1F).fillMaxWidth().padding(8.dp), |
|
||||||
label = { Text("Todo text") }, |
|
||||||
onValueChange = component::onTextChanged |
|
||||||
) |
|
||||||
|
|
||||||
Row(modifier = Modifier.padding(8.dp), verticalAlignment = Alignment.CenterVertically) { |
|
||||||
Text(text = "Completed") |
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(8.dp)) |
|
||||||
|
|
||||||
Checkbox( |
|
||||||
checked = model.isDone, |
|
||||||
onCheckedChange = component::onDoneChanged |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,158 +0,0 @@ |
|||||||
package example.todo.common.ui |
|
||||||
|
|
||||||
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.fillMaxHeight |
|
||||||
import androidx.compose.foundation.layout.padding |
|
||||||
import androidx.compose.foundation.layout.width |
|
||||||
import androidx.compose.foundation.lazy.LazyColumn |
|
||||||
import androidx.compose.foundation.lazy.items |
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState |
|
||||||
import androidx.compose.material.Checkbox |
|
||||||
import androidx.compose.material.Divider |
|
||||||
import androidx.compose.material.Icon |
|
||||||
import androidx.compose.material.IconButton |
|
||||||
import androidx.compose.material.OutlinedTextField |
|
||||||
import androidx.compose.material.Text |
|
||||||
import androidx.compose.material.TopAppBar |
|
||||||
import androidx.compose.material.icons.Icons |
|
||||||
import androidx.compose.material.icons.filled.Add |
|
||||||
import androidx.compose.material.icons.filled.Delete |
|
||||||
import androidx.compose.runtime.Composable |
|
||||||
import androidx.compose.runtime.getValue |
|
||||||
import androidx.compose.ui.Alignment |
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi |
|
||||||
import androidx.compose.ui.Modifier |
|
||||||
import androidx.compose.ui.input.key.Key |
|
||||||
import androidx.compose.ui.input.key.onKeyEvent |
|
||||||
import androidx.compose.ui.text.AnnotatedString |
|
||||||
import androidx.compose.ui.text.style.TextOverflow |
|
||||||
import androidx.compose.ui.unit.dp |
|
||||||
import com.arkivanov.decompose.extensions.compose.jetbrains.subscribeAsState |
|
||||||
import example.todo.common.main.TodoItem |
|
||||||
import example.todo.common.main.TodoMain |
|
||||||
|
|
||||||
@Composable |
|
||||||
fun TodoMainContent(component: TodoMain) { |
|
||||||
val model by component.models.subscribeAsState() |
|
||||||
|
|
||||||
Column { |
|
||||||
TopAppBar(title = { Text(text = "Todo List") }) |
|
||||||
|
|
||||||
Box(Modifier.weight(1F)) { |
|
||||||
TodoList( |
|
||||||
items = model.items, |
|
||||||
onItemClicked = component::onItemClicked, |
|
||||||
onDoneChanged = component::onItemDoneChanged, |
|
||||||
onDeleteItemClicked = component::onItemDeleteClicked |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
TodoInput( |
|
||||||
text = model.text, |
|
||||||
onAddClicked = component::onAddItemClicked, |
|
||||||
onTextChanged = component::onInputTextChanged |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@Composable |
|
||||||
private fun TodoList( |
|
||||||
items: List<TodoItem>, |
|
||||||
onItemClicked: (id: Long) -> Unit, |
|
||||||
onDoneChanged: (id: Long, isDone: Boolean) -> Unit, |
|
||||||
onDeleteItemClicked: (id: Long) -> Unit |
|
||||||
) { |
|
||||||
Box { |
|
||||||
val listState = rememberLazyListState() |
|
||||||
|
|
||||||
LazyColumn(state = listState) { |
|
||||||
items(items) { |
|
||||||
Item( |
|
||||||
item = it, |
|
||||||
onItemClicked = onItemClicked, |
|
||||||
onDoneChanged = onDoneChanged, |
|
||||||
onDeleteItemClicked = onDeleteItemClicked |
|
||||||
) |
|
||||||
|
|
||||||
Divider() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
VerticalScrollbar( |
|
||||||
modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(), |
|
||||||
adapter = rememberScrollbarAdapter( |
|
||||||
scrollState = listState, |
|
||||||
itemCount = items.size, |
|
||||||
averageItemSize = 37.dp |
|
||||||
) |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@Composable |
|
||||||
private fun Item( |
|
||||||
item: TodoItem, |
|
||||||
onItemClicked: (id: Long) -> Unit, |
|
||||||
onDoneChanged: (id: Long, isDone: Boolean) -> Unit, |
|
||||||
onDeleteItemClicked: (id: Long) -> Unit |
|
||||||
) { |
|
||||||
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( |
|
||||||
imageVector = Icons.Default.Delete, |
|
||||||
contentDescription = null |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(MARGIN_SCROLLBAR)) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@OptIn(ExperimentalComposeUiApi::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).onKeyEvent(onKeyUp(Key.Enter, onAddClicked)), |
|
||||||
onValueChange = onTextChanged, |
|
||||||
label = { Text(text = "Add a todo") } |
|
||||||
) |
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(8.dp)) |
|
||||||
|
|
||||||
IconButton(onClick = onAddClicked) { |
|
||||||
Icon( |
|
||||||
imageVector = Icons.Default.Add, |
|
||||||
contentDescription = null |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,25 +0,0 @@ |
|||||||
@file:Suppress("EXPERIMENTAL_API_USAGE") |
|
||||||
|
|
||||||
package example.todo.common.ui |
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable |
|
||||||
import com.arkivanov.decompose.extensions.compose.jetbrains.stack.Children |
|
||||||
import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.fade |
|
||||||
import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.plus |
|
||||||
import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.scale |
|
||||||
import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.stackAnimation |
|
||||||
import example.todo.common.root.TodoRoot |
|
||||||
import example.todo.common.root.TodoRoot.Child |
|
||||||
|
|
||||||
@Composable |
|
||||||
fun TodoRootContent(component: TodoRoot) { |
|
||||||
Children( |
|
||||||
stack = component.childStack, |
|
||||||
animation = stackAnimation(fade() + scale()), |
|
||||||
) { |
|
||||||
when (val child = it.instance) { |
|
||||||
is Child.Main -> TodoMainContent(child.component) |
|
||||||
is Child.Edit -> TodoEditContent(child.component) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,34 +0,0 @@ |
|||||||
package example.todo.common.ui |
|
||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi |
|
||||||
import androidx.compose.foundation.lazy.LazyListState |
|
||||||
import androidx.compose.runtime.Composable |
|
||||||
import androidx.compose.ui.Modifier |
|
||||||
import androidx.compose.ui.unit.Dp |
|
||||||
import androidx.compose.ui.unit.dp |
|
||||||
|
|
||||||
actual val MARGIN_SCROLLBAR: Dp = 8.dp |
|
||||||
|
|
||||||
actual typealias ScrollbarAdapter = androidx.compose.foundation.v2.ScrollbarAdapter |
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class) |
|
||||||
@Composable |
|
||||||
actual fun rememberScrollbarAdapter( |
|
||||||
scrollState: LazyListState, |
|
||||||
itemCount: Int, |
|
||||||
averageItemSize: Dp |
|
||||||
): ScrollbarAdapter = |
|
||||||
androidx.compose.foundation.rememberScrollbarAdapter( |
|
||||||
scrollState = scrollState |
|
||||||
) |
|
||||||
|
|
||||||
@Composable |
|
||||||
actual fun VerticalScrollbar( |
|
||||||
modifier: Modifier, |
|
||||||
adapter: ScrollbarAdapter |
|
||||||
) { |
|
||||||
androidx.compose.foundation.VerticalScrollbar( |
|
||||||
modifier = modifier, |
|
||||||
adapter = adapter |
|
||||||
) |
|
||||||
} |
|
@ -1,28 +0,0 @@ |
|||||||
package example.todo.common.ui |
|
||||||
|
|
||||||
import androidx.compose.desktop.ui.tooling.preview.Preview |
|
||||||
import androidx.compose.runtime.Composable |
|
||||||
import com.arkivanov.decompose.value.MutableValue |
|
||||||
import com.arkivanov.decompose.value.Value |
|
||||||
import example.todo.common.edit.TodoEdit |
|
||||||
import example.todo.common.edit.TodoEdit.Model |
|
||||||
|
|
||||||
@Composable |
|
||||||
@Preview |
|
||||||
fun TodoEditContentPreview() { |
|
||||||
TodoEditContent(TodoEditPreview()) |
|
||||||
} |
|
||||||
|
|
||||||
class TodoEditPreview : TodoEdit { |
|
||||||
override val models: Value<Model> = |
|
||||||
MutableValue( |
|
||||||
Model( |
|
||||||
text = "Some text", |
|
||||||
isDone = true |
|
||||||
) |
|
||||||
) |
|
||||||
|
|
||||||
override fun onTextChanged(text: String) {} |
|
||||||
override fun onDoneChanged(isDone: Boolean) {} |
|
||||||
override fun onCloseClicked() {} |
|
||||||
} |
|
@ -1,37 +0,0 @@ |
|||||||
package example.todo.common.ui |
|
||||||
|
|
||||||
import androidx.compose.desktop.ui.tooling.preview.Preview |
|
||||||
import androidx.compose.runtime.Composable |
|
||||||
import com.arkivanov.decompose.value.MutableValue |
|
||||||
import com.arkivanov.decompose.value.Value |
|
||||||
import example.todo.common.main.TodoItem |
|
||||||
import example.todo.common.main.TodoMain |
|
||||||
import example.todo.common.main.TodoMain.Model |
|
||||||
|
|
||||||
@Preview |
|
||||||
@Composable |
|
||||||
fun TodoMainContentPreview() { |
|
||||||
TodoMainContent(TodoMainPreview()) |
|
||||||
} |
|
||||||
|
|
||||||
class TodoMainPreview : TodoMain { |
|
||||||
override val models: Value<Model> = |
|
||||||
MutableValue( |
|
||||||
Model( |
|
||||||
items = List(5) { index -> |
|
||||||
TodoItem( |
|
||||||
id = index.toLong(), |
|
||||||
text = "Item $index", |
|
||||||
isDone = index % 2 == 0 |
|
||||||
) |
|
||||||
}, |
|
||||||
text = "Some text" |
|
||||||
) |
|
||||||
) |
|
||||||
|
|
||||||
override fun onItemClicked(id: Long) {} |
|
||||||
override fun onItemDoneChanged(id: Long, isDone: Boolean) {} |
|
||||||
override fun onItemDeleteClicked(id: Long) {} |
|
||||||
override fun onInputTextChanged(text: String) {} |
|
||||||
override fun onAddItemClicked() {} |
|
||||||
} |
|
@ -1,46 +0,0 @@ |
|||||||
plugins { |
|
||||||
id("multiplatform-setup") |
|
||||||
id("android-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) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
iosMain { |
|
||||||
dependencies { |
|
||||||
implementation(Deps.Squareup.SQLDelight.nativeDriver) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
jsMain { |
|
||||||
dependencies { |
|
||||||
implementation(Deps.Squareup.SQLDelight.sqljsDriver) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,2 +0,0 @@ |
|||||||
<?xml version="1.0" encoding="utf-8"?> |
|
||||||
<manifest package="example.todo.common.database"/> |
|
@ -1,14 +0,0 @@ |
|||||||
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" |
|
||||||
) |
|
@ -1,91 +0,0 @@ |
|||||||
package example.todo.common.database |
|
||||||
|
|
||||||
import com.badoo.reaktive.base.setCancellable |
|
||||||
import com.badoo.reaktive.completable.Completable |
|
||||||
import com.badoo.reaktive.maybe.Maybe |
|
||||||
import com.badoo.reaktive.observable.Observable |
|
||||||
import com.badoo.reaktive.observable.autoConnect |
|
||||||
import com.badoo.reaktive.observable.firstOrError |
|
||||||
import com.badoo.reaktive.observable.map |
|
||||||
import com.badoo.reaktive.observable.observable |
|
||||||
import com.badoo.reaktive.observable.observeOn |
|
||||||
import com.badoo.reaktive.observable.replay |
|
||||||
import com.badoo.reaktive.scheduler.ioScheduler |
|
||||||
import com.badoo.reaktive.single.Single |
|
||||||
import com.badoo.reaktive.single.asCompletable |
|
||||||
import com.badoo.reaktive.single.asObservable |
|
||||||
import com.badoo.reaktive.single.doOnBeforeSuccess |
|
||||||
import com.badoo.reaktive.single.flatMapObservable |
|
||||||
import com.badoo.reaktive.single.map |
|
||||||
import com.badoo.reaktive.single.mapNotNull |
|
||||||
import com.badoo.reaktive.single.observeOn |
|
||||||
import com.badoo.reaktive.single.singleOf |
|
||||||
import com.squareup.sqldelight.Query |
|
||||||
import com.squareup.sqldelight.db.SqlDriver |
|
||||||
import example.todo.database.TodoDatabase |
|
||||||
|
|
||||||
class DefaultTodoSharedDatabase(driver: Single<SqlDriver>) : TodoSharedDatabase { |
|
||||||
|
|
||||||
constructor(driver: SqlDriver) : this(singleOf(driver)) |
|
||||||
|
|
||||||
private val queries: Single<TodoDatabaseQueries> = |
|
||||||
driver |
|
||||||
.map { TodoDatabase(it).todoDatabaseQueries } |
|
||||||
.asObservable() |
|
||||||
.replay() |
|
||||||
.autoConnect() |
|
||||||
.firstOrError() |
|
||||||
|
|
||||||
override fun observeAll(): Observable<List<TodoItemEntity>> = |
|
||||||
query(TodoDatabaseQueries::selectAll) |
|
||||||
.observe { it.executeAsList() } |
|
||||||
|
|
||||||
override fun select(id: Long): Maybe<TodoItemEntity> = |
|
||||||
query { it.select(id = id) } |
|
||||||
.mapNotNull { it.executeAsOneOrNull() } |
|
||||||
|
|
||||||
override fun add(text: String): Completable = |
|
||||||
execute { it.add(text = text) } |
|
||||||
|
|
||||||
override fun setText(id: Long, text: String): Completable = |
|
||||||
execute { it.setText(id = id, text = text) } |
|
||||||
|
|
||||||
override fun setDone(id: Long, isDone: Boolean): Completable = |
|
||||||
execute { it.setDone(id = id, isDone = isDone) } |
|
||||||
|
|
||||||
override fun delete(id: Long): Completable = |
|
||||||
execute { it.delete(id = id) } |
|
||||||
|
|
||||||
override fun clear(): Completable = |
|
||||||
execute { it.clear() } |
|
||||||
|
|
||||||
private fun <T : Any> query(query: (TodoDatabaseQueries) -> Query<T>): Single<Query<T>> = |
|
||||||
queries |
|
||||||
.observeOn(ioScheduler) |
|
||||||
.map(query) |
|
||||||
|
|
||||||
private fun execute(query: (TodoDatabaseQueries) -> Unit): Completable = |
|
||||||
queries |
|
||||||
.observeOn(ioScheduler) |
|
||||||
.doOnBeforeSuccess(query) |
|
||||||
.asCompletable() |
|
||||||
|
|
||||||
private fun <T : Any, R> Single<Query<T>>.observe(get: (Query<T>) -> R): Observable<R> = |
|
||||||
flatMapObservable { it.observed() } |
|
||||||
.observeOn(ioScheduler) |
|
||||||
.map(get) |
|
||||||
|
|
||||||
private fun <T : Any> Query<T>.observed(): Observable<Query<T>> = |
|
||||||
observable { emitter -> |
|
||||||
val listener = |
|
||||||
object : Query.Listener { |
|
||||||
override fun queryResultsChanged() { |
|
||||||
emitter.onNext(this@observed) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
emitter.onNext(this@observed) |
|
||||||
addListener(listener) |
|
||||||
emitter.setCancellable { removeListener(listener) } |
|
||||||
} |
|
||||||
} |
|
@ -1,105 +0,0 @@ |
|||||||
package example.todo.common.database |
|
||||||
|
|
||||||
import com.badoo.reaktive.base.invoke |
|
||||||
import com.badoo.reaktive.completable.Completable |
|
||||||
import com.badoo.reaktive.completable.completableFromFunction |
|
||||||
import com.badoo.reaktive.completable.observeOn |
|
||||||
import com.badoo.reaktive.maybe.Maybe |
|
||||||
import com.badoo.reaktive.maybe.observeOn |
|
||||||
import com.badoo.reaktive.observable.Observable |
|
||||||
import com.badoo.reaktive.observable.map |
|
||||||
import com.badoo.reaktive.observable.observeOn |
|
||||||
import com.badoo.reaktive.scheduler.Scheduler |
|
||||||
import com.badoo.reaktive.single.notNull |
|
||||||
import com.badoo.reaktive.single.singleFromFunction |
|
||||||
import com.badoo.reaktive.subject.behavior.BehaviorSubject |
|
||||||
|
|
||||||
// There were problems when using real database in JS tests, hence the in-memory test implementation |
|
||||||
class TestTodoSharedDatabase( |
|
||||||
private val scheduler: Scheduler |
|
||||||
) : TodoSharedDatabase { |
|
||||||
|
|
||||||
private val itemsSubject = BehaviorSubject<Map<Long, TodoItemEntity>>(emptyMap()) |
|
||||||
private val itemsObservable = itemsSubject.observeOn(scheduler) |
|
||||||
val testing: Testing = Testing() |
|
||||||
|
|
||||||
override fun observeAll(): Observable<List<TodoItemEntity>> = |
|
||||||
itemsObservable.map { it.values.toList() } |
|
||||||
|
|
||||||
override fun select(id: Long): Maybe<TodoItemEntity> = |
|
||||||
singleFromFunction { testing.select(id = id) } |
|
||||||
.notNull() |
|
||||||
.observeOn(scheduler) |
|
||||||
|
|
||||||
override fun add(text: String): Completable = |
|
||||||
execute { testing.add(text = text) } |
|
||||||
|
|
||||||
override fun setText(id: Long, text: String): Completable = |
|
||||||
execute { testing.setText(id = id, text = text) } |
|
||||||
|
|
||||||
override fun setDone(id: Long, isDone: Boolean): Completable = |
|
||||||
execute { testing.setDone(id = id, isDone = isDone) } |
|
||||||
|
|
||||||
override fun delete(id: Long): Completable = |
|
||||||
execute { testing.delete(id = id) } |
|
||||||
|
|
||||||
override fun clear(): Completable = |
|
||||||
execute { testing.clear() } |
|
||||||
|
|
||||||
private fun execute(block: () -> Unit): Completable = |
|
||||||
completableFromFunction(block) |
|
||||||
.observeOn(scheduler) |
|
||||||
|
|
||||||
inner class Testing { |
|
||||||
fun select(id: Long): TodoItemEntity? = |
|
||||||
itemsSubject.value[id] |
|
||||||
|
|
||||||
fun selectRequired(id: Long): TodoItemEntity = |
|
||||||
requireNotNull(select(id = id)) |
|
||||||
|
|
||||||
fun add(text: String) { |
|
||||||
updateItems { items -> |
|
||||||
val nextId = items.keys.maxOrNull()?.plus(1L) ?: 1L |
|
||||||
|
|
||||||
val item = |
|
||||||
TodoItemEntity( |
|
||||||
id = nextId, |
|
||||||
orderNum = items.size.toLong(), |
|
||||||
text = text, |
|
||||||
isDone = false |
|
||||||
) |
|
||||||
|
|
||||||
items + (nextId to item) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fun setText(id: Long, text: String) { |
|
||||||
updateItem(id = id) { it.copy(text = text) } |
|
||||||
} |
|
||||||
|
|
||||||
fun setDone(id: Long, isDone: Boolean) { |
|
||||||
updateItem(id = id) { it.copy(isDone = isDone) } |
|
||||||
} |
|
||||||
|
|
||||||
fun delete(id: Long) { |
|
||||||
updateItems { it - id } |
|
||||||
} |
|
||||||
|
|
||||||
fun clear() { |
|
||||||
updateItems { emptyMap() } |
|
||||||
} |
|
||||||
|
|
||||||
fun getLastInsertId(): Long? = |
|
||||||
itemsSubject.value.values.lastOrNull()?.id |
|
||||||
|
|
||||||
private fun updateItems(func: (Map<Long, TodoItemEntity>) -> Map<Long, TodoItemEntity>) { |
|
||||||
itemsSubject(func(itemsSubject.value)) |
|
||||||
} |
|
||||||
|
|
||||||
private fun updateItem(id: Long, func: (TodoItemEntity) -> TodoItemEntity) { |
|
||||||
updateItems { |
|
||||||
it + (id to it.getValue(id).let(func)) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,22 +0,0 @@ |
|||||||
package example.todo.common.database |
|
||||||
|
|
||||||
import com.badoo.reaktive.completable.Completable |
|
||||||
import com.badoo.reaktive.maybe.Maybe |
|
||||||
import com.badoo.reaktive.observable.Observable |
|
||||||
|
|
||||||
interface TodoSharedDatabase { |
|
||||||
|
|
||||||
fun observeAll(): Observable<List<TodoItemEntity>> |
|
||||||
|
|
||||||
fun select(id: Long): Maybe<TodoItemEntity> |
|
||||||
|
|
||||||
fun add(text: String): Completable |
|
||||||
|
|
||||||
fun setText(id: Long, text: String): Completable |
|
||||||
|
|
||||||
fun setDone(id: Long, isDone: Boolean): Completable |
|
||||||
|
|
||||||
fun delete(id: Long): Completable |
|
||||||
|
|
||||||
fun clear(): Completable |
|
||||||
} |
|
@ -1,39 +0,0 @@ |
|||||||
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(); |
|
||||||
|
|
||||||
clear: |
|
||||||
DELETE FROM TodoItemEntity; |
|
@ -1,15 +0,0 @@ |
|||||||
package example.todo.common.database |
|
||||||
|
|
||||||
import com.squareup.sqldelight.db.SqlDriver |
|
||||||
import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver |
|
||||||
import example.todo.database.TodoDatabase |
|
||||||
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 |
|
||||||
} |
|
@ -1,9 +0,0 @@ |
|||||||
package example.todo.common.database |
|
||||||
|
|
||||||
import com.squareup.sqldelight.db.SqlDriver |
|
||||||
import com.squareup.sqldelight.drivers.native.NativeSqliteDriver |
|
||||||
import example.todo.database.TodoDatabase |
|
||||||
|
|
||||||
@Suppress("FunctionName") // Factory function |
|
||||||
fun TodoDatabaseDriver(): SqlDriver = |
|
||||||
NativeSqliteDriver(TodoDatabase.Schema, "TodoDatabase.db") |
|
@ -1,10 +0,0 @@ |
|||||||
package example.todo.common.database |
|
||||||
|
|
||||||
import com.badoo.reaktive.promise.asSingle |
|
||||||
import com.badoo.reaktive.single.Single |
|
||||||
import com.squareup.sqldelight.db.SqlDriver |
|
||||||
import com.squareup.sqldelight.drivers.sqljs.initSqlDriver |
|
||||||
import example.todo.database.TodoDatabase |
|
||||||
|
|
||||||
fun todoDatabaseDriver(): Single<SqlDriver> = |
|
||||||
initSqlDriver(TodoDatabase.Schema).asSingle() |
|
@ -1,19 +0,0 @@ |
|||||||
plugins { |
|
||||||
id("multiplatform-setup") |
|
||||||
id("android-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) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,2 +0,0 @@ |
|||||||
<?xml version="1.0" encoding="utf-8"?> |
|
||||||
<manifest package="example.todo.common.edit"/> |
|
@ -1,23 +0,0 @@ |
|||||||
package example.todo.common.edit |
|
||||||
|
|
||||||
import com.arkivanov.decompose.value.Value |
|
||||||
|
|
||||||
interface TodoEdit { |
|
||||||
|
|
||||||
val models: Value<Model> |
|
||||||
|
|
||||||
fun onTextChanged(text: String) |
|
||||||
|
|
||||||
fun onDoneChanged(isDone: Boolean) |
|
||||||
|
|
||||||
fun onCloseClicked() |
|
||||||
|
|
||||||
data class Model( |
|
||||||
val text: String, |
|
||||||
val isDone: Boolean |
|
||||||
) |
|
||||||
|
|
||||||
sealed class Output { |
|
||||||
object Finished : Output() |
|
||||||
} |
|
||||||
} |
|
@ -1,6 +0,0 @@ |
|||||||
package example.todo.common.edit |
|
||||||
|
|
||||||
internal data class TodoItem( |
|
||||||
val text: String, |
|
||||||
val isDone: Boolean |
|
||||||
) |
|
@ -1,12 +0,0 @@ |
|||||||
package example.todo.common.edit.integration |
|
||||||
|
|
||||||
import example.todo.common.edit.TodoEdit.Model |
|
||||||
import example.todo.common.edit.store.TodoEditStore.State |
|
||||||
|
|
||||||
internal val stateToModel: (State) -> Model = |
|
||||||
{ |
|
||||||
Model( |
|
||||||
text = it.text, |
|
||||||
isDone = it.isDone |
|
||||||
) |
|
||||||
} |
|
@ -1,48 +0,0 @@ |
|||||||
package example.todo.common.edit.integration |
|
||||||
|
|
||||||
import com.arkivanov.decompose.ComponentContext |
|
||||||
import com.arkivanov.decompose.value.Value |
|
||||||
import com.arkivanov.decompose.value.operator.map |
|
||||||
import com.arkivanov.mvikotlin.core.store.StoreFactory |
|
||||||
import com.badoo.reaktive.base.Consumer |
|
||||||
import com.badoo.reaktive.base.invoke |
|
||||||
import example.todo.common.database.TodoSharedDatabase |
|
||||||
import example.todo.common.edit.TodoEdit |
|
||||||
import example.todo.common.edit.TodoEdit.Model |
|
||||||
import example.todo.common.edit.TodoEdit.Output |
|
||||||
import example.todo.common.edit.store.TodoEditStore.Intent |
|
||||||
import example.todo.common.edit.store.TodoEditStoreProvider |
|
||||||
import example.todo.common.utils.asValue |
|
||||||
import example.todo.common.utils.getStore |
|
||||||
|
|
||||||
class TodoEditComponent( |
|
||||||
componentContext: ComponentContext, |
|
||||||
storeFactory: StoreFactory, |
|
||||||
database: TodoSharedDatabase, |
|
||||||
itemId: Long, |
|
||||||
private val output: Consumer<Output> |
|
||||||
) : TodoEdit, ComponentContext by componentContext { |
|
||||||
|
|
||||||
private val store = |
|
||||||
instanceKeeper.getStore { |
|
||||||
TodoEditStoreProvider( |
|
||||||
storeFactory = storeFactory, |
|
||||||
database = TodoEditStoreDatabase(database = database), |
|
||||||
id = itemId |
|
||||||
).provide() |
|
||||||
} |
|
||||||
|
|
||||||
override val models: Value<Model> = store.asValue().map(stateToModel) |
|
||||||
|
|
||||||
override fun onTextChanged(text: String) { |
|
||||||
store.accept(Intent.SetText(text = text)) |
|
||||||
} |
|
||||||
|
|
||||||
override fun onDoneChanged(isDone: Boolean) { |
|
||||||
store.accept(Intent.SetDone(isDone = isDone)) |
|
||||||
} |
|
||||||
|
|
||||||
override fun onCloseClicked() { |
|
||||||
output(Output.Finished) |
|
||||||
} |
|
||||||
} |
|
@ -1,31 +0,0 @@ |
|||||||
package example.todo.common.edit.integration |
|
||||||
|
|
||||||
import com.badoo.reaktive.completable.Completable |
|
||||||
import com.badoo.reaktive.maybe.Maybe |
|
||||||
import com.badoo.reaktive.maybe.map |
|
||||||
import example.todo.common.database.TodoItemEntity |
|
||||||
import example.todo.common.database.TodoSharedDatabase |
|
||||||
import example.todo.common.edit.TodoItem |
|
||||||
import example.todo.common.edit.store.TodoEditStoreProvider.Database |
|
||||||
|
|
||||||
internal class TodoEditStoreDatabase( |
|
||||||
private val database: TodoSharedDatabase |
|
||||||
) : Database { |
|
||||||
|
|
||||||
override fun load(id: Long): Maybe<TodoItem> = |
|
||||||
database |
|
||||||
.select(id = id) |
|
||||||
.map { it.toItem() } |
|
||||||
|
|
||||||
private fun TodoItemEntity.toItem(): TodoItem = |
|
||||||
TodoItem( |
|
||||||
text = text, |
|
||||||
isDone = isDone |
|
||||||
) |
|
||||||
|
|
||||||
override fun setText(id: Long, text: String): Completable = |
|
||||||
database.setText(id = id, text = text) |
|
||||||
|
|
||||||
override fun setDone(id: Long, isDone: Boolean): Completable = |
|
||||||
database.setDone(id = id, isDone = isDone) |
|
||||||
} |
|
@ -1,24 +0,0 @@ |
|||||||
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() |
|
||||||
} |
|
||||||
} |
|
@ -1,83 +0,0 @@ |
|||||||
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 Msg { |
|
||||||
data class Loaded(val item: TodoItem) : Msg() |
|
||||||
data class TextChanged(val text: String) : Msg() |
|
||||||
data class DoneChanged(val isDone: Boolean) : Msg() |
|
||||||
} |
|
||||||
|
|
||||||
private inner class ExecutorImpl : ReaktiveExecutor<Intent, Unit, State, Msg, Label>() { |
|
||||||
override fun executeAction(action: Unit, getState: () -> State) { |
|
||||||
database |
|
||||||
.load(id = id) |
|
||||||
.map(Msg::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(Msg.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(Msg.DoneChanged(isDone = isDone)) |
|
||||||
publish(Label.Changed(TodoItem(text = state.text, isDone = isDone))) |
|
||||||
database.setDone(id = id, isDone = isDone).subscribeScoped() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private object ReducerImpl : Reducer<State, Msg> { |
|
||||||
override fun State.reduce(msg: Msg): State = |
|
||||||
when (msg) { |
|
||||||
is Msg.Loaded -> copy(text = msg.item.text, isDone = msg.item.isDone) |
|
||||||
is Msg.TextChanged -> copy(text = msg.text) |
|
||||||
is Msg.DoneChanged -> copy(isDone = msg.isDone) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
interface Database { |
|
||||||
fun load(id: Long): Maybe<TodoItem> |
|
||||||
|
|
||||||
fun setText(id: Long, text: String): Completable |
|
||||||
|
|
||||||
fun setDone(id: Long, isDone: Boolean): Completable |
|
||||||
} |
|
||||||
} |
|
@ -1,27 +0,0 @@ |
|||||||
plugins { |
|
||||||
id("multiplatform-setup") |
|
||||||
id("android-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) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,2 +0,0 @@ |
|||||||
<?xml version="1.0" encoding="utf-8"?> |
|
||||||
<manifest package="example.todo.common.main"/> |
|
@ -1,8 +0,0 @@ |
|||||||
package example.todo.common.main |
|
||||||
|
|
||||||
data class TodoItem( |
|
||||||
val id: Long = 0L, |
|
||||||
val order: Long = 0L, |
|
||||||
val text: String = "", |
|
||||||
val isDone: Boolean = false |
|
||||||
) |
|
@ -1,27 +0,0 @@ |
|||||||
package example.todo.common.main |
|
||||||
|
|
||||||
import com.arkivanov.decompose.value.Value |
|
||||||
|
|
||||||
interface TodoMain { |
|
||||||
|
|
||||||
val models: Value<Model> |
|
||||||
|
|
||||||
fun onItemClicked(id: Long) |
|
||||||
|
|
||||||
fun onItemDoneChanged(id: Long, isDone: Boolean) |
|
||||||
|
|
||||||
fun onItemDeleteClicked(id: Long) |
|
||||||
|
|
||||||
fun onInputTextChanged(text: String) |
|
||||||
|
|
||||||
fun onAddItemClicked() |
|
||||||
|
|
||||||
data class Model( |
|
||||||
val items: List<TodoItem>, |
|
||||||
val text: String |
|
||||||
) |
|
||||||
|
|
||||||
sealed class Output { |
|
||||||
data class Selected(val id: Long) : Output() |
|
||||||
} |
|
||||||
} |
|
@ -1,12 +0,0 @@ |
|||||||
package example.todo.common.main.integration |
|
||||||
|
|
||||||
import example.todo.common.main.TodoMain.Model |
|
||||||
import example.todo.common.main.store.TodoMainStore.State |
|
||||||
|
|
||||||
internal val stateToModel: (State) -> Model = |
|
||||||
{ |
|
||||||
Model( |
|
||||||
items = it.items, |
|
||||||
text = it.text |
|
||||||
) |
|
||||||
} |
|
@ -1,54 +0,0 @@ |
|||||||
package example.todo.common.main.integration |
|
||||||
|
|
||||||
import com.arkivanov.decompose.ComponentContext |
|
||||||
import com.arkivanov.decompose.value.Value |
|
||||||
import com.arkivanov.decompose.value.operator.map |
|
||||||
import com.arkivanov.mvikotlin.core.store.StoreFactory |
|
||||||
import com.badoo.reaktive.base.Consumer |
|
||||||
import com.badoo.reaktive.base.invoke |
|
||||||
import example.todo.common.database.TodoSharedDatabase |
|
||||||
import example.todo.common.main.TodoMain |
|
||||||
import example.todo.common.main.TodoMain.Model |
|
||||||
import example.todo.common.main.TodoMain.Output |
|
||||||
import example.todo.common.main.store.TodoMainStore.Intent |
|
||||||
import example.todo.common.main.store.TodoMainStoreProvider |
|
||||||
import example.todo.common.utils.asValue |
|
||||||
import example.todo.common.utils.getStore |
|
||||||
|
|
||||||
class TodoMainComponent( |
|
||||||
componentContext: ComponentContext, |
|
||||||
storeFactory: StoreFactory, |
|
||||||
database: TodoSharedDatabase, |
|
||||||
private val output: Consumer<Output> |
|
||||||
) : TodoMain, ComponentContext by componentContext { |
|
||||||
|
|
||||||
private val store = |
|
||||||
instanceKeeper.getStore { |
|
||||||
TodoMainStoreProvider( |
|
||||||
storeFactory = storeFactory, |
|
||||||
database = TodoMainStoreDatabase(database = database) |
|
||||||
).provide() |
|
||||||
} |
|
||||||
|
|
||||||
override val models: Value<Model> = store.asValue().map(stateToModel) |
|
||||||
|
|
||||||
override fun onItemClicked(id: Long) { |
|
||||||
output(Output.Selected(id = id)) |
|
||||||
} |
|
||||||
|
|
||||||
override fun onItemDoneChanged(id: Long, isDone: Boolean) { |
|
||||||
store.accept(Intent.SetItemDone(id = id, isDone = isDone)) |
|
||||||
} |
|
||||||
|
|
||||||
override fun onItemDeleteClicked(id: Long) { |
|
||||||
store.accept(Intent.DeleteItem(id = id)) |
|
||||||
} |
|
||||||
|
|
||||||
override fun onInputTextChanged(text: String) { |
|
||||||
store.accept(Intent.SetText(text = text)) |
|
||||||
} |
|
||||||
|
|
||||||
override fun onAddItemClicked() { |
|
||||||
store.accept(Intent.AddItem) |
|
||||||
} |
|
||||||
} |
|
@ -1,36 +0,0 @@ |
|||||||
package example.todo.common.main.integration |
|
||||||
|
|
||||||
import com.badoo.reaktive.completable.Completable |
|
||||||
import com.badoo.reaktive.observable.Observable |
|
||||||
import com.badoo.reaktive.observable.mapIterable |
|
||||||
import example.todo.common.database.TodoItemEntity |
|
||||||
import example.todo.common.database.TodoSharedDatabase |
|
||||||
import example.todo.common.main.TodoItem |
|
||||||
import example.todo.common.main.store.TodoMainStoreProvider |
|
||||||
|
|
||||||
internal class TodoMainStoreDatabase( |
|
||||||
private val database: TodoSharedDatabase |
|
||||||
) : TodoMainStoreProvider.Database { |
|
||||||
|
|
||||||
override val updates: Observable<List<TodoItem>> = |
|
||||||
database |
|
||||||
.observeAll() |
|
||||||
.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 = |
|
||||||
database.setDone(id = id, isDone = isDone) |
|
||||||
|
|
||||||
override fun delete(id: Long): Completable = |
|
||||||
database.delete(id = id) |
|
||||||
|
|
||||||
override fun add(text: String): Completable = |
|
||||||
database.add(text = text) |
|
||||||
} |
|
@ -1,21 +0,0 @@ |
|||||||
package example.todo.common.main.store |
|
||||||
|
|
||||||
import com.arkivanov.mvikotlin.core.store.Store |
|
||||||
import example.todo.common.main.TodoItem |
|
||||||
import example.todo.common.main.store.TodoMainStore.Intent |
|
||||||
import example.todo.common.main.store.TodoMainStore.State |
|
||||||
|
|
||||||
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 = "" |
|
||||||
) |
|
||||||
} |
|
@ -1,107 +0,0 @@ |
|||||||
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.TodoItem |
|
||||||
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 Msg { |
|
||||||
data class ItemsLoaded(val items: List<TodoItem>) : Msg() |
|
||||||
data class ItemDoneChanged(val id: Long, val isDone: Boolean) : Msg() |
|
||||||
data class ItemDeleted(val id: Long) : Msg() |
|
||||||
data class TextChanged(val text: String) : Msg() |
|
||||||
} |
|
||||||
|
|
||||||
private inner class ExecutorImpl : ReaktiveExecutor<Intent, Unit, State, Msg, Nothing>() { |
|
||||||
override fun executeAction(action: Unit, getState: () -> State) { |
|
||||||
database |
|
||||||
.updates |
|
||||||
.observeOn(mainScheduler) |
|
||||||
.map(Msg::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(Msg.TextChanged(text = intent.text)) |
|
||||||
is Intent.AddItem -> addItem(state = getState()) |
|
||||||
} |
|
||||||
|
|
||||||
private fun setItemDone(id: Long, isDone: Boolean) { |
|
||||||
dispatch(Msg.ItemDoneChanged(id = id, isDone = isDone)) |
|
||||||
database.setDone(id = id, isDone = isDone).subscribeScoped() |
|
||||||
} |
|
||||||
|
|
||||||
private fun deleteItem(id: Long) { |
|
||||||
dispatch(Msg.ItemDeleted(id = id)) |
|
||||||
database.delete(id = id).subscribeScoped() |
|
||||||
} |
|
||||||
|
|
||||||
private fun addItem(state: State) { |
|
||||||
if (state.text.isNotEmpty()) { |
|
||||||
dispatch(Msg.TextChanged(text = "")) |
|
||||||
database.add(text = state.text).subscribeScoped() |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private object ReducerImpl : Reducer<State, Msg> { |
|
||||||
override fun State.reduce(msg: Msg): State = |
|
||||||
when (msg) { |
|
||||||
is Msg.ItemsLoaded -> copy(items = msg.items.sorted()) |
|
||||||
is Msg.ItemDoneChanged -> update(id = msg.id) { copy(isDone = msg.isDone) } |
|
||||||
is Msg.ItemDeleted -> copy(items = items.filterNot { it.id == msg.id }) |
|
||||||
is Msg.TextChanged -> copy(text = msg.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 |
|
||||||
} |
|
||||||
} |
|
@ -1,142 +0,0 @@ |
|||||||
package example.todo.common.main.integration |
|
||||||
|
|
||||||
import com.arkivanov.decompose.DefaultComponentContext |
|
||||||
import com.arkivanov.essenty.lifecycle.LifecycleRegistry |
|
||||||
import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory |
|
||||||
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.TestTodoSharedDatabase |
|
||||||
import example.todo.common.database.TodoItemEntity |
|
||||||
import example.todo.common.main.TodoItem |
|
||||||
import example.todo.common.main.TodoMain.Model |
|
||||||
import example.todo.common.main.TodoMain.Output |
|
||||||
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 = TestTodoSharedDatabase(TestScheduler()) |
|
||||||
private val outputSubject = PublishSubject<Output>() |
|
||||||
private val output = outputSubject.test() |
|
||||||
private val databaseTesting = database.testing |
|
||||||
|
|
||||||
private val impl by lazy { |
|
||||||
TodoMainComponent( |
|
||||||
componentContext = DefaultComponentContext(lifecycle = lifecycle), |
|
||||||
storeFactory = DefaultStoreFactory(), |
|
||||||
database = database, |
|
||||||
output = outputSubject |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
private val model: Model get() = impl.models.value |
|
||||||
|
|
||||||
@BeforeTest |
|
||||||
fun before() { |
|
||||||
overrideSchedulers( |
|
||||||
main = { TestScheduler() }, |
|
||||||
io = { TestScheduler() } |
|
||||||
) |
|
||||||
|
|
||||||
databaseTesting.clear() |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
fun WHEN_item_added_to_database_THEN_item_displayed() { |
|
||||||
databaseTesting.add("Item1") |
|
||||||
|
|
||||||
assertEquals("Item1", firstItem().text) |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
fun WHEN_item_deleted_from_database_THEN_item_not_displayed() { |
|
||||||
databaseTesting.add("Item1") |
|
||||||
val id = lastInsertItem().id |
|
||||||
|
|
||||||
databaseTesting.delete(id = id) |
|
||||||
|
|
||||||
assertFalse(model.items.any { it.id == id }) |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
fun WHEN_item_clicked_THEN_Output_Selected_emitted() { |
|
||||||
databaseTesting.add("Item1") |
|
||||||
val id = firstItem().id |
|
||||||
|
|
||||||
impl.onItemClicked(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() { |
|
||||||
databaseTesting.add("Item1") |
|
||||||
val id = firstItem().id |
|
||||||
databaseTesting.setDone(id = id, isDone = false) |
|
||||||
|
|
||||||
impl.onItemDoneChanged(id = id, isDone = true) |
|
||||||
|
|
||||||
assertTrue(databaseTesting.selectRequired(id = id).isDone) |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
fun GIVEN_item_isDone_true_WHEN_done_changed_to_false_THEN_item_isDone_false_in_database() { |
|
||||||
databaseTesting.add("Item1") |
|
||||||
val id = firstItem().id |
|
||||||
databaseTesting.setDone(id = id, isDone = true) |
|
||||||
|
|
||||||
impl.onItemDoneChanged(id = id, isDone = false) |
|
||||||
|
|
||||||
assertFalse(databaseTesting.selectRequired(id = id).isDone) |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
fun WHEN_item_delete_clicked_THEN_item_deleted_in_database() { |
|
||||||
databaseTesting.add("Item1") |
|
||||||
val id = firstItem().id |
|
||||||
|
|
||||||
impl.onItemDeleteClicked(id = id) |
|
||||||
|
|
||||||
assertNull(databaseTesting.select(id = id)) |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
fun WHEN_item_text_changed_in_database_THEN_item_updated() { |
|
||||||
databaseTesting.add("Item1") |
|
||||||
val id = firstItem().id |
|
||||||
|
|
||||||
databaseTesting.setText(id = id, text = "New text") |
|
||||||
|
|
||||||
assertEquals("New text", firstItem().text) |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
fun WHEN_input_text_changed_THEN_text_updated() { |
|
||||||
impl.onInputTextChanged(text = "Item text") |
|
||||||
|
|
||||||
assertEquals("Item text", model.text) |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
fun GIVEN_input_text_entered_WHEN_add_item_clicked_THEN_item_added_in_database() { |
|
||||||
impl.onInputTextChanged(text = "Item text") |
|
||||||
|
|
||||||
impl.onAddItemClicked() |
|
||||||
|
|
||||||
assertEquals("Item text", lastInsertItem().text) |
|
||||||
} |
|
||||||
|
|
||||||
private fun firstItem(): TodoItem = model.items[0] |
|
||||||
|
|
||||||
private fun lastInsertItem(): TodoItemEntity = |
|
||||||
databaseTesting.selectRequired(id = requireNotNull(databaseTesting.getLastInsertId())) |
|
||||||
} |
|
@ -1,40 +0,0 @@ |
|||||||
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 |
|
||||||
import example.todo.common.main.TodoItem |
|
||||||
|
|
||||||
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.maxByOrNull(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 } |
|
||||||
} |
|
||||||
} |
|
@ -1,135 +0,0 @@ |
|||||||
package example.todo.common.main.store |
|
||||||
|
|
||||||
import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory |
|
||||||
import com.badoo.reaktive.scheduler.overrideSchedulers |
|
||||||
import com.badoo.reaktive.test.scheduler.TestScheduler |
|
||||||
import example.todo.common.main.TodoItem |
|
||||||
import example.todo.common.main.store.TodoMainStore.Intent |
|
||||||
import kotlin.test.BeforeTest |
|
||||||
import kotlin.test.Test |
|
||||||
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) |
|
||||||
} |
|
||||||
} |
|
@ -1,55 +0,0 @@ |
|||||||
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget |
|
||||||
import org.jetbrains.kotlin.konan.target.Family |
|
||||||
|
|
||||||
plugins { |
|
||||||
id("multiplatform-setup") |
|
||||||
id("android-setup") |
|
||||||
id("kotlin-parcelize") |
|
||||||
} |
|
||||||
|
|
||||||
kotlin { |
|
||||||
targets |
|
||||||
.filterIsInstance<KotlinNativeTarget>() |
|
||||||
.filter { it.konanTarget.family == Family.IOS } |
|
||||||
.forEach { target -> |
|
||||||
target.binaries { |
|
||||||
framework { |
|
||||||
baseName = "Todo" |
|
||||||
linkerOpts.add("-lsqlite3") |
|
||||||
export(project(":common:database")) |
|
||||||
export(project(":common:main")) |
|
||||||
export(project(":common:edit")) |
|
||||||
export(Deps.ArkIvanov.Decompose.decompose) |
|
||||||
export(Deps.ArkIvanov.MVIKotlin.mvikotlinMain) |
|
||||||
export(Deps.ArkIvanov.Essenty.lifecycle) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
sourceSets { |
|
||||||
named("commonMain") { |
|
||||||
dependencies { |
|
||||||
implementation(project(":common:utils")) |
|
||||||
implementation(project(":common:database")) |
|
||||||
implementation(project(":common:main")) |
|
||||||
implementation(project(":common:edit")) |
|
||||||
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlin) |
|
||||||
implementation(Deps.ArkIvanov.Decompose.decompose) |
|
||||||
implementation(Deps.Badoo.Reaktive.reaktive) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
sourceSets { |
|
||||||
named("iosMain") { |
|
||||||
dependencies { |
|
||||||
api(project(":common:database")) |
|
||||||
api(project(":common:main")) |
|
||||||
api(project(":common:edit")) |
|
||||||
api(Deps.ArkIvanov.Decompose.decompose) |
|
||||||
api(Deps.ArkIvanov.MVIKotlin.mvikotlinMain) |
|
||||||
api(Deps.ArkIvanov.Essenty.lifecycle) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,2 +0,0 @@ |
|||||||
<?xml version="1.0" encoding="utf-8"?> |
|
||||||
<manifest package="example.todo.common.root"/> |
|
@ -1,16 +0,0 @@ |
|||||||
package example.todo.common.root |
|
||||||
|
|
||||||
import com.arkivanov.decompose.router.stack.ChildStack |
|
||||||
import com.arkivanov.decompose.value.Value |
|
||||||
import example.todo.common.edit.TodoEdit |
|
||||||
import example.todo.common.main.TodoMain |
|
||||||
|
|
||||||
interface TodoRoot { |
|
||||||
|
|
||||||
val childStack: Value<ChildStack<*, Child>> |
|
||||||
|
|
||||||
sealed class Child { |
|
||||||
data class Main(val component: TodoMain) : Child() |
|
||||||
data class Edit(val component: TodoEdit) : Child() |
|
||||||
} |
|
||||||
} |
|
@ -1,89 +0,0 @@ |
|||||||
package example.todo.common.root.integration |
|
||||||
|
|
||||||
import com.arkivanov.decompose.ComponentContext |
|
||||||
import com.arkivanov.decompose.router.stack.ChildStack |
|
||||||
import com.arkivanov.decompose.router.stack.StackNavigation |
|
||||||
import com.arkivanov.decompose.router.stack.childStack |
|
||||||
import com.arkivanov.decompose.router.stack.pop |
|
||||||
import com.arkivanov.decompose.router.stack.push |
|
||||||
import com.arkivanov.decompose.value.Value |
|
||||||
import com.arkivanov.essenty.parcelable.Parcelable |
|
||||||
import com.arkivanov.essenty.parcelable.Parcelize |
|
||||||
import com.arkivanov.mvikotlin.core.store.StoreFactory |
|
||||||
import com.badoo.reaktive.base.Consumer |
|
||||||
import example.todo.common.database.TodoSharedDatabase |
|
||||||
import example.todo.common.edit.TodoEdit |
|
||||||
import example.todo.common.edit.integration.TodoEditComponent |
|
||||||
import example.todo.common.main.TodoMain |
|
||||||
import example.todo.common.main.integration.TodoMainComponent |
|
||||||
import example.todo.common.root.TodoRoot |
|
||||||
import example.todo.common.root.TodoRoot.Child |
|
||||||
import example.todo.common.utils.Consumer |
|
||||||
|
|
||||||
class TodoRootComponent internal constructor( |
|
||||||
componentContext: ComponentContext, |
|
||||||
private val todoMain: (ComponentContext, Consumer<TodoMain.Output>) -> TodoMain, |
|
||||||
private val todoEdit: (ComponentContext, itemId: Long, Consumer<TodoEdit.Output>) -> TodoEdit |
|
||||||
) : TodoRoot, ComponentContext by componentContext { |
|
||||||
|
|
||||||
constructor( |
|
||||||
componentContext: ComponentContext, |
|
||||||
storeFactory: StoreFactory, |
|
||||||
database: TodoSharedDatabase |
|
||||||
) : this( |
|
||||||
componentContext = componentContext, |
|
||||||
todoMain = { childContext, output -> |
|
||||||
TodoMainComponent( |
|
||||||
componentContext = childContext, |
|
||||||
storeFactory = storeFactory, |
|
||||||
database = database, |
|
||||||
output = output |
|
||||||
) |
|
||||||
}, |
|
||||||
todoEdit = { childContext, itemId, output -> |
|
||||||
TodoEditComponent( |
|
||||||
componentContext = childContext, |
|
||||||
storeFactory = storeFactory, |
|
||||||
database = database, |
|
||||||
itemId = itemId, |
|
||||||
output = output |
|
||||||
) |
|
||||||
} |
|
||||||
) |
|
||||||
|
|
||||||
private val navigation = StackNavigation<Configuration>() |
|
||||||
|
|
||||||
private val stack = |
|
||||||
childStack( |
|
||||||
source = navigation, |
|
||||||
initialConfiguration = Configuration.Main, |
|
||||||
handleBackButton = true, |
|
||||||
childFactory = ::createChild |
|
||||||
) |
|
||||||
|
|
||||||
override val childStack: Value<ChildStack<*, Child>> = stack |
|
||||||
|
|
||||||
private fun createChild(configuration: Configuration, componentContext: ComponentContext): Child = |
|
||||||
when (configuration) { |
|
||||||
is Configuration.Main -> Child.Main(todoMain(componentContext, Consumer(::onMainOutput))) |
|
||||||
is Configuration.Edit -> Child.Edit(todoEdit(componentContext, configuration.itemId, Consumer(::onEditOutput))) |
|
||||||
} |
|
||||||
|
|
||||||
private fun onMainOutput(output: TodoMain.Output): Unit = |
|
||||||
when (output) { |
|
||||||
is TodoMain.Output.Selected -> navigation.push(Configuration.Edit(itemId = output.id)) |
|
||||||
} |
|
||||||
|
|
||||||
private fun onEditOutput(output: TodoEdit.Output): Unit = |
|
||||||
when (output) { |
|
||||||
is TodoEdit.Output.Finished -> navigation.pop() |
|
||||||
} |
|
||||||
|
|
||||||
private sealed class Configuration : Parcelable { |
|
||||||
@Parcelize |
|
||||||
object Main : Configuration() |
|
||||||
|
|
||||||
@Parcelize |
|
||||||
data class Edit(val itemId: Long) : Configuration() |
|
||||||
} |
|
||||||
} |
|
@ -1,24 +0,0 @@ |
|||||||
package example.todo.common.root.integration |
|
||||||
|
|
||||||
import com.arkivanov.decompose.value.Value |
|
||||||
import com.badoo.reaktive.base.Consumer |
|
||||||
import example.todo.common.edit.TodoEdit |
|
||||||
import example.todo.common.edit.TodoEdit.Model |
|
||||||
import example.todo.common.edit.TodoEdit.Output |
|
||||||
|
|
||||||
class TodoEditFake( |
|
||||||
val itemId: Long, |
|
||||||
val output: Consumer<Output> |
|
||||||
) : TodoEdit { |
|
||||||
|
|
||||||
override val models: Value<Model> get() = TODO("Not used") |
|
||||||
|
|
||||||
override fun onTextChanged(text: String) { |
|
||||||
} |
|
||||||
|
|
||||||
override fun onDoneChanged(isDone: Boolean) { |
|
||||||
} |
|
||||||
|
|
||||||
override fun onCloseClicked() { |
|
||||||
} |
|
||||||
} |
|
@ -1,29 +0,0 @@ |
|||||||
package example.todo.common.root.integration |
|
||||||
|
|
||||||
import com.arkivanov.decompose.value.Value |
|
||||||
import com.badoo.reaktive.base.Consumer |
|
||||||
import example.todo.common.main.TodoMain |
|
||||||
import example.todo.common.main.TodoMain.Model |
|
||||||
import example.todo.common.main.TodoMain.Output |
|
||||||
|
|
||||||
class TodoMainFake( |
|
||||||
val output: Consumer<Output> |
|
||||||
) : TodoMain { |
|
||||||
|
|
||||||
override val models: Value<Model> get() = TODO("Not used") |
|
||||||
|
|
||||||
override fun onItemClicked(id: Long) { |
|
||||||
} |
|
||||||
|
|
||||||
override fun onItemDoneChanged(id: Long, isDone: Boolean) { |
|
||||||
} |
|
||||||
|
|
||||||
override fun onItemDeleteClicked(id: Long) { |
|
||||||
} |
|
||||||
|
|
||||||
override fun onInputTextChanged(text: String) { |
|
||||||
} |
|
||||||
|
|
||||||
override fun onAddItemClicked() { |
|
||||||
} |
|
||||||
} |
|
@ -1,66 +0,0 @@ |
|||||||
package example.todo.common.root.integration |
|
||||||
|
|
||||||
import com.arkivanov.decompose.DefaultComponentContext |
|
||||||
import com.arkivanov.essenty.lifecycle.LifecycleRegistry |
|
||||||
import com.arkivanov.essenty.lifecycle.resume |
|
||||||
import com.badoo.reaktive.base.invoke |
|
||||||
import example.todo.common.edit.TodoEdit |
|
||||||
import example.todo.common.main.TodoMain |
|
||||||
import example.todo.common.root.TodoRoot |
|
||||||
import example.todo.common.root.TodoRoot.Child |
|
||||||
import kotlin.test.Test |
|
||||||
import kotlin.test.assertEquals |
|
||||||
import kotlin.test.assertTrue |
|
||||||
|
|
||||||
@Suppress("TestFunctionName") |
|
||||||
class TodoRootTest { |
|
||||||
|
|
||||||
private val lifecycle = LifecycleRegistry().apply { resume() } |
|
||||||
|
|
||||||
@Test |
|
||||||
fun WHEN_created_THEN_TodoMain_displayed() { |
|
||||||
val root = root() |
|
||||||
|
|
||||||
assertTrue(root.activeChild is Child.Main) |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
fun GIVEN_TodoMain_displayed_WHEN_TodoMain_Output_Selected_THEN_TodoEdit_displayed() { |
|
||||||
val root = root() |
|
||||||
|
|
||||||
root.activeChild.asTodoMain().output(TodoMain.Output.Selected(id = 10L)) |
|
||||||
|
|
||||||
assertTrue(root.activeChild is Child.Edit) |
|
||||||
assertEquals(10L, root.activeChild.asTodoEdit().itemId) |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
fun GIVEN_TodoEdit_displayed_WHEN_TodoEdit_Output_Finished_THEN_TodoMain_displayed() { |
|
||||||
val root = root() |
|
||||||
root.activeChild.asTodoMain().output(TodoMain.Output.Selected(id = 10L)) |
|
||||||
|
|
||||||
root.activeChild.asTodoEdit().output(TodoEdit.Output.Finished) |
|
||||||
|
|
||||||
assertTrue(root.activeChild is Child.Main) |
|
||||||
} |
|
||||||
|
|
||||||
private fun root(): TodoRoot = |
|
||||||
TodoRootComponent( |
|
||||||
componentContext = DefaultComponentContext(lifecycle = lifecycle), |
|
||||||
todoMain = { _, output -> TodoMainFake(output) }, |
|
||||||
todoEdit = { _, itemId, output -> TodoEditFake(itemId, output) } |
|
||||||
) |
|
||||||
|
|
||||||
private val TodoRoot.activeChild: Child get() = childStack.value.active.instance |
|
||||||
|
|
||||||
private val Child.component: Any |
|
||||||
get() = |
|
||||||
when (this) { |
|
||||||
is Child.Main -> component |
|
||||||
is Child.Edit -> component |
|
||||||
} |
|
||||||
|
|
||||||
private fun Child.asTodoMain(): TodoMainFake = component as TodoMainFake |
|
||||||
|
|
||||||
private fun Child.asTodoEdit(): TodoEditFake = component as TodoEditFake |
|
||||||
} |
|
@ -1,17 +0,0 @@ |
|||||||
plugins { |
|
||||||
id("multiplatform-setup") |
|
||||||
id("android-setup") |
|
||||||
} |
|
||||||
|
|
||||||
kotlin { |
|
||||||
sourceSets { |
|
||||||
named("commonMain") { |
|
||||||
dependencies { |
|
||||||
implementation(Deps.ArkIvanov.MVIKotlin.rx) |
|
||||||
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlin) |
|
||||||
implementation(Deps.ArkIvanov.Decompose.decompose) |
|
||||||
implementation(Deps.Badoo.Reaktive.reaktive) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,2 +0,0 @@ |
|||||||
<?xml version="1.0" encoding="utf-8"?> |
|
||||||
<manifest package="example.todo.common.list"/> |
|
@ -1,20 +0,0 @@ |
|||||||
package example.todo.common.utils |
|
||||||
|
|
||||||
import com.arkivanov.essenty.instancekeeper.InstanceKeeper |
|
||||||
import com.arkivanov.essenty.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() |
|
||||||
} |
|
||||||
} |
|
@ -1,11 +0,0 @@ |
|||||||
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) |
|
||||||
} |
|
||||||
} |
|
@ -1,22 +0,0 @@ |
|||||||
package example.todo.common.utils |
|
||||||
|
|
||||||
import com.arkivanov.decompose.value.Value |
|
||||||
import com.arkivanov.mvikotlin.core.store.Store |
|
||||||
import com.arkivanov.mvikotlin.rx.Disposable |
|
||||||
|
|
||||||
fun <T : Any> Store<*, T, *>.asValue(): Value<T> = |
|
||||||
object : Value<T>() { |
|
||||||
override val value: T get() = state |
|
||||||
private var disposables = emptyMap<(T) -> Unit, Disposable>() |
|
||||||
|
|
||||||
override fun subscribe(observer: (T) -> Unit) { |
|
||||||
val disposable = states(com.arkivanov.mvikotlin.rx.observer(onNext = observer)) |
|
||||||
this.disposables += observer to disposable |
|
||||||
} |
|
||||||
|
|
||||||
override fun unsubscribe(observer: (T) -> Unit) { |
|
||||||
val disposable = disposables[observer] ?: return |
|
||||||
this.disposables -= observer |
|
||||||
disposable.dispose() |
|
||||||
} |
|
||||||
} |
|
@ -1,51 +0,0 @@ |
|||||||
import org.jetbrains.compose.desktop.application.dsl.TargetFormat |
|
||||||
|
|
||||||
plugins { |
|
||||||
kotlin("multiplatform") // kotlin("jvm") doesn't work well in IDEA/AndroidStudio (https://github.com/JetBrains/compose-jb/issues/22) |
|
||||||
id("org.jetbrains.compose") |
|
||||||
} |
|
||||||
|
|
||||||
kotlin { |
|
||||||
jvm { |
|
||||||
withJava() |
|
||||||
} |
|
||||||
|
|
||||||
sourceSets { |
|
||||||
named("jvmMain") { |
|
||||||
dependencies { |
|
||||||
implementation(compose.desktop.currentOs) |
|
||||||
implementation(project(":common:utils")) |
|
||||||
implementation(project(":common:database")) |
|
||||||
implementation(project(":common:root")) |
|
||||||
implementation(project(":common:compose-ui")) |
|
||||||
implementation(Deps.JetBrains.Coroutines.swing) |
|
||||||
implementation(Deps.ArkIvanov.Decompose.decompose) |
|
||||||
implementation(Deps.ArkIvanov.Decompose.extensionsCompose) |
|
||||||
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlin) |
|
||||||
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlinMain) |
|
||||||
implementation(Deps.Badoo.Reaktive.reaktive) |
|
||||||
implementation(Deps.Badoo.Reaktive.coroutinesInterop) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
compose.desktop { |
|
||||||
application { |
|
||||||
mainClass = "example.todo.desktop.MainKt" |
|
||||||
|
|
||||||
nativeDistributions { |
|
||||||
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) |
|
||||||
packageName = "ComposeDesktopTodo" |
|
||||||
packageVersion = "1.0.0" |
|
||||||
|
|
||||||
modules("java.sql") |
|
||||||
|
|
||||||
windows { |
|
||||||
menuGroup = "Compose Examples" |
|
||||||
// see https://wixtoolset.org/documentation/manual/v3/howtos/general/generate_guids.html |
|
||||||
upgradeUuid = "BF9CDA6A-1391-46D5-9ED5-383D6E68CCEB" |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,53 +0,0 @@ |
|||||||
package example.todo.desktop |
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize |
|
||||||
import androidx.compose.material.MaterialTheme |
|
||||||
import androidx.compose.material.Surface |
|
||||||
import androidx.compose.ui.Modifier |
|
||||||
import androidx.compose.ui.window.Window |
|
||||||
import androidx.compose.ui.window.application |
|
||||||
import androidx.compose.ui.window.rememberWindowState |
|
||||||
import com.arkivanov.decompose.ComponentContext |
|
||||||
import com.arkivanov.decompose.DefaultComponentContext |
|
||||||
import com.arkivanov.decompose.extensions.compose.jetbrains.lifecycle.LifecycleController |
|
||||||
import com.arkivanov.essenty.lifecycle.LifecycleRegistry |
|
||||||
import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory |
|
||||||
import com.badoo.reaktive.coroutinesinterop.asScheduler |
|
||||||
import com.badoo.reaktive.scheduler.overrideSchedulers |
|
||||||
import example.todo.common.database.DefaultTodoSharedDatabase |
|
||||||
import example.todo.common.database.TodoDatabaseDriver |
|
||||||
import example.todo.common.root.TodoRoot |
|
||||||
import example.todo.common.root.integration.TodoRootComponent |
|
||||||
import example.todo.common.ui.TodoRootContent |
|
||||||
import kotlinx.coroutines.Dispatchers |
|
||||||
|
|
||||||
fun main() { |
|
||||||
overrideSchedulers(main = Dispatchers.Main::asScheduler) |
|
||||||
|
|
||||||
val lifecycle = LifecycleRegistry() |
|
||||||
val root = todoRoot(DefaultComponentContext(lifecycle = lifecycle)) |
|
||||||
|
|
||||||
application { |
|
||||||
val windowState = rememberWindowState() |
|
||||||
LifecycleController(lifecycle, windowState) |
|
||||||
|
|
||||||
Window( |
|
||||||
onCloseRequest = ::exitApplication, |
|
||||||
state = windowState, |
|
||||||
title = "Todo" |
|
||||||
) { |
|
||||||
Surface(modifier = Modifier.fillMaxSize()) { |
|
||||||
MaterialTheme { |
|
||||||
TodoRootContent(root) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private fun todoRoot(componentContext: ComponentContext): TodoRoot = |
|
||||||
TodoRootComponent( |
|
||||||
componentContext = componentContext, |
|
||||||
storeFactory = DefaultStoreFactory(), |
|
||||||
database = DefaultTodoSharedDatabase(TodoDatabaseDriver()) |
|
||||||
) |
|
@ -1,31 +0,0 @@ |
|||||||
# 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 |
|
||||||
kotlin.native.disableCompilerDaemon=true |
|
||||||
|
|
||||||
#TODO remove workaround after upgrading to AGP 7.3 |
|
||||||
# After updating Compose Multiplatform version, update corresponding Jetpack Compose versions |
|
||||||
# in Android module (search "Workaround for https://github.com/JetBrains/compose-jb/issues/2340") |
|
||||||
#TODO also change version in buildSrc/gradle.properties |
|
||||||
kotlin.version=1.9.0 |
|
||||||
compose.version=1.4.3 |
|
@ -1,5 +0,0 @@ |
|||||||
distributionBase=GRADLE_USER_HOME |
|
||||||
distributionPath=wrapper/dists |
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip |
|
||||||
zipStoreBase=GRADLE_USER_HOME |
|
||||||
zipStorePath=wrapper/dists |
|
@ -1,240 +0,0 @@ |
|||||||
#!/bin/sh |
|
||||||
|
|
||||||
# |
|
||||||
# Copyright © 2015-2021 the original 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 POSIX generated by Gradle. |
|
||||||
# |
|
||||||
# Important for running: |
|
||||||
# |
|
||||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is |
|
||||||
# noncompliant, but you have some other compliant shell such as ksh or |
|
||||||
# bash, then to run this script, type that shell name before the whole |
|
||||||
# command line, like: |
|
||||||
# |
|
||||||
# ksh Gradle |
|
||||||
# |
|
||||||
# Busybox and similar reduced shells will NOT work, because this script |
|
||||||
# requires all of these POSIX shell features: |
|
||||||
# * functions; |
|
||||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», |
|
||||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»; |
|
||||||
# * compound commands having a testable exit status, especially «case»; |
|
||||||
# * various built-in commands including «command», «set», and «ulimit». |
|
||||||
# |
|
||||||
# Important for patching: |
|
||||||
# |
|
||||||
# (2) This script targets any POSIX shell, so it avoids extensions provided |
|
||||||
# by Bash, Ksh, etc; in particular arrays are avoided. |
|
||||||
# |
|
||||||
# The "traditional" practice of packing multiple parameters into a |
|
||||||
# space-separated string is a well documented source of bugs and security |
|
||||||
# problems, so this is (mostly) avoided, by progressively accumulating |
|
||||||
# options in "$@", and eventually passing that to Java. |
|
||||||
# |
|
||||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, |
|
||||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; |
|
||||||
# see the in-line comments for details. |
|
||||||
# |
|
||||||
# There are tweaks for specific operating systems such as AIX, CygWin, |
|
||||||
# Darwin, MinGW, and NonStop. |
|
||||||
# |
|
||||||
# (3) This script is generated from the Groovy template |
|
||||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt |
|
||||||
# within the Gradle project. |
|
||||||
# |
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/. |
|
||||||
# |
|
||||||
############################################################################## |
|
||||||
|
|
||||||
# Attempt to set APP_HOME |
|
||||||
|
|
||||||
# Resolve links: $0 may be a link |
|
||||||
app_path=$0 |
|
||||||
|
|
||||||
# Need this for daisy-chained symlinks. |
|
||||||
while |
|
||||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path |
|
||||||
[ -h "$app_path" ] |
|
||||||
do |
|
||||||
ls=$( ls -ld "$app_path" ) |
|
||||||
link=${ls#*' -> '} |
|
||||||
case $link in #( |
|
||||||
/*) app_path=$link ;; #( |
|
||||||
*) app_path=$APP_HOME$link ;; |
|
||||||
esac |
|
||||||
done |
|
||||||
|
|
||||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit |
|
||||||
|
|
||||||
APP_NAME="Gradle" |
|
||||||
APP_BASE_NAME=${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 "$*" |
|
||||||
} >&2 |
|
||||||
|
|
||||||
die () { |
|
||||||
echo |
|
||||||
echo "$*" |
|
||||||
echo |
|
||||||
exit 1 |
|
||||||
} >&2 |
|
||||||
|
|
||||||
# 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 ;; #( |
|
||||||
MSYS* | 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" && ! "$darwin" && ! "$nonstop" ; then |
|
||||||
case $MAX_FD in #( |
|
||||||
max*) |
|
||||||
MAX_FD=$( ulimit -H -n ) || |
|
||||||
warn "Could not query maximum file descriptor limit" |
|
||||||
esac |
|
||||||
case $MAX_FD in #( |
|
||||||
'' | soft) :;; #( |
|
||||||
*) |
|
||||||
ulimit -n "$MAX_FD" || |
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD" |
|
||||||
esac |
|
||||||
fi |
|
||||||
|
|
||||||
# Collect all arguments for the java command, stacking in reverse order: |
|
||||||
# * args from the command line |
|
||||||
# * the main class name |
|
||||||
# * -classpath |
|
||||||
# * -D...appname settings |
|
||||||
# * --module-path (only if needed) |
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. |
|
||||||
|
|
||||||
# For Cygwin or MSYS, switch paths to Windows format before running java |
|
||||||
if "$cygwin" || "$msys" ; then |
|
||||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) |
|
||||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) |
|
||||||
|
|
||||||
JAVACMD=$( cygpath --unix "$JAVACMD" ) |
|
||||||
|
|
||||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh |
|
||||||
for arg do |
|
||||||
if |
|
||||||
case $arg in #( |
|
||||||
-*) false ;; # don't mess with options #( |
|
||||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath |
|
||||||
[ -e "$t" ] ;; #( |
|
||||||
*) false ;; |
|
||||||
esac |
|
||||||
then |
|
||||||
arg=$( cygpath --path --ignore --mixed "$arg" ) |
|
||||||
fi |
|
||||||
# Roll the args list around exactly as many times as the number of |
|
||||||
# args, so each arg winds up back in the position where it started, but |
|
||||||
# possibly modified. |
|
||||||
# |
|
||||||
# NB: a `for` loop captures its iteration list before it begins, so |
|
||||||
# changing the positional parameters here affects neither the number of |
|
||||||
# iterations, nor the values presented in `arg`. |
|
||||||
shift # remove old arg |
|
||||||
set -- "$@" "$arg" # push replacement arg |
|
||||||
done |
|
||||||
fi |
|
||||||
|
|
||||||
# Collect all arguments for the java command; |
|
||||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of |
|
||||||
# shell script including quotes and variable substitutions, so put them in |
|
||||||
# double quotes to make sure that they get re-expanded; and |
|
||||||
# * put everything else in single quotes, so that it's not re-expanded. |
|
||||||
|
|
||||||
set -- \ |
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \ |
|
||||||
-classpath "$CLASSPATH" \ |
|
||||||
org.gradle.wrapper.GradleWrapperMain \ |
|
||||||
"$@" |
|
||||||
|
|
||||||
# Stop when "xargs" is not available. |
|
||||||
if ! command -v xargs >/dev/null 2>&1 |
|
||||||
then |
|
||||||
die "xargs is not available" |
|
||||||
fi |
|
||||||
|
|
||||||
# Use "xargs" to parse quoted args. |
|
||||||
# |
|
||||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed. |
|
||||||
# |
|
||||||
# In Bash we could simply go: |
|
||||||
# |
|
||||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) && |
|
||||||
# set -- "${ARGS[@]}" "$@" |
|
||||||
# |
|
||||||
# but POSIX shell has neither arrays nor command substitution, so instead we |
|
||||||
# post-process each arg (as a line of input to sed) to backslash-escape any |
|
||||||
# character that might be a shell metacharacter, then use eval to reverse |
|
||||||
# that process (while maintaining the separation between arguments), and wrap |
|
||||||
# the whole thing up as a single "set" statement. |
|
||||||
# |
|
||||||
# This will of course break if any of these variables contains a newline or |
|
||||||
# an unmatched quote. |
|
||||||
# |
|
||||||
|
|
||||||
eval "set -- $( |
|
||||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | |
|
||||||
xargs -n1 | |
|
||||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | |
|
||||||
tr '\n' ' ' |
|
||||||
)" '"$@"' |
|
||||||
|
|
||||||
exec "$JAVACMD" "$@" |
|
@ -1,91 +0,0 @@ |
|||||||
@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% equ 0 goto execute |
|
||||||
|
|
||||||
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 execute |
|
||||||
|
|
||||||
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 |
|
||||||
|
|
||||||
: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 %* |
|
||||||
|
|
||||||
:end |
|
||||||
@rem End local scope for the variables with windows NT shell |
|
||||||
if %ERRORLEVEL% equ 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! |
|
||||||
set EXIT_CODE=%ERRORLEVEL% |
|
||||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1 |
|
||||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% |
|
||||||
exit /b %EXIT_CODE% |
|
||||||
|
|
||||||
:mainEnd |
|
||||||
if "%OS%"=="Windows_NT" endlocal |
|
||||||
|
|
||||||
:omega |
|
@ -1,18 +0,0 @@ |
|||||||
DerivedData/ |
|
||||||
*.pbxuser |
|
||||||
!default.pbxuser |
|
||||||
*.mode1v3 |
|
||||||
!default.mode1v3 |
|
||||||
*.mode2v3 |
|
||||||
!default.mode2v3 |
|
||||||
*.perspectivev3 |
|
||||||
!default.perspectivev3 |
|
||||||
xcuserdata/ |
|
||||||
*.moved-aside |
|
||||||
*.xccheckout |
|
||||||
*.xcscmblueprint |
|
||||||
*.hmap |
|
||||||
*.ipa |
|
||||||
*.dSYM.zip |
|
||||||
*.dSYM |
|
||||||
Pods |
|
@ -1,410 +0,0 @@ |
|||||||
// !$*UTF8*$! |
|
||||||
{ |
|
||||||
archiveVersion = 1; |
|
||||||
classes = { |
|
||||||
}; |
|
||||||
objectVersion = 50; |
|
||||||
objects = { |
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */ |
|
||||||
1F00F38D257599D800CFAF97 /* TodoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F00F38C257599D800CFAF97 /* TodoApp.swift */; }; |
|
||||||
1F00F38F257599D800CFAF97 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F00F38E257599D800CFAF97 /* ContentView.swift */; }; |
|
||||||
1F00F391257599DA00CFAF97 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1F00F390257599DA00CFAF97 /* Assets.xcassets */; }; |
|
||||||
1F00F394257599DA00CFAF97 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1F00F393257599DA00CFAF97 /* Preview Assets.xcassets */; }; |
|
||||||
1F00F3A82575A16400CFAF97 /* ObservableValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F00F3A72575A16400CFAF97 /* ObservableValue.swift */; }; |
|
||||||
1F00F3AA2575A71000CFAF97 /* MutableStateBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F00F3A92575A71000CFAF97 /* MutableStateBuilder.swift */; }; |
|
||||||
1F00F3AC2575AA4500CFAF97 /* ListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F00F3AB2575AA4500CFAF97 /* ListView.swift */; }; |
|
||||||
1F00F3AE2575AC6A00CFAF97 /* InputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F00F3AD2575AC6A00CFAF97 /* InputView.swift */; }; |
|
||||||
1F00F3B02575ADB500CFAF97 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F00F3AF2575ADB500CFAF97 /* MainView.swift */; }; |
|
||||||
1F00F3B22575B07700CFAF97 /* EditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F00F3B12575B07700CFAF97 /* EditView.swift */; }; |
|
||||||
1F00F3B42575B18200CFAF97 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F00F3B32575B18200CFAF97 /* RootView.swift */; }; |
|
||||||
1F00F3B62575B41900CFAF97 /* SimpleChildStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F00F3B52575B41900CFAF97 /* SimpleChildStack.swift */; }; |
|
||||||
1F00F3B82575B4F800CFAF97 /* ComponentHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F00F3B72575B4F800CFAF97 /* ComponentHolder.swift */; }; |
|
||||||
/* End PBXBuildFile section */ |
|
||||||
|
|
||||||
/* Begin PBXFileReference section */ |
|
||||||
1F00F389257599D800CFAF97 /* TodoApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TodoApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; |
|
||||||
1F00F38C257599D800CFAF97 /* TodoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoApp.swift; sourceTree = "<group>"; }; |
|
||||||
1F00F38E257599D800CFAF97 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; }; |
|
||||||
1F00F390257599DA00CFAF97 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; |
|
||||||
1F00F393257599DA00CFAF97 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; }; |
|
||||||
1F00F395257599DA00CFAF97 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; |
|
||||||
1F00F3A325759FEC00CFAF97 /* Todo.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Todo.framework; path = "../common/root/build/xcode-frameworks/Todo.framework"; sourceTree = "<group>"; }; |
|
||||||
1F00F3A72575A16400CFAF97 /* ObservableValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableValue.swift; sourceTree = "<group>"; }; |
|
||||||
1F00F3A92575A71000CFAF97 /* MutableStateBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutableStateBuilder.swift; sourceTree = "<group>"; }; |
|
||||||
1F00F3AB2575AA4500CFAF97 /* ListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListView.swift; sourceTree = "<group>"; }; |
|
||||||
1F00F3AD2575AC6A00CFAF97 /* InputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputView.swift; sourceTree = "<group>"; }; |
|
||||||
1F00F3AF2575ADB500CFAF97 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; }; |
|
||||||
1F00F3B12575B07700CFAF97 /* EditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditView.swift; sourceTree = "<group>"; }; |
|
||||||
1F00F3B32575B18200CFAF97 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = "<group>"; }; |
|
||||||
1F00F3B52575B41900CFAF97 /* SimpleChildStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleChildStack.swift; sourceTree = "<group>"; }; |
|
||||||
1F00F3B72575B4F800CFAF97 /* ComponentHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComponentHolder.swift; sourceTree = "<group>"; }; |
|
||||||
/* End PBXFileReference section */ |
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */ |
|
||||||
1F00F386257599D800CFAF97 /* Frameworks */ = { |
|
||||||
isa = PBXFrameworksBuildPhase; |
|
||||||
buildActionMask = 2147483647; |
|
||||||
files = ( |
|
||||||
); |
|
||||||
runOnlyForDeploymentPostprocessing = 0; |
|
||||||
}; |
|
||||||
/* End PBXFrameworksBuildPhase section */ |
|
||||||
|
|
||||||
/* Begin PBXGroup section */ |
|
||||||
1F00F380257599D800CFAF97 = { |
|
||||||
isa = PBXGroup; |
|
||||||
children = ( |
|
||||||
1F00F38B257599D800CFAF97 /* ios */, |
|
||||||
1F00F38A257599D800CFAF97 /* Products */, |
|
||||||
1F00F3A225759FEC00CFAF97 /* Frameworks */, |
|
||||||
); |
|
||||||
sourceTree = "<group>"; |
|
||||||
}; |
|
||||||
1F00F38A257599D800CFAF97 /* Products */ = { |
|
||||||
isa = PBXGroup; |
|
||||||
children = ( |
|
||||||
1F00F389257599D800CFAF97 /* TodoApp.app */, |
|
||||||
); |
|
||||||
name = Products; |
|
||||||
sourceTree = "<group>"; |
|
||||||
}; |
|
||||||
1F00F38B257599D800CFAF97 /* ios */ = { |
|
||||||
isa = PBXGroup; |
|
||||||
children = ( |
|
||||||
1F00F38C257599D800CFAF97 /* TodoApp.swift */, |
|
||||||
1F00F38E257599D800CFAF97 /* ContentView.swift */, |
|
||||||
1F00F390257599DA00CFAF97 /* Assets.xcassets */, |
|
||||||
1F00F395257599DA00CFAF97 /* Info.plist */, |
|
||||||
1F00F392257599DA00CFAF97 /* Preview Content */, |
|
||||||
1F00F3A72575A16400CFAF97 /* ObservableValue.swift */, |
|
||||||
1F00F3A92575A71000CFAF97 /* MutableStateBuilder.swift */, |
|
||||||
1F00F3AB2575AA4500CFAF97 /* ListView.swift */, |
|
||||||
1F00F3AD2575AC6A00CFAF97 /* InputView.swift */, |
|
||||||
1F00F3AF2575ADB500CFAF97 /* MainView.swift */, |
|
||||||
1F00F3B12575B07700CFAF97 /* EditView.swift */, |
|
||||||
1F00F3B32575B18200CFAF97 /* RootView.swift */, |
|
||||||
1F00F3B52575B41900CFAF97 /* SimpleChildStack.swift */, |
|
||||||
1F00F3B72575B4F800CFAF97 /* ComponentHolder.swift */, |
|
||||||
); |
|
||||||
path = ios; |
|
||||||
sourceTree = "<group>"; |
|
||||||
}; |
|
||||||
1F00F392257599DA00CFAF97 /* Preview Content */ = { |
|
||||||
isa = PBXGroup; |
|
||||||
children = ( |
|
||||||
1F00F393257599DA00CFAF97 /* Preview Assets.xcassets */, |
|
||||||
); |
|
||||||
path = "Preview Content"; |
|
||||||
sourceTree = "<group>"; |
|
||||||
}; |
|
||||||
1F00F3A225759FEC00CFAF97 /* Frameworks */ = { |
|
||||||
isa = PBXGroup; |
|
||||||
children = ( |
|
||||||
1F00F3A325759FEC00CFAF97 /* Todo.framework */, |
|
||||||
); |
|
||||||
name = Frameworks; |
|
||||||
sourceTree = "<group>"; |
|
||||||
}; |
|
||||||
/* End PBXGroup section */ |
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */ |
|
||||||
1F00F388257599D800CFAF97 /* TodoApp */ = { |
|
||||||
isa = PBXNativeTarget; |
|
||||||
buildConfigurationList = 1F00F398257599DA00CFAF97 /* Build configuration list for PBXNativeTarget "TodoApp" */; |
|
||||||
buildPhases = ( |
|
||||||
1F00F39D25759BB300CFAF97 /* ShellScript */, |
|
||||||
1F00F385257599D800CFAF97 /* Sources */, |
|
||||||
1F00F386257599D800CFAF97 /* Frameworks */, |
|
||||||
1F00F387257599D800CFAF97 /* Resources */, |
|
||||||
); |
|
||||||
buildRules = ( |
|
||||||
); |
|
||||||
dependencies = ( |
|
||||||
); |
|
||||||
name = TodoApp; |
|
||||||
productName = ios; |
|
||||||
productReference = 1F00F389257599D800CFAF97 /* TodoApp.app */; |
|
||||||
productType = "com.apple.product-type.application"; |
|
||||||
}; |
|
||||||
/* End PBXNativeTarget section */ |
|
||||||
|
|
||||||
/* Begin PBXProject section */ |
|
||||||
1F00F381257599D800CFAF97 /* Project object */ = { |
|
||||||
isa = PBXProject; |
|
||||||
attributes = { |
|
||||||
LastSwiftUpdateCheck = 1220; |
|
||||||
LastUpgradeCheck = 1220; |
|
||||||
TargetAttributes = { |
|
||||||
1F00F388257599D800CFAF97 = { |
|
||||||
CreatedOnToolsVersion = 12.2; |
|
||||||
}; |
|
||||||
}; |
|
||||||
}; |
|
||||||
buildConfigurationList = 1F00F384257599D800CFAF97 /* Build configuration list for PBXProject "TodoApp" */; |
|
||||||
compatibilityVersion = "Xcode 9.3"; |
|
||||||
developmentRegion = en; |
|
||||||
hasScannedForEncodings = 0; |
|
||||||
knownRegions = ( |
|
||||||
en, |
|
||||||
Base, |
|
||||||
); |
|
||||||
mainGroup = 1F00F380257599D800CFAF97; |
|
||||||
productRefGroup = 1F00F38A257599D800CFAF97 /* Products */; |
|
||||||
projectDirPath = ""; |
|
||||||
projectRoot = ""; |
|
||||||
targets = ( |
|
||||||
1F00F388257599D800CFAF97 /* TodoApp */, |
|
||||||
); |
|
||||||
}; |
|
||||||
/* End PBXProject section */ |
|
||||||
|
|
||||||
/* Begin PBXResourcesBuildPhase section */ |
|
||||||
1F00F387257599D800CFAF97 /* Resources */ = { |
|
||||||
isa = PBXResourcesBuildPhase; |
|
||||||
buildActionMask = 2147483647; |
|
||||||
files = ( |
|
||||||
1F00F394257599DA00CFAF97 /* Preview Assets.xcassets in Resources */, |
|
||||||
1F00F391257599DA00CFAF97 /* Assets.xcassets in Resources */, |
|
||||||
); |
|
||||||
runOnlyForDeploymentPostprocessing = 0; |
|
||||||
}; |
|
||||||
/* End PBXResourcesBuildPhase section */ |
|
||||||
|
|
||||||
/* Begin PBXShellScriptBuildPhase section */ |
|
||||||
1F00F39D25759BB300CFAF97 /* ShellScript */ = { |
|
||||||
isa = PBXShellScriptBuildPhase; |
|
||||||
buildActionMask = 2147483647; |
|
||||||
files = ( |
|
||||||
); |
|
||||||
inputFileListPaths = ( |
|
||||||
); |
|
||||||
inputPaths = ( |
|
||||||
); |
|
||||||
outputFileListPaths = ( |
|
||||||
); |
|
||||||
outputPaths = ( |
|
||||||
); |
|
||||||
runOnlyForDeploymentPostprocessing = 0; |
|
||||||
shellPath = /bin/sh; |
|
||||||
shellScript = "cd $SRCROOT/..\n./gradlew :common:root:embedAndSignAppleFrameworkForXcode\n"; |
|
||||||
}; |
|
||||||
/* End PBXShellScriptBuildPhase section */ |
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */ |
|
||||||
1F00F385257599D800CFAF97 /* Sources */ = { |
|
||||||
isa = PBXSourcesBuildPhase; |
|
||||||
buildActionMask = 2147483647; |
|
||||||
files = ( |
|
||||||
1F00F38F257599D800CFAF97 /* ContentView.swift in Sources */, |
|
||||||
1F00F3B02575ADB500CFAF97 /* MainView.swift in Sources */, |
|
||||||
1F00F3B22575B07700CFAF97 /* EditView.swift in Sources */, |
|
||||||
1F00F38D257599D800CFAF97 /* TodoApp.swift in Sources */, |
|
||||||
1F00F3AC2575AA4500CFAF97 /* ListView.swift in Sources */, |
|
||||||
1F00F3B82575B4F800CFAF97 /* ComponentHolder.swift in Sources */, |
|
||||||
1F00F3B42575B18200CFAF97 /* RootView.swift in Sources */, |
|
||||||
1F00F3B62575B41900CFAF97 /* SimpleChildStack.swift in Sources */, |
|
||||||
1F00F3AE2575AC6A00CFAF97 /* InputView.swift in Sources */, |
|
||||||
1F00F3AA2575A71000CFAF97 /* MutableStateBuilder.swift in Sources */, |
|
||||||
1F00F3A82575A16400CFAF97 /* ObservableValue.swift in Sources */, |
|
||||||
); |
|
||||||
runOnlyForDeploymentPostprocessing = 0; |
|
||||||
}; |
|
||||||
/* End PBXSourcesBuildPhase section */ |
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */ |
|
||||||
1F00F396257599DA00CFAF97 /* Debug */ = { |
|
||||||
isa = XCBuildConfiguration; |
|
||||||
buildSettings = { |
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO; |
|
||||||
CLANG_ANALYZER_NONNULL = YES; |
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; |
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; |
|
||||||
CLANG_CXX_LIBRARY = "libc++"; |
|
||||||
CLANG_ENABLE_MODULES = YES; |
|
||||||
CLANG_ENABLE_OBJC_ARC = YES; |
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES; |
|
||||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; |
|
||||||
CLANG_WARN_BOOL_CONVERSION = YES; |
|
||||||
CLANG_WARN_COMMA = YES; |
|
||||||
CLANG_WARN_CONSTANT_CONVERSION = YES; |
|
||||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; |
|
||||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; |
|
||||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES; |
|
||||||
CLANG_WARN_EMPTY_BODY = YES; |
|
||||||
CLANG_WARN_ENUM_CONVERSION = YES; |
|
||||||
CLANG_WARN_INFINITE_RECURSION = YES; |
|
||||||
CLANG_WARN_INT_CONVERSION = YES; |
|
||||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; |
|
||||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; |
|
||||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; |
|
||||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; |
|
||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; |
|
||||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; |
|
||||||
CLANG_WARN_STRICT_PROTOTYPES = YES; |
|
||||||
CLANG_WARN_SUSPICIOUS_MOVE = YES; |
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; |
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES; |
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; |
|
||||||
COPY_PHASE_STRIP = NO; |
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf; |
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES; |
|
||||||
ENABLE_TESTABILITY = YES; |
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11; |
|
||||||
GCC_DYNAMIC_NO_PIC = NO; |
|
||||||
GCC_NO_COMMON_BLOCKS = YES; |
|
||||||
GCC_OPTIMIZATION_LEVEL = 0; |
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = ( |
|
||||||
"DEBUG=1", |
|
||||||
"$(inherited)", |
|
||||||
); |
|
||||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES; |
|
||||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; |
|
||||||
GCC_WARN_UNDECLARED_SELECTOR = YES; |
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; |
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES; |
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES; |
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.2; |
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; |
|
||||||
MTL_FAST_MATH = YES; |
|
||||||
ONLY_ACTIVE_ARCH = YES; |
|
||||||
SDKROOT = iphoneos; |
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; |
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; |
|
||||||
}; |
|
||||||
name = Debug; |
|
||||||
}; |
|
||||||
1F00F397257599DA00CFAF97 /* Release */ = { |
|
||||||
isa = XCBuildConfiguration; |
|
||||||
buildSettings = { |
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO; |
|
||||||
CLANG_ANALYZER_NONNULL = YES; |
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; |
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; |
|
||||||
CLANG_CXX_LIBRARY = "libc++"; |
|
||||||
CLANG_ENABLE_MODULES = YES; |
|
||||||
CLANG_ENABLE_OBJC_ARC = YES; |
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES; |
|
||||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; |
|
||||||
CLANG_WARN_BOOL_CONVERSION = YES; |
|
||||||
CLANG_WARN_COMMA = YES; |
|
||||||
CLANG_WARN_CONSTANT_CONVERSION = YES; |
|
||||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; |
|
||||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; |
|
||||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES; |
|
||||||
CLANG_WARN_EMPTY_BODY = YES; |
|
||||||
CLANG_WARN_ENUM_CONVERSION = YES; |
|
||||||
CLANG_WARN_INFINITE_RECURSION = YES; |
|
||||||
CLANG_WARN_INT_CONVERSION = YES; |
|
||||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; |
|
||||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; |
|
||||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; |
|
||||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; |
|
||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; |
|
||||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; |
|
||||||
CLANG_WARN_STRICT_PROTOTYPES = YES; |
|
||||||
CLANG_WARN_SUSPICIOUS_MOVE = YES; |
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; |
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES; |
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; |
|
||||||
COPY_PHASE_STRIP = NO; |
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; |
|
||||||
ENABLE_NS_ASSERTIONS = NO; |
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES; |
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11; |
|
||||||
GCC_NO_COMMON_BLOCKS = YES; |
|
||||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES; |
|
||||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; |
|
||||||
GCC_WARN_UNDECLARED_SELECTOR = YES; |
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; |
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES; |
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES; |
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.2; |
|
||||||
MTL_ENABLE_DEBUG_INFO = NO; |
|
||||||
MTL_FAST_MATH = YES; |
|
||||||
SDKROOT = iphoneos; |
|
||||||
SWIFT_COMPILATION_MODE = wholemodule; |
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-O"; |
|
||||||
VALIDATE_PRODUCT = YES; |
|
||||||
}; |
|
||||||
name = Release; |
|
||||||
}; |
|
||||||
1F00F399257599DA00CFAF97 /* Debug */ = { |
|
||||||
isa = XCBuildConfiguration; |
|
||||||
buildSettings = { |
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; |
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; |
|
||||||
CODE_SIGN_STYLE = Automatic; |
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"ios/Preview Content\""; |
|
||||||
ENABLE_PREVIEWS = YES; |
|
||||||
FRAMEWORK_SEARCH_PATHS = "$SRCROOT/../common/root/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)"; |
|
||||||
INFOPLIST_FILE = ios/Info.plist; |
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0; |
|
||||||
LD_RUNPATH_SEARCH_PATHS = ( |
|
||||||
"$(inherited)", |
|
||||||
"@executable_path/Frameworks", |
|
||||||
); |
|
||||||
OTHER_LDFLAGS = ( |
|
||||||
"$(inherited)", |
|
||||||
"-framework", |
|
||||||
Todo, |
|
||||||
); |
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = org.jetbrains.todoapp; |
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)"; |
|
||||||
SWIFT_VERSION = 5.0; |
|
||||||
TARGETED_DEVICE_FAMILY = "1,2"; |
|
||||||
}; |
|
||||||
name = Debug; |
|
||||||
}; |
|
||||||
1F00F39A257599DA00CFAF97 /* Release */ = { |
|
||||||
isa = XCBuildConfiguration; |
|
||||||
buildSettings = { |
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; |
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; |
|
||||||
CODE_SIGN_STYLE = Automatic; |
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"ios/Preview Content\""; |
|
||||||
ENABLE_PREVIEWS = YES; |
|
||||||
FRAMEWORK_SEARCH_PATHS = "$SRCROOT/../common/root/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)"; |
|
||||||
INFOPLIST_FILE = ios/Info.plist; |
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0; |
|
||||||
LD_RUNPATH_SEARCH_PATHS = ( |
|
||||||
"$(inherited)", |
|
||||||
"@executable_path/Frameworks", |
|
||||||
); |
|
||||||
OTHER_LDFLAGS = ( |
|
||||||
"$(inherited)", |
|
||||||
"-framework", |
|
||||||
Todo, |
|
||||||
); |
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = org.jetbrains.todoapp; |
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)"; |
|
||||||
SWIFT_VERSION = 5.0; |
|
||||||
TARGETED_DEVICE_FAMILY = "1,2"; |
|
||||||
}; |
|
||||||
name = Release; |
|
||||||
}; |
|
||||||
/* End XCBuildConfiguration section */ |
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */ |
|
||||||
1F00F384257599D800CFAF97 /* Build configuration list for PBXProject "TodoApp" */ = { |
|
||||||
isa = XCConfigurationList; |
|
||||||
buildConfigurations = ( |
|
||||||
1F00F396257599DA00CFAF97 /* Debug */, |
|
||||||
1F00F397257599DA00CFAF97 /* Release */, |
|
||||||
); |
|
||||||
defaultConfigurationIsVisible = 0; |
|
||||||
defaultConfigurationName = Release; |
|
||||||
}; |
|
||||||
1F00F398257599DA00CFAF97 /* Build configuration list for PBXNativeTarget "TodoApp" */ = { |
|
||||||
isa = XCConfigurationList; |
|
||||||
buildConfigurations = ( |
|
||||||
1F00F399257599DA00CFAF97 /* Debug */, |
|
||||||
1F00F39A257599DA00CFAF97 /* Release */, |
|
||||||
); |
|
||||||
defaultConfigurationIsVisible = 0; |
|
||||||
defaultConfigurationName = Release; |
|
||||||
}; |
|
||||||
/* End XCConfigurationList section */ |
|
||||||
}; |
|
||||||
rootObject = 1F00F381257599D800CFAF97 /* Project object */; |
|
||||||
} |
|