diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/MainScreen.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/MainScreen.kt index 7fa5a76..c2ea5c0 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/MainScreen.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/MainScreen.kt @@ -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) } ) } } diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/MenuScreen.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/MenuScreen.kt index 339e687..a96a7de 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/MenuScreen.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/MenuScreen.kt @@ -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) ) } diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskScreen.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskScreen.kt index 24f2d5e..be3814c 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskScreen.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskScreen.kt @@ -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() } diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/ui/events/UiEvent.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/ui/events/UiEvent.kt new file mode 100644 index 0000000..e28e439 --- /dev/null +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/ui/events/UiEvent.kt @@ -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() +} \ No newline at end of file diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/MainViewModel.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/MainViewModel.kt index 4c78c32..3e6c85e 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/MainViewModel.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/MainViewModel.kt @@ -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") diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/MenuViewModel.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/MenuViewModel.kt index e848172..8efda50 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/MenuViewModel.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/MenuViewModel.kt @@ -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>(emptyList()) private set @@ -38,4 +41,10 @@ class MenuViewModel @Inject constructor( } .launchIn(viewModelScope) } + + fun navigateTo(route: String) { + viewModelScope.launch { + uiEventBus.send(UiEvent.Navigate(route)) + } + } } diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/TaskListViewModel.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/TaskListViewModel.kt index f4e40bb..7200185 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/TaskListViewModel.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/TaskListViewModel.kt @@ -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>(emptyList()) diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/TaskViewModel.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/TaskViewModel.kt index 61a131d..dae0c12 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/TaskViewModel.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/TaskViewModel.kt @@ -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() } }