¿Cómo podemos editar tareas en nuestra aplicación?
La edición de tareas dentro de una aplicación no es solo sobre la modificación de datos; implica una serie de pasos técnicos que requieren navegación y el paso de argumentos. En esta clase, profundizamos en el proceso de edición de tareas, desde pasar el ID de la tarea a través de la navegación hasta asegurar que los datos se guarden correctamente. Te guiaré a través de este proceso de manera clara y precisa, destacando las mejores prácticas para asegurar que tus funcionalidades sean eficientes y efectivas.
¿Cómo recibir argumentos en Task Screen?
Para poder editar una tarea, primero debemos modificar nuestra Task Screen para que pueda recibir un argumento. En lugar de usar un object, necesitamos cambiar a una data class que pueda recibir un Task ID. Este Task ID puede ser nulo en caso de que estemos creando una tarea nueva. Aquí está el procedimiento:
data classTaskScreenDestination(val taskId:String?=null)
¿Cómo se modifica la navegación?
El siguiente paso crucial es modificar cómo navegamos por nuestra aplicación. Esto implica ajustar el método de navegación para que acepte argumentos opcionales, lo que le permite recibir un Task ID:
fun navigateToScreen(taskId:String?=null){// Lógica de navegación}
Al hacer clic en "Agregar tarea", debes pasar un argumento nulo, mientras que al hacer clic en un elemento existente, deberás pasar el Task ID correspondiente.
¿Cómo se pre-cargan los datos en el modelo Task?
Una vez que la navegación está configurada para enviar el ID de la tarea, el siguiente paso es precargar los datos para la edición. En el ViewModel, usamos el handle de estado guardado para recuperar los datos necesarios:
val savedTaskId = savedStateHandle.get<String>("taskId")
¿Cómo se actualiza o crea una tarea?
Prepárate para el desafío de distinguir entre la creación y la actualización de tareas. Aquí te mostramos cómo lograrlo usando la variable editTask que creamos:
editTask?.let { fakeLocalDataSource.updateTask( it.copy( title = taskTitle, description = taskDescription, isCompleted = taskDone
))}?: run {// lógica para crear una nueva tarea}
Este enfoque asegura que reutilicemos el mismo código de creación y que nuestro Lazy Column no tenga errores debido a IDs duplicados.
¿Dónde se gestiona el evento de clic en la lista de tareas?
La interacción del usuario comienza en nuestra lista de tareas, donde debemos identificar cuando se hace clic en un ítem y así cargar los datos apropiados en la pantalla de edición. Lo configuramos de la siguiente manera:
¿Qué debemos considerar al actualizar la interfaz de usuario?
Finalmente, recuerda verificar que tanto la edición como la creación de tareas funcionen sin problemas y que cualquier cambio se refleje inmediatamente en la interfaz de usuario. Asegúrate de que las tareas completas se filtren y se muestren correctamente.
Al finalizar este proceso, habrás integrado exitosamente la funcionalidad de edición de tareas en tu aplicación, permitiéndote crear, editar y gestionar tareas de forma eficaz. Con esta base funcional en su lugar, estás listo para explorar implementaciones más avanzadas, como bases de datos e inyección de dependencias.
Cuándo se debe usar navegación con Jetpack compose o multiples Activities?
Buena pregunta Alex,
Si tu proyecto esta pensado para usar 100% compose yo te diría que vayas de una vez con la navegación de jetpack compose,
Hay casos en donde esto se extiende, hay escenarios en donde te tocara agregar más actividades a tu proyecto, como por ejemplo un a activity Login en donde implementes los OAuth2 para Google. Twitter y así para mantener el principio de single responsability y que no se acumule la lógica en una sola activity. Aquí diría es bueno tener un hibrido entre las dos.
También tendría buena presentación tener un proyecto en el que quieras tener una activity por feature en donde cada una tenga su propio mapa de navegación en compose.
Como yo no use navegacion tipada si no por string debido al problema que tenia con los @Serializable en la calse anterior tuve que implementar detalles minimos
NavigationRoot:@Composablefun NavigationRoot(navController:NavHostController){Box(modifier =Modifier.fillMaxSize()){NavHost( navController = navController, startDestination ="home"){composable(route ="home"){_:NavBackStackEntry->HomeScreenRoot( navigateToTaskScreen ={ taskId ->if(taskId !=null){ navController.navigate("task/$taskId")}else{ navController.navigate("task")}})}composable(route ="task"){_:NavBackStackEntry->TaskScreenRoot( navigateBack ={ navController.navigateUp()})}composable( route ="task/{taskId}", arguments =listOf(navArgument("taskId"){ type =NavType.StringType})){_:NavBackStackEntry->TaskScreenRoot( navigateBack ={ navController.navigateUp()})}}}}HomeScreen:@file:OptIn(ExperimentalMaterial3Api::class)package com.juandgaines.todoapp.presentation.homeimport android.widget.Toastimport androidx.compose.foundation.ExperimentalFoundationApiimport androidx.compose.foundation.backgroundimport androidx.compose.foundation.clickableimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Boximport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.paddingimport androidx.compose.foundation.lazy.LazyColumnimport androidx.compose.foundation.lazy.itemsimport androidx.compose.foundation.shape.RoundedCornerShapeimport androidx.compose.material.icons.Iconsimport androidx.compose.material.icons.filled.Addimport androidx.compose.material.icons.filled.MoreVertimport androidx.compose.material3.DropdownMenuimport androidx.compose.material3.DropdownMenuItemimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.FloatingActionButtonimport androidx.compose.material3.Iconimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Textimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.LaunchedEffectimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Modifierimport androidx.compose.ui.draw.clipimport androidx.compose.ui.platform.LocalContextimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.text.font.FontWeightimport androidx.compose.ui.tooling.preview.Previewimport androidx.compose.ui.tooling.preview.PreviewParameterimport androidx.compose.ui.unit.dpimport androidx.lifecycle.viewmodel.compose.viewModelimport com.juandgaines.todoapp.domain.Taskimport com.juandgaines.todoapp.Rimport com.juandgaines.todoapp.presentation.screens.home.providers.HomeScreenPreviewProviderimport com.juandgaines.todoapp.ui.theme.TodoAppTheme@Composablefun HomeScreenRoot(navigateToTaskScreen:(String?)->Unit){ val viewModel = viewModel<HomeScreenViewModel>() val state = viewModel.state val event = viewModel.event val context =LocalContext.currentLaunchedEffect(true){ event.collect{ event->when(event){HomeScreenEvent.DeletedAllTasks->{Toast.makeText( context, context.getString(R.string.all_task_deleted),Toast.LENGTH_SHORT).show()}HomeScreenEvent.DeletedTask->{Toast.makeText( context, context.getString(R.string.task_deleted),Toast.LENGTH_SHORT).show()}HomeScreenEvent.UpdatedTasks->{Toast.makeText( context, context.getString(R.string.task_updated),Toast.LENGTH_SHORT).show()}}}}HomeScreen( state = state, onAction ={ action ->when(action){ is HomeScreenAction.OnClickTask->{navigateToTaskScreen(action.taskId)}HomeScreenAction.OnAddTask->{navigateToTaskScreen(null)}else-> viewModel.onAction(action)}})}@OptIn(ExperimentalFoundationApi::class)@Composablefun HomeScreen(modifier:Modifier=Modifier,state:HomeDataState,onAction:(HomeScreenAction)->Unit){var isMenuExpanded by remember {mutableStateOf(false)}Scaffold( modifier = modifier.fillMaxSize(), topBar ={TopAppBar( title ={Text( text =stringResource(id =R.string.app_name), color =MaterialTheme.colorScheme.onSurfaceVariant, fontWeight =FontWeight.Bold, modifier =Modifier.fillMaxSize())}, actions ={Box(modifier =Modifier.padding(8.dp).clickable{ isMenuExpanded =true}){Icon( imageVector =Icons.Default.MoreVert, contentDescription ="Add Task", tint =MaterialTheme.colorScheme.onSurface)DropdownMenu(expanded = isMenuExpanded, modifier =Modifier.background( color =MaterialTheme.colorScheme.surfaceContainerHighest), onDismissRequest ={ isMenuExpanded =false}){DropdownMenuItem( text ={Text( text =stringResource(id =R.string.delete_all), color =MaterialTheme.colorScheme.onSurface)}, onClick ={onAction(HomeScreenAction.OnDeleteAllTasks) isMenuExpanded =false})}}})}, floatingActionButton ={FloatingActionButton( onClick ={onAction(HomeScreenAction.OnAddTask)}, content ={Icon( imageVector =Icons.Default.Add, contentDescription ="Add Task", tint =MaterialTheme.colorScheme.onSurface)})}){ paddingValues ->LazyColumn(modifier =Modifier.padding(paddingValues).padding(16.dp), verticalArrangement =Arrangement.spacedBy(4.dp)){ item{SummaryInfo( date = state.date, taskSymmary = state.summary, completedTask = state.completedTask.size, totalTask = state.completedTask.size+ state.pendingTask.size)} stickyHeader {SectionTitle( modifier =Modifier.fillParentMaxWidth().background( color =MaterialTheme.colorScheme.surface), title =stringResource(R.string.completed_task))}items(state.completedTask, key ={ task -> task.id}){ task ->TaskItem( modifier =Modifier.clip(RoundedCornerShape(8.dp)), task = task, onClickItem ={onAction(HomeScreenAction.OnClickTask(task.id))}, onDeleteItem ={onAction(HomeScreenAction.OnDeleteTask(task))}, onToggleCompletion ={onAction(HomeScreenAction.OnToggleTask(task))})} stickyHeader {SectionTitle( modifier =Modifier.fillParentMaxWidth().background( color =MaterialTheme.colorScheme.surface), title =stringResource(R.string.pending_task))}items(state.pendingTask, key ={ task -> task.id}){ task ->TaskItem( modifier =Modifier.clip(RoundedCornerShape(8.dp)), task = task, onClickItem ={onAction(HomeScreenAction.OnClickTask(task.id))}, onDeleteItem ={onAction(HomeScreenAction.OnDeleteTask(task))}, onToggleCompletion ={onAction(HomeScreenAction.OnToggleTask(task))})}}}}@Preview@Composablefun HomeScreenPreviewLight(@PreviewParameter(HomeScreenPreviewProvider::class) state:HomeDataState){TodoAppTheme{HomeScreen( state =HomeDataState( date = state.date, summary = state.summary, completedTask = state.completedTask, pendingTask = state.pendingTask), onAction ={})}}@Preview( showBackground =true, uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES)@Composablefun HomeScreenPreviewDark(@PreviewParameter(HomeScreenPreviewProvider::class) state:HomeDataState){TodoAppTheme{HomeScreen( state =HomeDataState( date = state.date, summary = state.summary, completedTask = state.completedTask, pendingTask = state.pendingTask), onAction ={})}}TaskViewModel:package com.juandgaines.todoapp.presentation.detailimport androidx.compose.foundation.text.input.TextFieldStateimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.setValueimport androidx.compose.runtime.snapshotFlowimport androidx.lifecycle.SavedStateHandleimport androidx.lifecycle.ViewModelimport androidx.lifecycle.viewModelScopeimport com.juandgaines.todoapp.data.FakeTaskLocalDataSourceimport com.juandgaines.todoapp.domain.Taskimport kotlinx.coroutines.channels.Channelimport kotlinx.coroutines.flow.launchInimport kotlinx.coroutines.flow.onEachimport kotlinx.coroutines.flow.receiveAsFlowimport kotlinx.coroutines.launchimport java.util.UUIDclassTaskViewModel(savedStateHandle:SavedStateHandle):ViewModel(){private val fakeTaskLocalDataSource =FakeTaskLocalDataSourceprivate val taskId:String?= savedStateHandle.get<String>("taskId")var state by mutableStateOf(TaskScreenState())privatesetprivatevar eventChannel =Channel<TaskEvent>() val event = eventChannel.receiveAsFlow()private val canSaveTask = snapshotFlow { state.taskName.text.toString()}privatevareditedTask:Task?=null init { taskId?.let{ viewModelScope.launch{ val task = fakeTaskLocalDataSource.getTaskById(it) editedTask = task
state = state.copy( taskName =TextFieldState(task?.title?:""), taskDescription =TextFieldState(task?.description?:""), isTaskDone = task?.isCompleted?:false, category = task?.category
)}} canSaveTask.onEach{ state = state.copy(canSaveTask = it.isNotEmpty())}.launchIn(viewModelScope)} fun onAction(action:ActionTask){ viewModelScope.launch{when(action){ is ActionTask.ChangeTaskCategory->{ state = state.copy(category = action.category,)} is ActionTask.ChangeTaskDone->{ state = state.copy(isTaskDone = action.isTaskDone,)} is ActionTask.SaveTask->{ editedTask?.let { fakeTaskLocalDataSource.updateTask( updatedTask = it.copy( id = it.id, title = state.taskName.text.toString(), description = state.taskDescription.text.toString(), isCompleted = state.isTaskDone, category = state.category))}?:run{ val task=Task( id =UUID.randomUUID().toString(), title = state.taskName.text.toString(), description = state.taskDescription.text.toString(), isCompleted = state.isTaskDone, category = state.category) fakeTaskLocalDataSource.addTask( task = task
)} eventChannel.send(TaskEvent.TaskCreated)}else->Unit}}}}
esta fue mi solucion y funciona excelente.
Para navegar con parámetros en Jetpack Compose, debes modificar tu función de navegación para recibir argumentos. Primero, cambia tu destination a una data class que reciba el parámetro, como el ID de una tarea. Al navegar, usa navigate pasando el ID, mientras que en el destino, utiliza SavedStateHandle para acceder al argumento. Esto permite que tu pantalla pueda precargar datos basados en el ID recibido.
En el contexto de tu curso de Jetpack Compose, esto se compagina con la arquitectura de acción-estado-evento en ViewModel, facilitando la gestión del estado en la UI.
Podemos crear una extension function de String para poder convertir rápidamente un String a un TextFieldState
fun String?.toTextFieldState()=TextFieldState(this?:"")