Reverted TaskItemVM to a dumb state-like VM

Task Edit events are now carried to the parent composable
This commit is contained in:
Geoffroy Bonneville
2025-10-10 18:23:07 -04:00
parent 53e716a690
commit 30d3efa9de
9 changed files with 78 additions and 70 deletions

View File

@@ -2,15 +2,15 @@
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
<SelectionState runConfigName="donextv2">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
<SelectionState runConfigName="overdueCount_correctlyCalculated()">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
<SelectionState runConfigName="donext">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>

View File

@@ -37,7 +37,8 @@ fun DueTodayTasksScreen(
modifier = Modifier.animateItem(),
task = task,
onSwipeLeft = { viewModel.updateTaskDone(task.id!!) },
onSwipeRight = { viewModel.deleteTask(task.id!!) }
onSwipeRight = { viewModel.deleteTask(task.id!!) },
onTaskClick = { viewModel.onTaskClicked(task) }
)
}
}

View File

@@ -80,7 +80,8 @@ fun RecycleBinScreen(
// TODO: add confirmation dialog
viewModel.deleteForever(item.task.id!!)
Toast.makeText(context, "Task deleted", Toast.LENGTH_SHORT).show()
}
},
onTaskClick = { viewModel.onTaskClicked(item.task) }
)
}
}

View File

@@ -28,6 +28,8 @@ import androidx.compose.material3.SwipeToDismissBoxValue
import androidx.compose.material3.Text
import androidx.compose.material3.rememberSwipeToDismissBoxState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
@@ -37,33 +39,42 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.wismna.geoffroy.donext.domain.model.Priority
import com.wismna.geoffroy.donext.domain.model.Task
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskItemViewModel
import kotlinx.coroutines.flow.filter
@Composable
fun TaskItemScreen(
modifier: Modifier = Modifier,
task: Task,
viewModel: TaskItemViewModel = hiltViewModel<TaskItemViewModel>(),
onSwipeLeft: () -> Unit,
onSwipeRight: () -> Unit
onSwipeRight: () -> Unit,
onTaskClick: (task: Task) -> Unit
) {
viewModel.populateTask(task)
// TODO: change this
val viewModel = TaskItemViewModel(task)
val dismissState = rememberSwipeToDismissBoxState(
confirmValueChange = {
when (it) {
SwipeToDismissBoxValue.StartToEnd -> { onSwipeRight() }
SwipeToDismissBoxValue.EndToStart -> { onSwipeLeft() }
SwipeToDismissBoxValue.Settled -> return@rememberSwipeToDismissBoxState false
}
return@rememberSwipeToDismissBoxState true
},
// positional threshold of 25%
positionalThreshold = { it * .25f }
)
LaunchedEffect(dismissState) {
snapshotFlow { dismissState.targetValue }
.filter { it != SwipeToDismissBoxValue.Settled }
.collect { target ->
when (target) {
SwipeToDismissBoxValue.StartToEnd -> {
onSwipeRight()
dismissState.reset()
}
SwipeToDismissBoxValue.EndToStart -> {
onSwipeLeft()
}
else -> Unit
}
}
}
val baseStyle = MaterialTheme.typography.bodyLarge.copy(
fontWeight = when (viewModel.priority) {
Priority.HIGH -> FontWeight.Bold
@@ -79,7 +90,7 @@ fun TaskItemScreen(
)
Card(
modifier = modifier,
onClick = { viewModel.onTaskClicked(task) },
onClick = { onTaskClick(task) },
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
),
@@ -110,7 +121,7 @@ fun TaskItemScreen(
) {
// Title
Text(
text = viewModel.name!!,
text = viewModel.name,
fontSize = 18.sp,
style = baseStyle,
modifier = Modifier
@@ -123,7 +134,7 @@ fun TaskItemScreen(
)
// Due date badge
viewModel.dueDate?.let { dueMillis ->
viewModel.dueDateText?.let { dueMillis ->
Badge(
modifier = Modifier
.align(
@@ -134,7 +145,7 @@ fun TaskItemScreen(
) {
Text(
modifier = Modifier.padding(start = 1.dp, end = 1.dp),
text = viewModel.dueDate!!,
text = viewModel.dueDateText,
color = if (viewModel.isOverdue) Color.White else MaterialTheme.colorScheme.onPrimaryContainer,
style = MaterialTheme.typography.bodySmall
)
@@ -151,7 +162,7 @@ fun TaskItemScreen(
) {
if (!viewModel.description.isNullOrBlank()) {
Text(
text = viewModel.description!!,
text = viewModel.description,
color = MaterialTheme.colorScheme.tertiary,
style = baseStyle.copy(
fontSize = MaterialTheme.typography.bodyMedium.fontSize,

View File

@@ -55,7 +55,8 @@ fun TaskListScreen(
modifier = Modifier.animateItem(),
task = task,
onSwipeLeft = { viewModel.updateTaskDone(task.id!!, true) },
onSwipeRight = { viewModel.deleteTask(task.id!!) }
onSwipeRight = { viewModel.deleteTask(task.id!!) },
onTaskClick = { viewModel.onTaskClicked(task) }
)
}
@@ -80,8 +81,8 @@ fun TaskListScreen(
task = task,
onSwipeLeft = { viewModel.updateTaskDone(task.id!!, false) },
onSwipeRight = { viewModel.deleteTask(task.id!!) },
onTaskClick = { viewModel.onTaskClicked(task) }
)
}
}
}

View File

@@ -36,6 +36,12 @@ class DueTodayViewModel @Inject constructor(
.launchIn(viewModelScope)
}
fun onTaskClicked(task: Task) {
viewModelScope.launch {
uiEventBus.send(UiEvent.EditTask(task))
}
}
fun updateTaskDone(taskId: Long) {
viewModelScope.launch {
toggleTaskDoneUseCase(taskId, true)

View File

@@ -5,6 +5,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wismna.geoffroy.donext.domain.model.Task
import com.wismna.geoffroy.donext.domain.model.TaskWithListName
import com.wismna.geoffroy.donext.domain.usecase.EmptyRecycleBinUseCase
import com.wismna.geoffroy.donext.domain.usecase.GetDeletedTasksUseCase
@@ -34,6 +35,12 @@ class RecycleBinViewModel @Inject constructor(
loadDeletedTasks()
}
fun onTaskClicked(task: Task) {
viewModelScope.launch {
uiEventBus.send(UiEvent.EditTask(task))
}
}
fun loadDeletedTasks() {
getDeletedTasksUseCase()
.onEach { tasks ->

View File

@@ -1,68 +1,43 @@
package com.wismna.geoffroy.donext.presentation.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wismna.geoffroy.donext.domain.extension.toLocalDate
import com.wismna.geoffroy.donext.domain.model.Priority
import com.wismna.geoffroy.donext.domain.model.Task
import com.wismna.geoffroy.donext.presentation.ui.events.UiEvent
import com.wismna.geoffroy.donext.presentation.ui.events.UiEventBus
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import java.time.LocalDate
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.time.format.TextStyle
import java.util.Locale
import javax.inject.Inject
@HiltViewModel
class TaskItemViewModel @Inject constructor(
private val uiEventBus: UiEventBus
): ViewModel() {
var id: Long? = null
var name: String? = null
var description: String? = null
var dueDate: String? = null
var isDone: Boolean = false
var isDeleted: Boolean = false
var priority: Priority = Priority.NORMAL
var isOverdue: Boolean = false
class TaskItemViewModel(task: Task) {
val id: Long = task.id!!
val name: String = task.name
val description: String? = task.description
val isDone: Boolean = task.isDone
val isDeleted: Boolean = task.isDeleted
val priority: Priority = task.priority
val today: LocalDate = LocalDate.now(ZoneId.systemDefault())
fun populateTask(task: Task) {
id = task.id!!
name = task.name
description = task.description
dueDate = task.dueDate?.let { formatDueDate(it) }
isDone = task.isDone
isDeleted = task.isDeleted
priority = task.priority
isOverdue = task.dueDate?.let { millis ->
val isOverdue: Boolean = task.dueDate?.let { millis ->
val dueDate = millis.toLocalDate()
dueDate.isBefore(today)
} ?: false
}
fun onTaskClicked(task: Task) {
viewModelScope.launch {
uiEventBus.send(UiEvent.EditTask(task))
}
}
val dueDateText: String? = task.dueDate?.let { formatDueDate(it) }
private fun formatDueDate(dueMillis: Long): String {
val dueDateLocal = dueMillis.toLocalDate()
val dueDate = dueMillis.toLocalDate()
return when {
dueDateLocal.isEqual(today) -> "Today"
dueDateLocal.isEqual(today.plusDays(1)) -> "Tomorrow"
dueDateLocal.isEqual(today.minusDays(1)) -> "Yesterday"
dueDateLocal.isAfter(today) && dueDateLocal.isBefore(today.plusDays(7)) ->
dueDateLocal.dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.getDefault())
dueDate.isEqual(today) -> "Today"
dueDate.isEqual(today.plusDays(1)) -> "Tomorrow"
dueDate.isEqual(today.minusDays(1)) -> "Yesterday"
dueDate.isAfter(today) && dueDate.isBefore(today.plusDays(7)) ->
dueDate.dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.getDefault())
else ->
dueDateLocal.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(Locale.getDefault()))
dueDate.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(Locale.getDefault()))
}
}
}

View File

@@ -43,6 +43,12 @@ class TaskListViewModel @Inject constructor(
.launchIn(viewModelScope)
}
fun onTaskClicked(task: Task) {
viewModelScope.launch {
uiEventBus.send(UiEvent.EditTask(task))
}
}
fun updateTaskDone(taskId: Long, isDone: Boolean) {
viewModelScope.launch {
toggleTaskDoneUseCase(taskId, isDone)