Vài năm trước, Google đã công bố DataStore, một giải pháp thay thế cho SharedPreferences đã được thử nghiệm và thực sự.
Nếu bạn sử dụng hoặc đã sử dụng SharedPreferences trong ứng dụng của mình, bạn có thể đang nghĩ đến việc chuyển đổi. Nhưng cũng như mọi thứ khác, câu hỏi chính ở đây là:chi phí phát triển sẽ là bao nhiêu?
Có những lợi ích khi sử dụng DataStore, nhưng chỉ có DataStore Proto cho phép bạn lưu các đối tượng trong khi vẫn cung cấp sự an toàn về loại.
Nếu xem tài liệu về Proto DataStore, bạn sẽ thấy rằng nó hơi lỗi thời và thiếu một số bước quan trọng khi làm việc với nó. Vì vậy, đó là lý do tại sao trong bài viết này, chúng ta sẽ tìm hiểu cách tích hợp Proto DataStore vào ứng dụng của bạn và cho thấy rằng việc sử dụng nó không quá rắc rối.
DataStore là gì?
Jetpack DataStore có hai biến thể:
- Cửa hàng dữ liệu tùy chọn
- Kho dữ liệu nguyên mẫu
Chúng ta sẽ không thảo luận về vấn đề đầu tiên vì nó tương tự như SharedPreferences và thực tế là nó đã được đề cập rộng rãi. Vậy bây giờ hãy cùng tìm hiểu ý nghĩa của Proto trong Proto DataStore.
Proto là tên Google chọn để đại diện cho Protocol Buffers. Đây là cơ chế (của Google) giúp bạn tuần tự hóa dữ liệu có cấu trúc. Chúng không dành riêng cho ngôn ngữ mã hóa và nói chung, bạn xác định loại dữ liệu mà bạn muốn làm việc, sau đó mã sẽ được tạo để giúp bạn đọc và ghi dữ liệu của mình.
✋ Chúng tôi sẽ sử dụng phiên bản Proto 3 trong bài viết này.
Định nghĩa đó trông như thế nào?
message MyItem {
string itemName = 1;
int32 itemId = 2;
}
Đầu tiên, bạn xác định một đối tượng bằng từ khóa tin nhắn. Bên trong nó, bạn liệt kê các trường liên quan đến đối tượng đó. Các số ở cuối mỗi trường được sử dụng để xác định chính trường đó và không thể thay đổi sau khi được đặt và đối tượng đang được sử dụng .
Nhưng nếu chúng ta muốn có nhiều đối tượng trong tệp .proto thì sao? Giả sử các đối tượng có liên quan với nhau, bạn có thể thực hiện việc này đơn giản bằng cách thêm các đối tượng thư khác:
message MyItem {
string itemName = 1;
int32 itemId = 2;
}
message MyListOfItems {
repeated MyItem items = 1;
}
Lưu ý rằng ở trên chúng tôi đã thêm một đối tượng tin nhắn khác dựa trên đối tượng MyItem được xác định ở trên. Nếu bạn muốn xác định danh sách các đối tượng, bạn cần sử dụng lặp lại từ khóa.
Cách thiết lập kho dữ liệu nguyên mẫu
Để bắt đầu, bạn cần thêm các phần phụ thuộc sau vào build.gradle cấp ứng dụng của mình:
implementation "androidx.datastore:datastore-preferences:1.0.0"
implementation "com.google.protobuf:protobuf-javalite:3.18.0"
Sau đó, bạn sẽ cần tạo một thư mục proto bên trong dự án của mình. Thư mục này cần phải là anh chị em của thư mục Java trong cấu trúc dự án của bạn.
Bên trong thư mục proto, bạn sẽ tạo một tệp .proto. Tệp này chịu trách nhiệm tạo các loại dữ liệu bạn muốn lưu trữ trong Proto DataStore.
Trong thư mục proto, tạo một tệp có phần mở rộng .proto. Tệp .proto của chúng tôi sẽ chứa các đối tượng đại diện cho danh sách Todo (còn gì nữa không?). Vì vậy, chúng tôi sẽ gọi tệp của mình là todo.proto và nó sẽ trông như thế này:
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;
}
Lưu ý cách chúng tôi xác định hai đối tượng tin nhắn:
- TodoItem – xác định một mục việc cần làm
- TodoItems – xác định danh sách các đối tượng TodoItem
Tiếp theo, xây dựng dự án để tạo các lớp cho TodoItem và TodoItems.
Sau khi các đối tượng dữ liệu của chúng ta đã được xác định, chúng ta cần tạo một lớp để tuần tự hóa chúng. Lớp này sẽ cho DataStore biết cách đọc/ghi các đối tượng của chúng ta.
// 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)
}
Hãy cùng xem lại những gì chúng ta có trong lớp này:
- Khi khai báo lớp, chúng ta cần triển khai Serializer giao diện với đối tượng của chúng tôi là loại (T)
- Chúng tôi xác định giá trị mặc định cho bộ tuần tự hóa trong trường hợp tệp không được tạo
- Chúng tôi ghi đè các phương thức readFrom/writeTo và đảm bảo đối tượng của chúng tôi có kiểu dữ liệu ở đó
Chúng tôi có tệp .proto với các kiểu dữ liệu và bộ tuần tự hóa của chúng tôi, vì vậy bước tiếp theo là khởi tạo DataStore. Chúng tôi thực hiện việc này bằng cách sử dụng thuộc tính ủy quyền được tạo bởi dataStore, yêu cầu cung cấp tên tệp nơi dữ liệu của chúng tôi sẽ được lưu và lớp trình tuần tự hóa của chúng tôi (mà chúng tôi đã xác định ở 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 mã này cần nằm ở đầu lớp bạn chọn phía trên định nghĩa của chính lớp đó. Đó 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 đối tượng này trong phần còn lại của ứng dụng, chúng ta sẽ cần sử dụng một ngữ cảnh. Một ví dụ là sử dụng ngữ cảnh ứng dụng trong lớp viewmodel của bạn:
class MyViewModel(application: Application): AndroidViewModel(application) {
val todoDataStore = application.todoItemDataStore
//...
}
Cách sử dụng Kotlin Flow
Bây giờ chúng ta đã thiết lập mọi thứ chúng ta cần cho DataStore, chúng ta sẽ thảo luận về cách chúng ta thực sự sẽ tương tác với nó. Chúng tôi sẽ muốn đọc và ghi dữ liệu đến/từ nó. Tuy nhiên, cách chúng tôi có thể thực hiện khác với cách bạn có thể quen thuộc với SharedPreferences.
DataStore mà chúng tôi đã xác định ở trên có trường dữ liệu hiển thị Luồng cho các thuộc tính mà chúng tôi đã xác định trong DataStore.
🚰 Nếu bạn chưa quen với các quy trình thì đâ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
}
}
Đoạn mã trên cho thấy cách bạn có thể xác định Luồng thu thập dữ liệu từ Proto DataStore. Khối bắt đã được thêm vào trong trường hợp ngoại lệ xảy ra. Bạn có thể đặt logic này trong lớp mà bạn đã xác định DataStore của mình và sử dụng nó như vậy trong viewmodel của bạn:
val todoItemsFlow: LiveData<TodoItems> = todoItemsRepository.todoItemFlow.asLiveData()
Lưu ý cách chúng tôi chuyển đổi Luồng của mình thành LiveData. Chúng tôi làm điều này vì hai lý do:
- Các luồng có thể tiếp tục hoạt động bất kể hoạt động/phân đoạn sử dụng chúng
- LiveData là một cái gì đó quen thuộc với nhiều nhà phát triển và tôi muốn làm cho ví dụ này dễ tiếp cận nhất có thể
Để có thể thực hiện việc này, bạn cần thêm phần phụ thuộc sau vào tệp build.gradle của mình:
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.2"
Trong lớp hoạt động/phân đoạn của bạn, bạn có thể quan sát dữ liệu trực tiếp này như sau:
myViewModel.todoItemFlow.observe(LocalLifecycleOwner.current) { todoItems ->
// Logic to access data from DataStore
}
Tại sao và khi nào nên sử dụng DataStore
Sau mọi thứ chúng ta đã xem xét, đã đến lúc nói về con voi trong phòng. Bạn có nên tiếp tục sử dụng DataStore (Preferences hoặc Proto) trong dự án hiện tại hoặc dự án tiếp theo của mình không?
Theo tôi, câu trả lời nên là Có . Bên cạnh thực tế là Google đang rời bỏ SharedPreferences, DataStore còn mang lại nhiều lợi ích để giúp bạn tập trung vào ứng dụng của mình chứ không phải vào sự tồn tại lâu dài của dữ liệu.
Việc tương tác với DataStore từ luồng giao diện người dùng là an toàn (vì nó tự động chuyển công việc sang I/O) và buộc bạn phải sử dụng Flow (nếu bạn vẫn chưa sử dụng) và tận hưởng tất cả lợi ích bên trong. Ngoài ra còn có một tùy chọn để di chuyển dễ dàng từ SharedPreferences sang Tùy chọn DataStore.
Nếu bạn đang dự định sử dụng Room thay vì Proto DataStore, điều đó tùy thuộc vào trường hợp sử dụng của bạn. Nếu lượng dữ liệu bạn định lưu (hoặc duy trì) khá nhỏ và không yêu cầu cập nhật một phần thì Proto DataStore là lựa chọn phù hợp. Nếu bạn có tập dữ liệu lớn hơn hoặc tập dữ liệu có thể phức tạp thì bạn nên chọn sử dụng Room.
Nếu bạn muốn xem tất cả mã này trông như thế nào trong một ứng dụng, bạn có thể xem tại đây:
Nếu bạn muốn đọc các bài viết khác tôi đã viết, bạn có thể xem chúng tại đây:
Cảm ơn đã đọc!
Tài liệu tham khảo:
- Tài liệu về vùng đệm giao thức (giao thức 3)
- Làm việc với Proto DataStore Codelab
- Tài liệu về DataStore
Học cách viết mã miễn phí. Chương trình giảng dạy mã nguồn mở của freeCodeCamp đã giúp hơn 40.000 người có được việc làm với tư cách là nhà phát triển. Bắt đầu