From 7426dc702a7ea2f7cba18deb42748046d6a4ee86 Mon Sep 17 00:00:00 2001 From: Arkadii Ivanov <> Date: Thu, 22 Oct 2020 22:31:20 +0100 Subject: [PATCH] Add Delete button --- .../todo/common/database/TodoDatabase.sq | 4 ++ .../main/integration/TodoMainStoreDatabase.kt | 4 ++ .../todo/common/main/store/TodoMainStore.kt | 1 + .../main/store/TodoMainStoreProvider.kt | 10 +++++ .../example/todo/common/main/ui/TodoMainUi.kt | 31 ++++++++++++---- .../common/main/integration/TodoMainTest.kt | 29 +++++++++++++-- .../main/store/TestTodoMainStoreDatabase.kt | 7 +++- .../common/main/store/TodoMainStoreTest.kt | 37 +++++++++++++++++++ 8 files changed, 109 insertions(+), 14 deletions(-) diff --git a/examples/todoapp/common/database/src/commonMain/sqldelight/example/todo/common/database/TodoDatabase.sq b/examples/todoapp/common/database/src/commonMain/sqldelight/example/todo/common/database/TodoDatabase.sq index e635976d70..7d85c7a49b 100644 --- a/examples/todoapp/common/database/src/commonMain/sqldelight/example/todo/common/database/TodoDatabase.sq +++ b/examples/todoapp/common/database/src/commonMain/sqldelight/example/todo/common/database/TodoDatabase.sq @@ -28,5 +28,9 @@ UPDATE TodoItemEntity SET isDone = :isDone WHERE id = :id; +delete: +DELETE FROM TodoItemEntity +WHERE id = :id; + getLastInsertId: SELECT last_insert_rowid(); diff --git a/examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/integration/TodoMainStoreDatabase.kt b/examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/integration/TodoMainStoreDatabase.kt index 9f8173403a..9524d2b833 100644 --- a/examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/integration/TodoMainStoreDatabase.kt +++ b/examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/integration/TodoMainStoreDatabase.kt @@ -35,6 +35,10 @@ internal class TodoMainStoreDatabase( completableFromFunction { queries.setDone(id = id, isDone = isDone) } .subscribeOn(ioScheduler) + override fun delete(id: Long): Completable = + completableFromFunction { queries.delete(id = id) } + .subscribeOn(ioScheduler) + override fun add(text: String): Completable = completableFromFunction { queries.transactionWithResult { diff --git a/examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/store/TodoMainStore.kt b/examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/store/TodoMainStore.kt index b07aee21b6..0a433b537b 100644 --- a/examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/store/TodoMainStore.kt +++ b/examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/store/TodoMainStore.kt @@ -8,6 +8,7 @@ internal interface TodoMainStore : Store { 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() } diff --git a/examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/store/TodoMainStoreProvider.kt b/examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/store/TodoMainStoreProvider.kt index f96f54a154..0036599e67 100644 --- a/examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/store/TodoMainStoreProvider.kt +++ b/examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/store/TodoMainStoreProvider.kt @@ -30,6 +30,7 @@ internal class TodoMainStoreProvider( private sealed class Result { data class ItemsLoaded(val items: List) : Result() data class ItemDoneChanged(val id: Long, val isDone: Boolean) : Result() + data class ItemDeleted(val id: Long) : Result() data class TextChanged(val text: String) : Result() } @@ -45,6 +46,7 @@ internal class TodoMainStoreProvider( override fun executeIntent(intent: Intent, getState: () -> State): Unit = when (intent) { is Intent.SetItemDone -> setItemDone(id = intent.id, isDone = intent.isDone) + is Intent.DeleteItem -> deleteItem(id = intent.id) is Intent.SetText -> dispatch(Result.TextChanged(text = intent.text)) is Intent.AddItem -> addItem(state = getState()) } @@ -54,6 +56,11 @@ internal class TodoMainStoreProvider( database.setDone(id = id, isDone = isDone).subscribeScoped() } + private fun deleteItem(id: Long) { + dispatch(Result.ItemDeleted(id = id)) + database.delete(id = id).subscribeScoped() + } + private fun addItem(state: State) { dispatch(Result.TextChanged(text = "")) database.add(text = state.text).subscribeScoped() @@ -65,6 +72,7 @@ internal class TodoMainStoreProvider( when (result) { is Result.ItemsLoaded -> copy(items = result.items.sorted()) is Result.ItemDoneChanged -> update(id = result.id) { copy(isDone = result.isDone) } + is Result.ItemDeleted -> copy(items = items.filterNot { it.id == result.id }) is Result.TextChanged -> copy(text = result.text) } @@ -89,6 +97,8 @@ internal class TodoMainStoreProvider( fun setDone(id: Long, isDone: Boolean): Completable + fun delete(id: Long): Completable + fun add(text: String): Completable } } diff --git a/examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/ui/TodoMainUi.kt b/examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/ui/TodoMainUi.kt index 609937f3ee..ef4ec6c464 100644 --- a/examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/ui/TodoMainUi.kt +++ b/examples/todoapp/common/main/src/commonMain/kotlin/example/todo/common/main/ui/TodoMainUi.kt @@ -1,5 +1,6 @@ package example.todo.common.main.ui +import androidx.compose.foundation.Icon import androidx.compose.foundation.Text import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box @@ -12,8 +13,11 @@ import androidx.compose.foundation.lazy.LazyColumnFor import androidx.compose.material.Button import androidx.compose.material.Checkbox import androidx.compose.material.Divider +import androidx.compose.material.IconButton import androidx.compose.material.OutlinedTextField import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -43,7 +47,8 @@ internal fun TodoMainUi( TodoList( items = state.items, onItemClicked = { output.onNext(Output.Selected(id = it)) }, - onDoneChanged = { id, isDone -> intents(Intent.SetItemDone(id = id, isDone = isDone)) } + onDoneChanged = { id, isDone -> intents(Intent.SetItemDone(id = id, isDone = isDone)) }, + onDeleteItemClicked = { intents(Intent.DeleteItem(id = it)) } ) } @@ -59,23 +64,33 @@ internal fun TodoMainUi( private fun TodoList( items: List, onItemClicked: (id: Long) -> Unit, - onDoneChanged: (id: Long, isDone: Boolean) -> Unit + onDoneChanged: (id: Long, isDone: Boolean) -> Unit, + onDeleteItemClicked: (id: Long) -> Unit ) { LazyColumnFor(items = items) { item -> - Row(modifier = Modifier.clickable(onClick = { onItemClicked(item.id) }).padding(8.dp)) { + 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), + modifier = Modifier.weight(1F).align(Alignment.CenterVertically), maxLines = 1, overflow = TextOverflow.Ellipsis ) Spacer(modifier = Modifier.width(8.dp)) - Checkbox( - checked = item.isDone, - onCheckedChange = { onDoneChanged(item.id, it) } - ) + IconButton(onClick = { onDeleteItemClicked(item.id) }) { + Icon(Icons.Default.Delete) + } } Divider() diff --git a/examples/todoapp/common/main/src/commonTest/kotlin/example/todo/common/main/integration/TodoMainTest.kt b/examples/todoapp/common/main/src/commonTest/kotlin/example/todo/common/main/integration/TodoMainTest.kt index a0f3999c6e..bd127a1a34 100644 --- a/examples/todoapp/common/main/src/commonTest/kotlin/example/todo/common/main/integration/TodoMainTest.kt +++ b/examples/todoapp/common/main/src/commonTest/kotlin/example/todo/common/main/integration/TodoMainTest.kt @@ -21,6 +21,7 @@ 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") @@ -59,6 +60,16 @@ class TodoMainTest { assertEquals("Item1", firstItem().text) } + @Test + fun WHEN_item_deleted_from_database_THEN_item_not_displayed() { + queries.add("Item1") + val id = lastInsertItem().id + + queries.delete(id = id) + + assertFalse(impl.state.items.any { it.id == id }) + } + @Test fun WHEN_item_selected_THEN_Output_Selected_emitted() { queries.add("Item1") @@ -70,25 +81,35 @@ class TodoMainTest { } @Test - fun GIVEN_item_isDone_false_WHEN_done_changed_to_true_THEN_item_isDone_true() { + fun GIVEN_item_isDone_false_WHEN_done_changed_to_true_THEN_item_isDone_true_in_database() { queries.add("Item1") val id = firstItem().id queries.setDone(id = id, isDone = false) impl.onIntent(Intent.SetItemDone(id = id, isDone = true)) - assertTrue(firstItem().isDone) + assertTrue(queries.select(id = id).executeAsOne().isDone) } @Test - fun GIVEN_item_isDone_true_WHEN_done_changed_to_false_THEN_item_isDone_false() { + fun GIVEN_item_isDone_true_WHEN_done_changed_to_false_THEN_item_isDone_false_in_database() { queries.add("Item1") val id = firstItem().id queries.setDone(id = id, isDone = true) impl.onIntent(Intent.SetItemDone(id = id, isDone = false)) - assertFalse(firstItem().isDone) + assertFalse(queries.select(id = id).executeAsOne().isDone) + } + + @Test + fun WHEN_delete_clicked_THEN_item_deleted_in_database() { + queries.add("Item1") + val id = firstItem().id + + impl.onIntent(Intent.DeleteItem(id = id)) + + assertNull(queries.select(id = id).executeAsOneOrNull()) } @Test diff --git a/examples/todoapp/common/main/src/commonTest/kotlin/example/todo/common/main/store/TestTodoMainStoreDatabase.kt b/examples/todoapp/common/main/src/commonTest/kotlin/example/todo/common/main/store/TestTodoMainStoreDatabase.kt index 7110534606..ecdbdfc397 100644 --- a/examples/todoapp/common/main/src/commonTest/kotlin/example/todo/common/main/store/TestTodoMainStoreDatabase.kt +++ b/examples/todoapp/common/main/src/commonTest/kotlin/example/todo/common/main/store/TestTodoMainStoreDatabase.kt @@ -4,8 +4,6 @@ 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.store.TodoItem -import example.todo.common.main.store.TodoMainStoreProvider internal class TestTodoMainStoreDatabase : TodoMainStoreProvider.Database { @@ -24,6 +22,11 @@ internal class TestTodoMainStoreDatabase : TodoMainStoreProvider.Database { update(id = id) { copy(isDone = isDone) } } + override fun delete(id: Long): Completable = + completableFromFunction { + this.items = items.filterNot { it.id == id } + } + override fun add(text: String): Completable = completableFromFunction { val id = items.maxBy(TodoItem::id)?.id?.inc() ?: 1L diff --git a/examples/todoapp/common/main/src/commonTest/kotlin/example/todo/common/main/store/TodoMainStoreTest.kt b/examples/todoapp/common/main/src/commonTest/kotlin/example/todo/common/main/store/TodoMainStoreTest.kt index 1d117c3046..d716bad008 100644 --- a/examples/todoapp/common/main/src/commonTest/kotlin/example/todo/common/main/store/TodoMainStoreTest.kt +++ b/examples/todoapp/common/main/src/commonTest/kotlin/example/todo/common/main/store/TodoMainStoreTest.kt @@ -7,6 +7,7 @@ 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") @@ -57,6 +58,42 @@ class TodoMainStoreTest { 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()