Creación de Pantallas de Búsqueda con Componentes Atómicos en Android

Clase 12 de 20Curso de Patrones MVVM en Android

Resumen

En esta clase te mostraremos cómo crear una pantalla de búsqueda (search screen) usando componentes atómicos basados en el principio de responsabilidad única. Además, integramos la funcionalidad de consumo de OpenFood API a través de un view model para garantizar una arquitectura limpia y eficiente.

¿Por qué es crucial el principio de responsabilidad única en la arquitectura NVM?

El principio de responsabilidad única es fundamental en la arquitectura Model-View-ViewModel (MVVM). Este principio dicta que una clase, módulo o componente debe encargarse de una única tarea o responsabilidad dentro del sistema. Aplicar este enfoque tiene diversas ventajas:

  • Mantenibilidad: al tener responsabilidades claras, es más fácil actualizar o modificar una parte del sistema sin afectar las demás.
  • Desacoplamiento: facilita el aislamiento de cada componente, reduciendo la dependencia entre ellos.
  • Pruebas unitarias óptimas: permite testear componentes de manera aislada y con mayor eficacia.

¿Cómo crear el componente searchTextField?

Vamos a comenzar por desarrollar el componente principal de nuestra pantalla de búsqueda, el searchTextField. Este componente se encargará de manejar la entrada de texto del usuario y actuar como el campo de búsqueda.

@Composable
fun SearchTextField(
    text: String,
    onValueChange: (String) -> Unit,
    onSearch: () -> Unit,
    modifier: Modifier = Modifier,
    hint: String = stringResource(R.string.search),
    shouldShowHint: Boolean = true,
    onFocusChange: (FocusState) -> Unit
) {
    val localSpacing = LocalSpacing.current
    Box(modifier = modifier) {
        BasicTextField(
            value = text,
            onValueChange = onValueChange,
            singleLine = true,
            keyboardActions = KeyboardActions(
                onSearch = {
                    onSearch()
                }
            ),
            keyboardOptions = KeyboardOptions(
                imeAction = ImeAction.Search
            ),
            modifier = Modifier
                .clip(RoundedCornerShape(5.dp))
                .padding(2.dp)
                .shadow(2.dp, RoundedCornerShape(5.dp))
                .background(MaterialTheme.colors.surface)
                .fillMaxWidth()
                .padding(localSpacing.spaceMedium)
                .onFocusChanged {
                    onFocusChange(it)
                }
                .testTag("searchTextField")
        )
        if (shouldShowHint) {
            Text(
                text = hint,
                style = MaterialTheme.typography.bodyLarge,
                fontWeight = FontWeight.Light,
                color = Color.LightGray,
                modifier = Modifier.align(Alignment.CenterStart).padding(localSpacing.spaceMedium)
            )
        }
        IconButton(onClick = onSearch, modifier = Modifier.align(Alignment.CenterEnd)) {
            Icon(
                imageVector = Icons.Default.Search,
                contentDescription = stringResource(R.string.search)
            )
        }
    }
}

¿Cómo implementar una pantalla searchScreen?

Al tener un componente de texto robusto, podemos pasar a construir la pantalla de búsqueda completa, aprovechando el componible searchTextField que hemos desarrollado.

@Composable
fun SearchScreen(
    snackbarHostState: SnackbarHostState,
    mealName: String,
    dayOfTheMonth: Int,
    month: Int,
    year: Int,
    onNavigateUp: () -> Unit,
    viewModel: SearchViewModel = hiltViewModel()
) {
    val localSpacing = LocalSpacing.current
    val keyboardController = LocalSoftwareKeyboardController.current
    Column(
        modifier = Modifier.fillMaxSize().padding(localSpacing.spaceMedium)
    ) {
        Spacer(modifier = Modifier.height(localSpacing.spaceLarge))
        Text(
            text = stringResource(id = R.string.add_meal_name, mealName),
            style = MaterialTheme.typography.titleMedium
        )
        Spacer(modifier = Modifier.height(localSpacing.spaceMedium))
        SearchTextField(
            text = "",
            onValueChange = {},
            onSearch = {
                keyboardController?.hide()
                // Lógica de búsqueda
            },
            shouldShowHint = true,
            onFocusChange = {}
        )
        Spacer(modifier = Modifier.height(localSpacing.spaceMedium))
    }
}

¿Cómo integrar el viewModel al flujo de búsqueda?

En una arquitectura MVVM, es crucial utilizar un ViewModel para gestionar los estados de la vista y la lógica de negocios. Inicialicemos nuestro ViewModel para manejar la búsqueda con la OpenFood API.

@HiltViewModel
class SearchViewModel @Inject constructor(
    private val trackerUseCase: TrackerUseCase
) : ViewModel() {

    private val _uiEvent = Channel()
    val uiEvent = _uiEvent.receiveAsFlow()

    fun executeSearch() {
        viewModelScope.launch {
            // Implementación de la lógica de búsqueda
            trackerUseCase.search("pizza")
        }
    }
}

La estructura del código y el diseño modular nos permiten mantener cada componente separado y enfocado en su tarea específica. Esto no solo mejora la experiencia de desarrollo y mantenimiento, sino que también ofrece flexibilidad para futuras expansiones. Además, el uso de un viewModel asegura que la lógica de negocios permanezca separada de la lógica de interfaz de usuario, lo que facilita las pruebas y el mantenimiento.

Sigamos adelante, entusiasmados con la capacidad de nuestras aplicaciones de ofrecer experiencias dinámicas y funcionales, siempre optimizando cada componente que creamos.