diff --git a/feature/task-details/impl/build.gradle.kts b/feature/task-details/impl/build.gradle.kts
index 702cac26..cf10c21c 100644
--- a/feature/task-details/impl/build.gradle.kts
+++ b/feature/task-details/impl/build.gradle.kts
@@ -1,5 +1,5 @@
plugins {
- id("todozy.android.library")
+ id("todozy.android.library.compose")
}
android {
@@ -26,6 +26,10 @@ dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.material)
+ implementation(libs.androidx.compose.ui)
+ implementation(libs.androidx.compose.material)
+ implementation(libs.androidx.compose.runtime.livedata)
+ implementation(libs.androidx.compose.material.icons.extended)
testImplementation(libs.junit4)
testImplementation(libs.mockk.core)
diff --git a/feature/task-details/impl/src/main/java/br/com/sailboat/todozy/feature/task/details/impl/presentation/TaskDetailsFragment.kt b/feature/task-details/impl/src/main/java/br/com/sailboat/todozy/feature/task/details/impl/presentation/TaskDetailsFragment.kt
index 98a1ef02..c9bfbdb3 100644
--- a/feature/task-details/impl/src/main/java/br/com/sailboat/todozy/feature/task/details/impl/presentation/TaskDetailsFragment.kt
+++ b/feature/task-details/impl/src/main/java/br/com/sailboat/todozy/feature/task/details/impl/presentation/TaskDetailsFragment.kt
@@ -3,20 +3,17 @@ package br.com.sailboat.todozy.feature.task.details.impl.presentation
import android.app.Activity
import android.os.Bundle
import android.view.LayoutInflater
-import android.view.Menu
-import android.view.MenuInflater
-import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
-import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.TooltipCompat
-import androidx.core.view.MenuProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager
-import br.com.sailboat.todozy.domain.model.TaskMetrics
import br.com.sailboat.todozy.domain.model.TaskProgressRange
import br.com.sailboat.todozy.feature.navigation.android.TaskFormNavigator
import br.com.sailboat.todozy.feature.task.details.impl.databinding.FrgTaskDetailsBinding
@@ -24,7 +21,6 @@ import br.com.sailboat.todozy.feature.task.details.impl.presentation.viewmodel.T
import br.com.sailboat.todozy.feature.task.details.impl.presentation.viewmodel.TaskDetailsViewIntent
import br.com.sailboat.todozy.feature.task.details.impl.presentation.viewmodel.TaskDetailsViewIntent.OnClickConfirmDeleteTask
import br.com.sailboat.todozy.feature.task.details.impl.presentation.viewmodel.TaskDetailsViewModel
-import br.com.sailboat.todozy.utility.android.fragment.hapticHandled
import br.com.sailboat.todozy.utility.android.view.gone
import br.com.sailboat.todozy.utility.android.view.setSafeClickListener
import br.com.sailboat.todozy.utility.android.view.visible
@@ -33,6 +29,7 @@ import br.com.sailboat.uicomponent.impl.dialog.twooptions.TwoOptionsDialog
import br.com.sailboat.uicomponent.impl.helper.getTaskId
import br.com.sailboat.uicomponent.impl.helper.putTaskId
import br.com.sailboat.uicomponent.impl.progress.TaskProgressHeaderAdapter
+import br.com.sailboat.uicomponent.impl.theme.TodozyTheme
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import br.com.sailboat.todozy.feature.task.details.impl.R as TaskDetailsR
@@ -96,7 +93,6 @@ internal class TaskDetailsFragment : Fragment() {
}
private fun initViews() {
- binding.toolbar.setTitle(UiR.string.task_details)
val fabLabel = getString(TaskDetailsR.string.fab_edit_task)
binding.fab.root.text = fabLabel
binding.fab.root.setIconResource(UiR.drawable.ic_edit_white_24dp)
@@ -124,12 +120,17 @@ internal class TaskDetailsFragment : Fragment() {
layoutManager = LinearLayoutManager(activity)
}
- (activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
- binding.toolbar.setNavigationIcon(UiR.drawable.ic_arrow_back_white_24dp)
- binding.toolbar.setNavigationOnClickListener {
- requireActivity().onBackPressedDispatcher.onBackPressed()
+ binding.taskDetailsTopBar.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ binding.taskDetailsTopBar.setContent {
+ TodozyTheme {
+ val taskMetrics by viewModel.viewState.taskMetrics.observeAsState()
+ TaskDetailsTopBar(
+ taskMetrics = taskMetrics,
+ onNavigateBack = { requireActivity().onBackPressedDispatcher.onBackPressed() },
+ onClickDelete = { viewModel.dispatchViewIntent(TaskDetailsViewIntent.OnClickMenuDelete) },
+ )
+ }
}
- addMenuProvider()
}
private fun observeViewModel() {
@@ -146,9 +147,6 @@ internal class TaskDetailsFragment : Fragment() {
viewModel.viewState.taskDetails.observe(viewLifecycleOwner) { items ->
taskDetailsAdapter?.submitList(items)
}
- viewModel.viewState.taskMetrics.observe(viewLifecycleOwner) { taskMetrics ->
- taskMetrics?.run { showMetrics(this) } ?: hideMetrics()
- }
viewModel.viewState.taskProgressDays.observe(viewLifecycleOwner) { renderProgress() }
viewModel.viewState.taskProgressDayOrder.observe(viewLifecycleOwner) { renderProgress() }
viewModel.viewState.taskProgressRange.observe(viewLifecycleOwner) { renderProgress() }
@@ -195,52 +193,6 @@ internal class TaskDetailsFragment : Fragment() {
Toast.makeText(activity, UiR.string.msg_error, Toast.LENGTH_SHORT).show()
}
- private fun addMenuProvider() {
- requireActivity().addMenuProvider(
- object : MenuProvider {
- override fun onCreateMenu(
- menu: Menu,
- menuInflater: MenuInflater,
- ) {
- menuInflater.inflate(TaskDetailsR.menu.menu_task_details, menu)
- }
-
- override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
- return when (menuItem.itemId) {
- TaskDetailsR.id.menu_delete -> {
- return hapticHandled {
- viewModel.dispatchViewIntent(TaskDetailsViewIntent.OnClickMenuDelete)
- }
- }
- else -> false
- }
- }
- },
- viewLifecycleOwner,
- )
- }
-
- private fun showMetrics(taskMetrics: TaskMetrics) {
- binding.taskMetrics.tvMetricsDone.text = taskMetrics.doneTasks.toString()
- binding.taskMetrics.tvMetricsNotDone.text = taskMetrics.notDoneTasks.toString()
- binding.taskMetrics.tvMetricsFire.text = taskMetrics.consecutiveDone.toString()
-
- if (taskMetrics.consecutiveDone == 0) {
- binding.taskMetrics.taskMetricsLlFire.gone()
- } else {
- binding.taskMetrics.taskMetricsLlFire.visible()
- }
-
- binding.appbar.setExpanded(true, true)
- binding.appbarTaskDetailsFlMetrics.visible()
- binding.taskMetrics.root.visible()
- }
-
- private fun hideMetrics() {
- binding.taskMetrics.root.gone()
- binding.appbarTaskDetailsFlMetrics.gone()
- }
-
private fun renderProgress() {
val progressDays = viewModel.viewState.taskProgressDays.value.orEmpty()
val range = viewModel.viewState.taskProgressRange.value ?: TaskProgressRange.LAST_YEAR
diff --git a/feature/task-details/impl/src/main/java/br/com/sailboat/todozy/feature/task/details/impl/presentation/TaskDetailsTopBar.kt b/feature/task-details/impl/src/main/java/br/com/sailboat/todozy/feature/task/details/impl/presentation/TaskDetailsTopBar.kt
new file mode 100644
index 00000000..94de34a5
--- /dev/null
+++ b/feature/task-details/impl/src/main/java/br/com/sailboat/todozy/feature/task/details/impl/presentation/TaskDetailsTopBar.kt
@@ -0,0 +1,105 @@
+package br.com.sailboat.todozy.feature.task.details.impl.presentation
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.material.DropdownMenu
+import androidx.compose.material.DropdownMenuItem
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import br.com.sailboat.todozy.domain.model.TaskMetrics
+import br.com.sailboat.uicomponent.impl.metrics.TaskMetricsRow
+import br.com.sailboat.uicomponent.impl.theme.LocalTodozySpacing
+import br.com.sailboat.uicomponent.impl.R as UiR
+
+@Composable
+internal fun TaskDetailsTopBar(
+ taskMetrics: TaskMetrics?,
+ onNavigateBack: () -> Unit,
+ onClickDelete: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val spacing = LocalTodozySpacing.current
+ var isMenuExpanded by remember { mutableStateOf(false) }
+ Surface(
+ color = colorResource(id = UiR.color.md_blue_500),
+ elevation = 4.dp,
+ modifier = modifier,
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .statusBarsPadding()
+ .padding(vertical = spacing.small),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ IconButton(onClick = onNavigateBack) {
+ Icon(
+ painter = painterResource(id = UiR.drawable.ic_arrow_back_white_24dp),
+ contentDescription = null,
+ tint = Color.White,
+ )
+ }
+ if (taskMetrics == null) {
+ Text(
+ text = stringResource(id = UiR.string.task_details),
+ style = MaterialTheme.typography.h6,
+ color = Color.White,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ modifier = Modifier.weight(1f),
+ )
+ } else {
+ TaskMetricsRow(
+ taskMetrics = taskMetrics,
+ modifier =
+ Modifier
+ .weight(1f)
+ .padding(horizontal = spacing.small),
+ )
+ }
+ Box {
+ IconButton(onClick = { isMenuExpanded = true }) {
+ Icon(
+ imageVector = Icons.Default.MoreVert,
+ contentDescription = null,
+ tint = Color.White,
+ )
+ }
+ DropdownMenu(
+ expanded = isMenuExpanded,
+ onDismissRequest = { isMenuExpanded = false },
+ ) {
+ DropdownMenuItem(
+ onClick = {
+ isMenuExpanded = false
+ onClickDelete()
+ },
+ ) {
+ Text(text = stringResource(id = UiR.string.delete))
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/feature/task-details/impl/src/main/res/layout/frg_task_details.xml b/feature/task-details/impl/src/main/res/layout/frg_task_details.xml
index b7245d51..6d9d0280 100644
--- a/feature/task-details/impl/src/main/res/layout/frg_task_details.xml
+++ b/feature/task-details/impl/src/main/res/layout/frg_task_details.xml
@@ -12,29 +12,11 @@
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
-
-
-
-
-
-
-
-
+ android:minHeight="?attr/actionBarSize" />
Edit Task
+ Metrics
diff --git a/feature/task-list/impl/build.gradle.kts b/feature/task-list/impl/build.gradle.kts
index f4a42bfe..61d6cd9f 100644
--- a/feature/task-list/impl/build.gradle.kts
+++ b/feature/task-list/impl/build.gradle.kts
@@ -37,6 +37,7 @@ dependencies {
implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.androidx.lifecycle.runtime)
implementation(libs.androidx.lifecycle.livedata)
+ implementation(libs.androidx.compose.lifecycle.runtime.compose)
implementation(libs.koin.android)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.recyclerview)
diff --git a/feature/task-list/impl/src/main/java/br/com/sailboat/todozy/feature/task/list/impl/presentation/TaskListFragment.kt b/feature/task-list/impl/src/main/java/br/com/sailboat/todozy/feature/task/list/impl/presentation/TaskListFragment.kt
index 56683ad2..98e4eb43 100644
--- a/feature/task-list/impl/src/main/java/br/com/sailboat/todozy/feature/task/list/impl/presentation/TaskListFragment.kt
+++ b/feature/task-list/impl/src/main/java/br/com/sailboat/todozy/feature/task/list/impl/presentation/TaskListFragment.kt
@@ -7,12 +7,15 @@ import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
import br.com.sailboat.todozy.domain.model.TaskProgressRange
import br.com.sailboat.todozy.feature.navigation.android.AboutNavigator
import br.com.sailboat.todozy.feature.navigation.android.HomeDestination
@@ -25,6 +28,7 @@ import br.com.sailboat.todozy.feature.task.list.impl.presentation.viewmodel.Task
import br.com.sailboat.todozy.feature.task.list.impl.presentation.viewmodel.TaskListViewIntent
import br.com.sailboat.todozy.feature.task.list.impl.presentation.viewmodel.TaskListViewModel
import br.com.sailboat.uicomponent.impl.helper.NotificationHelper
+import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import br.com.sailboat.uicomponent.impl.R as UiR
@@ -56,14 +60,14 @@ internal class TaskListFragment : Fragment() {
)
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
- val tasksLoading by viewModel.viewState.tasksLoading.observeAsState(false)
- val items by viewModel.viewState.itemsView.observeAsState(mutableListOf())
- val taskMetrics by viewModel.viewState.taskMetrics.observeAsState()
- val taskProgressDays by viewModel.viewState.taskProgressDays.observeAsState(emptyList())
- val taskProgressRange by viewModel.viewState.taskProgressRange.observeAsState(
+ val tasksLoading by viewModel.viewState.tasksLoading.collectAsStateWithLifecycle(false)
+ val items by viewModel.viewState.itemsView.collectAsStateWithLifecycle(emptyList())
+ val taskMetrics by viewModel.viewState.taskMetrics.collectAsStateWithLifecycle()
+ val taskProgressDays by viewModel.viewState.taskProgressDays.collectAsStateWithLifecycle(emptyList())
+ val taskProgressRange by viewModel.viewState.taskProgressRange.collectAsStateWithLifecycle(
TaskProgressRange.LAST_YEAR,
)
- val taskProgressLoading by viewModel.viewState.taskProgressLoading.observeAsState(false)
+ val taskProgressLoading by viewModel.viewState.taskProgressLoading.collectAsStateWithLifecycle(false)
val haptics = LocalHapticFeedback.current
TaskListScreen(
@@ -129,16 +133,21 @@ internal class TaskListFragment : Fragment() {
}
private fun observeActions() {
- viewModel.viewState.viewAction.observe(viewLifecycleOwner) { viewAction ->
- when (viewAction) {
- is TaskListViewAction.CloseNotifications -> closeNotifications()
- is TaskListViewAction.NavigateToAbout -> navigateToAbout()
- is TaskListViewAction.NavigateToHistory -> navigateToHistory()
- is TaskListViewAction.NavigateToSettings -> navigateToSettings()
- is TaskListViewAction.NavigateToTaskForm -> navigateToTaskForm()
- is TaskListViewAction.NavigateToTaskDetails -> navigateToTaskDetails(viewAction.taskId)
- is TaskListViewAction.ShowErrorCompletingTask -> showErrorCompletingTask()
- is TaskListViewAction.ShowErrorLoadingTasks -> showErrorLoadingTasks()
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.viewState.viewAction.collect { viewAction ->
+ when (viewAction) {
+ is TaskListViewAction.CloseNotifications -> closeNotifications()
+ is TaskListViewAction.NavigateToAbout -> navigateToAbout()
+ is TaskListViewAction.NavigateToHistory -> navigateToHistory()
+ is TaskListViewAction.NavigateToSettings -> navigateToSettings()
+ is TaskListViewAction.NavigateToTaskForm -> navigateToTaskForm()
+ is TaskListViewAction.NavigateToTaskDetails -> navigateToTaskDetails(viewAction.taskId)
+ is TaskListViewAction.ShowErrorCompletingTask -> showErrorCompletingTask()
+ is TaskListViewAction.ShowErrorLoadingTasks -> showErrorLoadingTasks()
+ }
+ viewModel.viewState.viewAction.resetReplayCache()
+ }
}
}
}
diff --git a/feature/task-list/impl/src/main/java/br/com/sailboat/todozy/feature/task/list/impl/presentation/TaskListScreen.kt b/feature/task-list/impl/src/main/java/br/com/sailboat/todozy/feature/task/list/impl/presentation/TaskListScreen.kt
index 12631956..085c2e85 100644
--- a/feature/task-list/impl/src/main/java/br/com/sailboat/todozy/feature/task/list/impl/presentation/TaskListScreen.kt
+++ b/feature/task-list/impl/src/main/java/br/com/sailboat/todozy/feature/task/list/impl/presentation/TaskListScreen.kt
@@ -26,7 +26,6 @@ import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.DismissDirection
import androidx.compose.material.DismissValue
@@ -64,7 +63,6 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
@@ -75,11 +73,11 @@ import br.com.sailboat.todozy.domain.model.TaskProgressRange
import br.com.sailboat.todozy.domain.model.TaskStatus
import br.com.sailboat.todozy.feature.task.list.impl.R
import br.com.sailboat.todozy.utility.kotlin.extension.isTrue
+import br.com.sailboat.uicomponent.impl.metrics.TaskMetricsRow
import br.com.sailboat.uicomponent.impl.progress.TaskProgressContent
import br.com.sailboat.uicomponent.impl.skeleton.TaskSkeletonItem
import br.com.sailboat.uicomponent.impl.subhead.SubheadItem
import br.com.sailboat.uicomponent.impl.task.TaskItem
-import br.com.sailboat.uicomponent.impl.theme.LocalTodozySemanticColors
import br.com.sailboat.uicomponent.impl.theme.LocalTodozySpacing
import br.com.sailboat.uicomponent.impl.theme.TodozyTheme
import br.com.sailboat.uicomponent.model.SubheadUiModel
@@ -327,7 +325,10 @@ private fun TaskListTopBar(
taskMetrics?.let {
Spacer(modifier = Modifier.height(spacing.small))
- MetricsRow(taskMetrics = it)
+ TaskMetricsRow(
+ taskMetrics = it,
+ modifier = Modifier.fillMaxWidth(),
+ )
}
}
}
@@ -358,82 +359,6 @@ private fun SwipeableTaskItem(
)
}
-@Composable
-private fun MetricsRow(taskMetrics: TaskMetrics) {
- val spacing = LocalTodozySpacing.current
- val semanticColors = LocalTodozySemanticColors.current
- val doneTint = colorResource(id = UiR.color.md_teal_300)
- val notDoneTint = colorResource(id = UiR.color.md_red_300)
- Row(
- modifier = Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(spacing.medium),
- ) {
- if (taskMetrics.consecutiveDone > 0) {
- MetricIconWithValue(
- iconId = UiR.drawable.ic_fire_black_24dp,
- tint = semanticColors.warning,
- value = taskMetrics.consecutiveDone,
- modifier = Modifier.weight(1f),
- )
- }
- MetricIconWithValue(
- iconId = UiR.drawable.ic_vec_thumb_up_white_24dp,
- tint = doneTint,
- value = taskMetrics.doneTasks,
- modifier = Modifier.weight(1f),
- )
- MetricIconWithValue(
- iconId = UiR.drawable.ic_vect_thumb_down_white_24dp,
- tint = notDoneTint,
- value = taskMetrics.notDoneTasks,
- modifier = Modifier.weight(1f),
- )
- }
-}
-
-@Composable
-private fun MetricIconWithValue(
- iconId: Int,
- tint: Color,
- value: Int,
- modifier: Modifier = Modifier,
-) {
- val spacing = LocalTodozySpacing.current
- Row(
- modifier = modifier,
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(spacing.xsmall),
- ) {
- Surface(
- shape = CircleShape,
- color = Color.White,
- elevation = 0.dp,
- ) {
- Box(
- modifier =
- Modifier
- .size(28.dp)
- .padding(spacing.xxsmall),
- contentAlignment = Alignment.Center,
- ) {
- Icon(
- painter = painterResource(id = iconId),
- contentDescription = null,
- tint = tint,
- modifier = Modifier.height(18.dp),
- )
- }
- }
- Text(
- text = value.toString(),
- style = MaterialTheme.typography.subtitle1,
- color = Color.White,
- fontWeight = FontWeight.Bold,
- )
- }
-}
-
@Composable
private fun EmptyState() {
val spacing = LocalTodozySpacing.current
diff --git a/feature/task-list/impl/src/main/java/br/com/sailboat/todozy/feature/task/list/impl/presentation/viewmodel/TaskListViewModel.kt b/feature/task-list/impl/src/main/java/br/com/sailboat/todozy/feature/task/list/impl/presentation/viewmodel/TaskListViewModel.kt
index a8f0705c..34d09438 100644
--- a/feature/task-list/impl/src/main/java/br/com/sailboat/todozy/feature/task/list/impl/presentation/viewmodel/TaskListViewModel.kt
+++ b/feature/task-list/impl/src/main/java/br/com/sailboat/todozy/feature/task/list/impl/presentation/viewmodel/TaskListViewModel.kt
@@ -117,14 +117,14 @@ internal class TaskListViewModel(
private suspend fun performFullLoad(closeNotifications: Boolean = false) {
try {
- viewState.tasksLoading.postValue(true)
- viewState.taskProgressLoading.postValue(true)
+ viewState.tasksLoading.value = true
+ viewState.taskProgressLoading.value = true
clearProgressCache()
if (closeNotifications) {
- viewState.viewAction.postValue(TaskListViewAction.CloseNotifications)
+ viewState.viewAction.tryEmit(TaskListViewAction.CloseNotifications)
}
loadTasks()
- viewState.tasksLoading.postValue(false)
+ viewState.tasksLoading.value = false
loadTaskMetrics()
loadProgress()
scheduleAllAlarmsUseCase()
@@ -132,30 +132,30 @@ internal class TaskListViewModel(
hasLoaded = true
} catch (e: Exception) {
logService.error(e)
- viewState.viewAction.value = TaskListViewAction.ShowErrorLoadingTasks
+ viewState.viewAction.tryEmit(TaskListViewAction.ShowErrorLoadingTasks)
} finally {
- viewState.taskProgressLoading.postValue(false)
+ viewState.taskProgressLoading.value = false
}
}
private fun onClickMenuAbout() {
- viewState.viewAction.value = TaskListViewAction.NavigateToAbout
+ viewState.viewAction.tryEmit(TaskListViewAction.NavigateToAbout)
}
private fun onClickMenuSettings() {
- viewState.viewAction.value = TaskListViewAction.NavigateToSettings
+ viewState.viewAction.tryEmit(TaskListViewAction.NavigateToSettings)
}
private fun onClickMenuHistory() {
- viewState.viewAction.value = TaskListViewAction.NavigateToHistory
+ viewState.viewAction.tryEmit(TaskListViewAction.NavigateToHistory)
}
private fun onClickNewTask() {
- viewState.viewAction.value = TaskListViewAction.NavigateToTaskForm
+ viewState.viewAction.tryEmit(TaskListViewAction.NavigateToTaskForm)
}
private fun onClickTask(taskId: Long) {
- viewState.viewAction.value = TaskListViewAction.NavigateToTaskDetails(taskId = taskId)
+ viewState.viewAction.tryEmit(TaskListViewAction.NavigateToTaskDetails(taskId = taskId))
}
private fun onSubmitSearchTerm(term: String) {
@@ -163,8 +163,8 @@ internal class TaskListViewModel(
searchJob = viewModelScope.launch {
try {
delay(SEARCH_DEBOUNCE_IN_MILLIS)
- viewState.tasksLoading.postValue(true)
- viewState.taskProgressLoading.postValue(true)
+ viewState.tasksLoading.value = true
+ viewState.taskProgressLoading.value = true
val hasQueryChanged = taskFilter.text != term
taskFilter = taskFilter.copy(text = term)
if (hasQueryChanged) {
@@ -177,10 +177,10 @@ internal class TaskListViewModel(
return@launch
} catch (e: Exception) {
logService.error(e)
- viewState.viewAction.value = TaskListViewAction.ShowErrorLoadingTasks
+ viewState.viewAction.tryEmit(TaskListViewAction.ShowErrorLoadingTasks)
} finally {
- viewState.tasksLoading.postValue(false)
- viewState.taskProgressLoading.postValue(false)
+ viewState.tasksLoading.value = false
+ viewState.taskProgressLoading.value = false
}
}
}
@@ -190,7 +190,7 @@ internal class TaskListViewModel(
return
}
selectedProgressRange = range
- viewState.taskProgressRange.postValue(range)
+ viewState.taskProgressRange.value = range
loadProgress()
viewModelScope.launch { loadTaskMetrics() }
}
@@ -199,18 +199,18 @@ internal class TaskListViewModel(
if (currentTasks.isEmpty()) {
progressJob?.cancel()
progressCache.remove(currentProgressCacheKey())
- viewState.taskProgressLoading.postValue(false)
- viewState.taskProgressRange.postValue(selectedProgressRange)
- viewState.taskProgressDays.postValue(emptyList())
+ viewState.taskProgressLoading.value = false
+ viewState.taskProgressRange.value = selectedProgressRange
+ viewState.taskProgressDays.value = emptyList()
return
}
val cacheKey = currentProgressCacheKey()
val cachedProgress = progressCache[cacheKey]
if (cachedProgress != null && force.not()) {
- viewState.taskProgressRange.postValue(selectedProgressRange)
- viewState.taskProgressDays.postValue(cachedProgress)
- viewState.taskProgressLoading.postValue(false)
+ viewState.taskProgressRange.value = selectedProgressRange
+ viewState.taskProgressDays.value = cachedProgress
+ viewState.taskProgressLoading.value = false
return
}
@@ -218,7 +218,7 @@ internal class TaskListViewModel(
progressJob =
viewModelScope.launch {
try {
- viewState.taskProgressLoading.postValue(true)
+ viewState.taskProgressLoading.value = true
val progressFilter =
TaskProgressFilter(
@@ -230,12 +230,12 @@ internal class TaskListViewModel(
getTaskProgressUseCase(progressFilter).getOrThrow()
}
progressCache[cacheKey] = progress
- viewState.taskProgressRange.postValue(selectedProgressRange)
- viewState.taskProgressDays.postValue(progress)
+ viewState.taskProgressRange.value = selectedProgressRange
+ viewState.taskProgressDays.value = progress
} catch (e: Exception) {
logService.error(e)
} finally {
- viewState.taskProgressLoading.postValue(false)
+ viewState.taskProgressLoading.value = false
}
}
}
@@ -262,7 +262,7 @@ internal class TaskListViewModel(
currentTasks = taskCategories.flatMap { category ->
domainTasksByCategory[category].orEmpty()
}
- viewState.itemsView.postValue(itemsWithFeedback.toMutableList())
+ viewState.itemsView.value = itemsWithFeedback.toMutableList()
}
private fun refreshTasksFromCache() {
@@ -276,7 +276,7 @@ internal class TaskListViewModel(
}.flatten()
val itemsWithFeedback = applyInlineFeedbacks(uiModels)
- viewState.itemsView.postValue(itemsWithFeedback.toMutableList())
+ viewState.itemsView.value = itemsWithFeedback.toMutableList()
}
private suspend fun loadTaskMetrics() {
@@ -286,8 +286,7 @@ internal class TaskListViewModel(
currentTasks.map { it.id }
} else {
viewState.itemsView.value
- ?.mapNotNull { (it as? TaskUiModel)?.taskId }
- .orEmpty()
+ .mapNotNull { (it as? TaskUiModel)?.taskId }
}
if (taskIds.isEmpty()) {
@@ -330,7 +329,7 @@ internal class TaskListViewModel(
}.onSuccess { metrics ->
if (metrics == null) {
baseTaskMetrics = null
- viewState.taskMetrics.postValue(null)
+ viewState.taskMetrics.value = null
} else {
baseTaskMetrics = metrics
publishTaskMetricsWithPending()
@@ -339,7 +338,7 @@ internal class TaskListViewModel(
logService.error(throwable)
baseTaskMetrics = null
baseTaskConsecutive.clear()
- viewState.taskMetrics.postValue(null)
+ viewState.taskMetrics.value = null
}
}
@@ -375,7 +374,7 @@ internal class TaskListViewModel(
publishTaskMetricsWithPending()
} catch (e: Exception) {
logService.error(e)
- viewState.viewAction.value = TaskListViewAction.ShowErrorCompletingTask
+ viewState.viewAction.tryEmit(TaskListViewAction.ShowErrorCompletingTask)
}
}
@@ -388,9 +387,9 @@ internal class TaskListViewModel(
logService.error(throwable)
}.getOrNull()
- private fun publishItemsWithInlineFeedback(baseItems: List? = viewState.itemsView.value) {
- val updatedItems = applyInlineFeedbacks(baseItems.orEmpty())
- viewState.itemsView.postValue(updatedItems.toMutableList())
+ private fun publishItemsWithInlineFeedback(baseItems: List = viewState.itemsView.value) {
+ val updatedItems = applyInlineFeedbacks(baseItems)
+ viewState.itemsView.value = updatedItems.toMutableList()
}
private fun applyInlineFeedbacks(items: List): List {
@@ -436,7 +435,7 @@ internal class TaskListViewModel(
private fun publishTaskMetricsWithPending() {
val pending = inlineTaskFeedbacks.values
if (pending.isEmpty() && baseTaskMetrics == null) {
- viewState.taskMetrics.postValue(null)
+ viewState.taskMetrics.value = null
return
}
@@ -459,13 +458,12 @@ internal class TaskListViewModel(
}
}
- viewState.taskMetrics.postValue(
+ viewState.taskMetrics.value =
TaskMetrics(
doneTasks = doneTasks,
notDoneTasks = notDoneTasks,
consecutiveDone = consecutiveDone,
- ),
- )
+ )
}
private fun scheduleInlineFeedbackCommit(taskId: Long) {
@@ -496,7 +494,7 @@ internal class TaskListViewModel(
publishTaskMetricsWithPending()
}.onFailure { throwable ->
logService.error(throwable)
- viewState.viewAction.postValue(TaskListViewAction.ShowErrorCompletingTask)
+ viewState.viewAction.tryEmit(TaskListViewAction.ShowErrorCompletingTask)
publishItemsWithInlineFeedback()
publishTaskMetricsWithPending()
}
@@ -528,7 +526,7 @@ internal class TaskListViewModel(
}
private fun removeTaskFromList(taskId: Long) {
- val itemsView = viewState.itemsView.value?.toMutableList() ?: mutableListOf()
+ val itemsView = viewState.itemsView.value.toMutableList()
val position = itemsView.indexOfFirst { (it as? TaskUiModel)?.taskId == taskId }
if (position >= 0) {
@@ -551,7 +549,7 @@ internal class TaskListViewModel(
) {
viewModelScope.launch {
try {
- val itemsView = viewState.itemsView.value ?: return@launch
+ val itemsView = viewState.itemsView.value
val taskUiModel =
itemsView.firstOrNull { (it as? TaskUiModel)?.taskId == taskId } as? TaskUiModel
?: return@launch
@@ -572,7 +570,7 @@ internal class TaskListViewModel(
scheduleInlineFeedbackCommit(taskUiModel.taskId)
} catch (e: Exception) {
logService.error(e)
- viewState.viewAction.value = TaskListViewAction.ShowErrorCompletingTask
+ viewState.viewAction.tryEmit(TaskListViewAction.ShowErrorCompletingTask)
}
}
}
@@ -594,8 +592,8 @@ internal class TaskListViewModel(
progressCache.putAll(updatedCache)
progressCache[cacheKey]?.let { updatedDays ->
- viewState.taskProgressDays.postValue(updatedDays)
- viewState.taskProgressRange.postValue(selectedProgressRange)
+ viewState.taskProgressDays.value = updatedDays
+ viewState.taskProgressRange.value = selectedProgressRange
}
}
diff --git a/feature/task-list/impl/src/main/java/br/com/sailboat/todozy/feature/task/list/impl/presentation/viewmodel/TaskListViewState.kt b/feature/task-list/impl/src/main/java/br/com/sailboat/todozy/feature/task/list/impl/presentation/viewmodel/TaskListViewState.kt
index e918c1fb..3f631241 100644
--- a/feature/task-list/impl/src/main/java/br/com/sailboat/todozy/feature/task/list/impl/presentation/viewmodel/TaskListViewState.kt
+++ b/feature/task-list/impl/src/main/java/br/com/sailboat/todozy/feature/task/list/impl/presentation/viewmodel/TaskListViewState.kt
@@ -1,18 +1,18 @@
package br.com.sailboat.todozy.feature.task.list.impl.presentation.viewmodel
-import androidx.lifecycle.MutableLiveData
import br.com.sailboat.todozy.domain.model.TaskMetrics
import br.com.sailboat.todozy.domain.model.TaskProgressDay
import br.com.sailboat.todozy.domain.model.TaskProgressRange
-import br.com.sailboat.todozy.utility.android.livedata.Event
import br.com.sailboat.uicomponent.model.UiModel
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
internal class TaskListViewState {
- val viewAction = Event()
- val tasksLoading = MutableLiveData(false)
- val itemsView = MutableLiveData>()
- val taskMetrics = MutableLiveData()
- val taskProgressDays = MutableLiveData>(emptyList())
- val taskProgressRange = MutableLiveData(TaskProgressRange.LAST_YEAR)
- val taskProgressLoading = MutableLiveData(false)
+ val viewAction = MutableSharedFlow(replay = 1, extraBufferCapacity = 1)
+ val tasksLoading = MutableStateFlow(false)
+ val itemsView = MutableStateFlow>(emptyList())
+ val taskMetrics = MutableStateFlow(null)
+ val taskProgressDays = MutableStateFlow>(emptyList())
+ val taskProgressRange = MutableStateFlow(TaskProgressRange.LAST_YEAR)
+ val taskProgressLoading = MutableStateFlow(false)
}
diff --git a/feature/task-list/impl/src/test/java/br/com/sailboat/todozy/feature/task/list/impl/presentation/viewmodel/TaskListViewModelTest.kt b/feature/task-list/impl/src/test/java/br/com/sailboat/todozy/feature/task/list/impl/presentation/viewmodel/TaskListViewModelTest.kt
index b71f59f8..b877710e 100644
--- a/feature/task-list/impl/src/test/java/br/com/sailboat/todozy/feature/task/list/impl/presentation/viewmodel/TaskListViewModelTest.kt
+++ b/feature/task-list/impl/src/test/java/br/com/sailboat/todozy/feature/task/list/impl/presentation/viewmodel/TaskListViewModelTest.kt
@@ -81,7 +81,7 @@ internal class TaskListViewModelTest {
assertEquals(
TaskListViewAction.CloseNotifications,
- viewModel.viewState.viewAction.value,
+ latestAction(),
)
}
@@ -191,7 +191,7 @@ internal class TaskListViewModelTest {
viewModel.dispatchViewIntent(TaskListViewIntent.OnClickMenuAbout)
- assertEquals(TaskListViewAction.NavigateToAbout, viewModel.viewState.viewAction.value)
+ assertEquals(TaskListViewAction.NavigateToAbout, latestAction())
}
@Test
@@ -200,7 +200,7 @@ internal class TaskListViewModelTest {
viewModel.dispatchViewIntent(TaskListViewIntent.OnClickMenuHistory)
- assertEquals(TaskListViewAction.NavigateToHistory, viewModel.viewState.viewAction.value)
+ assertEquals(TaskListViewAction.NavigateToHistory, latestAction())
}
@Test
@@ -209,7 +209,7 @@ internal class TaskListViewModelTest {
viewModel.dispatchViewIntent(TaskListViewIntent.OnClickMenuSettings)
- assertEquals(TaskListViewAction.NavigateToSettings, viewModel.viewState.viewAction.value)
+ assertEquals(TaskListViewAction.NavigateToSettings, latestAction())
}
@Test
@@ -217,7 +217,7 @@ internal class TaskListViewModelTest {
prepareScenario()
viewModel.dispatchViewIntent(TaskListViewIntent.OnClickNewTask)
- assertEquals(TaskListViewAction.NavigateToTaskForm, viewModel.viewState.viewAction.value)
+ assertEquals(TaskListViewAction.NavigateToTaskForm, latestAction())
}
@Test
@@ -228,7 +228,7 @@ internal class TaskListViewModelTest {
viewModel.dispatchViewIntent(TaskListViewIntent.OnClickTask(taskId = taskId))
val expected = TaskListViewAction.NavigateToTaskDetails(taskId = taskId)
- assertEquals(expected, viewModel.viewState.viewAction.value)
+ assertEquals(expected, latestAction())
}
@Test
@@ -586,4 +586,6 @@ internal class TaskListViewModelTest {
coEvery { getTaskMetricsUseCase(any()) } returns Result.success(TaskMetrics(0, 0, 0))
coEvery { completeTaskUseCase(any(), any()) } returns Result.success(Unit)
}
+
+ private fun latestAction(): TaskListViewAction? = viewModel.viewState.viewAction.replayCache.lastOrNull()
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index da9ed386..8c9ce72f 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -110,6 +110,7 @@ androidx-compose-material = { module = "androidx.compose.material:material", ver
androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "composeMaterialIcons" }
androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" }
androidx-compose-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata", version.ref = "compose" }
+androidx-compose-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "composeLifecycle" }
androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose" }
androidx-compose-activity = { module = "androidx.activity:activity-compose", version.ref = "composeActivity" }
androidx-compose-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "composeLifecycle" }
diff --git a/ui-component/impl/src/main/java/br/com/sailboat/uicomponent/impl/metrics/TaskMetricsRow.kt b/ui-component/impl/src/main/java/br/com/sailboat/uicomponent/impl/metrics/TaskMetricsRow.kt
new file mode 100644
index 00000000..61a0ec60
--- /dev/null
+++ b/ui-component/impl/src/main/java/br/com/sailboat/uicomponent/impl/metrics/TaskMetricsRow.kt
@@ -0,0 +1,106 @@
+package br.com.sailboat.uicomponent.impl.metrics
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import br.com.sailboat.todozy.domain.model.TaskMetrics
+import br.com.sailboat.uicomponent.impl.R
+import br.com.sailboat.uicomponent.impl.theme.LocalTodozySemanticColors
+import br.com.sailboat.uicomponent.impl.theme.LocalTodozySpacing
+
+@Composable
+fun TaskMetricsRow(
+ taskMetrics: TaskMetrics,
+ modifier: Modifier = Modifier,
+ distributeEvenly: Boolean = true,
+) {
+ val spacing = LocalTodozySpacing.current
+ val semanticColors = LocalTodozySemanticColors.current
+ val doneTint = colorResource(id = R.color.md_teal_300)
+ val notDoneTint = colorResource(id = R.color.md_red_300)
+ Row(
+ modifier = modifier,
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(spacing.medium),
+ ) {
+ val metricModifier = if (distributeEvenly) Modifier.weight(1f) else Modifier
+ if (taskMetrics.consecutiveDone > 0) {
+ MetricIconWithValue(
+ iconId = R.drawable.ic_fire_black_24dp,
+ tint = semanticColors.warning,
+ value = taskMetrics.consecutiveDone,
+ modifier = metricModifier,
+ )
+ }
+ MetricIconWithValue(
+ iconId = R.drawable.ic_vec_thumb_up_white_24dp,
+ tint = doneTint,
+ value = taskMetrics.doneTasks,
+ modifier = metricModifier,
+ )
+ MetricIconWithValue(
+ iconId = R.drawable.ic_vect_thumb_down_white_24dp,
+ tint = notDoneTint,
+ value = taskMetrics.notDoneTasks,
+ modifier = metricModifier,
+ )
+ }
+}
+
+@Composable
+private fun MetricIconWithValue(
+ iconId: Int,
+ tint: Color,
+ value: Int,
+ modifier: Modifier = Modifier,
+) {
+ val spacing = LocalTodozySpacing.current
+ Row(
+ modifier = modifier,
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(spacing.xsmall),
+ ) {
+ Surface(
+ shape = CircleShape,
+ color = Color.White,
+ elevation = 0.dp,
+ ) {
+ Box(
+ modifier =
+ Modifier
+ .size(28.dp)
+ .padding(spacing.xxsmall),
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(
+ painter = painterResource(id = iconId),
+ contentDescription = null,
+ tint = tint,
+ modifier = Modifier.height(18.dp),
+ )
+ }
+ }
+ Text(
+ text = value.toString(),
+ style = MaterialTheme.typography.subtitle1,
+ color = Color.White,
+ fontWeight = FontWeight.Bold,
+ )
+ }
+}