DataStore   Thuộc Android Jetpack.

Jetpack DataStore là một giải pháp lưu trữ dữ liệu cho phép bạn lưu trữ các cặp khoá-giá trị hoặc đối tượng đã nhập có vùng đệm giao thức. DataStore sử dụng coroutine Kotlin và Luồng (Flow) để lưu trữ dữ liệu một cách không đồng bộ, nhất quán và có thể chia sẻ.

Nếu bạn hiện đang sử dụng SharedPreferences để lưu trữ dữ liệu, hãy cân nhắc chuyển sang DataStore.

DataStore Preference và DataStore Proto

Có hai phương thức triển khai DataStore: DataStore Preference và DataStore Proto.

  • Preference DataStore lưu trữ và truy cập vào dữ liệu bằng các khoá. Phương thức triển khai này không yêu cầu có giản đồ được xác định trước và không đảm bảo an toàn về kiểu.
  • Proto DataStore lưu trữ dữ liệu theo một kiểu dữ liệu tuỳ chỉnh. Phương thức triển khai này yêu cầu bạn xác định một giản đồ bằng cách sử dụng vùng đệm giao thức, nhưng có đảm bảo an toàn về kiểu.

Sử dụng DataStore đúng cách

Để sử dụng DataStore đúng cách, hãy luôn ghi nhớ các quy tắc sau:

  1. Tuyệt đối không tạo nhiều thực thể của DataStore cho một tệp nhất định trong cùng một quy trình. Làm như vậy có thể phá vỡ tất cả chức năng của DataStore. Nếu có nhiều DataStore đang hoạt động cho một tệp nhất định trong cùng một quy trình, thì DataStore sẽ gửi IllegalStateException khi đọc hoặc cập nhật dữ liệu.

  2. Không được thay đổi loại dữ liệu chung của DataStore. Thay đổi kiểu dữ liệu dùng trong DataStore sẽ vô hiệu hoá mọi đảm bảo mà DataStore cung cấp và có thể tạo ra các lỗi nghiêm trọng và khó phát hiện. Bạn nên sử dụng vùng đệm giao thức – một cơ chế đảm bảo tính bất biến, có một API đơn giản và quá trình chuyển đổi tuần tự hiệu quả.

  3. Tuyệt đối không sử dụng kết hợp SingleProcessDataStoreMultiProcessDataStore cho cùng một tệp. Nếu bạn có ý định truy cập vào DataStore từ nhiều quy trình, hãy luôn sử dụng MultiProcessDataStore.

Thiết lập

Để sử dụng Jetpack DataStore trong ứng dụng, hãy thêm đoạn mã sau vào tệp Gradle tuỳ thuộc vào phương thức triển khai mà bạn muốn dùng:

Preferences DataStore

Groovy

    // Preferences DataStore (SharedPreferences like APIs)
    dependencies {
        implementation "androidx.datastore:datastore-preferences:1.0.0"

        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-preferences-rxjava2:1.0.0"

        // optional - RxJava3 support
        implementation "androidx.datastore:datastore-preferences-rxjava3:1.0.0"
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation "androidx.datastore:datastore-preferences-core:1.0.0"
    }
    

Kotlin

    // Preferences DataStore (SharedPreferences like APIs)
    dependencies {
        implementation("androidx.datastore:datastore-preferences:1.0.0")

        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-preferences-rxjava2:1.0.0")

        // optional - RxJava3 support
        implementation("androidx.datastore:datastore-preferences-rxjava3:1.0.0")
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation("androidx.datastore:datastore-preferences-core:1.0.0")
    }
    

Proto DataStore

Groovy

    // Typed DataStore (Typed API surface, such as Proto)
    dependencies {
        implementation "androidx.datastore:datastore:1.0.0"

        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-rxjava2:1.0.0"

        // optional - RxJava3 support
        implementation "androidx.datastore:datastore-rxjava3:1.0.0"
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation "androidx.datastore:datastore-core:1.0.0"
    }
    

Kotlin

    // Typed DataStore (Typed API surface, such as Proto)
    dependencies {
        implementation("androidx.datastore:datastore:1.0.0")

        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-rxjava2:1.0.0")

        // optional - RxJava3 support
        implementation("androidx.datastore:datastore-rxjava3:1.0.0")
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation("androidx.datastore:datastore-core:1.0.0")
    }
    

Lưu trữ các cặp khoá-giá trị với Preferences DataStore

Phương thức triển khai Bộ tuỳ chọn DataStore sử dụng các lớp DataStorePreferences để lưu trữ các cặp khoá-giá trị đơn giản vào ổ đĩa.

Tạo một DataStore Preference

Hãy sử dụng tính năng uỷ quyền thuộc tính do preferencesDataStore tạo để tạo một bản sao của Datastore<Preferences>. Gọi tệp kotlin của bạn một lần ở cấp cao nhất và truy cập tệp thông qua thuộc tính này trong suốt phần còn lại của ứng dụng. Thao tác này giúp bạn dễ dàng giữ DataStore ở dạng một singleton. Bên cạnh đó, hãy sử dụng RxPreferenceDataStoreBuilder nếu bạn đang dùng RxJava. Tham số bắt buộc name là tên của DataStore Preference.

Kotlin

// At the top level of your kotlin file:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

Java

RxDataStore<Preferences> dataStore =
  new RxPreferenceDataStoreBuilder(context, /*name=*/ "settings").build();

Đọc trong một DataStore Preference

Do DataStore Preference không sử dụng giản đồ được xác định trước, bạn phải sử dụng hàm loại khoá tương ứng để xác định khoá cho mỗi giá trị mà bạn cần lưu trữ trong phiên bản DataStore<Preferences>. Ví dụ: để xác định khoá cho một giá trị int, hãy sử dụng intPreferencesKey(). Sau đó, dùng thuộc tính DataStore.data để hiển thị giá trị được lưu trữ phù hợp bằng Flow.

Kotlin

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
val exampleCounterFlow: Flow<Int> = context.dataStore.data
  .map { preferences ->
    // No type safety.
    preferences[EXAMPLE_COUNTER] ?: 0
}

Java

Preferences.Key<Integer> EXAMPLE_COUNTER = PreferencesKeys.int("example_counter");

Flowable<Integer> exampleCounterFlow =
  dataStore.data().map(prefs -> prefs.get(EXAMPLE_COUNTER));

Ghi vào một DataStore Preference

DataStore Preference cung cấp một hàm edit() có thể chia sẻ cập nhật dữ liệu trong một DataStore. Tham số transform của hàm chấp nhận một khối mã để bạn có thể cập nhật các giá trị nếu cần. Tất cả mã trong khối biến đổi được coi là một lượt chia sẻ.

Kotlin

suspend fun incrementCounter() {
  context.dataStore.edit { settings ->
    val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
    settings[EXAMPLE_COUNTER] = currentCounterValue + 1
  }
}

Java

Single<Preferences> updateResult =  dataStore.updateDataAsync(prefsIn -> {
  MutablePreferences mutablePreferences = prefsIn.toMutablePreferences();
  Integer currentInt = prefsIn.get(INTEGER_KEY);
  mutablePreferences.set(INTEGER_KEY, currentInt != null ? currentInt + 1 : 1);
  return Single.just(mutablePreferences);
});
// The update is completed once updateResult is completed.

Lưu trữ các đối tượng đã nhập bằng DataStore Proto

Phương thức triển khai DataStore Proto sử dụng DataStore và các bộ đệm giao thức để lưu trữ các đối tượng đã nhập vào ổ đĩa.

Xác định một giản đồ

DataStore Proto yêu cầu một giản đồ được xác định trước trong tệp proto trong thư mục app/src/main/proto/. Giản đồ này xác định loại cho các đối tượng mà bạn lưu trữ trong DataStore Proto. Để tìm hiểu thêm về cách xác định một giản đồ proto, hãy xem hướng dẫn về ngôn ngữ protobuf.

syntax = "proto3";

option java_package = "com.example.application";
option java_multiple_files = true;

message Settings {
  int32 example_counter = 1;
}

Tạo một DataStore Proto

Bạn cần thực hiện hai bước để tạo một DataStore Proto cho việc lưu trữ các đối tượng đã nhập:

  1. Xác định một lớp triển khai Serializer<T>, trong đó T là loại được xác định trong tệp proto. Lớp chuyển đổi tuần tự này cho DataStore biết cách đọc và ghi loại dữ liệu của bạn. Hãy đảm bảo bạn cung cấp giá trị mặc định để trình chuyển đổi tuần tự sử dụng nếu chưa có tệp nào được tạo.
  2. Sử dụng tính năng uỷ quyền thuộc tính do dataStore tạo để tạo một bản sao của DataStore<T>, trong đó T là loại được xác định trong tệp proto. Hãy gọi tệp kotlin của bạn một lần ở cấp cao nhất và truy cập tệp thông qua tính năng uỷ quyền thuộc tính này trong suốt thời gian còn lại của ứng dụng. Tham số filename cho DataStore biết nên sử dụng tệp nào để lưu trữ dữ liệu, còn tham số serializer cho DataStore biết tên của lớp chuyển đổi tuần tự đã xác định trong bước 1.

Kotlin

object SettingsSerializer : Serializer<Settings> {
  override val defaultValue: Settings = Settings.getDefaultInstance()

  override suspend fun readFrom(input: InputStream): Settings {
    try {
      return Settings.parseFrom(input)
    } catch (exception: InvalidProtocolBufferException) {
      throw CorruptionException("Cannot read proto.", exception)
    }
  }

  override suspend fun writeTo(
    t: Settings,
    output: OutputStream) = t.writeTo(output)
}

val Context.settingsDataStore: DataStore<Settings> by dataStore(
  fileName = "settings.pb",
  serializer = SettingsSerializer
)

Java

private static class SettingsSerializer implements Serializer<Settings> {
  @Override
  public Settings getDefaultValue() {
    Settings.getDefaultInstance();
  }

  @Override
  public Settings readFrom(@NotNull InputStream input) {
    try {
      return Settings.parseFrom(input);
    } catch (exception: InvalidProtocolBufferException) {
      throw CorruptionException(“Cannot read proto.”, exception);
    }
  }

  @Override
  public void writeTo(Settings t, @NotNull OutputStream output) {
    t.writeTo(output);
  }
}

RxDataStore<Byte> dataStore =
    new RxDataStoreBuilder<Byte>(context, /* fileName= */ "settings.pb", new SettingsSerializer()).build();

Đọc trong một DataStore Proto

Hãy sử dụng DataStore.data để hiển thị Flow của thuộc tính phù hợp trong đối tượng được lưu trữ của bạn.

Kotlin

val exampleCounterFlow: Flow<Int> = context.settingsDataStore.data
  .map { settings ->
    // The exampleCounter property is generated from the proto schema.
    settings.exampleCounter
  }

Java

Flowable<Integer> exampleCounterFlow =
  dataStore.data().map(settings -> settings.getExampleCounter());

Ghi vào một DataStore Proto

DataStore Proto cung cấp một hàm updateData() có thể chia sẻ cập nhật đối tượng được lưu trữ. updateData() cho bạn biết trạng thái hiện tại của dữ liệu làm phiên bản cho loại dữ liệu và cập nhật dữ liệu có thể chia sẻ trong thao tác đọc-ghi-sửa đổi nguyên tử.

Kotlin

suspend fun incrementCounter() {
  context.settingsDataStore.updateData { currentSettings ->
    currentSettings.toBuilder()
      .setExampleCounter(currentSettings.exampleCounter + 1)
      .build()
    }
}

Java

Single<Settings> updateResult =
  dataStore.updateDataAsync(currentSettings ->
    Single.just(
      currentSettings.toBuilder()
        .setExampleCounter(currentSettings.getExampleCounter() + 1)
        .build()));

Sử dụng DataStore trong mã đồng bộ

Một trong những lợi ích chính của DataStore là API không đồng bộ, nhưng không phải lúc nào bạn cũng có thể thay đổi mã xung quanh thành không đồng bộ. Điều này có thể xảy ra trong trường hợp bạn đang xử lý một cơ sở mã hiện có sử dụng ổ đĩa I/O đồng bộ hoặc nếu bạn có một phần phụ thuộc không cung cấp API không đồng bộ.

Coroutine của Kotlin cung cấp trình tạo coroutine runBlocking() để giúp thu hẹp khoảng cách giữa mã đồng bộ và không đồng bộ. Bạn có thể sử dụng runBlocking() để đọc dữ liệu trong DataStore một cách đồng bộ. RxJava cung cấp các phương thức chặn trên Flowable. Mã sau đây chặn luồng gọi cho đến khi DataStore trả về dữ liệu:

Kotlin

val exampleData = runBlocking { context.dataStore.data.first() }

Java

Settings settings = dataStore.data().blockingFirst();

Việc thực hiện các thao tác I/O đồng bộ trên luồng giao diện người dùng có thể khiến ANR hoặc giao diện người dùng bị giật. Bạn có thể giảm thiểu những vấn đề này bằng cách tải trước không đồng bộ dữ liệu trong DataStore:

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    lifecycleScope.launch {
        context.dataStore.data.first()
        // You should also handle IOExceptions here.
    }
}

Java

dataStore.data().first().subscribe();

Bằng cách này, DataStore đọc không đồng bộ dữ liệu và lưu dữ liệu vào bộ nhớ đệm trong bộ nhớ. Các lượt đọc đồng bộ sau này sử dụng runBlocking() có thể nhanh hơn hoặc có thể tránh được toàn bộ thao tác của ổ đĩa I/O nếu lần đọc ban đầu hoàn tất.

Sử dụng DataStore trong mã đa quá trình

Bạn có thể định cấu hình DataStore để truy cập vào cùng một dữ liệu trong nhiều quy trình với sự đảm bảo về tính nhất quán của dữ liệu như trong một quy trình. Cụ thể, DataStore đảm bảo:

  • Hoạt động đọc chỉ trả về dữ liệu đã lưu trữ trên ổ đĩa.
  • Tính nhất quán đọc sau khi ghi.
  • Hoạt động ghi được chuyển đổi tuần tự.
  • Hoạt động đọc tuyệt đối không bị chặn bởi hoạt động ghi.

Hãy xem xét một ứng dụng mẫu có dịch vụ và hoạt động:

  1. Dịch vụ đang chạy trong một quy trình riêng và định kỳ cập nhật DataStore

    <service
      android:name=".MyService"
      android:process=":my_process_id" />
    
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
          scope.launch {
              while(isActive) {
                  dataStore.updateData {
                      Settings(lastUpdate = System.currentTimeMillis())
                  }
                  delay(1000)
              }
          }
    }
    
  2. Đồng thời, ứng dụng sẽ thu thập những thay đổi đó và cập nhật giao diện người dùng

    val settings: Settings by dataStore.data.collectAsState()
    Text(
      text = "Last updated: $${settings.timestamp}",
    )
    

Để có thể sử dụng DataStore trong nhiều quy trình, bạn cần xây dựng đối tượng DataStore bằng MultiProcessDataStoreFactory.

val dataStore: DataStore<Settings> = MultiProcessDataStoreFactory.create(
   serializer = SettingsSerializer(),
   produceFile = {
       File("${context.cacheDir.path}/myapp.preferences_pb")
   }
)

serializer cho DataStore biết cách đọc và ghi loại dữ liệu của bạn. Hãy đảm bảo rằng bạn cung cấp giá trị mặc định để trình chuyển đổi tuần tự sử dụng nếu chưa có tệp nào được tạo. Dưới đây là một ví dụ về phương thức triển khai bằng kotlinx.serialization:

@Serializable
data class Settings(
   val lastUpdate: Long
)

@Singleton
class SettingsSerializer @Inject constructor() : Serializer<Settings> {

   override val defaultValue = Settings(lastUpdate = 0)

   override suspend fun readFrom(input: InputStream): Timer =
       try {
           Json.decodeFromString(
               Settings.serializer(), input.readBytes().decodeToString()
           )
       } catch (serialization: SerializationException) {
           throw CorruptionException("Unable to read Settings", serialization)
       }

   override suspend fun writeTo(t: Settings, output: OutputStream) {
       output.write(
           Json.encodeToString(Settings.serializer(), t)
               .encodeToByteArray()
       )
   }
}

Bạn có thể sử dụng tính năng chèn phần phụ thuộc Hilt để đảm bảo thực thể DataStore là duy nhất cho mỗi quy trình:

@Provides
@Singleton
fun provideDataStore(@ApplicationContext context: Context): DataStore<Settings> =
   MultiProcessDataStoreFactory.create(...)

Gửi phản hồi

Hãy chia sẻ phản hồi và ý kiến của bạn với chúng tôi thông qua các tài nguyên sau:

Công cụ theo dõi lỗi
Báo cáo sự cố để chúng tôi có thể sửa lỗi.

Tài nguyên khác

Để tìm hiểu thêm về Jetpack DataStore, hãy xem thêm các tài nguyên sau:

Mẫu

Blog

Lớp học lập trình