DataStore เป็นคอมโพเนนต์จากไลบรารี Jetpack ที่ออกแบบมาเพื่อจัดเก็บข้อมูลปริมาณน้อยในแอปพลิเคชัน Android ต่างจาก SharedPreferences ตรงที่มันทำงานแบบอะซิงโครนัสและรับประกันความสอดคล้องของข้อมูลภายใต้การเข้าถึงพร้อมกัน ตามข้อมูลของ Google, 2024 DataStore ใช้ Kotlin Coroutines และ Flow ซึ่งทำให้ปลอดภัยสำหรับเธรดหลักและเหมาะสำหรับสถาปัตยกรรมแบบรีแอกทีฟ
ประเด็นสำคัญ
DataStore เป็นโซลูชันของ Google สำหรับการจัดเก็บข้อมูลในเครื่องบน Android ซึ่งเปิดตัวในปี 2020 เพื่อเป็นทางเลือกแทน SharedPreferences รองรับสองโหมด: Preferences DataStore (คู่คีย์-ค่าแบบง่าย) และ Proto DataStore (สคีมาแบบมีชนิดบนพื้นฐานของ Protocol Buffers)
ข้อได้เปรียบหลักคือการทำงานแบบอะซิงโครนัสอย่างสมบูรณ์: การอ่านทั้งหมดส่งคืน Flow จาก Kotlin Coroutines และการเขียนดำเนินการในบริบทของ coroutine ซึ่งช่วยขจัดการบล็อกเธรดหลักซึ่งเป็นปัญหาทั่วไปของ SharedPreferences เมื่อจัดการกับข้อมูลปริมาณมาก
DataStore รับประกันความเป็นอะตอมของการดำเนินการ: การเขียนพร้อมกันไม่ทำให้ข้อมูลสูญหายเนื่องจากโมเดลธุรกรรม หากคอมโพเนนต์สองตัวแก้ไขค่าเดียวกันพร้อมกัน DataStore จะจัดการข้อขัดแย้งอย่างถูกต้องผ่านกลไก compare-and-swap
ตามข้อมูลของ Google I/O 2023 DataStore ถูกใช้ใน 40% ของโปรเจกต์ Android ใหม่ และ Google แนะนำให้ย้ายจาก SharedPreferences ในแอปพลิเคชันทั้งหมดที่ต้องการความเสถียรของการจัดเก็บการตั้งค่า
หัวใจของ DataStore คือ SingleProcessDataStore — การทำงานภายในกระบวนการเดียว ใช้พื้นที่จัดเก็บแบบไฟล์พร้อมการล็อกระดับไฟล์: เมื่อเขียนข้อมูล ไฟล์จะถูกล็อกป้องกันการเสียหายภายใต้การเข้าถึงพร้อมกัน
DataStore จัดการข้อผิดพลาดการดีซีเรียลไลเซชันโดยอัตโนมัติ: หากไฟล์เสียหาย จะส่งคืนค่าเริ่มต้นและเขียนทับไฟล์ พฤติกรรมนี้สามารถกำหนดค่าได้ผ่าน corruptionHandler ซึ่งสามารถตั้งค่าได้เมื่อสร้าง DataStore
SharedPreferences มีปัญหาพื้นฐานสามประการ: การอ่านจากดิสก์แบบซิงโครนัสบนเธรดหลัก การขาดการรับประกันความเป็นอะตอมสำหรับการเขียนพร้อมกัน และไม่สามารถติดตามการเปลี่ยนแปลงแบบรีแอกทีฟ DataStore แก้ไขทั้งสาม: Flow สำหรับการสังเกต การล็อกไฟล์สำหรับความเป็นอะตอม และ API แบบอะซิงโครนัสสำหรับความปลอดภัยของเธรด
DataStore จัดเก็บข้อมูลในไฟล์บนพื้นที่จัดเก็บภายในของอุปกรณ์ Preferences DataStore ใช้รูปแบบไฟล์คล้ายกับ SharedPreferences แต่มีเมตาดาต้าเพิ่มเติมสำหรับตรวจสอบความสมบูรณ์ Proto DataStore ใช้รูปแบบไบนารี Protocol Buffers ซึ่งลดขนาดไฟล์และเพิ่มความเร็วในการซีเรียลไลเซชัน
เมื่ออ่านข้อมูล DataStore จะโหลดไฟล์ทั้งหมดลงในหน่วยความจำครั้งเดียว จากนั้นสมาชิกจะได้รับสถานะปัจจุบันผ่าน Flow การเปลี่ยนแปลงจะถูกส่งไปยังสมาชิกที่ใช้งานอยู่ทั้งหมดโดยอัตโนมัติ — ไม่ต้องลงทะเบียน listener ด้วยตนเองเหมือน SharedPreferences
Preferences DataStore ใช้กลไกการซีเรียลไลเซชันในตัวบนพื้นฐานของ Map แต่ละรายการคือคู่ของสตริงและชนิดพื้นฐาน (Int, Boolean, Float, Long, String, Set) ข้อมูลถูกเก็บในไฟล์ XML คล้าย SharedPreferences แต่เขียนแบบอะตอมผ่านการล็อกไฟล์
ตัวอย่างการสร้าง Preferences DataStore: ส่วนขยาย preferencesDataStore บน Context สร้างซิงเกิลตันด้วยชื่อไฟล์ เมื่อเรียกซ้ำ จะส่งคืนอินสแตนซ์เดียวกัน — ซึ่งช่วยลดการซ้ำซ้อนของไฟล์และความสับสนระหว่างอินสแตนซ์พื้นที่จัดเก็บต่างๆ
Proto DataStore ต้องการการกำหนดสคีมาข้อมูลผ่านไฟล์ .proto และคอมไพล์ด้วยปลั๊กอิน protobuf คลาส Java ที่สร้างขึ้นจะถูกใช้เป็นจุดเข้าเดียวสำหรับทุกฟิลด์ — ซึ่งช่วยขจัดการพิมพ์ผิดในคีย์ซึ่งพบได้บ่อยใน SharedPreferences
สคีมาของ Proto DataStore ถูกกำหนดครั้งเดียวและรองรับการเพิ่มฟิลด์ใหม่โดยไม่สูญเสียข้อมูลเก่า หากแอปเวอร์ชันใหม่เพิ่มฟิลด์ที่มีค่าเริ่มต้น ไฟล์เก่าจะถูกดีซีเรียลไลซ์อย่างถูกต้อง — ความเข้ากันได้ย้อนหลัง ถูกสร้างไว้ในโปรโตคอล
การเลือกระหว่าง Preferences DataStore และ Proto DataStore ขึ้นอยู่กับความซับซ้อนของข้อมูลและข้อกำหนดด้านชนิด ทั้งสองตัวเลือกเป็นแบบอะซิงโครนัสและธุรกรรม แต่แตกต่างกันในความปลอดภัยของชนิดและประสิทธิภาพการซีเรียลไลเซชัน
| คุณลักษณะ | Preferences DataStore | Proto DataStore |
|---|---|---|
| การกำหนดชนิด | อ่อน (คีย์-ค่า) | เข้มแข็ง (คลาสที่สร้างขึ้น) |
| การซีเรียลไลเซชัน | XML (ในตัว) | Protocol Buffers (protobuf) |
| ขนาดไฟล์ | ใหญ่ (XML ที่อ่านได้) | เล็ก (ไบนารี) |
| ความซับซ้อน | ต่ำ (ไม่ต้องใช้ .proto) | ปานกลาง (ต้องใช้ .proto) |
| การย้ายสคีมา | ไม่มีสคีมา | อัตโนมัติ (proto) |
| ความเข้ากันได้ | SharedPreferences (ผ่านการย้าย) | เฉพาะ Proto DataStore |
Preferences DataStore เหมาะสำหรับการตั้งค่าอย่างง่าย: แฟล็กคุณสมบัติ สตริงโทเคนการอนุญาต จำนวนครั้งที่เปิดแอป หากข้อมูลมีน้อย (มากถึง 10–15 คีย์) และไม่ต้องการสคีมาที่เข้มงวด Preferences DataStore ให้เกณฑ์การเข้าขั้นต่ำโดยไม่ต้องเชื่อมต่อปลั๊กอิน protobuf
Proto DataStore เหมาะสมเมื่อโครงสร้างข้อมูลซับซ้อนหรืออาจเปลี่ยนแปลงระหว่างเวอร์ชันของแอป ตัวอย่างเช่น การตั้งค่าโปรไฟล์ผู้ใช้หรือการกำหนดค่าทดสอบ A/B ที่มี 20+ ฟิลด์ Protobuf ให้การกำหนดชนิดที่เข้มแข็งและการย้ายข้อมูลอัตโนมัติ ช่วยขจัดข้อผิดพลาดรันไทม์เนื่องจากคีย์ไม่ตรงกัน
Google มีกลไกการย้ายข้อมูลในตัวผ่านคลาส SharedPreferencesMigration การย้ายจะดำเนินการครั้งเดียวเมื่อเปิดครั้งแรกหลังจากอัปเดตแอป: DataStore อ่านข้อมูลจาก SharedPreferences เขียนในรูปแบบของตัวเอง และทำเครื่องหมายว่าการย้ายเสร็จสมบูรณ์
การย้ายรองรับการแปลงแบบกำหนดเอง: หากคีย์ใน SharedPreferences ไม่ตรงกับคีย์ DataStore ที่ต้องการ คุณสามารถระบุฟังก์ชันการแปลงผ่าน SharedPreferencesMigration ซึ่งช่วยให้เปลี่ยนชื่อคีย์และเปลี่ยนชนิดข้อมูลระหว่างการย้ายได้
ขั้นแรก เพิ่ม DataStore ใน build.gradle และสร้างอินสแตนซ์ DataStore พร้อมการย้าย: SharedPreferencesMigration รับชื่อไฟล์ SharedPreferences และชุดของคีย์ที่จะย้าย ขั้นที่สอง ลบโค้ดทั้งหมดที่ทำงานผ่าน SharedPreferences และแทนที่ด้วยการเรียก DataStore ขั้นที่สาม ทดสอบการย้าย: เมื่อเปิดครั้งแรก ข้อมูลควรปรากฏใน DataStore และไฟล์ SharedPreferences เก่าควรจะไม่ถูกใช้อีกต่อไป
val Context.dataStore by preferencesDataStore(
name = "settings",
produceMigrations = { context ->
listOf(
SharedPreferencesMigration(context, "old_prefs")
)
}
)
DataStore ผสานรวมเข้ากับโปรเจกต์ที่มีอยู่ได้อย่างง่ายดาย ด้านล่างเป็นตัวอย่างเชิงปฏิบัติสำหรับ Preferences DataStore และ Proto DataStore — ทั้งสองสาธิตการอ่าน การเขียน และการสังเกตการเปลี่ยนแปลงแบบรีแอกทีฟ
ในตัวอย่างนี้ Preferences DataStore จัดเก็บสามการตั้งค่า: ธีมมืด ชื่อผู้ใช้ และจำนวนครั้งที่เปิด การอ่านทำผ่านส่วนขยาย .data ซึ่งส่งคืน Flow การเขียนทำผ่านฟังก์ชันระงับ .edit ซึ่งรับประกันความเป็นอะตอมของการเปลี่ยนแปลง
val Context.settingsDataStore by preferencesDataStore(name = "settings")
val isDarkMode: Flow<Boolean> = settingsDataStore.data
.map { preferences ->
preferences[booleanPreferencesKey("dark_mode")] ?: false
}
suspend fun toggleDarkMode() {
settingsDataStore.edit { prefs ->
val current = prefs[booleanPreferencesKey("dark_mode")] ?: false
prefs[booleanPreferencesKey("dark_mode")] = !current
}
}
Proto DataStore ต้องการการกำหนดไฟล์ .proto หลังจากคอมไพล์ จะสร้างคลาส UserSettings ที่ใช้สำหรับอ่านและเขียน การย้ายเวอร์ชันของสคีมาอธิบายไว้ในไฟล์ .proto เดียวกันและถูกนำไปใช้โดยอัตโนมัติ
// user_preferences.proto
syntax = "proto3";
message UserPreferences {
string display_name = 1;
int32 notification_count = 2;
bool notifications_enabled = 3;
}
// การอ่านจาก DataStore
val userPreferencesFlow: Flow<UserPreferences> =
protoDataStore.data
// การเขียนค่าใหม่
suspend fun updateDisplayName(name: String) {
protoDataStore.updateData { prefs ->
prefs.toBuilder()
.setDisplayName(name)
.build()
}
}
DataStore ผสานรวมกับสถาปัตยกรรม MVVM ผ่าน ViewModel Flow จาก DataStore ถูกรวบรวมผ่าน .stateIn และใช้ใน UI ทุกครั้งที่ข้อมูลเปลี่ยนแปลง UI จะอัปเดตโดยอัตโนมัติ — ไม่ต้องอัปเดตด้วยตนเองหรือ LiveData
class SettingsViewModel(
private val dataStore: DataStore<Preferences>
) : ViewModel() {
val uiState: StateFlow<SettingsUiState> =
dataStore.data
.map { prefs ->
SettingsUiState(
isDarkMode = prefs[booleanPreferencesKey("dark_mode")] ?: false,
counter = prefs[intPreferencesKey("launch_count")] ?: 0
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = SettingsUiState()
)
}
คำถามที่พบบ่อย
DataStore ทำงานแบบอะซิงโครนัส (ไม่บล็อกเธรด UI) รองรับการเข้าถึงพร้อมกันผ่านธุรกรรม และอนุญาตให้สมัครรับการเปลี่ยนแปลงแบบรีแอกทีฟผ่าน Flow SharedPreferences เป็น API แบบซิงโครนัสที่มีความเสี่ยง ANR เมื่อมีข้อมูลปริมาณมากและไม่มีการสนับสนุนรีแอกทีฟในตัว
DataStore เขียนด้วย Kotlin และต้องใช้ Kotlin Coroutines การใช้จาก Java เป็นไปได้แต่ไม่สะดวก: คุณจะต้องสร้าง wrapper ด้วย CompletableFuture หรือจัดการ coroutine ด้วยตนเอง สำหรับโปรเจกต์ Java Google แนะนำให้ใช้ SharedPreferences ต่อไปหรือเพิ่ม Kotlin ในโมดูล
DataStore โหลดไฟล์ทั้งหมดในหน่วยความจำเมื่ออ่าน ดังนั้นจึงไม่เหมาะสำหรับจัดเก็บรายการหรือวัตถุขนาดใหญ่ สำหรับสถานการณ์ดังกล่าว ให้ใช้ Room หรือ SQLite DataStore ถูกปรับให้เหมาะสมสำหรับการตั้งค่าและข้อมูลที่มีโครงสร้างขนาดเล็ก — สูงสุดหลายร้อยกิโลไบต์
เมื่อสร้าง DataStore คุณสามารถส่ง corruptionHandler — ฟังก์ชันที่ถูกเรียกเมื่อไฟล์เสียหาย โดยค่าเริ่มต้น DataStore จะโยน CorruptionException ใน corruptionHandler คุณสามารถส่งคืนข้อมูลว่าง จากนั้น DataStore จะเขียนทับไฟล์ด้วยสถานะที่ถูกต้อง
ใช่ Proto DataStore ต้องการการกำหนดสคีมาในไฟล์ .proto และเชื่อมต่อปลั๊กอิน protobuf-gradle-plugin หากโปรเจกต์เล็กและข้อมูลเรียบง่าย การใช้ Preferences DataStore จะง่ายกว่า — ไม่ต้องกำหนดค่าการ build เพิ่มเติม
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม