เปลี่ยนรูปแบบสตรีมข้อมูล

เมื่อทำงานกับข้อมูลที่แบ่งหน้า คุณมักจะต้อง เปลี่ยนรูปแบบสตรีมข้อมูลขณะโหลด เช่น คุณอาจต้องกรองรายการ หรือแปลงรายการเป็นประเภทอื่นก่อนที่จะแสดงใน UI กรณีการใช้งานทั่วไปอีกอย่างหนึ่งสำหรับการเปลี่ยนรูปแบบสตรีมข้อมูลคือการเพิ่มตัวคั่นรายการ

โดยทั่วไปแล้ว การใช้การเปลี่ยนรูปแบบกับสตรีมข้อมูลโดยตรงจะช่วยให้คุณ แยกโครงสร้างที่เก็บและโครงสร้าง UI ออกจากกันได้

หน้านี้ถือว่าคุณคุ้นเคยกับการใช้งานพื้นฐานของไลบรารีการแบ่งหน้าอยู่แล้ว

ใช้การเปลี่ยนรูปแบบพื้นฐาน

เนื่องจาก PagingData อยู่ในสตรีมแบบรีแอกทีฟ คุณจึงใช้การดำเนินการเปลี่ยนรูปแบบกับข้อมูลได้ทีละรายการระหว่างการโหลดข้อมูลและการนำเสนอ

หากต้องการใช้การเปลี่ยนรูปแบบกับออบเจ็กต์ PagingData แต่ละรายการในสตรีม ให้วางการเปลี่ยนรูปแบบไว้ภายใน map() การดำเนินการในสตรีม

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

แปลงข้อมูล

การดำเนินการขั้นพื้นฐานที่สุดในสตรีมข้อมูลคือการแปลงข้อมูลเป็นประเภทอื่น เมื่อมีสิทธิ์เข้าถึงออบเจ็กต์ PagingData แล้ว คุณจะดำเนินการ map() กับแต่ละรายการในรายการที่แบ่งหน้าภายในออบเจ็กต์ PagingData ได้

กรณีการใช้งานทั่วไปอย่างหนึ่งคือการแมปออบเจ็กต์เลเยอร์เครือข่ายหรือฐานข้อมูลกับออบเจ็กต์ที่ใช้ในเลเยอร์ UI โดยเฉพาะ ตัวอย่างด้านล่างแสดงวิธี ใช้การดำเนินการแมปประเภทนี้

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

การแปลงข้อมูลที่พบบ่อยอีกอย่างคือการรับอินพุตจากผู้ใช้ เช่น สตริงการค้นหา แล้วแปลงเป็นเอาต์พุตคำขอเพื่อแสดง การตั้งค่านี้ ต้องคอยรับฟังและบันทึกข้อมูลคำค้นหาของผู้ใช้ ทำการ ขอ และส่งผลการค้นหากลับไปยัง UI

คุณสามารถฟังอินพุตการค้นหาโดยใช้ Stream API เก็บข้อมูลอ้างอิงสตรีม ไว้ใน ViewModel เลเยอร์ UI ไม่ควรมีสิทธิ์เข้าถึงโดยตรง แต่ควร กำหนดฟังก์ชันเพื่อแจ้ง ViewModel เกี่ยวกับคำค้นหาของผู้ใช้แทน

private val queryFlow = MutableStateFlow("")

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

เมื่อค่าการค้นหาเปลี่ยนแปลงในสตรีมข้อมูล คุณสามารถดำเนินการเพื่อ แปลงค่าการค้นหาเป็นประเภทข้อมูลที่ต้องการและแสดงผลลัพธ์ไปยังเลเยอร์ UI ฟังก์ชัน Conversion ที่เฉพาะเจาะจงจะขึ้นอยู่กับภาษาและเฟรมเวิร์ก ที่ใช้ แต่ทั้งหมดจะให้ฟังก์ชันการทำงานที่คล้ายกัน

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

การใช้การดำเนินการอย่าง flatMapLatest หรือ switchMap จะช่วยให้มั่นใจได้ว่าระบบจะแสดงเฉพาะผลลัพธ์ล่าสุดใน UI หากผู้ใช้เปลี่ยนอินพุตคำค้นหาก่อนที่การดำเนินการฐานข้อมูลจะเสร็จสมบูรณ์ การดำเนินการเหล่านี้จะทิ้งผลลัพธ์จากคำค้นหาเก่าและเปิดการค้นหาใหม่ทันที

กรองข้อมูล

การดำเนินการอีกอย่างที่พบบ่อยคือการกรอง คุณสามารถกรองข้อมูลตามเกณฑ์ จากผู้ใช้ หรือนำข้อมูลออกจาก UI ได้หากควรซ่อนข้อมูลตาม เกณฑ์อื่นๆ

คุณต้องวางการดำเนินการตัวกรองเหล่านี้ไว้ภายในเรียกใช้ map() เนื่องจากตัวกรองใช้กับออบเจ็กต์ PagingData เมื่อกรองข้อมูลออกจาก PagingDataแล้ว ระบบจะส่งอินสแตนซ์ PagingData ใหม่ไปยังเลเยอร์ UI เพื่อ แสดง

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

เพิ่มตัวคั่นรายการ

ไลบรารีการแบ่งหน้าจะรองรับตัวคั่นรายการแบบไดนามิก คุณปรับปรุงความสามารถในการอ่านรายการได้โดยการแทรกตัวคั่นลงในสตรีมข้อมูลโดยตรงเป็น คอมโพสในเลย์เอาต์ ด้วยเหตุนี้ ตัวคั่นจึงเป็น Composable ที่มีฟีเจอร์ครบถ้วน ซึ่งช่วยให้โต้ตอบ จัดรูปแบบ และเข้าถึงความหมายของการช่วยเหลือพิเศษได้อย่างเต็มที่

ขั้นตอนในการแทรกตัวคั่นลงในรายการแบบแบ่งหน้ามี 3 ขั้นตอนดังนี้

  1. แปลงโมเดล UI เพื่อรองรับรายการตัวคั่น วิธีหนึ่งในการทำเช่นนี้ คือการรวมรายการข้อมูลและตัวคั่นไว้ในคลาสที่ปิดผนึกรายการเดียว ซึ่งช่วยให้ UI จัดการรายการประเภทต่างๆ ในรายการเดียวกันได้
  2. เปลี่ยนสตรีมข้อมูลเพื่อเพิ่มตัวคั่นระหว่างการโหลดข้อมูลและการนำเสนอข้อมูลแบบไดนามิก
  3. อัปเดต UI เพื่อจัดการรายการตัวคั่น

แปลงโมเดล UI

ไลบรารีการแบ่งหน้าจะแทรกตัวคั่นรายการลงใน UI เป็นรายการจริง แต่ต้องแยกรายการตัวคั่นออกจากรายการข้อมูล ในรายการเพื่อให้มั่นใจว่าระบบจะแสดงผลทั้ง 2 ประเภทที่ประกอบได้แตกต่างกัน วิธีแก้คือการสร้างคลาสที่ปิดผนึกของ Kotlin พร้อมคลาสย่อยเพื่อแสดงข้อมูลและตัวคั่น หรือคุณจะสร้างคลาสพื้นฐานที่คลาสรายการในลิสต์และคลาสตัวคั่นขยายก็ได้

สมมติว่าคุณต้องการเพิ่มตัวคั่นลงในรายการของUserรายการที่แบ่งหน้า ข้อมูลโค้ดต่อไปนี้แสดงวิธีสร้างคลาสพื้นฐานที่อินสแตนซ์อาจเป็น UserModel หรือ 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()
}

เปลี่ยนรูปแบบสตรีมข้อมูล

คุณต้องใช้การเปลี่ยนรูปแบบกับสตรีมข้อมูลหลังจากโหลดและก่อนที่จะนำเสนอ การเปลี่ยนรูปแบบควรทำสิ่งต่อไปนี้

  • แปลงรายการในรายการที่โหลดแล้วเพื่อให้ตรงกับประเภทรายการพื้นฐานใหม่
  • ใช้วิธี PagingData.insertSeparators() เพื่อเพิ่มตัวคั่น

ดูข้อมูลเพิ่มเติมเกี่ยวกับการดำเนินการเปลี่ยนรูปแบบได้ที่ใช้การเปลี่ยนรูปแบบพื้นฐาน

ตัวอย่างต่อไปนี้แสดงการดำเนินการเปลี่ยนรูปแบบเพื่ออัปเดตสตรีม PagingData<User> เป็นสตรีม PagingData<UiModel> โดยเพิ่มตัวคั่น

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

จัดการตัวคั่นใน UI

ขั้นตอนสุดท้ายคือการเปลี่ยน UI เพื่อรองรับประเภทรายการตัวคั่น ในเลย์เอาต์แบบเลซี่ คุณสามารถจัดการรายการหลายประเภทได้โดยการตรวจสอบประเภทของ แต่ละ UiModel ที่ปล่อยออกมา เมื่อวนซ้ำผ่านข้อมูลที่แบ่งหน้า ให้ใช้คำสั่ง when เพื่อเรียก Composable ที่เหมาะสม ซึ่งช่วยให้คุณระบุ UI ที่แตกต่างกันสำหรับรายการข้อมูลและตัวคั่นได้

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

หลีกเลี่ยงการทำงานซ้ำ

ปัญหาสำคัญที่ควรหลีกเลี่ยงคือการให้แอปทำงานที่ไม่จำเป็น การดึงข้อมูลเป็น การดำเนินการที่ใช้ทรัพยากรมาก และการเปลี่ยนรูปแบบข้อมูลก็อาจใช้เวลาอันมีค่าของคุณด้วย เมื่อโหลดและเตรียมข้อมูลเพื่อแสดงใน UI แล้ว คุณควรบันทึกข้อมูลไว้ ในกรณีที่มีการเปลี่ยนแปลงการกำหนดค่าและต้องสร้าง UI ใหม่

การดำเนินการ cachedIn() จะแคชผลลัพธ์ของการเปลี่ยนรูปแบบที่เกิดขึ้น ก่อนการดำเนินการดังกล่าว โดยปกติ คุณจะใช้ตัวดำเนินการนี้ใน ViewModel ก่อน แสดง Flow ต่อ Composable

หากต้องการจัดการแคชอย่างถูกต้อง ให้ส่ง CoroutineScope ไปยัง cachedIn() ดังที่แสดง ในตัวอย่างต่อไปนี้โดยใช้ viewModelScope

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

ดูข้อมูลเพิ่มเติมเกี่ยวกับการใช้ cachedIn() กับสตรีมของ PagingData ได้ที่ ตั้งค่าสตรีมของ PagingData

แหล่งข้อมูลเพิ่มเติม

ดูข้อมูลเพิ่มเติมเกี่ยวกับไลบรารี Paging ได้ที่แหล่งข้อมูลเพิ่มเติมต่อไปนี้

เอกสาร

ดูเนื้อหา