DataStore là một thành phần từ thư viện Jetpack được thiết kế để lưu trữ một lượng nhỏ dữ liệu trong các ứng dụng Android. Không giống như SharedPreferences, nó hoạt động không đồng bộ và đảm bảo tính nhất quán của dữ liệu khi truy cập đồng thời. Theo Google, 2024, DataStore sử dụng Kotlin Coroutines và Flow, giúp nó an toàn cho luồng chính và phù hợp với kiến trúc phản ứng.
Các Điểm Chính
DataStore là giải pháp của Google để lưu trữ dữ liệu cục bộ trong Android, được giới thiệu vào năm 2020 như một giải pháp thay thế cho SharedPreferences. Nó hỗ trợ hai chế độ: Preferences DataStore (cặp khóa-giá trị đơn giản) và Proto DataStore (lược đồ có kiểu dựa trên Protocol Buffers).
Ưu điểm chính là không đồng bộ hoàn toàn: tất cả các thao tác đọc trả về Flow từ Kotlin Coroutines và viết được thực hiện trong ngữ cảnh coroutine. Điều này loại bỏ việc chặn luồng chính, vốn là vấn đề điển hình của SharedPreferences khi xử lý khối lượng lớn dữ liệu.
DataStore đảm bảo tính nguyên tử của các thao tác: viết đồng thời không gây mất dữ liệu nhờ mô hình giao dịch của nó. Nếu hai thành phần sửa đổi cùng một giá trị đồng thời, DataStore xử lý xung đột một cách chính xác thông qua cơ chế compare-and-swap.
Theo Google I/O 2023, DataStore được sử dụng trong 40% các dự án Android mới và Google khuyến nghị di chuyển từ SharedPreferences trong tất cả các ứng dụng yêu cầu độ ổn định của lưu trữ cài đặt.
Cốt lõi của DataStore là SingleProcessDataStore — một triển khai hoạt động trong một tiến trình duy nhất. Nó sử dụng bộ lưu trữ dựa trên tệp với khóa ở cấp tệp: khi ghi dữ liệu, tệp bị khóa, ngăn ngừa hỏng hóc khi truy cập đồng thời.
DataStore tự động xử lý lỗi giải tuần tự hóa: nếu tệp bị hỏng, nó trả về giá trị mặc định và ghi đè lên tệp. Hành vi này có thể cấu hình thông qua corruptionHandler, có thể được đặt khi tạo DataStore.
SharedPreferences gặp ba vấn đề cơ bản: đọc đồng bộ từ đĩa trên luồng chính, thiếu đảm bảo nguyên tử cho việc ghi đồng thời và không có khả năng theo dõi thay đổi một cách phản ứng. DataStore giải quyết cả ba: Flow để quan sát, khóa tệp cho tính nguyên tử và API không đồng bộ cho an toàn luồng.
DataStore lưu trữ dữ liệu trong các tệp trên bộ nhớ trong của thiết bị. Preferences DataStore sử dụng định dạng tệp tương tự SharedPreferences nhưng có thêm siêu dữ liệu để kiểm tra tính toàn vẹn. Proto DataStore sử dụng định dạng nhị phân Protocol Buffers, giúp giảm kích thước tệp và tăng tốc độ tuần tự hóa.
Khi đọc dữ liệu, DataStore tải toàn bộ tệp vào bộ nhớ một lần, sau đó người đăng ký nhận trạng thái hiện tại qua Flow. Các thay đổi được truyền tới tất cả người đăng ký đang hoạt động một cách tự động — không cần đăng ký trình lắng nghe thủ công như SharedPreferences.
Preferences DataStore sử dụng cơ chế tuần tự hóa tích hợp dựa trên Map. Mỗi mục nhập là một cặp chuỗi và kiểu nguyên thủy (Int, Boolean, Float, Long, String, Set). Dữ liệu được lưu trữ trong tệp XML, tương tự SharedPreferences, nhưng với ghi nguyên tử thông qua khóa tệp.
Ví dụ tạo Preferences DataStore: extension preferencesDataStore trên Context tạo một singleton với tên tệp. Khi gọi nhiều lần, cùng một phiên bản được trả về — điều này loại bỏ trùng lặp tệp và nhầm lẫn giữa các phiên bản lưu trữ khác nhau.
Proto DataStore yêu cầu định nghĩa lược đồ dữ liệu thông qua tệp .proto và biên dịch với plugin protobuf. Lớp Java được tạo ra được sử dụng làm điểm vào duy nhất cho tất cả các trường — điều này loại bỏ lỗi gõ sai khóa, thường gặp với SharedPreferences.
Lược đồ Proto DataStore được định nghĩa một lần và hỗ trợ thêm trường mới mà không mất dữ liệu cũ. Nếu phiên bản mới của ứng dụng thêm một trường có giá trị mặc định, tệp cũ sẽ được giải tuần tự hóa chính xác — tương thích ngược được tích hợp trong giao thức.
Việc lựa chọn giữa Preferences DataStore và Proto DataStore phụ thuộc vào độ phức tạp của dữ liệu và yêu cầu về kiểu. Cả hai tùy chọn đều không đồng bộ và giao dịch, nhưng khác nhau về an toàn kiểu và hiệu suất tuần tự hóa.
| Đặc điểm | Preferences DataStore | Proto DataStore |
|---|---|---|
| Kiểu | Yếu (khóa-giá trị) | Mạnh (lớp được tạo) |
| Tuần tự hóa | XML (tích hợp) | Protocol Buffers (protobuf) |
| Kích thước tệp | Lớn (XML có thể đọc) | Nhỏ (nhị phân) |
| Độ phức tạp | Thấp (không cần .proto) | Trung bình (cần .proto) |
| Di chuyển lược đồ | Không có lược đồ | Tự động (proto) |
| Tương thích | SharedPreferences (qua di chuyển) | Chỉ Proto DataStore |
Preferences DataStore phù hợp cho các cài đặt đơn giản: cờ tính năng, chuỗi mã thông báo ủy quyền, số lần khởi chạy ứng dụng. Nếu dữ liệu nhỏ (tới 10–15 khóa) và không yêu cầu lược đồ nghiêm ngặt, Preferences DataStore cung cấp ngưỡng vào tối thiểu mà không cần kết nối plugin protobuf.
Proto DataStore phù hợp khi cấu trúc dữ liệu phức tạp hoặc có thể thay đổi giữa các phiên bản ứng dụng. Ví dụ: cài đặt hồ sơ người dùng hoặc cấu hình thử nghiệm A/B với 20+ trường. Protobuf cung cấp kiểu mạnh và di chuyển tự động, loại bỏ lỗi thời gian chạy do không khớp khóa.
Google cung cấp cơ chế di chuyển tích hợp thông qua lớp SharedPreferencesMigration. Việc di chuyển được thực hiện một lần khi khởi chạy đầu tiên sau khi cập nhật ứng dụng: DataStore đọc dữ liệu từ SharedPreferences, ghi vào định dạng riêng và đánh dấu di chuyển hoàn tất.
Di chuyển hỗ trợ chuyển đổi tùy chỉnh: nếu khóa trong SharedPreferences không khớp với khóa DataStore mong muốn, bạn có thể chỉ định hàm chuyển đổi thông qua SharedPreferencesMigration. Điều này cho phép đổi tên khóa và thay đổi kiểu dữ liệu trong quá trình di chuyển.
Đầu tiên, thêm DataStore vào build.gradle và tạo phiên bản DataStore với di chuyển: SharedPreferencesMigration chấp nhận tên tệp SharedPreferences và tập hợp các khóa cần chuyển. Thứ hai, xóa tất cả mã hoạt động qua SharedPreferences và thay thế bằng các lệnh gọi DataStore. Thứ ba, kiểm tra di chuyển: khi khởi chạy đầu tiên, dữ liệu sẽ xuất hiện trong DataStore và tệp SharedPreferences cũ sẽ không còn được sử dụng.
val Context.dataStore by preferencesDataStore(
name = "settings",
produceMigrations = { context ->
listOf(
SharedPreferencesMigration(context, "old_prefs")
)
}
)
DataStore tích hợp dễ dàng vào dự án hiện có. Dưới đây là các ví dụ thực tế cho Preferences DataStore và Proto DataStore — cả hai đều minh họa việc đọc, ghi và quan sát phản ứng dữ liệu.
Trong ví dụ này, Preferences DataStore lưu trữ ba cài đặt: chủ đề tối, tên người dùng và số lần khởi chạy. Đọc được thực hiện qua extension .data, trả về Flow. Ghi được thực hiện qua hàm tạm dừng .edit, đảm bảo tính nguyên tử của các thay đổi.
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 yêu cầu định nghĩa tệp .proto. Sau khi biên dịch, một lớp UserSettings được tạo ra để đọc và ghi. Di chuyển phiên bản lược đồ được mô tả trong cùng tệp .proto và được áp dụng tự động.
// user_preferences.proto
syntax = "proto3";
message UserPreferences {
string display_name = 1;
int32 notification_count = 2;
bool notifications_enabled = 3;
}
// Đọc từ DataStore
val userPreferencesFlow: Flow<UserPreferences> =
protoDataStore.data
// Ghi giá trị mới
suspend fun updateDisplayName(name: String) {
protoDataStore.updateData { prefs ->
prefs.toBuilder()
.setDisplayName(name)
.build()
}
}
DataStore tích hợp với kiến trúc MVVM thông qua ViewModel. Flow từ DataStore được thu thập qua .stateIn và sử dụng trong UI. Mỗi khi dữ liệu thay đổi, UI tự động cập nhật — không cần cập nhật thủ công hay 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()
)
}
Các Câu Hỏi Thường Gặp
DataStore hoạt động không đồng bộ (không chặn luồng UI), hỗ trợ truy cập đồng thời qua giao dịch và cho phép đăng ký phản ứng các thay đổi qua Flow. SharedPreferences là API đồng bộ có nguy cơ ANR với khối lượng dữ liệu lớn và không có hỗ trợ phản ứng tích hợp.
DataStore được viết bằng Kotlin và yêu cầu Kotlin Coroutines. Sử dụng từ Java có thể nhưng bất tiện: bạn sẽ phải tạo wrapper với CompletableFuture hoặc quản lý coroutine thủ công. Đối với các dự án Java, Google khuyến nghị giữ lại SharedPreferences hoặc thêm Kotlin vào mô-đun.
DataStore tải toàn bộ tệp vào bộ nhớ khi đọc, vì vậy nó không phù hợp để lưu trữ danh sách hoặc đối tượng lớn. Đối với các tình huống như vậy, hãy sử dụng Room hoặc SQLite. DataStore được tối ưu hóa cho cài đặt và dữ liệu có cấu trúc nhỏ — lên tới hàng trăm kilobyte.
Khi tạo DataStore, bạn có thể truyền corruptionHandler — một hàm được gọi khi tệp bị hỏng. Theo mặc định, DataStore ném ngoại lệ CorruptionException. Trong corruptionHandler, bạn có thể trả về dữ liệu trống, sau đó DataStore sẽ ghi đè tệp với trạng thái chính xác.
Có, Proto DataStore yêu cầu định nghĩa lược đồ trong tệp .proto và kết nối protobuf-gradle-plugin. Nếu dự án nhỏ và dữ liệu đơn giản, sẽ dễ dàng hơn khi sử dụng Preferences DataStore — nó không yêu cầu cấu hình xây dựng bổ sung.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm