DataStore بخشی از Android Jetpack .

Jetpack DataStore یک راه حل ذخیره سازی داده است که به شما امکان می دهد جفت های کلید-مقدار یا اشیاء تایپ شده را با بافرهای پروتکل ذخیره کنید. DataStore از کوروتین های Kotlin و Flow برای ذخیره داده ها به صورت ناهمزمان، پیوسته و تراکنش استفاده می کند.

اگر در حال حاضر از SharedPreferences برای ذخیره داده ها استفاده می کنید، به جای آن به DataStore مهاجرت کنید.

DataStore و Proto DataStore را ترجیح می دهد

DataStore دو پیاده سازی مختلف را ارائه می دهد: Preferences DataStore و Proto DataStore.

  • Preferences DataStore داده ها را با استفاده از کلیدها ذخیره می کند و به آنها دسترسی پیدا می کند. این پیاده سازی به یک طرح از پیش تعریف شده نیاز ندارد و ایمنی نوع را فراهم نمی کند.
  • Proto DataStore داده ها را به عنوان نمونه هایی از یک نوع داده سفارشی ذخیره می کند. این پیاده سازی به شما نیاز دارد که یک طرحواره را با استفاده از بافرهای پروتکل تعریف کنید، اما ایمنی نوع را فراهم می کند.

استفاده صحیح از DataStore

برای استفاده صحیح از DataStore همیشه قوانین زیر را در نظر داشته باشید:

  1. هرگز بیش از یک نمونه از DataStore برای یک فایل معین در یک فرآیند ایجاد نکنید. انجام این کار می تواند تمام قابلیت های DataStore را از بین ببرد. اگر چندین DataStore برای یک فایل مشخص در یک فرآیند فعال باشد، DataStore هنگام خواندن یا به‌روزرسانی داده‌ها، IllegalStateException پرتاب می‌کند.

  2. نوع عمومی DataStore باید تغییر ناپذیر باشد جهش یک نوع مورد استفاده در DataStore، هرگونه تضمینی را که DataStore ارائه می‌کند بی اعتبار می‌کند و اشکالات بالقوه جدی و سخت‌گیر ایجاد می‌کند. اکیداً توصیه می‌شود که از بافرهای پروتکل استفاده کنید که تضمین‌های تغییرناپذیری، API ساده و سریال‌سازی کارآمد را ارائه می‌دهند.

  3. هرگز استفاده از SingleProcessDataStore و MultiProcessDataStore را برای یک فایل مخلوط نکنید . اگر قصد دارید از بیش از یک فرآیند به DataStore دسترسی داشته باشید، همیشه از MultiProcessDataStore استفاده کنید.

راه اندازی

برای استفاده از Jetpack DataStore در برنامه خود، بسته به اینکه از کدام پیاده سازی می خواهید استفاده کنید، موارد زیر را به فایل Gradle خود اضافه کنید:

Preferences DataStore

Groovy

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

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

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

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

Kotlin

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

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

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

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

Proto DataStore

Groovy

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

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

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

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

Kotlin

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

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

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

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

جفت‌های کلید-مقدار را با Preferences DataStore ذخیره کنید

اجرای Preferences DataStore از کلاس های DataStore و Preferences برای تداوم جفت های کلید-مقدار ساده روی دیسک استفاده می کند.

یک فروشگاه داده ترجیحی ایجاد کنید

برای ایجاد نمونه ای از DataStore<Preferences> از ویژگی نماینده ایجاد شده توسط preferencesDataStore استفاده کنید. یک بار آن را در سطح بالای فایل kotlin خود تماس بگیرید و از طریق این ویژگی در بقیه برنامه خود به آن دسترسی داشته باشید. این باعث می‌شود که DataStore خود را به‌صورت تکی راحت‌تر نگه دارید. یا اگر از RxJava استفاده می کنید، از RxPreferenceDataStoreBuilder استفاده کنید. پارامتر name اجباری نام Preferences DataStore است.

کاتلین

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

جاوا

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

از یک Preferences DataStore بخوانید

از آنجا که Preferences DataStore از طرحی از پیش تعریف شده استفاده نمی کند، باید از تابع نوع کلید مربوطه برای تعریف یک کلید برای هر مقداری که باید در نمونه DataStore<Preferences> ذخیره کنید استفاده کنید. به عنوان مثال، برای تعریف یک کلید برای مقدار int، از intPreferencesKey() استفاده کنید. سپس، از ویژگی DataStore.data برای نمایش مقدار ذخیره شده مناسب با استفاده از یک Flow استفاده کنید.

کاتلین

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

جاوا

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

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

در یک DataStore Preferences بنویسید

Preferences DataStore یک تابع edit() ارائه می دهد که به صورت تراکنشی داده ها را در یک DataStore به روز می کند. پارامتر transform تابع یک بلوک کد را می پذیرد که در آن می توانید مقادیر را در صورت نیاز به روز کنید. تمام کدهای موجود در بلوک تبدیل به عنوان یک تراکنش واحد در نظر گرفته می شود.

کاتلین

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

جاوا

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.

اشیاء تایپ شده را با Proto DataStore ذخیره کنید

اجرای Proto DataStore از DataStore و بافرهای پروتکل برای ماندگاری اشیاء تایپ شده در دیسک استفاده می کند.

یک طرح واره را تعریف کنید

Proto DataStore به یک طرح از پیش تعریف شده در یک فایل پروتو در دایرکتوری app/src/main/proto/ نیاز دارد. این طرح، نوع اشیایی را که در Proto DataStore خود باقی می مانند، تعریف می کند. برای کسب اطلاعات بیشتر در مورد تعریف طرحواره پروتو، به راهنمای زبان پروتوباف مراجعه کنید.

syntax = "proto3";

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

message Settings {
  int32 example_counter = 1;
}

یک Proto DataStore ایجاد کنید

دو مرحله برای ایجاد یک Proto DataStore برای ذخیره اشیاء تایپ شده شما وجود دارد:

  1. کلاسی را تعریف کنید که Serializer<T> پیاده سازی کند، جایی که T نوع تعریف شده در فایل پروتو است. این کلاس سریال ساز به DataStore می گوید که چگونه نوع داده شما را بخواند و بنویسد. مطمئن شوید که یک مقدار پیش‌فرض برای سریال‌ساز در نظر بگیرید تا اگر هنوز فایلی ایجاد نشده است، استفاده شود.
  2. از نماینده خاصیت ایجاد شده توسط dataStore برای ایجاد نمونه ای از DataStore<T> استفاده کنید، جایی که T نوع تعریف شده در فایل پروتو است. این را یک بار در سطح بالای فایل kotlin خود تماس بگیرید و از طریق این ویژگی در بقیه برنامه خود به آن دسترسی داشته باشید. پارامتر filename به DataStore می گوید که از کدام فایل برای ذخیره داده ها استفاده کند و پارامتر serializer نام کلاس سریال ساز تعریف شده در مرحله 1 را به DataStore می گوید.

کاتلین

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
)

جاوا

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();

از Proto DataStore بخوانید

از DataStore.data برای افشای Flow از ویژگی مناسب از شی ذخیره شده خود استفاده کنید.

کاتلین

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

جاوا

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

در Proto DataStore بنویسید

Proto DataStore یک تابع updateData() ارائه می کند که به صورت تراکنشی یک شی ذخیره شده را به روز می کند. updateData() وضعیت فعلی داده ها را به عنوان نمونه ای از نوع داده شما به شما می دهد و داده ها را به صورت تراکنشی در یک عملیات خواندن-نوشتن-تغییر اتمی به روز می کند.

کاتلین

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

جاوا

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

از DataStore در کدهای همزمان استفاده کنید

یکی از مزایای اصلی DataStore API ناهمزمان است، اما ممکن است همیشه امکان پذیر نباشد که کد اطراف خود را به ناهمزمان تغییر دهید. اگر با یک پایگاه کد موجود کار می‌کنید که از ورودی/خروجی دیسک همزمان استفاده می‌کند یا اگر وابستگی دارید که یک API ناهمزمان ارائه نمی‌دهد، ممکن است این مورد صادق باشد.

کوروتین‌های Kotlin سازنده کوروتین runBlocking() را برای کمک به پر کردن شکاف بین کدهای همزمان و ناهمزمان ارائه می‌کنند. می توانید از runBlocking() برای خواندن همزمان داده ها از DataStore استفاده کنید. RxJava روش های مسدود کردن را در Flowable ارائه می دهد. کد زیر تا زمانی که DataStore داده ها را برگرداند، رشته تماس گیرنده را مسدود می کند:

کاتلین

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

جاوا

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

انجام عملیات ورودی/خروجی همزمان روی رشته رابط کاربری می‌تواند باعث ANR یا جابجایی UI شود. می‌توانید با بارگیری ناهمزمان داده‌ها از DataStore، این مشکلات را کاهش دهید:

کاتلین

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

جاوا

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

به این ترتیب DataStore به صورت ناهمزمان داده ها را می خواند و در حافظه پنهان می کند. خواندن همزمان بعدی با استفاده از runBlocking() ممکن است سریعتر باشد یا اگر خواندن اولیه کامل شده باشد، ممکن است به طور کلی از عملیات ورودی/خروجی دیسک جلوگیری کند.

از DataStore در کدهای چند فرآیندی استفاده کنید

می‌توانید DataStore را برای دسترسی به داده‌های یکسان در فرآیندهای مختلف با تضمین‌های یکسانی داده‌ها از داخل یک فرآیند پیکربندی کنید. به طور خاص، DataStore تضمین می کند:

  • Reads فقط داده‌هایی را که حفظ شده‌اند به دیسک برمی‌گرداند.
  • سازگاری خواندن پس از نوشتن
  • نوشته ها به صورت سریالی هستند.
  • خواندن هرگز توسط نوشتن مسدود نمی شود.

یک نمونه برنامه کاربردی با یک سرویس و یک فعالیت را در نظر بگیرید:

  1. این سرویس در یک فرآیند جداگانه اجرا می شود و به طور دوره ای 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. در حالی که برنامه این تغییرات را جمع آوری کرده و رابط کاربری خود را به روز می کند

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

برای اینکه بتوانید از DataStore در فرآیندهای مختلف استفاده کنید، باید شی DataStore را با استفاده از MultiProcessDataStoreFactory بسازید.

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

serializer به DataStore می گوید که چگونه نوع داده شما را بخواند و بنویسد. مطمئن شوید که یک مقدار پیش‌فرض برای سریال‌ساز در نظر بگیرید تا اگر هنوز فایلی ایجاد نشده است، استفاده شود. در زیر نمونه ای از پیاده سازی با استفاده از 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()
       )
   }
}

می‌توانید از تزریق وابستگی Hilt استفاده کنید تا مطمئن شوید نمونه DataStore شما در هر فرآیند منحصربه‌فرد است:

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

مدیریت خرابی فایل ها

موارد نادری وجود دارد که فایل دائمی روی دیسک DataStore ممکن است خراب شود. به‌طور پیش‌فرض، DataStore به‌طور خودکار از خرابی بازیابی نمی‌شود، و تلاش برای خواندن از آن باعث می‌شود که سیستم یک CorruptionException ایجاد کند.

DataStore یک API کنترل کننده فساد ارائه می دهد که می تواند به شما کمک کند در چنین سناریویی به خوبی بازیابی کنید و از استثناء کردن اجتناب کنید. هنگام پیکربندی، کنترل کننده خرابی فایل خراب را با یک فایل جدید حاوی یک مقدار پیش فرض از پیش تعریف شده جایگزین می کند.

برای راه‌اندازی این کنترل‌کننده، هنگام ایجاد نمونه DataStore by dataStore() یا در روش کارخانه DataStoreFactory ، یک corruptionHandler ارائه دهید:

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

بازخورد ارائه دهید

نظرات و ایده های خود را از طریق این منابع با ما در میان بگذارید:

ردیاب مشکل
مشکلات را گزارش کنید تا بتوانیم اشکالات را برطرف کنیم.

منابع اضافی

برای کسب اطلاعات بیشتر در مورد Jetpack DataStore، به منابع اضافی زیر مراجعه کنید:

نمونه ها

وبلاگ ها

Codelabs

{% کلمه به کلمه %} {% آخر کلمه %} {% کلمه به کلمه %} {% آخر کلمه %}