สถาปัตยกรรมแอป: ชั้นข้อมูล - ที่เก็บข้อมูล - นักพัฒนาซอฟต์แวร์ Android

โปรเจ็กต์: /architecture/_project.yaml หนังสือ: /architecture/_book.yaml คำหลัก: datastore, architecture, api:JetpackDataStore คำอธิบาย: สำรวจคู่มือสถาปัตยกรรมแอปนี้ในไลบรารีเลเยอร์ข้อมูลเพื่อดูข้อมูลเกี่ยวกับ Preferences DataStore และ Proto DataStore, การตั้งค่า และอื่นๆ hide_page_heading: true

DataStore   ส่วนหนึ่งของ Android Jetpack

ลองใช้ Kotlin Multiplatform
Kotlin Multiplatform ช่วยให้แชร์เลเยอร์ข้อมูลกับแพลตฟอร์มอื่นๆ ได้ ดูวิธีตั้งค่าและทำงานกับ DataStore ใน KMP

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

หากใช้ SharedPreferences เพื่อจัดเก็บข้อมูล ให้พิจารณาย้ายข้อมูลไปยัง DataStore แทน

DataStore API

อินเทอร์เฟซ DataStore มี API ดังนี้

  1. โฟลว์ที่ใช้เพื่ออ่านข้อมูลจาก DataStore ได้

    val data: Flow<T>
    
  2. ฟังก์ชันสำหรับอัปเดตข้อมูลใน DataStore

    suspend updateData(transform: suspend (t) -> T)
    

การกำหนดค่า DataStore

หากต้องการจัดเก็บและเข้าถึงข้อมูลโดยใช้คีย์ ให้ใช้การติดตั้งใช้งาน Preferences DataStore ซึ่งไม่ต้องมีสคีมาที่กำหนดไว้ล่วงหน้า และไม่มี การรับประกันความปลอดภัยของประเภท มี API ที่คล้ายกับ SharedPreferences แต่ไม่มีข้อเสียที่เกี่ยวข้องกับค่ากำหนดที่แชร์

DataStore ช่วยให้คุณบันทึกคลาสที่กำหนดเองได้ โดยคุณต้องกำหนด สคีมาสำหรับข้อมูลและระบุ Serializer เพื่อแปลงข้อมูลเป็นรูปแบบที่คงอยู่ได้ คุณเลือกใช้ Protocol Buffers, JSON หรือกลยุทธ์การซีเรียลไลซ์อื่นๆ ได้

ตั้งค่า

หากต้องการใช้ Jetpack DataStore ในแอป ให้เพิ่มข้อมูลต่อไปนี้ลงในไฟล์ Gradle โดยขึ้นอยู่กับการติดตั้งใช้งานที่คุณต้องการใช้

Preferences DataStore

Add the following lines to the dependencies part of your gradle file:

Groovy

    dependencies {
        // Preferences DataStore (SharedPreferences like APIs)
        implementation "androidx.datastore:datastore-preferences:1.1.7"

        // Alternatively - without an Android dependency.
        implementation "androidx.datastore:datastore-preferences-core:1.1.7"
    }
    

Kotlin

    dependencies {
        // Preferences DataStore (SharedPreferences like APIs)
        implementation("androidx.datastore:datastore-preferences:1.1.7")

        // Alternatively - without an Android dependency.
        implementation("androidx.datastore:datastore-preferences-core:1.1.7")
    }
    

To add optional RxJava support, add the following dependencies:

Groovy

    dependencies {
        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-preferences-rxjava2:1.1.7"

        // optional - RxJava3 support
        implementation "androidx.datastore:datastore-preferences-rxjava3:1.1.7"
    }
    

Kotlin

    dependencies {
        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-preferences-rxjava2:1.1.7")

        // optional - RxJava3 support
        implementation("androidx.datastore:datastore-preferences-rxjava3:1.1.7")
    }
    

DataStore

Add the following lines to the dependencies part of your gradle file:

Groovy

    dependencies {
        // Typed DataStore for custom data objects (for example, using Proto or JSON).
        implementation "androidx.datastore:datastore:1.1.7"

        // Alternatively - without an Android dependency.
        implementation "androidx.datastore:datastore-core:1.1.7"
    }
    

Kotlin

    dependencies {
        // Typed DataStore for custom data objects (for example, using Proto or JSON).
        implementation("androidx.datastore:datastore:1.1.7")

        // Alternatively - without an Android dependency.
        implementation("androidx.datastore:datastore-core:1.1.7")
    }
    

Add the following optional dependencies for RxJava support:

Groovy

    dependencies {
        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-rxjava2:1.1.7"

        // optional - RxJava3 support
        implementation "androidx.datastore:datastore-rxjava3:1.1.7"
    }
    

Kotlin

    dependencies {
        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-rxjava2:1.1.7")

        // optional - RxJava3 support
        implementation("androidx.datastore:datastore-rxjava3:1.1.7")
    }
    

To serialize content, add dependencies for either Protocol Buffers or JSON serialization.

JSON serialization

To use JSON serialization, add the following to your Gradle file:

Groovy

    plugins {
        id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20"
    }

    dependencies {
        implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0"
    }
    

Kotlin

    plugins {
        id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20"
    }

    dependencies {
        implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
    }
    

Protobuf serialization

To use Protobuf serialization, add the following to your Gradle file:

Groovy

    plugins {
        id("com.google.protobuf") version "0.9.5"
    }
    dependencies {
        implementation "com.google.protobuf:protobuf-kotlin-lite:4.32.1"

    }

    protobuf {
        protoc {
            artifact = "com.google.protobuf:protoc:4.32.1"
        }
        generateProtoTasks {
            all().forEach { task ->
                task.builtins {
                    create("java") {
                        option("lite")
                    }
                    create("kotlin")
                }
            }
        }
    }
    

Kotlin

    plugins {
        id("com.google.protobuf") version "0.9.5"
    }
    dependencies {
        implementation("com.google.protobuf:protobuf-kotlin-lite:4.32.1")
    }

    protobuf {
        protoc {
            artifact = "com.google.protobuf:protoc:4.32.1"
        }
        generateProtoTasks {
            all().forEach { task ->
                task.builtins {
                    create("java") {
                        option("lite")
                    }
                    create("kotlin")
                }
            }
        }
    }
    

ใช้ DataStore อย่างถูกต้อง

โปรดคำนึงถึงกฎต่อไปนี้เสมอเพื่อให้ใช้ DataStore ได้อย่างถูกต้อง

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

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

  3. อย่าใช้ SingleProcessDataStore และ MultiProcessDataStore ร่วมกันสำหรับไฟล์เดียวกัน หากต้องการเข้าถึง DataStore จากกระบวนการมากกว่า 1 รายการ คุณต้องใช้ MultiProcessDataStore

คำจำกัดความของข้อมูล

Preferences DataStore

กำหนดคีย์ที่จะใช้เพื่อบันทึกข้อมูลลงในดิสก์

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")

JSON DataStore

สำหรับที่เก็บข้อมูล JSON ให้เพิ่มคำอธิบายประกอบ @Serialization ลงในข้อมูลที่ต้องการเก็บไว้

@Serializable
data class Settings(
    val exampleCounter: Int
)

กำหนดคลาสที่ใช้ Serializer<T> โดยที่ T คือประเภทของคลาสที่คุณเพิ่มคำอธิบายประกอบก่อนหน้านี้ ตรวจสอบว่าคุณได้รวมค่าเริ่มต้น สำหรับ Serializer ที่จะใช้ในกรณีที่ยังไม่มีการสร้างไฟล์

object SettingsSerializer : Serializer<Settings> {

    override val defaultValue: Settings = Settings(exampleCounter = 0)

    override suspend fun readFrom(input: InputStream): Settings =
        try {
            Json.decodeFromString<Settings>(
                input.readBytes().decodeToString()
            )
        } catch (serialization: SerializationException) {
            throw CorruptionException("Unable to read Settings", serialization)
        }

    override suspend fun writeTo(t: Settings, output: OutputStream) {
        output.write(
            Json.encodeToString(t)
                .encodeToByteArray()
        )
    }
}

Proto DataStore

การใช้งาน Proto DataStore จะใช้ DataStore และบัฟเฟอร์โปรโตคอลเพื่อ บันทึกออบเจ็กต์ที่มีการพิมพ์ลงในดิสก์

Proto DataStore ต้องมีสคีมาที่กำหนดไว้ล่วงหน้าในไฟล์ Proto ในไดเรกทอรี app/src/main/proto/ สคีมานี้กำหนดประเภทของออบเจ็กต์ ที่คุณคงไว้ใน Proto DataStore ดูข้อมูลเพิ่มเติมเกี่ยวกับการกำหนดสคีมาโปรโต ได้ที่คู่มือภาษา Protobuf

เพิ่มไฟล์ชื่อ settings.proto ในโฟลเดอร์ src/main/proto

syntax = "proto3";

option java_package = "com.example.datastore.snippets.proto";
option java_multiple_files = true;

message Settings {
  int32 example_counter = 1;
}

กำหนดคลาสที่ใช้ Serializer<T> โดยที่ T คือประเภทที่กำหนด ในไฟล์ Proto คลาส Serializer นี้จะกำหนดวิธีที่ DataStore อ่านและ เขียนประเภทข้อมูลของคุณ ตรวจสอบว่าคุณได้ระบุค่าเริ่มต้นสำหรับ Serializer ที่จะใช้ในกรณีที่ยังไม่มีการสร้างไฟล์

object SettingsSerializer : Serializer<Settings> {
    override val defaultValue: Settings = Settings.getDefaultInstance()

    override suspend fun readFrom(input: InputStream): Settings {
        try {
            return Settings.parseFrom(input)
        } catch (exception: InvalidProtocolBufferException) {
            throw CorruptionException("Cannot read proto.", exception)
        }
    }

    override suspend fun writeTo(t: Settings, output: OutputStream) {
        return t.writeTo(output)
    }
}

สร้าง DataStore

คุณต้องระบุชื่อไฟล์ที่ใช้เพื่อคงข้อมูลไว้

Preferences DataStore

การใช้งาน DataStore ของค่ากําหนดจะใช้คลาส DataStore และ Preferences เพื่อบันทึกคู่คีย์-ค่าลงในดิสก์ ใช้ตัวแทนพร็อพเพอร์ตี้ที่สร้างโดย preferencesDataStore เพื่อสร้างอินสแตนซ์ ของ DataStore<Preferences> เรียกใช้ 1 ครั้งที่ระดับบนสุดของไฟล์ Kotlin เข้าถึง DataStore ผ่านพร็อพเพอร์ตี้นี้ตลอดทั้งแอปพลิเคชันที่เหลือ ซึ่งจะช่วยให้คุณเก็บ DataStore ไว้เป็น Singleton ได้ง่ายขึ้น หรือใช้ RxPreferenceDataStoreBuilder หากคุณใช้ RxJava พารามิเตอร์ name ที่จำเป็นคือชื่อของ Preferences DataStore

// At the top level of your kotlin file:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

JSON DataStore

ใช้ผู้มอบสิทธิ์พร็อพเพอร์ตี้ที่สร้างโดย dataStore เพื่อสร้างอินสแตนซ์ของ DataStore<T> โดยที่ T คือคลาสข้อมูลที่แปลงเป็นอนุกรมได้ เรียกใช้ครั้งเดียว ที่ระดับบนสุดของไฟล์ Kotlin และเข้าถึงผ่านพร็อพเพอร์ตี้นี้ ตลอดทั้งแอป พารามิเตอร์ fileName จะบอก DataStore ว่าจะใช้ไฟล์ใดในการจัดเก็บข้อมูล และพารามิเตอร์ serializer จะบอก DataStore ว่าชื่อของคลาส Serializer ที่กำหนดไว้ในขั้นตอนที่ 1 คืออะไร

val Context.dataStore: DataStore<Settings> by dataStore(
    fileName = "settings.json",
    serializer = SettingsSerializer,
)

Proto DataStore

ใช้ตัวแทนพร็อพเพอร์ตี้ที่สร้างโดย dataStore เพื่อสร้างอินสแตนซ์ของ DataStore<T> โดยที่ T คือประเภทที่กำหนดไว้ในไฟล์ Proto เรียกใช้ ครั้งเดียวที่ระดับบนสุดของไฟล์ Kotlin และเข้าถึงผ่านตัวแทนพร็อพเพอร์ตี้นี้ ตลอดทั้งแอป พารามิเตอร์ fileName จะบอก DataStore ว่าจะใช้ไฟล์ใดในการจัดเก็บข้อมูล และพารามิเตอร์ serializer จะบอก DataStore ถึงชื่อของคลาส Serializer ที่กำหนดไว้ในขั้นตอนที่ 1

val Context.dataStore: DataStore<Settings> by dataStore(
    fileName = "settings.pb",
    serializer = SettingsSerializer,
)

อ่านจาก DataStore

คุณต้องระบุชื่อไฟล์ที่ใช้เพื่อคงข้อมูลไว้

Preferences DataStore

เนื่องจาก Preferences DataStore ไม่ได้ใช้สคีมาที่กำหนดไว้ล่วงหน้า คุณจึงต้องใช้ฟังก์ชันประเภทคีย์ที่เกี่ยวข้องเพื่อกำหนดคีย์สำหรับแต่ละค่าที่ต้องการจัดเก็บในอินสแตนซ์ DataStore<Preferences> เช่น หากต้องการกำหนดคีย์สำหรับค่า int ให้ใช้ intPreferencesKey() จากนั้นใช้พร็อพเพอร์ตี้ DataStore.data เพื่อแสดงค่าที่จัดเก็บที่เหมาะสมโดยใช้ Flow

fun counterFlow(): Flow<Int> = context.dataStore.data.map { preferences ->
    preferences[EXAMPLE_COUNTER] ?: 0
}

JSON DataStore

ใช้ DataStore.data เพื่อแสดง Flow ของพร็อพเพอร์ตี้ที่เหมาะสมจาก ออบเจ็กต์ที่จัดเก็บ

fun counterFlow(): Flow<Int> = context.dataStore.data.map { settings ->
    settings.exampleCounter
}

Proto DataStore

ใช้ DataStore.data เพื่อแสดง Flow ของพร็อพเพอร์ตี้ที่เหมาะสมจาก ออบเจ็กต์ที่จัดเก็บ

fun counterFlow(): Flow<Int> = context.dataStore.data.map { settings ->
    settings.exampleCounter
}

เขียนไปยัง DataStore

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

Preferences DataStore

suspend fun incrementCounter() {
    context.dataStore.updateData {
        it.toMutablePreferences().also { preferences ->
            preferences[EXAMPLE_COUNTER] = (preferences[EXAMPLE_COUNTER] ?: 0) + 1
        }
    }
}

JSON DataStore

suspend fun incrementCounter() {
    context.dataStore.updateData { settings ->
        settings.copy(exampleCounter = settings.exampleCounter + 1)
    }
}

Proto DataStore

suspend fun incrementCounter() {
    context.dataStore.updateData { settings ->
        settings.copy { exampleCounter = exampleCounter + 1 }
    }
}

ตัวอย่างการเขียน

คุณสามารถรวมฟังก์ชันเหล่านี้ไว้ในคลาสเดียวและใช้ในแอป Compose ได้

Preferences DataStore

ตอนนี้เราสามารถใส่ฟังก์ชันเหล่านี้ลงในคลาสที่ชื่อ PreferencesDataStore และ ใช้ในแอป Compose ได้แล้ว

val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val preferencesDataStore = remember(context) { PreferencesDataStore(context) }

// Display counter value.
val exampleCounter by preferencesDataStore.counterFlow()
    .collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
    text = "Counter $exampleCounter",
    fontSize = 25.sp
)

// Update the counter.
Button(
    onClick = {
        coroutineScope.launch { preferencesDataStore.incrementCounter() }
    }
) {
    Text("increment")
}

JSON DataStore

ตอนนี้เราสามารถใส่ฟังก์ชันเหล่านี้ลงในคลาสที่ชื่อ JSONDataStore และใช้ในแอป Compose ได้แล้ว

val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val jsonDataStore = remember(context) { JsonDataStore(context) }

// Display counter value.
val exampleCounter by jsonDataStore.counterFlow()
    .collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
    text = "Counter $exampleCounter",
    fontSize = 25.sp
)

// Update the counter.
Button(onClick = { coroutineScope.launch { jsonDataStore.incrementCounter() } }) {
    Text("increment")
}

Proto DataStore

ตอนนี้เราสามารถใส่ฟังก์ชันเหล่านี้ลงในคลาสที่ชื่อ ProtoDataStore และใช้ในแอป Compose ได้แล้ว

val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val protoDataStore = remember(context) { ProtoDataStore(context) }

// Display counter value.
val exampleCounter by protoDataStore.counterFlow()
    .collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
    text = "Counter $exampleCounter",
    fontSize = 25.sp
)

// Update the counter.
Button(onClick = { coroutineScope.launch { protoDataStore.incrementCounter() } }) {
    Text("increment")
}

ใช้ DataStore ในโค้ดแบบซิงโครนัส

ประโยชน์หลักอย่างหนึ่งของ DataStore คือ API แบบอะซิงโครนัส แต่การเปลี่ยนโค้ดโดยรอบให้เป็นแบบอะซิงโครนัสอาจทำไม่ได้เสมอไป กรณีนี้อาจเกิดขึ้นหากคุณทำงานกับโค้ดเบสที่มีอยู่ซึ่งใช้ I/O ของดิสก์แบบซิงโครนัส หรือหากคุณมี Dependency ที่ไม่ได้ให้ API แบบอะซิงโครนัส

โคโรทีน Kotlin มีตัวสร้างโคโรทีน runBlocking() เพื่อช่วย เชื่อมช่องว่างระหว่างโค้ดแบบซิงโครนัสและแบบอะซิงโครนัส คุณใช้ runBlocking()เพื่ออ่านข้อมูลจาก DataStore แบบซิงโครนัสได้ RxJava มีเมธอดที่บล็อกใน Flowable โค้ดต่อไปนี้จะบล็อกเธรดที่เรียกใช้ จนกว่า DataStore จะแสดงข้อมูล

Kotlin

val exampleData = runBlocking { context.dataStore.data.first() }

Java

Settings settings = dataStore.data().blockingFirst();

การดำเนินการ I/O แบบซิงโครนัสในเทรด UI อาจทำให้เกิด ANR หรือ UI ที่ไม่ตอบสนอง คุณสามารถลดปัญหาเหล่านี้ได้โดยการโหลดข้อมูลจาก DataStore ล่วงหน้าแบบไม่พร้อมกัน ดังนี้

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    lifecycleScope.launch {
        context.dataStore.data.first()
        // You should also handle IOExceptions here.
    }
}

Java

dataStore.data().first().subscribe();

ด้วยวิธีนี้ DataStore จะอ่านข้อมูลแบบไม่พร้อมกันและแคชข้อมูลไว้ในหน่วยความจำ การอ่านแบบซิงโครนัสในภายหลังโดยใช้ runBlocking() อาจเร็วกว่าหรืออาจหลีกเลี่ยงการดำเนินการ I/O ของดิสก์ ทั้งหมดได้หากการอ่านครั้งแรกเสร็จสมบูรณ์แล้ว

ใช้ DataStore ในโค้ดแบบหลายกระบวนการ

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

  • การอ่านจะแสดงเฉพาะข้อมูลที่บันทึกลงในดิสก์เท่านั้น
  • ความสอดคล้องแบบ Read-After-Write
  • ระบบจะจัดลำดับการเขียน
  • การอ่านจะไม่ถูกบล็อกโดยการเขียน

ลองพิจารณาแอปพลิเคชันตัวอย่างที่มีบริการและกิจกรรมซึ่งบริการ ทำงานในกระบวนการแยกต่างหากและอัปเดต DataStore เป็นระยะ

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

@Serializable
data class Time(
    val lastUpdateMillis: Long
)

ซีเรียลไลเซอร์จะบอก DataStore วิธีอ่านและเขียนประเภทข้อมูล ตรวจสอบว่าคุณได้ระบุมูลค่าเริ่มต้นสำหรับ Serializer ที่จะใช้ในกรณีที่ยังไม่มีการสร้างไฟล์ ตัวอย่างการใช้งานโดยใช้ kotlinx.serialization มีดังนี้

object TimeSerializer : Serializer<Time> {

    override val defaultValue: Time = Time(lastUpdateMillis = 0L)

    override suspend fun readFrom(input: InputStream): Time =
        try {
            Json.decodeFromString<Time>(
                input.readBytes().decodeToString()
            )
        } catch (serialization: SerializationException) {
            throw CorruptionException("Unable to read Time", serialization)
        }

    override suspend fun writeTo(t: Time, output: OutputStream) {
        output.write(
            Json.encodeToString(t)
                .encodeToByteArray()
        )
    }
}

หากต้องการใช้ DataStore ในกระบวนการต่างๆ คุณต้องสร้างออบเจ็กต์ DataStore โดยใช้ MultiProcessDataStoreFactory สำหรับทั้งโค้ดแอปและโค้ดบริการ

val dataStore = MultiProcessDataStoreFactory.create(
    serializer = TimeSerializer,
    produceFile = {
        File("${context.cacheDir.path}/time.pb")
    },
    corruptionHandler = null
)

เพิ่มโค้ดต่อไปนี้ใน AndroidManifiest.xml

<service
    android:name=".TimestampUpdateService"
    android:process=":my_process_id" />

บริการจะเรียกใช้ updateLastUpdateTime() เป็นระยะๆ ซึ่งจะเขียนไปยัง Datastore โดยใช้ updateData

suspend fun updateLastUpdateTime() {
    dataStore.updateData { time ->
        time.copy(lastUpdateMillis = System.currentTimeMillis())
    }
}

แอปจะอ่านค่าที่บริการเขียนโดยใช้โฟลว์ข้อมูลต่อไปนี้

fun timeFlow(): Flow<Long> = dataStore.data.map { time ->
    time.lastUpdateMillis
}

ตอนนี้เราสามารถรวมฟังก์ชันทั้งหมดนี้ไว้ในคลาสที่ชื่อ MultiProcessDataStore และใช้ในแอปได้แล้ว

รหัสบริการมีดังนี้

class TimestampUpdateService : Service() {
    val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
    val multiProcessDataStore by lazy { MultiProcessDataStore(applicationContext) }


    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        serviceScope.launch {
            while (true) {
                multiProcessDataStore.updateLastUpdateTime()
                delay(1000)
            }
        }
        return START_NOT_STICKY
    }

    override fun onDestroy() {
        super.onDestroy()
        serviceScope.cancel()
    }
}

และโค้ดของแอป

val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val multiProcessDataStore = remember(context) { MultiProcessDataStore(context) }

// Display time written by other process.
val lastUpdateTime by multiProcessDataStore.timeFlow()
    .collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
    text = "Last updated: $lastUpdateTime",
    fontSize = 25.sp
)

DisposableEffect(context) {
    val serviceIntent = Intent(context, TimestampUpdateService::class.java)
    context.startService(serviceIntent)
    onDispose {
        context.stopService(serviceIntent)
    }
}

คุณสามารถใช้การแทรกทรัพยากร Dependency ของ Hilt เพื่อให้อินสแตนซ์ DataStore ไม่ซ้ำกันต่อกระบวนการ

@Provides
@Singleton
fun provideDataStore(@ApplicationContext context: Context): DataStore<Settings> =
   MultiProcessDataStoreFactory.create(...)

จัดการไฟล์ที่เสียหาย

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

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

หากต้องการตั้งค่าแฮนเดิลนี้ ให้ระบุ corruptionHandler เมื่อสร้างอินสแตนซ์ DataStore ใน by dataStore() หรือในเมธอด DataStoreFactory ของ Factory

val dataStore: DataStore<Settings> = DataStoreFactory.create(
   serializer = SettingsSerializer(),
   produceFile = {
       File("${context.cacheDir.path}/myapp.preferences_pb")
   },
   corruptionHandler = ReplaceFileCorruptionHandler { Settings(lastUpdate = 0) }
)

แสดงความคิดเห็น

แชร์ความคิดเห็นและไอเดียกับเราผ่านแหล่งข้อมูลต่อไปนี้

เครื่องมือติดตามปัญหา:
รายงานปัญหาเพื่อให้เราแก้ไขข้อบกพร่องได้

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

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

ตัวอย่าง

บล็อก

Codelabs