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

แอปที่เน้นการทำงานแบบออฟไลน์เป็นหลักคือแอปที่สามารถดำเนินการได้ทั้งหมดหรือเฉพาะบางส่วนที่สำคัญ ของฟังก์ชันหลักโดยไม่ต้องเชื่อมต่ออินเทอร์เน็ต กล่าวคือ สามารถดำเนินการได้ ดำเนินการตรรกะทางธุรกิจบางส่วนหรือทั้งหมดแบบออฟไลน์

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

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

  • แบนด์วิดท์อินเทอร์เน็ตที่จำกัด
  • การหยุดชะงักของการเชื่อมต่อชั่วคราว เช่น เมื่ออยู่ในลิฟต์หรือ อุโมงค์ข้อมูล
  • การเข้าถึงข้อมูลเป็นครั้งคราว ตัวอย่างเช่น แท็บเล็ตที่ใช้ Wi-Fi อย่างเดียว

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

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

แอปที่ตรงตามเกณฑ์ข้างต้นมักจะเรียกว่าแอปที่เน้นการทำงานแบบออฟไลน์เป็นหลัก

ออกแบบแอปที่นิยมใช้แบบออฟไลน์

เมื่อออกแบบแอปที่เน้นแบบออฟไลน์เป็นหลัก คุณควรเริ่มในชั้นข้อมูลและ การดำเนินการหลัก 2 อย่างที่คุณทำกับข้อมูลแอปได้

  • การอ่าน: การดึงข้อมูลเพื่อนำมาใช้โดยส่วนอื่นๆ ของแอป เช่น การแสดง ให้แก่ผู้ใช้
  • การเขียน: เก็บข้อมูลที่ผู้ใช้ป้อนไว้เพื่อดึงข้อมูลในภายหลัง

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

สร้างโมเดลข้อมูลในแอปที่ใช้แบบออฟไลน์เป็นหลัก

แอปที่ใช้แบบออฟไลน์เป็นหลักจะมีแหล่งข้อมูลอย่างน้อย 2 แหล่งสําหรับที่เก็บทุกแห่งที่ ใช้ทรัพยากรเครือข่าย:

  • แหล่งข้อมูลในเครื่อง
  • แหล่งข้อมูลเครือข่าย
ชั้นข้อมูลที่ใช้ออฟไลน์เป็นหลักประกอบไปด้วยแหล่งข้อมูลทั้งระดับท้องถิ่นและของเครือข่าย
รูปที่ 1: ที่เก็บที่ทำงานแบบออฟไลน์เป็นหลัก

แหล่งข้อมูลในเครื่อง

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

  • แหล่งข้อมูลที่มีโครงสร้าง เช่น ฐานข้อมูลเชิงสัมพันธ์ เช่น ห้อง
  • แหล่งข้อมูลที่ไม่มีโครงสร้าง เช่น บัฟเฟอร์โปรโตคอลกับ Datastore
  • ไฟล์แบบง่าย

แหล่งข้อมูลเครือข่าย

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

กำลังแสดงทรัพยากร

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

โครงสร้างไดเรกทอรีด้านล่างแสดงภาพแนวคิดนี้ AuthorEntity เป็น การนำเสนอผู้เขียนที่อ่านจากฐานข้อมูลภายในของแอป และ NetworkAuthor ตัวแทนของผู้เขียนที่เรียงลำดับผ่านเครือข่าย:

data/
├─ local/
│ ├─ entities/
│ │ ├─ AuthorEntity
│ ├─ dao/
│ ├─ NiADatabase
├─ network/
│ ├─ NiANetwork
│ ├─ models/
│ │ ├─ NetworkAuthor
├─ model/
│ ├─ Author
├─ repository/

รายละเอียดของ AuthorEntity และ NetworkAuthor มีดังนี้

/**
 * Network representation of [Author]
 */
@Serializable
data class NetworkAuthor(
    val id: String,
    val name: String,
    val imageUrl: String,
    val twitter: String,
    val mediumPage: String,
    val bio: String,
)

/**
 * Defines an author for either an [EpisodeEntity] or [NewsResourceEntity].
 * It has a many-to-many relationship with both entities
 */
@Entity(tableName = "authors")
data class AuthorEntity(
    @PrimaryKey
    val id: String,
    val name: String,
    @ColumnInfo(name = "image_url")
    val imageUrl: String,
    @ColumnInfo(defaultValue = "")
    val twitter: String,
    @ColumnInfo(name = "medium_page", defaultValue = "")
    val mediumPage: String,
    @ColumnInfo(defaultValue = "")
    val bio: String,
)

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

/**
 * External data layer representation of a "Now in Android" Author
 */
data class Author(
    val id: String,
    val name: String,
    val imageUrl: String,
    val twitter: String,
    val mediumPage: String,
    val bio: String,
)

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

/**
 * Converts the network model to the local model for persisting
 * by the local data source
 */
fun NetworkAuthor.asEntity() = AuthorEntity(
    id = id,
    name = name,
    imageUrl = imageUrl,
    twitter = twitter,
    mediumPage = mediumPage,
    bio = bio,
)

/**
 * Converts the local model to the external model for use
 * by layers external to the data layer
 */
fun AuthorEntity.asExternalModel() = Author(
    id = id,
    name = name,
    imageUrl = imageUrl,
    twitter = twitter,
    mediumPage = mediumPage,
    bio = bio,
)

อ่าน

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

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

class OfflineFirstTopicsRepository(
    private val topicDao: TopicDao,
    private val network: NiaNetworkDataSource,
) : TopicsRepository {

    override fun getTopicsStream(): Flow<List<Topic>> =
        topicDao.getTopicEntitiesStream()
            .map { it.map(TopicEntity::asExternalModel) }
}

กลยุทธ์การจัดการข้อผิดพลาด

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

แหล่งข้อมูลในเครื่อง

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

การใช้โอเปอเรเตอร์ catch ใน ViewModel มีดังนี้

class AuthorViewModel(
    authorsRepository: AuthorsRepository,
    ...
) : ViewModel() {
   private val authorId: String = ...

   // Observe author information
    private val authorStream: Flow<Author> =
        authorsRepository.getAuthorStream(
            id = authorId
        )
        .catch { emit(Author.empty()) }
}

แหล่งข้อมูลเครือข่าย

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

Exponential Backoff

ใน Exponential Backoff แอปจะพยายามอ่านจาก แหล่งข้อมูลเครือข่ายโดยเพิ่มช่วงเวลาจนกว่าจะสำเร็จ หรือ จากสภาวะอื่นๆ ที่บอกว่าโฆษณาควรหยุด

วันที่ การอ่านข้อมูลด้วย Exponential Backoff
รูปที่ 2: การอ่านข้อมูลด้วย Exponential Backoff

เกณฑ์ในการประเมินว่าควรปิดแอปต่อไปหรือไม่มีดังต่อไปนี้

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

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

วันที่ การอ่านข้อมูลด้วยการตรวจสอบเครือข่ายและคิว
รูปที่ 3: คิวการอ่านที่มีการตรวจสอบเครือข่าย

เขียน

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

interface UserDataRepository {
    /**
     * Updates the bookmarked status for a news resource
     */
    suspend fun updateNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean)
}

ในตัวอย่างข้างต้น API แบบอะซิงโครนัสที่เลือกคือ Coroutines เป็น เหนือการระงับ

เขียนกลยุทธ์

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

การเขียนแบบออนไลน์เท่านั้น

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

วันที่ เขียนแบบออนไลน์เท่านั้น
รูปที่ 4: การเขียนทางออนไลน์เท่านั้น

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

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

การเขียนที่อยู่ในคิว

เมื่อมีออบเจ็กต์ที่ต้องการเขียน ให้แทรกวัตถุลงในคิว ดำเนินการต่อ เพื่อระบายคิวด้วย Exponential Back off เมื่อแอปกลับมาออนไลน์ เปิด Android การระบายคิวออฟไลน์ถือเป็นงานถาวรที่มักได้รับมอบอำนาจให้แก่ WorkManager

วันที่ เขียนคิวที่มีการลองใหม่
รูปที่ 5: เขียนคิวที่มีการลองใหม่

วิธีนี้เป็นตัวเลือกที่ดีในกรณีต่อไปนี้

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

กรณีการใช้งานสำหรับแนวทางนี้รวมถึงเหตุการณ์การวิเคราะห์และการบันทึก

การเขียนแบบ Lazy Loading

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

วันที่ การเขียนแบบ Lazy Loading พร้อมการตรวจสอบเครือข่าย
ภาพที่ 6: การเขียนแบบ Lazy Loading

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

การซิงค์และการแก้ปัญหาความขัดแย้ง

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

  • การซิงค์แบบพุล
  • การซิงค์แบบพุช

การซิงค์แบบพุล

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

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

วันที่ การซิงค์ข้อมูลแบบพุล
รูปที่ 7: การซิงค์แบบพุล: อุปกรณ์ A เข้าถึงทรัพยากรสำหรับหน้าจอ A และ B เฉพาะในขณะที่อุปกรณ์ B เข้าถึงทรัพยากรสำหรับหน้าจอ B, C และ D เท่านั้น

พิจารณาแอปที่ใช้โทเค็นหน้าเว็บเพื่อดึงข้อมูลรายการแบบไม่สิ้นสุด รายการแบบเลื่อนได้สำหรับหน้าจอหนึ่งๆ การติดตั้งใช้งานอาจติดต่อไปแบบ Lazy Loading กับเครือข่ายให้คงข้อมูลไว้ในแหล่งข้อมูลในตัวเครื่อง จากนั้นอ่านจาก ของแหล่งข้อมูลในเครื่องเพื่อนำเสนอข้อมูลกลับไปยังผู้ใช้ ในกรณีที่ ไม่มีการเชื่อมต่อเครือข่าย ที่เก็บอาจขอข้อมูลจาก แหล่งข้อมูลเท่านั้น ซึ่งเป็นรูปแบบที่ Jetpack Paging Library ใช้ ด้วย RemoteMediator API

class FeedRepository(...) {

    fun feedPagingSource(): PagingSource<FeedItem> { ... }
}

class FeedViewModel(
    private val repository: FeedRepository
) : ViewModel() {
    private val pager = Pager(
        config = PagingConfig(
            pageSize = NETWORK_PAGE_SIZE,
            enablePlaceholders = false
        ),
        remoteMediator = FeedRemoteMediator(...),
        pagingSourceFactory = feedRepository::feedPagingSource
    )

    val feedPagingData = pager.flow
}

ข้อดีและข้อเสียของการซิงค์ข้อมูลแบบพุลสรุปได้ จากตารางด้านล่าง

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

การซิงค์แบบพุช

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

วันที่ การซิงค์แบบพุช
รูปที่ 8: การซิงค์แบบพุช: เครือข่ายจะแจ้งแอปเมื่อข้อมูลมีการเปลี่ยนแปลง และ แอปตอบสนองโดยการดึงข้อมูลที่เปลี่ยนแปลง

เมื่อได้รับการแจ้งเตือนที่ไม่มีอัปเดต แอปจะติดต่อกับเครือข่ายเพื่อ อัปเดตเฉพาะข้อมูลที่ทำเครื่องหมายว่าไม่มีอัปเดต งานนี้ได้รับมอบให้แก่ Repository ซึ่งเข้าถึงแหล่งข้อมูลของเครือข่ายและยังคงเก็บข้อมูลไว้ ดึงข้อมูลไปยังแหล่งข้อมูลในเครื่องแล้ว เนื่องจากที่เก็บเปิดเผยข้อมูลด้วย ประเภทที่สังเกตได้ ผู้อ่านจะได้รับแจ้งเมื่อมีการเปลี่ยนแปลง

class UserDataRepository(...) {

    suspend fun synchronize() {
        val userData = networkDataSource.fetchUserData()
        localDataSource.saveUserData(userData)
    }
}

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

ข้อดีและข้อเสียของการซิงค์ข้อมูลแบบพุชจะสรุปไว้ดังนี้ จากตารางด้านล่าง

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

การซิงค์แบบผสม

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

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

การแก้ไขข้อขัดแย้ง

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

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

ชนะการเขียนครั้งล่าสุด

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

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

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

WorkManager ในแอปที่ทำงานแบบออฟไลน์เป็นหลัก

กลยุทธ์ทั้งการอ่านและเขียนที่กล่าวถึงข้างต้นมี 2 กลยุทธ์ที่ใช้กันโดยทั่วไป ยูทิลิตี:

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

ทั้ง 2 กรณีเป็นตัวอย่างของงานถาวรที่ WorkManager ทำได้ยอดเยี่ยม ตัวอย่างเช่น ในแอปตัวอย่าง Now in Android WorkManager จะใช้เป็นทั้งคิวการอ่านและการตรวจสอบเครือข่ายเมื่อซิงค์ข้อมูล แหล่งข้อมูลในเครื่อง เมื่อเริ่มต้นทำงาน แอปจะดำเนินการต่อไปนี้

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

ตัวอย่างข้างต้นแสดงในแผนภาพด้านล่าง

วันที่ การซิงค์ข้อมูลในแอป Now in Android
ภาพที่ 10: การซิงค์ข้อมูลในแอป Now in Android

ลำดับของการซิงค์จะทำงานกับ WorkManager ตามด้วย การระบุให้เป็นงานที่ไม่ซ้ำใครด้วย KEEP ExistingWorkPolicy:

class SyncInitializer : Initializer<Sync> {
   override fun create(context: Context): Sync {
       WorkManager.getInstance(context).apply {
           // Queue sync on app startup and ensure only one
           // sync worker runs at any time
           enqueueUniqueWork(
               SyncWorkName,
               ExistingWorkPolicy.KEEP,
               SyncWorker.startUpSyncWork()
           )
       }
       return Sync
   }
}

โดยที่ SyncWorker.startupSyncWork() มีความหมายดังนี้


/**
 Create a WorkRequest to call the SyncWorker using a DelegatingWorker.
 This allows for dependency injection into the SyncWorker in a different
 module than the app module without having to create a custom WorkManager
 configuration.
*/
fun startUpSyncWork() = OneTimeWorkRequestBuilder<DelegatingWorker>()
    // Run sync as expedited work if the app is able to.
    // If not, it runs as regular work.
   .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
   .setConstraints(SyncConstraints)
    // Delegate to the SyncWorker.
   .setInputData(SyncWorker::class.delegatedData())
   .build()

val SyncConstraints
   get() = Constraints.Builder()
       .setRequiredNetworkType(NetworkType.CONNECTED)
       .build()

กล่าวอย่างเจาะจงคือ Constraints ที่ SyncConstraints กำหนดไว้กำหนดให้ NetworkType จะเป็น NetworkType.CONNECTED กล่าวคือ ระบบจะรอจนกว่า เครือข่ายที่พร้อมใช้งานก่อนที่จะเรียกใช้

เมื่อเครือข่ายพร้อมใช้งาน ผู้ปฏิบัติงานจะระบายคิวงานที่ไม่ซ้ำกัน ที่ระบุโดย SyncWorkName โดยมอบสิทธิ์ Repository ที่เหมาะสม อินสแตนซ์ ถ้าการซิงค์ล้มเหลว เมธอด doWork() จะแสดงผลพร้อม Result.retry() WorkManager จะลองซิงค์ข้อมูลใหม่โดยอัตโนมัติกับ Exponential Backoff มิเช่นนั้น ระบบจะแสดงผล Result.success() เสร็จสมบูรณ์ การซิงค์ข้อมูล

class SyncWorker(...) : CoroutineWorker(appContext, workerParams), Synchronizer {

    override suspend fun doWork(): Result = withContext(ioDispatcher) {
        // First sync the repositories in parallel
        val syncedSuccessfully = awaitAll(
            async { topicRepository.sync() },
            async { authorsRepository.sync() },
            async { newsRepository.sync() },
        ).all { it }

        if (syncedSuccessfully) Result.success()
        else Result.retry()
    }
}

ตัวอย่าง

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