Transformasi aliran data

Saat bekerja dengan data yang di-page, aliran data sering kali perlu ditransformasi saat Anda memuatnya. Misalnya, Anda mungkin perlu memfilter daftar item atau mengonversi item menjadi jenis yang berbeda sebelum menyajikannya dalam UI. Kasus penggunaan umum lainnya terkait transformasi aliran data adalah menambahkan pemisah daftar.

Secara lebih umum, penerapan transformasi ke aliran data secara langsung akan memungkinkan Anda memisahkan konstruksi repositori dan UI.

Halaman ini mengasumsikan bahwa Anda telah memahami penggunaan dasar library Paging.

Menerapkan transformasi dasar

Karena PagingData dienkapsulasi dalam aliran reaktif, Anda dapat menerapkan operasi transformasi pada data secara bertahap antara proses pemuatan dan penyajian data.

Untuk menerapkan transformasi ke setiap objek PagingData di dalam aliran, tempatkan transformasi di dalam operasi map() di aliran tersebut:

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.
}

Mengonversi data

Operasi paling dasar pada aliran data adalah mengonversinya ke jenis lain. Setelah memiliki akses ke objek PagingData, Anda dapat menjalankan operasi map() pada setiap item individual dalam daftar yang di-page dalam objek PagingData.

Salah satu kasus penggunaan umum untuk hal ini adalah memetakan objek lapisan database atau jaringan ke objek yang secara khusus digunakan di lapisan UI. Contoh di bawah menunjukkan cara menerapkan jenis operasi peta ini:

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

Konversi data umum lainnya adalah menggunakan input dari pengguna, seperti string kueri, dan mengonversinya menjadi output permintaan untuk ditampilkan. Untuk menyiapkannya, Anda harus memproses dan merekam input kueri pengguna, melakukan permintaannya, dan mengirimkan hasil kuerinya kembali ke UI.

Anda dapat memproses input kueri menggunakan API aliran data. Simpan referensi aliran data dalam ViewModel. Lapisan UI tidak boleh memiliki akses langsung ke input tersebut; sebagai gantinya, tetapkan fungsi untuk memberi tahu ViewModel tentang kueri pengguna.

private val queryFlow = MutableStateFlow("")

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

Saat nilai kueri berubah dalam aliran data, Anda dapat menjalankan operasi untuk mengonversi nilai kueri ke jenis data yang diinginkan dan menampilkan hasilnya ke lapisan UI. Fungsi konversi khusus bergantung pada bahasa dan framework yang digunakan, tetapi semuanya menyediakan fungsi yang serupa.

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

Menggunakan operasi seperti flatMapLatest atau switchMap memastikan bahwa hanya hasil terbaru yang akan ditampilkan ke UI. Jika pengguna mengubah input kueri sebelum operasi database selesai, operasi ini akan membuang hasil dari kueri lama dan segera meluncurkan penelusuran baru.

Memfilter data

Operasi umum lainnya adalah pemfilteran. Anda dapat memfilter data berdasarkan kriteria dari pengguna, atau Anda dapat menghapus data dari UI jika data harus disembunyikan berdasarkan kriteria lain.

Anda harus menempatkan operasi filter ini dalam panggilan map() karena filternya berlaku untuk objek PagingData. Setelah data difilter dari PagingData, instance PagingData baru akan diteruskan ke lapisan UI untuk ditampilkan.

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

Menambahkan pemisah daftar

Library Paging mendukung pemisah daftar dinamis. Anda dapat meningkatkan keterbacaan daftar dengan memasukkan pemisah secara langsung ke aliran data sebagai composable dalam tata letak Anda. Akibatnya, pemisah menjadi composable berfitur lengkap, yang memungkinkan interaktivitas, gaya, dan semantik aksesibilitas penuh.

Ada tiga langkah saat Anda memasukan separator ke dalam daftar yang di-page:

  1. Mengonversi model UI untuk mengakomodasi item pemisah. Salah satu caranya adalah dengan membungkus item data dan pemisah ke dalam satu class tertutup. Hal ini memungkinkan UI menangani beberapa jenis item dalam daftar yang sama.
  2. Mentransformasi aliran data untuk menambahkan pemisah antara proses pemuatan dan penyajian data secara dinamis.
  3. Mengupdate UI untuk menangani item pemisah.

Mengonversi model UI

Library Paging akan memasukkan pemisah daftar ke dalam UI sebagai item daftar yang sebenarnya, tetapi item pemisah harus dapat dibedakan dari item data dalam daftar untuk memastikan kedua jenis composable dirender secara berbeda. Solusinya adalah dengan membuat class terbatas dalam Kotlin dengan subclass untuk mewakili data dan pemisah Anda. Selain itu, Anda juga dapat membuat class dasar yang diperpanjang oleh class item daftar dan class pemisah.

Anggaplah Anda ingin menambahkan pemisah ke daftar item User yang di-page. Cuplikan berikut menunjukkan cara pembuatan class dasar saat instancenya berupa UserModel atau 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()
}

Mentransformasi aliran data

Anda harus melakukan transformasi aliran data setelah memuat dan sebelum menampilkannya. Transformasi harus melakukan hal berikut:

  • Mengonversi item daftar yang dimuat untuk merefleksikan jenis item dasar yang baru.
  • Menggunakan metode PagingData.insertSeparators() untuk menambahkan pemisah.

Untuk mempelajari operasi transformasi lebih lanjut, lihat Menerapkan transformasi dasar.

Contoh berikut menunjukkan operasi transformasi untuk memperbarui aliran PagingData<User> menjadi aliran PagingData<UiModel> dengan pemisah yang ditambahkan:

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
    }
  }
}

Menangani pemisah di UI

Langkah terakhir adalah mengubah UI agar mengakomodasi jenis item pemisah. Dalam tata letak lambat, Anda dapat menangani beberapa jenis item dengan memeriksa jenis setiap UiModel yang dipancarkan. Saat melakukan iterasi pada data yang di-page, gunakan pernyataan when untuk memanggil composable yang sesuai. Dengan demikian, Anda dapat menyediakan UI yang berbeda untuk item data dan pemisah.

@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()
      }
    }
  }
}

Menghindari tugas duplikat

Satu masalah utama yang harus dihindari adalah membuat aplikasi melakukan tugas yang tidak perlu. Pengambilan data adalah operasi yang mahal, dan transformasi data juga dapat menghabiskan waktu yang berharga. Setelah dimuat dan disiapkan untuk ditampilkan di UI, data harus disimpan jika perubahan konfigurasi terjadi dan UI harus dibuat ulang.

Operasi cachedIn() akan meng-cache semua hasil transformasi yang terjadi di belakangnya. Biasanya, Anda menerapkan operator ini di ViewModel sebelum mengekspos Flow ke composable Anda.

Untuk mengelola cache dengan benar, teruskan CoroutineScope ke cachedIn(), seperti yang ditunjukkan dalam contoh berikut menggunakan viewModelScope.

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

Untuk informasi selengkapnya tentang cara menggunakan cachedIn() dengan aliran data PagingData, lihat Menyiapkan aliran data PagingData.

Referensi lainnya

Untuk mempelajari library Paging lebih lanjut, lihat referensi tambahan berikut:

Dokumentasi

Melihat konten