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

RemoteMediator และการแคช

ผสานการแบ่งหน้าจากเครือข่ายและฐานข้อมูล

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

การแบ่งหน้าแบบออฟไลน์เป็นหลัก

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

นี่คือสิ่งที่ RemoteMediator ช่วยให้ทำได้พอดี: แบ่งหน้าข้อมูลจากฐานข้อมูลในเครื่อง และดึงข้อมูลเพิ่มเติมจากเครือข่ายเมื่อฐานข้อมูลไม่มีข้อมูลเหลือแล้ว

สถาปัตยกรรม

เมื่อใช้ RemoteMediator ข้อมูลจะไหลไปในทิศทางเดียว:

  • UI แบ่งหน้าข้อมูลจาก PagingSource ของ Room
  • เมื่อ Room ใกล้ถึงจุดสิ้นสุด RemoteMediator จะดึงหน้าถัดไปจากเครือข่าย
  • ผลลัพธ์จากเครือข่ายจะถูกเขียนลงใน Room
  • Room ส่งแถวข้อมูลใหม่ออกมา แล้ว UI จะอัปเดต

ผู้ใช้จะเห็นข้อมูลที่แคชไว้ได้ทันทีเสมอ แม้จะออฟไลน์

// Network ---> RemoteMediator ---> Room (source of truth) ---> UI
// UI never reads the network directly

PagingSource ของ Room ที่ได้มาโดยอัตโนมัติ

Room สามารถสร้าง PagingSource ให้โดยอัตโนมัติ เพียงประกาศคิวรีที่คืนค่า PagingSource<Int, Entity> แล้ว Room จะจัดการสร้างให้

นี่คือแหล่งข้อมูลที่ UI ใช้แบ่งหน้าจริง

import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Query

@Dao
interface ArticleDao {
    @Query("SELECT * FROM articles ORDER BY position ASC")
    fun pagingSource(): PagingSource<Int, ArticleEntity>
}

การสืบทอด RemoteMediator

RemoteMediator<Key, Value> มีเมธอดที่ต้องใช้อยู่หนึ่งเมธอดคือ load() และมี initialize() เป็นเมธอดเสริมที่เลือกใช้ได้

load() รับค่า LoadType (REFRESH, PREPEND หรือ APPEND) และ PagingState ปัจจุบัน จากนั้นคืนค่าเป็น MediatorResult

import androidx.paging.ExperimentalPagingApi
import androidx.paging.RemoteMediator

@OptIn(ExperimentalPagingApi::class)
class ArticleRemoteMediator(
    private val api: ArticleApi,
    private val db: AppDatabase
) : RemoteMediator<Int, ArticleEntity>() {
    // override suspend fun load(loadType, state): MediatorResult
}

การจัดการ LoadType

ใน load() คุณจะตัดสินใจว่าควรดึงหน้าข้อมูลใด โดยอิงจาก LoadType:

  • REFRESH - โหลดหน้าแรก (หรือหน้าบริเวณจุดอ้างอิง)
  • PREPEND - โดยปกติไม่มีอะไรต้องทำ ให้คืนค่า endOfPaginationReached = true
  • APPEND - โหลดหน้าถัดจากหน้าสุดท้ายที่มีอยู่
val page: Int = when (loadType) {
    LoadType.REFRESH -> 1
    LoadType.PREPEND ->
        return MediatorResult.Success(endOfPaginationReached = true)
    LoadType.APPEND -> {
        val lastKey = db.remoteKeyDao().last()?.nextKey
            ?: return MediatorResult.Success(endOfPaginationReached = true)
        lastKey
    }
}

ตารางคีย์ระยะไกล

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

วิธีนี้ช่วยให้คุณดำเนินการ APPEND/PREPEND ต่อได้แม้แอปจะเริ่มทำงานใหม่

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "remote_keys")
data class RemoteKey(
    @PrimaryKey val articleId: Int,
    val prevKey: Int?,
    val nextKey: Int?
)

การเขียนผลลัพธ์จากเครือข่ายลงใน Room

หลังจากดึงข้อมูลมาแล้ว ให้เขียนรายการและคีย์ของรายการเหล่านั้นลงใน Room ภายใน ธุรกรรม เมื่อเป็น REFRESH ให้ล้างตารางเก่าก่อน เพื่อให้แคชยังคงสอดคล้องกัน

db.withTransaction {
    if (loadType == LoadType.REFRESH) {
        db.remoteKeyDao().clearAll()
        db.articleDao().clearAll()
    }
    val nextKey = if (response.items.isEmpty()) null else page + 1
    db.remoteKeyDao().insertAll(
        response.items.map { RemoteKey(it.id, prevKey = page - 1, nextKey = nextKey) }
    )
    db.articleDao().insertAll(response.items.map { it.toEntity() })
}

การคืนค่า MediatorResult

จบการทำงานของ load() ด้วยการคืนค่า MediatorResult:

  • Success(endOfPaginationReached = true) เมื่อไม่มีหน้าเหลือแล้ว
  • Success(endOfPaginationReached = false) เมื่ออาจยังมีหน้าเพิ่มเติม
  • Error(e) เมื่อเกิดข้อผิดพลาด ซึ่งจะแสดงเป็น LoadState.Error
return try {
    val response = api.getArticles(page = page, size = state.config.pageSize)
    db.withTransaction { /* write items + keys */ }
    MediatorResult.Success(
        endOfPaginationReached = response.items.isEmpty()
    )
} catch (e: IOException) {
    MediatorResult.Error(e)
} catch (e: HttpException) {
    MediatorResult.Error(e)
}

การเชื่อม Pager กับ remoteMediator

สร้าง Pager โดยใช้ทั้ง remoteMediator และ pagingSourceFactory ของ Room ตัวทำหน้าที่สื่อกลางจะเติมข้อมูลลงใน Room ส่วน UI จะแบ่งหน้าข้อมูลจาก Room

เนื่องจากใช้ API รุ่นทดลอง ให้เปิดใช้งานด้วย @OptIn(ExperimentalPagingApi::class)

@OptIn(ExperimentalPagingApi::class)
fun articles(): Flow<PagingData<ArticleEntity>> = Pager(
    config = PagingConfig(pageSize = 20),
    remoteMediator = ArticleRemoteMediator(api, db),
    pagingSourceFactory = { db.articleDao().pagingSource() }
).flow

การค้นหาคีย์สำหรับ APPEND

สำหรับการโหลดแบบ APPEND คุณต้องใช้ nextKey ของรายการสุดท้ายที่โหลดมา ใช้จุดอ้างอิงของ PagingState เพื่อค้นหาคีย์ระยะไกลที่ถูกต้องจากตารางคีย์

ฟังก์ชันช่วยทำให้ load() อ่านได้ง่ายในกรณี REFRESH, APPEND และ PREPEND

private suspend fun lastRemoteKey(
    state: PagingState<Int, ArticleEntity>
): RemoteKey? {
    return state.pages.lastOrNull { it.data.isNotEmpty() }
        ?.data?.lastOrNull()
        ?.let { db.remoteKeyDao().keyFor(it.id) }
}

// In load(): for APPEND
val nextKey = lastRemoteKey(state)?.nextKey
    ?: return MediatorResult.Success(endOfPaginationReached = true)

การแคชและความใหม่ของข้อมูล

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

override suspend fun initialize(): InitializeAction {
    val lastUpdate = db.remoteKeyDao().lastUpdatedMillis() ?: 0L
    val cacheTimeout = TimeUnit.HOURS.toMillis(1)
    return if (System.currentTimeMillis() - lastUpdate >= cacheTimeout) {
        InitializeAction.LAUNCH_INITIAL_REFRESH
    } else {
        InitializeAction.SKIP_INITIAL_REFRESH
    }
}

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

ในการตั้งค่า RemoteMediator แหล่งข้อมูลเดียวที่เชื่อถือได้ซึ่ง UI ใช้แบ่งหน้าคืออะไร

สรุป: RemoteMediator และการแคช

คุณสร้างกระบวนการแบ่งหน้าแบบออฟไลน์เป็นหลักแล้ว:

  • UI แบ่งหน้าข้อมูลจาก PagingSource ของ Room ซึ่งเป็นแหล่งข้อมูลเดียวที่เชื่อถือได้
  • RemoteMediator.load() จัดการ REFRESH/PREPEND/APPEND และเขียนผลลัพธ์ลงใน Room ภายในธุรกรรม
  • ตาราง คีย์ระยะไกล จะเก็บคีย์ถัดไปหรือคีย์ก่อนหน้าไว้แม้เริ่มแอปใหม่
  • Pager รับทั้ง remoteMediator และ pagingSourceFactory
  • initialize() ควบคุมความใหม่ของแคช

ตอนนี้คุณสามารถโหลดรายการไม่รู้จบได้อย่างมีประสิทธิภาพ ทั้งแบบออนไลน์และออฟไลน์ ขอแสดงความยินดีที่เรียนจบหลักสูตร Paging 3

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

บทเรียน “RemoteMediator และการแคช” ฟรีหรือไม่

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

คุณจะเรียนรู้อะไรในบทเรียน “RemoteMediator และการแคช”

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

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

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

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

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

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

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

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

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