Cómo transformar transmisiones de datos

Cuando trabajas con datos paginados, a menudo necesitas transformar el flujo de datos a medida que se carga. Por ejemplo, es posible que debas filtrar una lista de elementos o convertirlos en un tipo diferente antes de presentarlos en la IU. Otro caso de uso común para la transformación del flujo de datos es agregar separadores de lista.

En términos más generales, aplicar transformaciones directamente al flujo de datos te permite mantener separadas las construcciones de repositorio y las de IU.

En esta página, se asume que conoces el uso básico de la biblioteca de paginación.

Cómo aplicar transformaciones básicas

Debido a que PagingData se encapsula en un flujo reactivo, puedes aplicar operaciones de transformación a los datos de manera incremental entre la carga de los datos y su presentación.

Para aplicar transformaciones a cada objeto PagingData del flujo, ubica las transformaciones dentro de una operación map() en el flujo:

pager.flow // Type is Flow<PagingData<User>>.
  // Map the outer stream so that the transformations are applied to
  // each new generation of PagingData.
  .map { pagingData ->
    // Transformations in this block are applied to the items
    // in the paged data.
}

Cómo convertir datos

La operación más básica en un flujo de datos es convertirlo en un tipo diferente. Una vez que tengas acceso al objeto PagingData, podrás realizar una operación map() en cada elemento individual de la lista paginada dentro del objeto PagingData.

Un caso de uso común para esto es mapear un objeto de capa de red o de base de datos a un objeto específicamente usado en la capa de la IU. En el siguiente ejemplo, se muestra cómo aplicar este tipo de operación de mapeo:

pager.flow // Type is Flow<PagingData<User>>.
  .map { pagingData ->
    pagingData.map { user -> UiModel(user) }
  }

Otra conversión de datos común es tomar una entrada del usuario, como una string de consulta, y convertirla en el resultado para mostrar de la solicitud. Para configurar esta función, se debe escuchar y capturar la entrada de consulta del usuario, realizar la solicitud y enviar el resultado de la consulta a la IU.

Puedes escuchar la entrada de la consulta con una API de transmisión. Mantén la referencia de transmisión en tu ViewModel. La capa de IU no debería tener acceso directo a ella. En su lugar, define una función para notificar a ViewModel de la consulta del usuario.

private val queryFlow = MutableStateFlow("")

fun onQueryChanged(query: String) {
  queryFlow.value = query
}

Cuando el valor de la consulta cambia en el flujo de datos, puedes realizar operaciones para convertir el valor de la consulta en el tipo de datos deseado y mostrar el resultado en la capa de la IU. La función de conversión específica depende del lenguaje y el marco de trabajo utilizado, pero todos proporcionan una funcionalidad similar.

val querySearchResults: Flow<User> = queryFlow.flatMapLatest { query ->
  // The database query returns a Flow which is output through
  // querySearchResults
  userDatabase.searchBy(query)
}

Usar operaciones como flatMapLatest o switchMap garantiza que solo se muestren los resultados más recientes a la IU. Si el usuario cambia su entrada de consulta antes de que se complete la operación de base de datos, estas operaciones descartan los resultados de la consulta anterior e inician la búsqueda nueva de inmediato.

Cómo filtrar datos

Otra operación común es el filtrado. Puedes filtrar datos en función de criterios del usuario o quitar datos de la IU en caso de que deban estar ocultos según otros criterios.

Debes colocar estas operaciones de filtro dentro de la llamada map() porque el filtro se aplica al objeto PagingData. Una vez que los datos se filtran fuera de PagingData, la instancia de PagingData nueva se pasa a la capa de la IU para mostrarla.

pager.flow // Type is Flow<PagingData<User>>.
  .map { pagingData ->
    pagingData.filter { user -> !user.hiddenFromUi }
  }

Cómo agregar separadores de lista

La biblioteca de Paging admite separadores de lista dinámicos. Para mejorar la legibilidad de la lista, inserta separadores directamente en el flujo de datos como elementos componibles en tu diseño. Como resultado, los separadores son elementos componibles con todas las funciones, lo que permite una interactividad, un diseño y una semántica de accesibilidad completos.

Para insertar separadores en la lista paginada, debes seguir estos tres pasos:

  1. Convierte el modelo de IU en función de los elementos del separador. Una forma de hacerlo es incluir el separador y el elemento de datos en una sola clase sellada. Esto permite que la IU controle varios tipos de elementos en la misma lista.
  2. Transforma el flujo de datos para agregar dinámicamente los separadores entre la carga de datos y su presentación.
  3. Actualiza la IU para manejar los elementos del separador.

Cómo convertir el modelo de IU

La biblioteca de Paging inserta separadores de lista en la IU como elementos de lista reales, pero los elementos del separador deben distinguirse de los elementos de datos de la lista para garantizar que ambos tipos componibles se rendericen de forma distinta. La solución es crear una clase sellada de Kotlin con subclases para representar tus datos y los separadores. Como alternativa, puedes crear una clase base que extiendan tu clase de elemento de lista y la clase del separador.

Supongamos que deseas agregar separadores a una lista paginada de elementos User. En el siguiente fragmento, se muestra cómo crear una clase base en la que las instancias pueden ser UserModel o SeparatorModel:

sealed class UiModel {
  class UserModel(val id: String, val label: String) : UiModel() {
    constructor(user: User) : this(user.id, user.label)
  }

  class SeparatorModel(val description: String) : UiModel()
}

Cómo transformar el flujo de datos

Debes aplicar transformaciones al flujo de datos después de la cargar y antes de la presentación. Las transformaciones deben realizar lo siguiente:

  • Convertir los elementos de lista cargados para reflejar el nuevo tipo de elemento base
  • Usar el método PagingData.insertSeparators() para agregar los separadores

Para obtener más información sobre las operaciones de transformación, consulta Cómo aplicar transformaciones básicas.

En el siguiente ejemplo, se muestran las operaciones de transformación para actualizar el flujo de PagingData<User> a uno de PagingData<UiModel> con separadores agregados:

pager.flow.map { pagingData: PagingData<User> ->
  // Map outer stream, so you can perform transformations on
  // each paging generation.
  pagingData
  .map { user ->
    // Convert items in stream to UiModel.UserModel.
    UiModel.UserModel(user)
  }
  .insertSeparators<UiModel.UserModel, UiModel> { before, after ->
    when {
      before == null -> UiModel.SeparatorModel("HEADER")
      after == null -> UiModel.SeparatorModel("FOOTER")
      shouldSeparate(before, after) -> UiModel.SeparatorModel(
        "BETWEEN ITEMS $before AND $after"
      )
      // Return null to avoid adding a separator between two items.
      else -> null
    }
  }
}

Cómo manejar los separadores en la IU

El último paso es cambiar la IU para adaptarla al tipo de elemento del separador. En un diseño diferido, puedes controlar varios tipos de elementos verificando el tipo de cada UiModel emitido. Cuando iteres tus datos paginados, usa una instrucción when para llamar al elemento componible adecuado. Esto te permite proporcionar una IU distinta para los elementos de datos y los separadores.

@Composable fun UserList(pagingItems: LazyPagingItems) {
  LazyColumn {
    items(
      count = pagingItems.itemCount,
      key = { index ->
        val item = pagingItems.peek(index)
        when (item) {
          is UiModel.UserModel -> item.user.id
          is UiModel.SeparatorModel -> item.description
          else -> index
        }
      }
    ) { index ->
      when (val item = pagingItems[index]) {
        is UiModel.UserModel -> UserItemComposable(item.user)
        is UiModel.SeparatorModel -> SeparatorComposable(item.description)
        null -> PlaceholderComposable()
      }
    }
  }
}

Cómo evitar el trabajo duplicado

Un problema clave que debes evitar es hacer que la app realice trabajos innecesarios. La recuperación de datos es una operación costosa, y las transformaciones de datos también pueden consumir tiempo valioso. Una vez que los datos se cargan y se preparan para mostrarse en la IU, deben guardarse en caso de que se produzca un cambio de configuración y se deba volver a crear la IU.

La operación cachedIn() almacena en caché los resultados de las transformaciones que ocurran. Por lo general, aplicas este operador en tu ViewModel antes de exponer el Flow a tus elementos componibles.

Para administrar la caché correctamente, pasa un CoroutineScope a cachedIn(), como se muestra en el siguiente ejemplo con viewModelScope.

pager.flow // Type is Flow<PagingData<User>>.
  .map { pagingData ->
    pagingData.filter { user -> !user.hiddenFromUi }
      .map { user -> UiModel.UserModel(user) }
  }
  .cachedIn(viewModelScope)

Para obtener más información sobre el uso de cachedIn() con un flujo de PagingData, consulta Cómo configurar un flujo de PagingData.

Recursos adicionales

Para obtener más información sobre la biblioteca de Paging, consulta los siguientes recursos adicionales:

Documentación

Mira contenido