0Pricing
Android Academy · บทเรียน

PagingSource และ Pager

กำหนดวิธีโหลดแต่ละหน้า

PagingSource และ Pager เป็นบทเรียน Android Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Android Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Android Academy มีบทเรียนทั้งหมด 4 บทเรียน

กำหนดวิธีโหลดหน้า

หากต้องการแบ่งหน้าข้อมูลจาก API เครือข่าย คุณต้องเขียน PagingSource ซึ่งจะตอบคำถามสองข้อให้ Paging 3:

  • จะโหลดหน้าสำหรับคีย์ที่กำหนดได้อย่างไร
  • หากผู้ใช้รีเฟรช ควรกลับมาโหลดต่อจากที่ใด

จากนั้นห่อหุ้มด้วย Pager เพื่อแปลงเป็น Flow<PagingData>

พารามิเตอร์ประเภทของ PagingSource

PagingSource<Key, Value> รับพารามิเตอร์ประเภทสองตัว:

  • Key - ใช้ระบุหน้า สำหรับ API ที่ใช้หมายเลขหน้า จะเป็น Int ส่วน API ที่ใช้เคอร์เซอร์จะเป็นโทเค็น String
  • Value - ประเภทของรายการ เช่น Article
import androidx.paging.PagingSource
import androidx.paging.PagingState

class ArticlePagingSource(
    private val api: ArticleApi
) : PagingSource<Int, Article>() {
    // implement load() and getRefreshKey()
}

การใช้งาน load()

load() เป็นฟังก์ชัน suspend โดยรับ params.key (หน้าที่ต้องการดึงข้อมูล) และส่งคืน LoadResult

เมื่อสำเร็จ ให้ส่งคืน LoadResult.Page พร้อมรายการ รวมถึงคีย์ก่อนหน้าและคีย์ถัดไป หากคีย์เป็น null หมายความว่าไม่มีหน้าในทิศทางนั้น

override suspend fun load(
    params: LoadParams<Int>
): LoadResult<Int, Article> {
    val page = params.key ?: 1   // first load has a null key
    return try {
        val response = api.getArticles(page = page, size = params.loadSize)
        LoadResult.Page(
            data = response.items,
            prevKey = if (page == 1) null else page - 1,
            nextKey = if (response.items.isEmpty()) null else page + 1
        )
    } catch (e: Exception) {
        LoadResult.Error(e)
    }
}

เหตุผลที่ prevKey และ nextKey สำคัญ

Paging ใช้ nextKey เพื่อโหลดข้อมูลไปข้างหน้าเมื่อผู้ใช้เลื่อนลง และใช้ prevKey เพื่อโหลดข้อมูลย้อนกลับ (มีประโยชน์เมื่อเริ่มต้นจากตรงกลางรายการ)

การส่งคืน null สำหรับ nextKey จะบอก Paging ว่าไม่มีหน้าถัดไปแล้ว จึงหยุดการขอข้อมูล หากลืมทำเช่นนี้ อาจทำให้เกิดคำขอว่างเปล่าไม่รู้จบ

// Stop forward paging when the server returns an empty page
nextKey = if (response.items.isEmpty()) null else page + 1

// Stop backward paging at the first page
prevKey = if (page == 1) null else page - 1

การใช้งาน getRefreshKey()

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

getRefreshKey() ใช้ anchorPosition ปัจจุบัน ซึ่งคือตำแหน่งของรายการที่ใกล้กับพื้นที่แสดงผลที่สุด เพื่อเลือกคีย์ที่เหมาะสม

override fun getRefreshKey(state: PagingState<Int, Article>): Int? {
    return state.anchorPosition?.let { anchor ->
        val closestPage = state.closestPageToPosition(anchor)
        closestPage?.prevKey?.plus(1)
            ?: closestPage?.nextKey?.minus(1)
    }
}

จัดการข้อผิดพลาดอย่างเหมาะสม

ครอบการเรียกเครือข่ายด้วย try/catch และส่งคืน LoadResult.Error(e) เมื่อเกิดข้อผิดพลาด Paging จะแสดงสถานะนี้ใน UI เป็น LoadState.Error เพื่อให้คุณแสดงปุ่มลองใหม่ได้

อย่าปล่อยให้ข้อยกเว้นหลุดออกจาก load() ให้ดักจับและแปลงเป็น LoadResult.Error เสมอ

return try {
    val response = api.getArticles(page = page, size = params.loadSize)
    LoadResult.Page(
        data = response.items,
        prevKey = if (page == 1) null else page - 1,
        nextKey = if (response.items.isEmpty()) null else page + 1
    )
} catch (e: IOException) {        // no network
    LoadResult.Error(e)
} catch (e: HttpException) {       // non-2xx response
    LoadResult.Error(e)
}

การสร้าง Pager

Pager เชื่อม PagingConfig ของคุณเข้ากับโรงงานที่สร้าง PagingSource ใหม่ คุณสมบัติ .flow ของมันคือ Flow<PagingData> ที่ UI จะรวบรวม

แลมบ์ดาของโรงงานต้องสร้างแหล่งข้อมูลใหม่ทุกครั้ง เพราะ Paging จะทำให้แหล่งข้อมูลใช้ไม่ได้และสร้างขึ้นใหม่เมื่อรีเฟรช

import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import kotlinx.coroutines.flow.Flow

class ArticleRepository(private val api: ArticleApi) {
    fun articleStream(): Flow<PagingData<Article>> = Pager(
        config = PagingConfig(pageSize = 20, prefetchDistance = 5),
        pagingSourceFactory = { ArticlePagingSource(api) }
    ).flow
}

cachedIn สำหรับ ViewModels

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

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.cachedIn

class ArticleViewModel(
    repo: ArticleRepository
) : ViewModel() {
    val articles = repo.articleStream()
        .cachedIn(viewModelScope)
}

loadSize เทียบกับ pageSize

โปรดสังเกตว่า load() อ่านค่า params.loadSize ไม่ใช่ pageSize ที่คุณกำหนดค่าไว้โดยตรง

ในการโหลดครั้งแรก Paging อาจขอข้อมูลก้อนแรกที่มีขนาดใหญ่กว่า (ควบคุมโดย initialLoadSize ใน PagingConfig ซึ่งค่าเริ่มต้นคือ 3 เท่าของขนาดหน้า) ให้ส่ง params.loadSize ไปยัง API เสมอ เพื่อให้คำขอตรงกับขนาดที่ Paging คาดว่าจะได้รับ

// Correct: respect the size Paging asks for
val response = api.getArticles(page = page, size = params.loadSize)

// PagingConfig can tune the first load:
PagingConfig(pageSize = 20, initialLoadSize = 40)

API แบบใช้เคอร์เซอร์

ไม่ใช่ทุก API ที่ใช้หมายเลขหน้า บาง API จะส่งคืนเคอร์เซอร์หรือโทเค็นที่ชี้ไปยังหน้าถัดไป รูปแบบการทำงานเหมือนเดิม เพียงเปลี่ยนประเภท Key เป็น String และใช้โทเค็นจากการตอบกลับ

class CursorArticleSource(
    private val api: ArticleApi
) : PagingSource<String, Article>() {
    override suspend fun load(
        params: LoadParams<String>
    ): LoadResult<String, Article> {
        val cursor = params.key   // null on first load
        return try {
            val res = api.getArticles(cursor = cursor, size = params.loadSize)
            LoadResult.Page(
                data = res.items,
                prevKey = null,            // forward-only cursor
                nextKey = res.nextCursor   // null when exhausted
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }

    override fun getRefreshKey(state: PagingState<String, Article>) = null
}

นำทุกส่วนมาประกอบกัน

ตอนนี้คุณมีชั้นข้อมูลครบถ้วนแล้ว ได้แก่ PagingSource ที่โหลดทีละหน้า Pager ที่สตรีมหน้าเหล่านั้น และ ViewModel ที่แคชโฟลว์

ชั้น UI เพียงรวบรวม viewModel.articles ซึ่งเราจะแสดงผลในบทเรียนถัดไปด้วย Compose LazyColumn

// Data layer summary
// 1. ArticlePagingSource : PagingSource<Int, Article>
// 2. Pager(config, factory).flow  -> Flow<PagingData<Article>>
// 3. ViewModel: repo.articleStream().cachedIn(viewModelScope)
// UI just collects viewModel.articles

ตรวจสอบความเข้าใจ

ใน PagingSource ที่ใช้หมายเลขหน้า การส่งคืน nextKey = null จาก load() สื่อถึงอะไร

สรุป: PagingSource และ Pager

คุณได้สร้างชั้นข้อมูลสำหรับการแบ่งหน้าแล้ว:

  • PagingSource<Key, Value> ใช้งาน load() และ getRefreshKey()
  • load() ส่งคืน LoadResult.Page พร้อม prevKey/nextKey หรือ LoadResult.Error
  • คีย์ที่เป็น null จะหยุดการแบ่งหน้าในทิศทางนั้น
  • Pager(config, factory).flow สร้าง Flow<PagingData>
  • cachedIn(viewModelScope) ช่วยเก็บข้อมูลไว้เมื่อการกำหนดค่าเปลี่ยนแปลง

ถัดไป: การแสดงผลโฟลว์นี้ในรายการ Compose

คำถามที่พบบ่อย

บทเรียน “PagingSource และ Pager” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “PagingSource และ Pager” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Android Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Android Academy มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “PagingSource และ Pager”

กำหนดวิธีโหลดแต่ละหน้า คุณปฏิบัติ Android Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Android Academy หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน Android Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน

บทเรียน “PagingSource และ Pager” ใช้เวลานานแค่ไหน

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

ฉันเขียนและรันโค้ดในบทเรียน Android Academy นี้ได้ไหม

ได้ บทเรียน Android Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. เหตุใดจึงต้องแบ่งหน้า
  2. PagingSource และ Pager
  3. การแบ่งหน้าในรายการ Compose
  4. RemoteMediator และการแคช
← กลับไปที่ Android Academy