Vài năm trước, Google công bố DataStore, thay thế cho SharedPreferences quen thuộc.
Nếu bạn dùng hoặc từng dùng SharedPreferences, có thể đang cân nhắc chuyển đổi. Nhưng như mọi thứ, câu hỏi chính là: chi phí phát triển sẽ ra sao?
Có lợi ích khi dùng DataStore, nhưng chỉ Proto DataStore cho phép lưu object đồng thời cung cấp type safety (an toàn kiểu).
Nếu xem tài liệu Proto DataStore, bạn sẽ thấy nó hơi cũ và thiếu vài bước quan trọng khi làm việc với nó. Vì vậy, bài này sẽ đi qua cách tích hợp Proto DataStore vào ứng dụng và chứng minh nó không phiền toái lắm.
DataStore là gì?
Jetpack DataStore có hai biến thể:
- Preferences DataStore
- Proto DataStore
Chúng ta không bàn về cái đầu, do tương đồng với SharedPreferences và đã được cover rộng rãi. Giờ hiểu Proto trong Proto DataStore nghĩa là gì.
Proto là tên Google chọn cho Protocol Buffers. Đây là cơ chế (của Google) giúp serialize dữ liệu có cấu trúc. Chúng không phụ thuộc ngôn ngữ lập trình và nói chung, bạn định nghĩa kiểu dữ liệu muốn làm việc rồi code được sinh ra giúp đọc/ghi dữ liệu.
✋ Chúng ta sẽ dùng Proto 3 trong bài này.
Định nghĩa đó trông như nào?
message MyItem { string itemName = 1; int32 itemId = 2; }
Trước tiên, định nghĩa object với từ khóa message. Bên trong, liệt kê field liên quan object đó. Số ở cuối mỗi field dùng để xác định field bản thân và không thể thay đổi sau khi set và object đang dùng.
Nhưng nếu muốn nhiều object trong file .proto? Giả sử object liên quan nhau, bạn làm đơn giản bằng thêm message object:
message MyItem { string itemName = 1; int32 itemId = 2; } message MyListOfItems { repeated MyItem items = 1; }
Chú ý ở trên chúng ta thêm message object khác dựa trên MyItem đã định nghĩa. Nếu muốn định nghĩa list object, cần dùng từ khóa repeated.
Cách setup Proto DataStore
Để bắt đầu, cần thêm dependencies sau vào build.gradle cấp ứng dụng:
implementation "androidx.datastore:datastore-preferences:1.0.0" implementation "com.google.protobuf:protobuf-javalite:3.18.0"
Sau đó, tạo thư mục proto trong dự án. Thư mục này cần là sibling của thư mục Java trong cấu trúc dự án.
Bên trong thư mục proto, tạo file .proto. File này chịu trách nhiệm sinh ra kiểu dữ liệu bạn muốn lưu trong Proto DataStore.
Bên trong thư mục proto, tạo file đuôi .proto. File .proto của chúng ta sẽ chứa object đại diện cho Todo list (còn gì khác?). Gọi file todo.proto và nó trông như sau:
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; }
Chú ý chúng ta định nghĩa hai message object:
- TodoItem – định nghĩa một todo item
- TodoItems – định nghĩa list các TodoItem object
Tiếp theo, build dự án để class được sinh cho TodoItem và TodoItems.
Sau khi object dữ liệu đã định nghĩa, cần tạo class serialize chúng. Class này nói cho DataStore cách đọc/ghi object.
// 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) }
Xem lại class này:
- Khai báo class, implement interface Serializer với object làm type (T)
- Định nghĩa giá trị mặc định cho serializer nếu file chưa tạo
- Override readFrom/writeTo và đảm bảo object làm kiểu dữ liệu ở đó
Chúng ta có file .proto với kiểu dữ liệu và serializer, bước tiếp theo khởi tạo DataStore. Làm bằng property delegate do dataStore tạo, yêu cầu filename lưu dữ liệu và class serializer (định nghĩa trên).
private const val DATA_STORE_FILE_NAME = "todo.pb" private val Context.todoItemDatastore: DataStore<TodoItems> by dataStore( fileName = DATA_STORE_FILE_NAME, serializer = TodoItemSerializer, )
Đoạn code này cần nằm ở đầu class bạn chọn, trên định nghĩa class. Đó là:
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 { }
Để truy cập object này trong ứng dụng, cần dùng context. Ví dụ dùng application context trong viewmodel:
class MyViewModel(application: Application): AndroidViewModel(application) { val todoDataStore = application.todoItemDataStore //... }
Cách dùng Kotlin Flow
Giờ đã setup xong mọi thứ cho DataStore, bàn cách thực sự tương tác với nó. Chúng ta muốn đọc/ghi dữ liệu từ/vào nó. Nhưng cách làm khác với SharedPreferences.
DataStore định nghĩa trên có trường data expose Flow cho thuộc tính định nghĩa trong DataStore.
🚰 Nếu chưa quen Flow, đây là nơi tốt để bắt đầu.
val todoItemFlow: Flow<TodoItems> = todoItemDataStore.data .catch { exception -> if (exception is IOException) { emit(TodoItems.getDefaultInstance()) } else { throw exception } }
Code trên chỉ cách định nghĩa Flow thu thập dữ liệu từ Proto DataStore. Thêm catch block phòng exception. Bạn đặt logic này ở class định nghĩa DataStore và dùng trong viewmodel như sau:
val todoItemsFlow: LiveData<TodoItems> = todoItemsRepository.todoItemFlow.asLiveData()
Chú ý chuyển Flow sang LiveData. Làm vậy vì hai lý do:
- Flow có thể active bất kể activity/fragment dùng nó
- LiveData quen thuộc với nhiều developer, tôi muốn ví dụ dễ tiếp cận nhất
Để làm được, cần thêm dependency vào build.gradle:
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.2"
Trong class activity/fragment, observe live data như sau:
myViewModel.todoItemFlow.observe(LocalLifecycleOwner.current) { todoItems -> // Logic truy cập dữ liệu từ DataStore }
Tại sao và khi nào dùng DataStore
Sau tất cả review, đến lúc nói về "voi trong phòng". Bạn có nên dùng DataStore (Preferences hay Proto) trong dự án hiện tại hoặc tiếp theo?
Theo tôi, câu trả lời Có. Bên cạnh việc Google đang bỏ SharedPreferences, DataStore mang lại nhiều lợi ích giúp bạn tập trung vào ứng dụng chứ không phải persistence dữ liệu.
An toàn tương tác DataStore từ UI thread (vì tự chuyển việc sang I/O), và nó ép bạn dùng Flow (nếu chưa) tận hưởng lợi ích bên trong. Cũng có tùy chọn migrate dễ dàng từ SharedPreferences sang Preferences DataStore.
Nếu cân nhắc Room thay vì Proto DataStore, phụ thuộc use case. Nếu lượng dữ liệu lưu (hoặc persist) nhỏ và không yêu cầu cập nhật một phần, Proto DataStore là lựa chọn. Nếu dữ liệu lớn hoặc phức tạp, nên chọn Room.
Nếu muốn xem code trông như nào trong app, xem tại đây:
Nếu muốn đọc bài khác tôi viết, xem tại đây:
Cảm ơn đã đọc!
Tham khảo:
- Protocol Buffers Documentation (proto 3)
- Working With Proto DataStore Codelab
- DataStore Documentation
Học lập trình miễn phí. Chương trình mở của freeCodeCamp đã giúp hơn 40.000 người có việc làm developer. Bắt đầu ngay