Skip to content

Commit

Permalink
Merge pull request #39 from felipejoglar/felipejoglar/load-tasks-use-…
Browse files Browse the repository at this point in the history
…case

Load tasks use case
  • Loading branch information
felipejoglar committed Feb 1, 2024
2 parents c121ce3 + 3e481de commit fb72ac1
Show file tree
Hide file tree
Showing 22 changed files with 247 additions and 110 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import androidx.compose.runtime.setValue
import com.taskodoro.android.app.tasks.new.TaskNewScreen
import com.taskodoro.android.app.tasks.new.TaskNewViewModel
import com.taskodoro.tasks.feature.new.TaskNewUseCase
import kotlinx.coroutines.Dispatchers
import moe.tlaster.precompose.navigation.BackHandler
import moe.tlaster.precompose.navigation.NavOptions
import moe.tlaster.precompose.navigation.Navigator
Expand All @@ -46,7 +45,7 @@ fun RouteBuilder.taskNewScreen(
route = TASK_NEW_ROUTE,
) {
val viewModel = viewModel { savedStateHolder ->
TaskNewViewModel(taskNew, Dispatchers.IO, savedStateHolder)
TaskNewViewModel(taskNew, savedStateHolder)
}

val state by viewModel.uiState.collectAsState()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,18 @@ import androidx.annotation.StringRes
import com.taskodoro.android.R
import com.taskodoro.tasks.feature.new.TaskNewUseCase
import com.taskodoro.tasks.validator.TaskValidatorError
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import moe.tlaster.precompose.stateholder.SavedStateHolder
import moe.tlaster.precompose.viewmodel.ViewModel
import moe.tlaster.precompose.viewmodel.viewModelScope

class TaskNewViewModel(
private val taskNew: TaskNewUseCase,
private val dispatcher: CoroutineDispatcher,
savedStateHolder: SavedStateHolder,
) : ViewModel() {

Expand Down Expand Up @@ -68,19 +62,19 @@ class TaskNewViewModel(
}

fun onSubmitClicked() {
taskNew(_uiState.value)
.flowOn(dispatcher)
.onStart { updateWith(loading = true) }
.onSuccess { updateWith(isTaskSaved = true) }
.onFailure { handleError(it as TaskValidatorError) }
.catch { updateWithError() }
.launchIn(viewModelScope)
viewModelScope.launch {
runCatching {
updateWith(loading = true)
taskNew(_uiState.value)
.onSuccess { updateWith(isTaskSaved = true) }
.onFailure { handleError(it as TaskValidatorError) }
}.onFailure { updateWithError() }
}
}

private fun taskNew(state: TaskNewUiState) =
flow {
emit(taskNew(state.title, state.description, state.dueDate))
}
private suspend fun taskNew(state: TaskNewUiState): Result<Unit> {
return taskNew(state.title, state.description, state.dueDate)
}

private fun updateWith(
loading: Boolean = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import com.taskodoro.android.app.helpers.expectEquals
import com.taskodoro.tasks.feature.new.TaskNewUseCase
import com.taskodoro.tasks.validator.TaskValidatorError
import com.taskodoro.tasks.validator.ValidatorError
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import moe.tlaster.precompose.stateholder.SavedStateHolder
import org.junit.Assert
import org.junit.Test
Expand Down Expand Up @@ -217,7 +216,6 @@ class TaskNewViewModelTest {
val taskNew = TaskNewUseCaseStub()
val sut = TaskNewViewModel(
taskNew = taskNew,
dispatcher = UnconfinedTestDispatcher(),
savedStateHolder = SavedStateHolder("", null),
)

Expand Down
1 change: 1 addition & 0 deletions gradle/catalogs/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-
sqlDelight-android-driver = { module = "app.cash.sqldelight:android-driver", version.ref = "sqlDelight" }
sqlDelight-native-driver = { module = "app.cash.sqldelight:native-driver", version.ref = "sqlDelight" }
sqlDelight-jvm-driver = { module = "app.cash.sqldelight:sqlite-driver", version.ref = "sqlDelight" }
sqlDelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqlDelight" }

# DataStore - https://developer.android.com/jetpack/androidx/releases/datastore
datastore = { module = "androidx.datastore:datastore-preferences-core", version.ref = "datastore" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ CREATE TABLE LocalTask (
id TEXT NOT NULL PRIMARY KEY UNIQUE,
title TEXT NOT NULL,
description TEXT,
priority INTEGER DEFAULT 1,
completed INTEGER AS Boolean NOT NULL DEFAULT 0,
dueDate INTEGER NOT NULL,
createdAt INTEGER NOT NULL,
updatedAt INTEGER
dueDate TEXT NOT NULL,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL
);

CREATE UNIQUE INDEX task_id ON LocalTask(id);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
insert:
INSERT INTO LocalTask(id, title, description, priority, completed, dueDate, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?);
INSERT INTO LocalTask VALUES ?;

load:
SELECT * FROM LocalTask;
SELECT * FROM LocalTask ORDER BY dueDate ASC;
1 change: 1 addition & 0 deletions taskodoro/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ kotlin {
implementation(projects.infra.preferences)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.coroutines)
implementation(libs.sqlDelight.coroutines)
}

commonTest.dependencies {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,7 @@ actual class TaskStoreFactory(
) {

actual fun create(): TaskStore {
val database = DatabaseFactory(context).create().apply {
taskdoroDBQueries.clearDB()
}

val database = DatabaseFactory(context).create()
return SQLDelightTaskStore(database = database)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@

package com.taskodoro.tasks.data

import com.taskodoro.tasks.feature.TaskLoader
import com.taskodoro.tasks.feature.TaskSaver
import com.taskodoro.tasks.feature.model.Task
import kotlinx.coroutines.flow.Flow

class TaskRepository(
private val store: TaskStore,
) : TaskSaver {
) : TaskSaver, TaskLoader {

object SaveFailed : Exception()

Expand All @@ -32,4 +34,8 @@ class TaskRepository(
} catch (exception: Exception) {
Result.failure(SaveFailed)
}

override fun load(): Flow<List<Task>> {
return store.load()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
package com.taskodoro.tasks.data

import com.taskodoro.tasks.feature.model.Task
import kotlinx.coroutines.flow.Flow

interface TaskStore {
suspend fun save(task: Task)
fun load(): Flow<List<Task>>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright 2024 Felipe Joglar
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.taskodoro.tasks.data.local

import com.taskodoro.model.Uuid
import com.taskodoro.storage.db.LocalTask
import com.taskodoro.tasks.feature.model.Task
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime

fun Task.toLocal() = LocalTask(
id = id.uuidString,
title = title,
description = description,
completed = isCompleted,
dueDate = dueDate.toLocal(),
createdAt = createdAt.toLocal(),
updatedAt = updatedAt.toLocal(),
)

fun List<LocalTask>.toModels() =
mapNotNull { task ->
val id = Uuid.from(task.id)
if (id != null) id to task else null
}.map { (id, task) ->
Task(
id = id,
title = task.title,
description = task.description,
isCompleted = task.completed,
dueDate = task.dueDate.toModel(),
createdAt = task.createdAt.toModel(),
updatedAt = task.updatedAt.toModel(),
)
}

private fun LocalDateTime.toLocal() =
toInstant(TimeZone.currentSystemDefault()).toString()

private fun String.toModel() =
Instant.parse(this).toLocalDateTime(TimeZone.currentSystemDefault())
Original file line number Diff line number Diff line change
Expand Up @@ -16,31 +16,42 @@

package com.taskodoro.tasks.data.local

import app.cash.sqldelight.coroutines.asFlow
import app.cash.sqldelight.coroutines.mapToList
import com.taskodoro.model.Uuid
import com.taskodoro.storage.db.LocalTask
import com.taskodoro.storage.db.TaskodoroDB
import com.taskodoro.tasks.data.TaskStore
import com.taskodoro.tasks.feature.model.Task
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime

internal class SQLDelightTaskStore(
database: TaskodoroDB,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) : TaskStore {

internal val tasksQueries = database.localTaskQueries
private val tasksQueries = database.localTaskQueries

override suspend fun save(task: Task) {
withContext(Dispatchers.IO) {
tasksQueries.insert(
id = task.id.uuidString,
title = task.title,
description = task.description,
priority = task.priority.ordinal.toLong(),
completed = false,
dueDate = task.dueDate,
createdAt = task.createdAt,
updatedAt = 0,
)
tasksQueries.insert(task.toLocal())
}
}

override fun load(): Flow<List<Task>> {
return tasksQueries.load()
.asFlow()
.mapToList(dispatcher)
.map { it.toModels() }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
package com.taskodoro.tasks.feature

import com.taskodoro.tasks.feature.model.Task
import kotlinx.coroutines.flow.Flow

interface TaskLoader {
fun load(onChange: (List<Task>) -> Unit)
fun load(): Flow<List<Task>>
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,14 @@
package com.taskodoro.tasks.feature.model

import com.taskodoro.model.Uuid
import kotlinx.datetime.LocalDateTime

data class Task(
val id: Uuid = Uuid(),
val title: String,
val description: String? = null,
val priority: Priority = Priority.MEDIUM,
val dueDate: Long,
val isCompleted: Boolean = false,
val createdAt: Long,
val updatedAt: Long = 0,
) {

enum class Priority {
LOW, MEDIUM, HIGH;

companion object {
fun fromValue(value: Int) =
when {
value < 1 -> LOW
value == 1 -> MEDIUM
else -> HIGH
}
}
}
}
val dueDate: LocalDateTime,
val createdAt: LocalDateTime,
val updatedAt: LocalDateTime,
)
Original file line number Diff line number Diff line change
Expand Up @@ -20,31 +20,34 @@ import com.taskodoro.tasks.feature.TaskSaver
import com.taskodoro.tasks.feature.model.Task
import com.taskodoro.tasks.validator.TaskValidatorError
import com.taskodoro.tasks.validator.Validator
import kotlinx.datetime.LocalDateTime

interface TaskNewUseCase {
suspend operator fun invoke(
title: String,
description: String? = null,
dueDate: Long? = null,
dueDate: LocalDateTime? = null,
): Result<Unit>
}

class TaskNew(
private val saver: TaskSaver,
private val validator: Validator<Task>,
private val now: () -> Long,
private val now: () -> LocalDateTime,
) : TaskNewUseCase {

override suspend operator fun invoke(
title: String,
description: String?,
dueDate: Long?,
dueDate: LocalDateTime?,
): Result<Unit> {
val now = now()
val task = Task(
title = title.trim(),
description = description,
dueDate = dueDate ?: now(),
createdAt = now(),
dueDate = dueDate ?: now,
createdAt = now,
updatedAt = now
)

return try {
Expand Down
Loading

0 comments on commit fb72ac1

Please sign in to comment.