ไม่กี่ปีก่อน Google ประกาศ DataStore ซึ่งเป็นตัวแทนที่ของ SharedPreferences ที่ทดลองและพิสูจน์แล้ว
หากคุณใช้หรือเคยใช้ SharedPreferences ในแอปพลิเคชันของคุณ คุณอาจคิดว่าจะเปลี่ยนมาใช้ แต่เช่นเดียวกับทุกอย่าง คำถามหลักคือ: ค่าใช้จ่ายในการพัฒนาจะเป็นอย่างไร?
มีประโยชน์ในการใช้ DataStore แต่เพียง Proto DataStore เท่านั้นที่ให้คุณบันทึก objects พร้อมให้ type safety
หากคุณดูเอกสารสำหรับ Proto DataStore คุณจะพบว่ามันค่อนข้างเก่าและขาดขั้นตอนสำคัญบางอย่างเมื่อทำงานกับมัน ดังนั้น ในบทความนี้ เราจะผ่านวิธีการรวม Proto DataStore เข้าแอปพลิเคชันของคุณ และแสดงให้เห็นว่ามันไม่ได้ยุ่งยากอย่างที่คิด
DataStore คืออะไร?
Jetpack DataStore มี 2 รูปแบบ:
- Preferences DataStore
- Proto DataStore
เราจะไม่พูดถึงตัวแรก เนื่องจากความคล้ายคลึงกับ SharedPreferences และข้อเท็จจริงที่ว่ามันถูกครอบคลุมอย่างกว้างขวางแล้ว มาเข้าใจว่า Proto ใน Proto DataStore หมายถึงอะไร
Proto เป็นชื่อที่ Google เลือกแทน Protocol Buffers นี่คือกลไกของ Google ที่ช่วยให้คุณ serialize structured data พวกมันไม่เฉพาะเจาะจงภาษาการเขียนโปรแกรม และโดยทั่วไป คุณกำหนดประเภทข้อมูลที่คุณต้องการทำงาน และโค้ดจะถูกสร้างขึ้นช่วยให้คุณอ่านและเขียนข้อมูลของคุณ
✋ เราจะใช้ Proto 3 version ในบทความนี้
การนิยามนั้นดูอย่างไร?
message MyItem {
string itemName = 1;
int32 itemId = 2;
}
ก่อนอื่น คุณกำหนด object ด้วยคีย์เวิร์ด message ภายใน คุณระบุฟิลด์ที่เกี่ยวข้องกับ object นั้น ตัวเลขท้ายแต่ละฟิลด์ใช้ระบุฟิลด์เองและ ไม่สามารถเปลี่ยนแปลงได้หลังจากตั้งค่าและ object ถูกใช้งานแล้ว
แต่ถ้าเราอยากมีหลาย objects ใน .proto file? สมมติว่า objects เกี่ยวข้องกัน คุณทำได้ง่ายๆ โดยเพิ่ม message objects:
message MyItem {
string itemName = 1;
int32 itemId = 2;
}
message MyListOfItems {
repeated MyItem items = 1;
}
สังเกตด้านบน เราเพิ่ม message object อีกตัวที่ขึ้นอยู่กับ MyItem ที่กำหนดด้านบน หากคุณต้องการกำหนดรายการของ objects คุณต้องใช้คีย์เวิร์ด repeated
วิธีตั้งค่า Proto DataStore
เพื่อเริ่มต้น คุณจะต้องเพิ่ม dependencies ต่อไปนี้ใน build.gradle ระดับแอปพลิเคชัน:
implementation "androidx.datastore:datastore-preferences:1.0.0"
implementation "com.google.protobuf:protobuf-javalite:3.18.0"
จากนั้น คุณจะต้องสร้าง proto directory ภายในโปรเจกต์ของคุณ ไดเรกทอรีนี้ต้องเป็น sibling ของโฟลเดอร์ Java ในโครงสร้างโปรเจกต์ของคุณ
ภายใน proto directory คุณจะสร้างไฟล์ .proto ไฟล์นี้รับผิดชอบสร้างประเภทข้อมูลที่คุณต้องการเก็บใน Proto DataStore
ภายใน proto directory สร้างไฟล์ด้วยนามสกุล .proto ไฟล์ .proto ของเราจะเก็บ objects ที่แทน Todo list (อะไรอีก?). เราจะเรียกไฟล์ของเรา todo.proto และจะเป็นแบบนี้:
syntax = "proto3";
option java_package = "com.yourPackageName.todo";
option java_multiple_files = true;
message TodoItem {
string itemId = 1;
string itemDescription = 2;
}
message TodoItems {
repeated TodoItem items = 1;
}
สังเกตว่าเรากำหนด message objects 2 ตัว:
- TodoItem – กำหนด todo item
- TodoItems – กำหนดรายการของ TodoItem objects
ต่อไป build โปรเจกต์เพื่อให้ classes ถูกสร้างสำหรับ TodoItem และ TodoItems
หลังจาก data objects ถูกกำหนดแล้ว เราต้องสร้าง class เพื่อ serialize พวกมัน Class นี้จะบอก DataStore ว่าจะอ่าน/เขียน objects ของเราอย่างไร
// 1
object TodoItemSerializer: Serializer<TodoItems> {
// 2
override val defaultValue: TodoItems = TodoItems.getDefaultInstance()
// 3
override suspend fun readFrom(input: InputStream): TodoItems {
try {
return TodoItems.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto.", exception)
}
}
// 3
override suspend fun writeTo(
t: TodoItems,
output: OutputStream
) = t.writeTo(output)
}
ลองทบทวนสิ่งที่เรามีใน class นี้:
- เมื่อประกาศ class เราต้อง implement interface Serializer ด้วย object ของเราเป็น type (T)
- เรากำหนดค่าเริ่มต้นสำหรับ serializer หากไฟล์ยังไม่ถูกสร้าง
- เรา override เมธอด readFrom/writeTo และเราตรวจสอบให้แน่ใจว่ามี object ของเราเป็น data type ที่นั่น
เรามี .proto file พร้อม data types และ serializer แล้ว ขั้นตอนต่อไปคือ instantiate DataStore เราทำได้โดยใช้ property delegate ที่สร้างโดย dataStore ซึ่งต้องการ filename ที่ข้อมูลจะถูกเก็บ และ serializer class (ซึ่งเรากำหนดด้านบน)
private const val DATA_STORE_FILE_NAME = "todo.pb"
private val Context.todoItemDatastore: DataStore<TodoItems> by dataStore(
fileName = DATA_STORE_FILE_NAME,
serializer = TodoItemSerializer,
)
ชิ้นโค้ดนี้ต้องอยู่ที่ด้านบนของ class ที่คุณเลือก เหนือการกำหนด class เอง นั่นคือ:
private const val DATA_STORE_FILE_NAME = "todo.pb"
private val Context.todoItemDatastore: DataStore<TodoItems> by dataStore(
fileName = DATA_STORE_FILE_NAME,
serializer = TodoItemSerializer,
)
class YourClassName {
}
เพื่อเข้าถึง object นี้ในส่วนอื่นของแอปพลิเคชัน เราจะต้องใช้ context ตัวอย่างคือใช้ application context ใน viewmodel class:
class MyViewModel(application: Application): AndroidViewModel(application) {
val todoDataStore = application.todoItemDataStore
//...
}
วิธีใช้ Kotlin Flow
ตอนนี้เราได้ผ่านการตั้งค่าทุกอย่างที่เราต้องการสำหรับ DataStore แล้ว เราจะพูดถึงวิธีที่เราจะโต้ตอบกับมันจริงๆ เราจะอยากอ่านและเขียนข้อมูลไป/จากมัน แต่วิธีที่เราทำได้แตกต่างจากสิ่งที่คุณคุ้นเคยจาก SharedPreferences
DataStore ที่เรากำหนดด้านบนมีฟิลด์ data ที่ expose Flow สำหรับ properties ที่เรากำหนดใน DataStore
🚰 หากคุณไม่คุ้นเคยกับ flows นี่เป็นที่เริ่มต้นที่ดี
val todoItemFlow: Flow<TodoItems> = todoItemDataStore.data
.catch { exception ->
if (exception is IOException) {
emit(TodoItems.getDefaultInstance())
} else {
throw exception
}
}
โค้ดด้านบนแสดงวิธีกำหนด Flow ที่เก็บข้อมูลจาก Proto DataStore มีการเพิ่ม catch block กรณีเกิด exception คุณสามารถวางตรรกะนี้ใน class ที่คุณกำหนด DataStore และใช้แบบนี้ใน viewmodel:
val todoItemsFlow: LiveData<TodoItems> = todoItemsRepository.todoItemFlow.asLiveData()
สังเกตว่าเราแปลง Flow เป็น LiveData เราทำเช่นนี้ 2 เหตุผล:
- Flows สามารถคงสถานะ active ได้โดยไม่คำนึงถึง activity/fragment ที่ใช้มัน
- LiveData เป็นสิ่งที่คุ้นเคยสำหรับนักพัฒนาหลายคน และผมอยากทำให้ตัวอย่างนี้เข้าใจง่ายที่สุด
เพื่อให้ทำได้ คุณต้องเพิ่ม dependency ต่อไปนี้ใน build.gradle:
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.2"
ใน activity/fragment class คุณสามารถ observe live data นี้ได้แบบนี้:
myViewModel.todoItemFlow.observe(LocalLifecycleOwner.current) { todoItems ->
// Logic to access data from DataStore
}
ทำไมและเมื่อไหร่ควรใช้ DataStore
หลังจากทบทวนทุกอย่างแล้ว มาถึงประเด็นสำคัญ คุณควรไปข้างหน้าและใช้ DataStore (Preferences หรือ Proto) ในโปรเจกต์ที่มีอยู่หรือโปรเจกต์ถัดไปหรือไม่?
ในความคิดเห็นของผม คำตอบควรเป็น Yes นอกจากข้อเท็จจริงที่ Google กำลังขยับออกจาก SharedPreferences DataStore ยังเสนอประโยชน์มากมายที่ช่วยให้คุณโฟกัสที่แอปพลิเคชันของคุณ ไม่ใช่การ persistence ของข้อมูล
มันปลอดภาที่จะโต้ตอบกับ DataStore จาก UI thread (เพราะมันย้ายงานไป I/O อัตโนมัติ) และมันบังคับให้คุณใช้ Flow (หากคุณยังไม่ได้ใช้) และเพลิดเพลินกับประโยชน์ทั้งหมดภายใน มีตัวเลือกในการย้ายจาก SharedPreferences ไป Preferences DataStore ได้อย่างง่ายดาย
หากคุณกำลังคิดว่าจะใช้ Room แทน Proto DataStore นั่นขึ้นอยู่กับ use case ของคุณ หากปริมาณข้อมูลที่คุณจะเก็บ (หรือ persist) ค่อนข้างน้อย และไม่ต้องการ partial updating Proto DataStore คือทางที่จะไป หากคุณมีชุดข้อมูลใหญ่กว่านั้น หรือซับซ้อน คุณควรเลือกใช้ Room แทน
หากคุณอยากดูว่าโค้ดทั้งหมดนี้เป็นอย่างไรในแอปพลิเคชัน คุณสามารถดูได้ที่นี่:
หากคุณอยากอ่านบทความอื่นๆ ที่ผมเขียน สามารถดูได้ที่นี่:
ขอบคุณที่อ่าน!
References:
- Protocol Buffers Documentation (proto 3)
- Working With Proto DataStore Codelab
- DataStore Documentation
เรียนรู้การเขียนโค้ดฟรี หลักสูตรโอเพนซอร์สของ freeCodeCamp ช่วยให้ผู้คนมากกว่า 40,000 คนได้งานเป็นนักพัฒนา เริ่มต้นเลยวันนี้