mirror of
https://github.com/wismna/DoNext.git
synced 2025-12-06 08:12:37 -05:00
Create UI events
Create a event bus singleton Handle navigation through events Handle task creation and edition through events
This commit is contained in:
@@ -50,10 +50,11 @@ import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import com.wismna.geoffroy.donext.domain.model.AppDestination
|
||||
import com.wismna.geoffroy.donext.presentation.ui.events.UiEvent
|
||||
import com.wismna.geoffroy.donext.presentation.viewmodel.MainViewModel
|
||||
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskListViewModel
|
||||
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
@@ -63,8 +64,6 @@ fun MainScreen(
|
||||
val navController = rememberNavController()
|
||||
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
||||
val scope = rememberCoroutineScope()
|
||||
// TODO: find a way to do this better
|
||||
val taskViewModel: TaskViewModel = hiltViewModel()
|
||||
|
||||
if (viewModel.isLoading) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
@@ -74,7 +73,7 @@ fun MainScreen(
|
||||
}
|
||||
|
||||
if (viewModel.showTaskSheet) {
|
||||
TaskBottomSheet(taskViewModel) { viewModel.showTaskSheet = false }
|
||||
TaskBottomSheet { viewModel.onDismissTaskSheet() }
|
||||
}
|
||||
if (viewModel.showAddListSheet) {
|
||||
AddListBottomSheet { viewModel.showAddListSheet = false }
|
||||
@@ -85,21 +84,11 @@ fun MainScreen(
|
||||
|
||||
ModalNavigationDrawer(
|
||||
drawerContent = {
|
||||
MenuScreen (
|
||||
currentDestination = viewModel.currentDestination,
|
||||
onNavigate = { route ->
|
||||
scope.launch {
|
||||
drawerState.close()
|
||||
navController.navigate(route) {
|
||||
restoreState = true
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
MenuScreen (currentDestination = viewModel.currentDestination)
|
||||
},
|
||||
drawerState = drawerState
|
||||
) {
|
||||
AppContent(viewModel = viewModel, taskViewModel = taskViewModel, navController = navController, scope = scope, drawerState = drawerState)
|
||||
AppContent(viewModel = viewModel, navController = navController, scope = scope, drawerState = drawerState)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,11 +96,22 @@ fun MainScreen(
|
||||
fun AppContent(
|
||||
modifier : Modifier = Modifier,
|
||||
viewModel: MainViewModel,
|
||||
taskViewModel: TaskViewModel,
|
||||
navController: NavHostController,
|
||||
scope: CoroutineScope,
|
||||
drawerState: DrawerState
|
||||
) {
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.uiEventBus.events.collectLatest { event ->
|
||||
when (event) {
|
||||
is UiEvent.Navigate -> {
|
||||
drawerState.close()
|
||||
navController.navigate(event.route)
|
||||
}
|
||||
is UiEvent.NavigateBack -> navController.popBackStack()
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Scaffold(
|
||||
modifier = modifier.background(MaterialTheme.colorScheme.primaryContainer),
|
||||
containerColor = Color.Transparent,
|
||||
@@ -126,7 +126,7 @@ fun AppContent(
|
||||
navigationIcon = {
|
||||
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onPrimaryContainer) {
|
||||
if (viewModel.currentDestination.showBackButton) {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
IconButton(onClick = { viewModel.navigateBack() }) {
|
||||
Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back")
|
||||
}
|
||||
} else {
|
||||
@@ -158,10 +158,7 @@ fun AppContent(
|
||||
when (val dest = viewModel.currentDestination) {
|
||||
is AppDestination.TaskList -> {
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = {
|
||||
taskViewModel.startNewTask(dest.taskListId)
|
||||
viewModel.showTaskSheet = true
|
||||
},
|
||||
onClick = { viewModel.onNewTaskButtonClicked(dest.taskListId) },
|
||||
icon = { Icon(Icons.Filled.Add, "Create a task.") },
|
||||
text = { Text("Create a task") },
|
||||
)
|
||||
@@ -205,17 +202,14 @@ fun AppContent(
|
||||
}
|
||||
LaunchedEffect(listExists) {
|
||||
if (!viewModel.doesListExist(taskListId)) {
|
||||
navController.popBackStack()
|
||||
viewModel.navigateBack()
|
||||
}
|
||||
}
|
||||
|
||||
val taskListViewModel: TaskListViewModel = hiltViewModel(navBackStackEntry)
|
||||
TaskListScreen(
|
||||
viewModel = taskListViewModel,
|
||||
onTaskClick = { task ->
|
||||
taskViewModel.startEditTask(task)
|
||||
viewModel.showTaskSheet = true
|
||||
}
|
||||
onTaskClick = { task -> viewModel.onTaskClicked(task) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -228,19 +222,13 @@ fun AppContent(
|
||||
composable(AppDestination.DueTodayList.route) {
|
||||
DueTodayTasksScreen (
|
||||
modifier = Modifier,
|
||||
onTaskClick = { task ->
|
||||
taskViewModel.startEditTask(task)
|
||||
viewModel.showTaskSheet = true
|
||||
}
|
||||
onTaskClick = { task -> viewModel.onTaskClicked(task) }
|
||||
)
|
||||
}
|
||||
composable(AppDestination.RecycleBin.route) {
|
||||
RecycleBinScreen(
|
||||
modifier = Modifier,
|
||||
onTaskClick = { task ->
|
||||
taskViewModel.startEditTask(task)
|
||||
viewModel.showTaskSheet = true
|
||||
}
|
||||
onTaskClick = { task -> viewModel.onTaskClicked(task) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ import com.wismna.geoffroy.donext.presentation.viewmodel.MenuViewModel
|
||||
fun MenuScreen(
|
||||
viewModel: MenuViewModel = hiltViewModel(),
|
||||
currentDestination: AppDestination,
|
||||
onNavigate: (String) -> Unit
|
||||
) {
|
||||
ModalDrawerSheet(
|
||||
drawerContainerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
@@ -58,7 +57,7 @@ fun MenuScreen(
|
||||
},
|
||||
icon = { Icon(Icons.Default.Today, contentDescription = "Due Today") },
|
||||
selected = currentDestination is AppDestination.DueTodayList,
|
||||
onClick = { onNavigate(AppDestination.DueTodayList.route) },
|
||||
onClick = { viewModel.navigateTo(AppDestination.DueTodayList.route) },
|
||||
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
|
||||
)
|
||||
HorizontalDivider()
|
||||
@@ -74,7 +73,7 @@ fun MenuScreen(
|
||||
icon = { Icon(Icons.Default.LineWeight, contentDescription = list.name) },
|
||||
selected = currentDestination is AppDestination.TaskList &&
|
||||
currentDestination.taskListId == list.id,
|
||||
onClick = { onNavigate("taskList/${list.id}") },
|
||||
onClick = { viewModel.navigateTo("taskList/${list.id}") },
|
||||
badge = {
|
||||
if (list.overdueCount > 0) {
|
||||
Badge { Text(list.overdueCount.toString()) }
|
||||
@@ -91,14 +90,14 @@ fun MenuScreen(
|
||||
label = { Text("Recycle Bin") },
|
||||
icon = { Icon(Icons.Default.Delete, contentDescription = "Recycle Bin") },
|
||||
selected = currentDestination is AppDestination.RecycleBin,
|
||||
onClick = { onNavigate(AppDestination.RecycleBin.route) },
|
||||
onClick = { viewModel.navigateTo(AppDestination.RecycleBin.route) },
|
||||
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
|
||||
)
|
||||
NavigationDrawerItem(
|
||||
label = { Text("Edit Lists") },
|
||||
icon = { Icon(Icons.Default.EditNote, contentDescription = "Edit Lists") },
|
||||
selected = currentDestination is AppDestination.ManageLists,
|
||||
onClick = { onNavigate(AppDestination.ManageLists.route) },
|
||||
onClick = { viewModel.navigateTo(AppDestination.ManageLists.route) },
|
||||
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import com.wismna.geoffroy.donext.domain.extension.toLocalDate
|
||||
import com.wismna.geoffroy.donext.domain.model.Priority
|
||||
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskViewModel
|
||||
@@ -53,7 +54,7 @@ import java.time.format.FormatStyle
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TaskBottomSheet(
|
||||
viewModel: TaskViewModel,
|
||||
viewModel: TaskViewModel = hiltViewModel(),
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val titleFocusRequester = remember { FocusRequester() }
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.wismna.geoffroy.donext.presentation.ui.events
|
||||
|
||||
import com.wismna.geoffroy.donext.domain.model.Task
|
||||
|
||||
sealed class UiEvent {
|
||||
data class Navigate(val route: String) : UiEvent()
|
||||
data object NavigateBack : UiEvent()
|
||||
data class ShowSnackbar(val message: String) : UiEvent()
|
||||
|
||||
data class EditTask(val task: Task) : UiEvent()
|
||||
data class CreateNewTask(val taskListId: Long) : UiEvent()
|
||||
data object CloseTask : UiEvent()
|
||||
}
|
||||
@@ -7,15 +7,20 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import com.wismna.geoffroy.donext.domain.model.AppDestination
|
||||
import com.wismna.geoffroy.donext.domain.model.Task
|
||||
import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsUseCase
|
||||
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.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class MainViewModel @Inject constructor(
|
||||
getTaskListsUseCase: GetTaskListsUseCase
|
||||
getTaskListsUseCase: GetTaskListsUseCase,
|
||||
val uiEventBus: UiEventBus
|
||||
) : ViewModel() {
|
||||
|
||||
var isLoading by mutableStateOf(true)
|
||||
@@ -50,6 +55,33 @@ class MainViewModel @Inject constructor(
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
fun navigateBack() {
|
||||
viewModelScope.launch {
|
||||
uiEventBus.send(UiEvent.NavigateBack)
|
||||
}
|
||||
}
|
||||
|
||||
fun onNewTaskButtonClicked(taskLisId: Long) {
|
||||
showTaskSheet = true
|
||||
viewModelScope.launch {
|
||||
uiEventBus.send(UiEvent.CreateNewTask(taskLisId))
|
||||
}
|
||||
}
|
||||
|
||||
fun onTaskClicked(task: Task) {
|
||||
showTaskSheet = true
|
||||
viewModelScope.launch {
|
||||
uiEventBus.send(UiEvent.EditTask(task))
|
||||
}
|
||||
}
|
||||
|
||||
fun onDismissTaskSheet() {
|
||||
showTaskSheet = false
|
||||
viewModelScope.launch {
|
||||
uiEventBus.send(UiEvent.CloseTask)
|
||||
}
|
||||
}
|
||||
|
||||
fun setCurrentDestination(navBackStackEntry: NavBackStackEntry?) {
|
||||
val route = navBackStackEntry?.destination?.route
|
||||
val taskListId = navBackStackEntry?.arguments?.getLong("taskListId")
|
||||
|
||||
@@ -9,17 +9,20 @@ import androidx.lifecycle.viewModelScope
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue
|
||||
import com.wismna.geoffroy.donext.domain.usecase.GetDueTodayTasksUseCase
|
||||
import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsWithOverdueUseCase
|
||||
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.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class MenuViewModel @Inject constructor(
|
||||
getTaskListsWithOverdue: GetTaskListsWithOverdueUseCase,
|
||||
getDueTodayTasks: GetDueTodayTasksUseCase
|
||||
getDueTodayTasks: GetDueTodayTasksUseCase,
|
||||
private val uiEventBus: UiEventBus
|
||||
) : ViewModel() {
|
||||
|
||||
var taskLists by mutableStateOf<List<TaskListWithOverdue>>(emptyList())
|
||||
private set
|
||||
|
||||
@@ -38,4 +41,10 @@ class MenuViewModel @Inject constructor(
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
fun navigateTo(route: String) {
|
||||
viewModelScope.launch {
|
||||
uiEventBus.send(UiEvent.Navigate(route))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,9 @@ import javax.inject.Inject
|
||||
@HiltViewModel
|
||||
class TaskListViewModel @Inject constructor(
|
||||
getTasks: GetTasksForListUseCase,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val toggleTaskDone: ToggleTaskDoneUseCase,
|
||||
private val toggleTaskDeleted: ToggleTaskDeletedUseCase,
|
||||
savedStateHandle: SavedStateHandle
|
||||
) : ViewModel() {
|
||||
|
||||
var tasks by mutableStateOf<List<Task>>(emptyList())
|
||||
|
||||
@@ -9,6 +9,8 @@ import com.wismna.geoffroy.donext.domain.model.Priority
|
||||
import com.wismna.geoffroy.donext.domain.model.Task
|
||||
import com.wismna.geoffroy.donext.domain.usecase.AddTaskUseCase
|
||||
import com.wismna.geoffroy.donext.domain.usecase.UpdateTaskUseCase
|
||||
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.Instant
|
||||
@@ -19,7 +21,8 @@ import javax.inject.Inject
|
||||
@HiltViewModel
|
||||
class TaskViewModel @Inject constructor(
|
||||
private val createTaskUseCase: AddTaskUseCase,
|
||||
private val updateTaskUseCase: UpdateTaskUseCase
|
||||
private val updateTaskUseCase: UpdateTaskUseCase,
|
||||
private val uiEventBus: UiEventBus
|
||||
) : ViewModel() {
|
||||
|
||||
var title by mutableStateOf("")
|
||||
@@ -38,10 +41,23 @@ class TaskViewModel @Inject constructor(
|
||||
private var editingTaskId: Long? = null
|
||||
private var taskListId: Long? = null
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
uiEventBus.events.collect { event ->
|
||||
when (event) {
|
||||
is UiEvent.EditTask -> startEditTask(event.task)
|
||||
is UiEvent.CreateNewTask -> startNewTask(event.taskListId)
|
||||
is UiEvent.CloseTask -> reset()
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun screenTitle(): String = if (isDeleted) "Task details" else if (isEditing()) "Edit Task" else "New Task"
|
||||
fun isEditing(): Boolean = editingTaskId != null
|
||||
|
||||
fun startNewTask(selectedListId: Long) {
|
||||
private fun startNewTask(selectedListId: Long) {
|
||||
editingTaskId = null
|
||||
taskListId = selectedListId
|
||||
title = ""
|
||||
@@ -51,7 +67,7 @@ class TaskViewModel @Inject constructor(
|
||||
isDeleted = false
|
||||
}
|
||||
|
||||
fun startEditTask(task: Task) {
|
||||
private fun startEditTask(task: Task) {
|
||||
editingTaskId = task.id
|
||||
taskListId = task.taskListId
|
||||
title = task.name
|
||||
@@ -84,8 +100,6 @@ class TaskViewModel @Inject constructor(
|
||||
} else {
|
||||
createTaskUseCase(taskListId!!, title, description, priority, dueDate)
|
||||
}
|
||||
// reset state after save
|
||||
reset()
|
||||
onDone?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user