diff --git a/README.md b/README.md index 4ed789298c..14b40adda7 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Preview functionality (check your application UI without building/running it) fo * [compose-bird](examples/web-compose-bird) - A flappy bird clone using Compose for Web * [notepad](examples/notepad) - Notepad, using the new experimental Composable Window API * [todoapp](examples/todoapp) - TODO items tracker with persistence and multiple screens + * [todoapp-lite](examples/todoapp-lite) - A simplified version of [todoapp](examples/todoapp), fully based on Jetpack Compose * [widgets gallery](examples/widgets-gallery) - Gallery of standard widgets * [IDEA plugin](examples/intellij-plugin) - Plugin for IDEA using Compose for Desktop * [gradle-plugins](gradle-plugins) - a plugin, simplifying usage of Compose Multiplatform with Gradle diff --git a/examples/todoapp-lite/.gitignore b/examples/todoapp-lite/.gitignore new file mode 100644 index 0000000000..a32b16597b --- /dev/null +++ b/examples/todoapp-lite/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +build/ +/captures +.externalNativeBuild +.cxx diff --git a/examples/todoapp-lite/README.md b/examples/todoapp-lite/README.md new file mode 100755 index 0000000000..67023dabed --- /dev/null +++ b/examples/todoapp-lite/README.md @@ -0,0 +1,20 @@ +A simplified version of the [TodoApp example](https://github.com/JetBrains/compose-jb/tree/master/examples/todoapp), fully based on Jetpack Compose and without using any third-party libraries. + +Supported targets: Android and Desktop. + +### Running desktop application +``` +./gradlew :desktop:run +``` + +### Building native desktop distribution +``` +./gradlew :desktop:package +# outputs are written to desktop/build/compose/binaries +``` + +### Running Android application + +Open project in IntelliJ IDEA or Android Studio and run "android" configuration. + +![Desktop](screenshots/todoapplite.png) diff --git a/examples/todoapp-lite/android/build.gradle.kts b/examples/todoapp-lite/android/build.gradle.kts new file mode 100755 index 0000000000..4a07b39add --- /dev/null +++ b/examples/todoapp-lite/android/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + id("com.android.application") + kotlin("android") + id("org.jetbrains.compose") +} + +android { + compileSdk = 31 + + defaultConfig { + minSdk = 21 + targetSdk = 31 + versionCode = 1 + versionName = "1.0" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation(project(":common")) + implementation(compose.material) + implementation("androidx.appcompat:appcompat:1.3.0") + implementation("androidx.activity:activity-compose:1.3.0") +} + diff --git a/examples/todoapp-lite/android/src/main/AndroidManifest.xml b/examples/todoapp-lite/android/src/main/AndroidManifest.xml new file mode 100755 index 0000000000..e55a10c995 --- /dev/null +++ b/examples/todoapp-lite/android/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + diff --git a/examples/todoapp-lite/android/src/main/java/example/todoapp/lite/MainActivity.kt b/examples/todoapp-lite/android/src/main/java/example/todoapp/lite/MainActivity.kt new file mode 100755 index 0000000000..0635d7d922 --- /dev/null +++ b/examples/todoapp-lite/android/src/main/java/example/todoapp/lite/MainActivity.kt @@ -0,0 +1,26 @@ +package example.todoapp.lite + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.ui.Modifier +import example.todoapp.lite.common.RootContent + +class MainActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + MaterialTheme { + Surface(color = MaterialTheme.colors.background) { + RootContent(modifier = Modifier.fillMaxSize()) + } + } + } + } +} + diff --git a/examples/todoapp-lite/build.gradle.kts b/examples/todoapp-lite/build.gradle.kts new file mode 100755 index 0000000000..72f7546fef --- /dev/null +++ b/examples/todoapp-lite/build.gradle.kts @@ -0,0 +1,22 @@ +buildscript { + repositories { + google() + mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + } + + dependencies { + // __LATEST_COMPOSE_RELEASE_VERSION__ + classpath("org.jetbrains.compose:compose-gradle-plugin:1.0.0-alpha4-build331") + classpath("com.android.tools.build:gradle:4.1.0") + classpath(kotlin("gradle-plugin", version = "1.5.30")) + } +} + +allprojects { + repositories { + google() + mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + } +} diff --git a/examples/todoapp-lite/common/build.gradle.kts b/examples/todoapp-lite/common/build.gradle.kts new file mode 100755 index 0000000000..a80db145fd --- /dev/null +++ b/examples/todoapp-lite/common/build.gradle.kts @@ -0,0 +1,44 @@ +import org.jetbrains.compose.compose + +plugins { + id("com.android.library") + kotlin("multiplatform") + id("org.jetbrains.compose") +} + +kotlin { + android() + jvm("desktop") + sourceSets { + named("commonMain") { + dependencies { + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.material) + implementation(compose.ui) + } + } + } +} + +android { + compileSdk = 31 + + defaultConfig { + minSdk = 21 + targetSdk = 31 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + sourceSets { + named("main") { + manifest.srcFile("src/androidMain/AndroidManifest.xml") + res.srcDirs("src/androidMain/res") + } + } +} + diff --git a/examples/todoapp-lite/common/src/androidMain/AndroidManifest.xml b/examples/todoapp-lite/common/src/androidMain/AndroidManifest.xml new file mode 100755 index 0000000000..ccd0950b6f --- /dev/null +++ b/examples/todoapp-lite/common/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/examples/todoapp-lite/common/src/androidMain/kotlin/example/todoapp/lite/common/Utils.kt b/examples/todoapp-lite/common/src/androidMain/kotlin/example/todoapp/lite/common/Utils.kt new file mode 100644 index 0000000000..b52bcab2c8 --- /dev/null +++ b/examples/todoapp-lite/common/src/androidMain/kotlin/example/todoapp/lite/common/Utils.kt @@ -0,0 +1,75 @@ +@file:JvmName("Utils") + +package example.todoapp.lite.common + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material.Button +import androidx.compose.material.Card +import androidx.compose.material.MaterialTheme +import androidx.compose.material.ProvideTextStyle +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +internal actual val MARGIN_SCROLLBAR: Dp = 0.dp + +internal actual interface ScrollbarAdapter + +@Composable +internal actual fun rememberScrollbarAdapter(scrollState: LazyListState): ScrollbarAdapter = + object : ScrollbarAdapter {} + +@Composable +internal actual fun VerticalScrollbar( + modifier: Modifier, + adapter: ScrollbarAdapter +) { + // no-op +} + +@Composable +internal actual fun Dialog( + title: String, + onCloseRequest: () -> Unit, + content: @Composable () -> Unit +) { + androidx.compose.ui.window.Dialog( + onDismissRequest = onCloseRequest, + ) { + Card(elevation = 8.dp) { + Column( + modifier = Modifier + .padding(8.dp) + .height(IntrinsicSize.Min) + ) { + ProvideTextStyle(MaterialTheme.typography.subtitle1) { + Text(text = title) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Box(modifier = Modifier.weight(1F)) { + content() + } + + Spacer(modifier = Modifier.height(8.dp)) + + Button( + onClick = onCloseRequest, + modifier = Modifier.align(Alignment.End) + ) { + Text(text = "Done") + } + } + } + } +} diff --git a/examples/todoapp-lite/common/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml b/examples/todoapp-lite/common/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000000..1f6bb29060 --- /dev/null +++ b/examples/todoapp-lite/common/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/examples/todoapp-lite/common/src/androidMain/res/drawable/ic_launcher_background.xml b/examples/todoapp-lite/common/src/androidMain/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000000..0d025f9bf6 --- /dev/null +++ b/examples/todoapp-lite/common/src/androidMain/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/todoapp-lite/common/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml b/examples/todoapp-lite/common/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000000..6b78462d61 --- /dev/null +++ b/examples/todoapp-lite/common/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/examples/todoapp-lite/common/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml b/examples/todoapp-lite/common/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000000..6b78462d61 --- /dev/null +++ b/examples/todoapp-lite/common/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/examples/todoapp-lite/common/src/androidMain/res/mipmap-hdpi/ic_launcher.png b/examples/todoapp-lite/common/src/androidMain/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000..898f3ed59a Binary files /dev/null and b/examples/todoapp-lite/common/src/androidMain/res/mipmap-hdpi/ic_launcher.png differ diff --git a/examples/todoapp-lite/common/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png b/examples/todoapp-lite/common/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000..dffca3601e Binary files /dev/null and b/examples/todoapp-lite/common/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/examples/todoapp-lite/common/src/androidMain/res/mipmap-mdpi/ic_launcher.png b/examples/todoapp-lite/common/src/androidMain/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000..64ba76f75e Binary files /dev/null and b/examples/todoapp-lite/common/src/androidMain/res/mipmap-mdpi/ic_launcher.png differ diff --git a/examples/todoapp-lite/common/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png b/examples/todoapp-lite/common/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000000..dae5e08234 Binary files /dev/null and b/examples/todoapp-lite/common/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/examples/todoapp-lite/common/src/androidMain/res/mipmap-xhdpi/ic_launcher.png b/examples/todoapp-lite/common/src/androidMain/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000..e5ed46597e Binary files /dev/null and b/examples/todoapp-lite/common/src/androidMain/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/examples/todoapp-lite/common/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png b/examples/todoapp-lite/common/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000..14ed0af350 Binary files /dev/null and b/examples/todoapp-lite/common/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/examples/todoapp-lite/common/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png b/examples/todoapp-lite/common/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000..b0907cac3b Binary files /dev/null and b/examples/todoapp-lite/common/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/examples/todoapp-lite/common/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png b/examples/todoapp-lite/common/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000..d8ae031549 Binary files /dev/null and b/examples/todoapp-lite/common/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/examples/todoapp-lite/common/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png b/examples/todoapp-lite/common/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000..2c18de9e66 Binary files /dev/null and b/examples/todoapp-lite/common/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/examples/todoapp-lite/common/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png b/examples/todoapp-lite/common/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000..beed3cdd2c Binary files /dev/null and b/examples/todoapp-lite/common/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/examples/todoapp-lite/common/src/androidMain/res/values/strings.xml b/examples/todoapp-lite/common/src/androidMain/res/values/strings.xml new file mode 100644 index 0000000000..5dee5cbdd0 --- /dev/null +++ b/examples/todoapp-lite/common/src/androidMain/res/values/strings.xml @@ -0,0 +1,4 @@ + + + TodoApp Lite + diff --git a/examples/todoapp-lite/common/src/commonMain/kotlin/example/todoapp/lite/common/EditDialog.kt b/examples/todoapp-lite/common/src/commonMain/kotlin/example/todoapp/lite/common/EditDialog.kt new file mode 100644 index 0000000000..308b83588c --- /dev/null +++ b/examples/todoapp-lite/common/src/commonMain/kotlin/example/todoapp/lite/common/EditDialog.kt @@ -0,0 +1,51 @@ +package example.todoapp.lite.common + +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.height +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.layout.width +import androidx.compose.material.Checkbox +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +internal fun EditDialog( + item: TodoItem, + onCloseClicked: () -> Unit, + onTextChanged: (String) -> Unit, + onDoneChanged: (Boolean) -> Unit, +) { + Dialog( + title = "Edit todo", + onCloseRequest = onCloseClicked, + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + TextField( + value = item.text, + modifier = Modifier.weight(1F).fillMaxWidth().sizeIn(minHeight = 192.dp), + label = { Text("Todo text") }, + onValueChange = onTextChanged, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Row { + Text(text = "Completed") + + Spacer(modifier = Modifier.width(8.dp)) + + Checkbox( + checked = item.isDone, + onCheckedChange = onDoneChanged, + ) + } + } + } +} diff --git a/examples/todoapp-lite/common/src/commonMain/kotlin/example/todoapp/lite/common/MainContent.kt b/examples/todoapp-lite/common/src/commonMain/kotlin/example/todoapp/lite/common/MainContent.kt new file mode 100644 index 0000000000..7d8c390bdf --- /dev/null +++ b/examples/todoapp-lite/common/src/commonMain/kotlin/example/todoapp/lite/common/MainContent.kt @@ -0,0 +1,158 @@ +package example.todoapp.lite.common + +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.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp + +@Composable +internal fun MainContent( + modifier: Modifier = Modifier, + items: List, + inputText: String, + onItemClicked: (id: Long) -> Unit, + onItemDoneChanged: (id: Long, isDone: Boolean) -> Unit, + onItemDeleteClicked: (id: Long) -> Unit, + onAddItemClicked: () -> Unit, + onInputTextChanged: (String) -> Unit, +) { + Column(modifier) { + TopAppBar(title = { Text(text = "Todo List") }) + + Box(Modifier.weight(1F)) { + ListContent( + items = items, + onItemClicked = onItemClicked, + onItemDoneChanged = onItemDoneChanged, + onItemDeleteClicked = onItemDeleteClicked + ) + } + + Input( + text = inputText, + onAddClicked = onAddItemClicked, + onTextChanged = onInputTextChanged + ) + } +} + +@Composable +private fun ListContent( + items: List, + onItemClicked: (id: Long) -> Unit, + onItemDoneChanged: (id: Long, isDone: Boolean) -> Unit, + onItemDeleteClicked: (id: Long) -> Unit, +) { + Box { + val listState = rememberLazyListState() + + LazyColumn(state = listState) { + items(items) { item -> + Item( + item = item, + onClicked = { onItemClicked(item.id) }, + onDoneChanged = { onItemDoneChanged(item.id, it) }, + onDeleteClicked = { onItemDeleteClicked(item.id) } + ) + + Divider() + } + } + + VerticalScrollbar( + modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(), + adapter = rememberScrollbarAdapter(scrollState = listState) + ) + } +} + +@Composable +private fun Item( + item: TodoItem, + onClicked: () -> Unit, + onDoneChanged: (Boolean) -> Unit, + onDeleteClicked: () -> Unit +) { + Row(modifier = Modifier.clickable(onClick = onClicked)) { + Spacer(modifier = Modifier.width(8.dp)) + + Checkbox( + checked = item.isDone, + modifier = Modifier.align(Alignment.CenterVertically), + onCheckedChange = onDoneChanged, + ) + + 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 = onDeleteClicked) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = null + ) + } + + Spacer(modifier = Modifier.width(MARGIN_SCROLLBAR)) + } +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +private fun Input( + text: String, + onTextChanged: (String) -> Unit, + onAddClicked: () -> Unit +) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(8.dp)) { + OutlinedTextField( + value = text, + modifier = Modifier + .weight(weight = 1F) + .onKeyUp(key = Key.Enter, action = 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 + ) + } + } +} diff --git a/examples/todoapp-lite/common/src/commonMain/kotlin/example/todoapp/lite/common/RootContent.kt b/examples/todoapp-lite/common/src/commonMain/kotlin/example/todoapp/lite/common/RootContent.kt new file mode 100644 index 0000000000..84fa449e36 --- /dev/null +++ b/examples/todoapp-lite/common/src/commonMain/kotlin/example/todoapp/lite/common/RootContent.kt @@ -0,0 +1,38 @@ +package example.todoapp.lite.common + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import example.todoapp.lite.common.RootStore.RootState + +@Composable +fun RootContent(modifier: Modifier = Modifier) { + val model = remember { RootStore() } + val state = model.state + + MainContent( + modifier = modifier, + items = state.items, + inputText = state.inputText, + onItemClicked = model::onItemClicked, + onItemDoneChanged = model::onItemDoneChanged, + onItemDeleteClicked = model::onItemDeleteClicked, + onAddItemClicked = model::onAddItemClicked, + onInputTextChanged = model::onInputTextChanged, + ) + + state.editingItem?.also { item -> + EditDialog( + item = item, + onCloseClicked = model::onEditorCloseClicked, + onTextChanged = model::onEditorTextChanged, + onDoneChanged = model::onEditorDoneChanged, + ) + } +} + +private val RootState.editingItem: TodoItem? + get() = editingItemId?.let(items::firstById) + +private fun List.firstById(id: Long): TodoItem = + first { it.id == id } diff --git a/examples/todoapp-lite/common/src/commonMain/kotlin/example/todoapp/lite/common/RootStore.kt b/examples/todoapp-lite/common/src/commonMain/kotlin/example/todoapp/lite/common/RootStore.kt new file mode 100644 index 0000000000..91dfe7df5b --- /dev/null +++ b/examples/todoapp-lite/common/src/commonMain/kotlin/example/todoapp/lite/common/RootStore.kt @@ -0,0 +1,80 @@ +package example.todoapp.lite.common + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue + +internal class RootStore { + + var state: RootState by mutableStateOf(initialState()) + private set + + fun onItemClicked(id: Long) { + setState { copy(editingItemId = id) } + } + + fun onItemDoneChanged(id: Long, isDone: Boolean) { + setState { + updateItem(id = id) { it.copy(isDone = isDone) } + } + } + + fun onItemDeleteClicked(id: Long) { + setState { copy(items = items.filterNot { it.id == id }) } + } + + fun onAddItemClicked() { + setState { + val newItem = + TodoItem( + id = items.maxOfOrNull(TodoItem::id)?.plus(1L) ?: 1L, + text = inputText, + ) + + copy(items = items + newItem, inputText = "") + } + } + + fun onInputTextChanged(text: String) { + setState { copy(inputText = text) } + } + + fun onEditorCloseClicked() { + setState { copy(editingItemId = null) } + } + + fun onEditorTextChanged(text: String) { + setState { + updateItem(id = requireNotNull(editingItemId)) { it.copy(text = text) } + } + } + + fun onEditorDoneChanged(isDone: Boolean) { + setState { + updateItem(id = requireNotNull(editingItemId)) { it.copy(isDone = isDone) } + } + } + + private fun RootState.updateItem(id: Long, transformer: (TodoItem) -> TodoItem): RootState = + copy(items = items.updateItem(id = id, transformer = transformer)) + + private fun List.updateItem(id: Long, transformer: (TodoItem) -> TodoItem): List = + map { item -> if (item.id == id) transformer(item) else item } + + private fun initialState(): RootState = + RootState( + items = (1L..5L).map { id -> + TodoItem(id = id, text = "Some text $id") + } + ) + + private inline fun setState(update: RootState.() -> RootState) { + state = state.update() + } + + data class RootState( + val items: List = emptyList(), + val inputText: String = "", + val editingItemId: Long? = null, + ) +} diff --git a/examples/todoapp-lite/common/src/commonMain/kotlin/example/todoapp/lite/common/TodoItem.kt b/examples/todoapp-lite/common/src/commonMain/kotlin/example/todoapp/lite/common/TodoItem.kt new file mode 100644 index 0000000000..94da3f2834 --- /dev/null +++ b/examples/todoapp-lite/common/src/commonMain/kotlin/example/todoapp/lite/common/TodoItem.kt @@ -0,0 +1,7 @@ +package example.todoapp.lite.common + +internal data class TodoItem( + val id: Long = 0L, + val text: String = "", + val isDone: Boolean = false +) diff --git a/examples/todoapp-lite/common/src/commonMain/kotlin/example/todoapp/lite/common/Utils.kt b/examples/todoapp-lite/common/src/commonMain/kotlin/example/todoapp/lite/common/Utils.kt new file mode 100644 index 0000000000..9345b740a2 --- /dev/null +++ b/examples/todoapp-lite/common/src/commonMain/kotlin/example/todoapp/lite/common/Utils.kt @@ -0,0 +1,41 @@ +package example.todoapp.lite.common + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.input.key.type +import androidx.compose.ui.unit.Dp + +internal expect val MARGIN_SCROLLBAR: Dp + +internal expect interface ScrollbarAdapter + +@Composable +internal expect fun rememberScrollbarAdapter(scrollState: LazyListState): ScrollbarAdapter + +@Composable +internal expect fun VerticalScrollbar( + modifier: Modifier, + adapter: ScrollbarAdapter +) + +@Composable +internal expect fun Dialog( + title: String, + onCloseRequest: () -> Unit, + content: @Composable () -> Unit +) + +internal fun Modifier.onKeyUp(key: Key, action: () -> Unit): Modifier = + onKeyEvent { event -> + if ((event.type == KeyEventType.KeyUp) && (event.key == key)) { + action() + true + } else { + false + } + } diff --git a/examples/todoapp-lite/common/src/desktopMain/kotlin/example/todoapp/lite/common/Utils.kt b/examples/todoapp-lite/common/src/desktopMain/kotlin/example/todoapp/lite/common/Utils.kt new file mode 100644 index 0000000000..1e36024aa1 --- /dev/null +++ b/examples/todoapp-lite/common/src/desktopMain/kotlin/example/todoapp/lite/common/Utils.kt @@ -0,0 +1,55 @@ +@file:JvmName("Utils") + +package example.todoapp.lite.common + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +internal actual val MARGIN_SCROLLBAR: Dp = 8.dp + +@Suppress("ACTUAL_WITHOUT_EXPECT") // Workaround https://youtrack.jetbrains.com/issue/KT-37316 +internal actual typealias ScrollbarAdapter = androidx.compose.foundation.ScrollbarAdapter + +@Composable +internal actual fun rememberScrollbarAdapter(scrollState: LazyListState): ScrollbarAdapter = + androidx.compose.foundation.rememberScrollbarAdapter(scrollState) + +@Composable +internal actual fun VerticalScrollbar( + modifier: Modifier, + adapter: ScrollbarAdapter +) { + androidx.compose.foundation.VerticalScrollbar( + modifier = modifier, + adapter = adapter + ) +} + +@Composable +internal actual fun Dialog( + title: String, + onCloseRequest: () -> Unit, + content: @Composable () -> Unit +) { + androidx.compose.ui.window.Dialog( + onCloseRequest = onCloseRequest, + focusable = true, + title = title, + ) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(8.dp), + contentAlignment = Alignment.Center, + ) { + content() + } + } +} diff --git a/examples/todoapp-lite/desktop/build.gradle.kts b/examples/todoapp-lite/desktop/build.gradle.kts new file mode 100755 index 0000000000..e634f8ca9c --- /dev/null +++ b/examples/todoapp-lite/desktop/build.gradle.kts @@ -0,0 +1,40 @@ +import org.jetbrains.compose.compose +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")) + } + } + } +} + +compose.desktop { + application { + mainClass = "example.todoapp.lite.MainKt" + + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageName = "TodoApp Lite" + packageVersion = "1.0.0" + + windows { + menuGroup = "Compose Examples" + // see https://wixtoolset.org/documentation/manual/v3/howtos/general/generate_guids.html + upgradeUuid = "5ac63736-d8c7-4a65-a66b-6870df88ddfe" + } + } + } +} + diff --git a/examples/todoapp-lite/desktop/src/jvmMain/kotlin/example/todoapp/lite/Main.kt b/examples/todoapp-lite/desktop/src/jvmMain/kotlin/example/todoapp/lite/Main.kt new file mode 100644 index 0000000000..835c00b79b --- /dev/null +++ b/examples/todoapp-lite/desktop/src/jvmMain/kotlin/example/todoapp/lite/Main.kt @@ -0,0 +1,29 @@ +package example.todoapp.lite + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.MaterialTheme +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowPosition +import androidx.compose.ui.window.application +import androidx.compose.ui.window.rememberWindowState +import example.todoapp.lite.common.RootContent + +fun main() { + application { + Window( + onCloseRequest = ::exitApplication, + title = "TodoApp Lite", + state = rememberWindowState( + position = WindowPosition(alignment = Alignment.Center), + ), + ) { + MaterialTheme { + RootContent( + modifier = Modifier.fillMaxSize() + ) + } + } + } +} diff --git a/examples/todoapp-lite/gradle.properties b/examples/todoapp-lite/gradle.properties new file mode 100755 index 0000000000..4d15d015f8 --- /dev/null +++ b/examples/todoapp-lite/gradle.properties @@ -0,0 +1,21 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official \ No newline at end of file diff --git a/examples/todoapp-lite/gradle/wrapper/gradle-wrapper.jar b/examples/todoapp-lite/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..f3d88b1c2f Binary files /dev/null and b/examples/todoapp-lite/gradle/wrapper/gradle-wrapper.jar differ diff --git a/examples/todoapp-lite/gradle/wrapper/gradle-wrapper.properties b/examples/todoapp-lite/gradle/wrapper/gradle-wrapper.properties new file mode 100755 index 0000000000..05679dc3c1 --- /dev/null +++ b/examples/todoapp-lite/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/todoapp-lite/gradlew b/examples/todoapp-lite/gradlew new file mode 100755 index 0000000000..fbd7c51583 --- /dev/null +++ b/examples/todoapp-lite/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/examples/todoapp-lite/gradlew.bat b/examples/todoapp-lite/gradlew.bat new file mode 100755 index 0000000000..5093609d51 --- /dev/null +++ b/examples/todoapp-lite/gradlew.bat @@ -0,0 +1,104 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/todoapp-lite/screenshots/todoapplite.png b/examples/todoapp-lite/screenshots/todoapplite.png new file mode 100644 index 0000000000..39702b6b87 Binary files /dev/null and b/examples/todoapp-lite/screenshots/todoapplite.png differ diff --git a/examples/todoapp-lite/settings.gradle.kts b/examples/todoapp-lite/settings.gradle.kts new file mode 100755 index 0000000000..9a0d554e9b --- /dev/null +++ b/examples/todoapp-lite/settings.gradle.kts @@ -0,0 +1 @@ +include(":common", ":android", ":desktop")