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 directlyPagingSource ของ 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และpagingSourceFactoryinitialize()ควบคุมความใหม่ของแคช
ตอนนี้คุณสามารถโหลดรายการไม่รู้จบได้อย่างมีประสิทธิภาพ ทั้งแบบออนไลน์และออฟไลน์ ขอแสดงความยินดีที่เรียนจบหลักสูตร 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 ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- เหตุใดจึงต้องแบ่งหน้า
- PagingSource และ Pager
- การแบ่งหน้าในรายการ Compose
- RemoteMediator และการแคช