معماری برنامه: لایه داده - DataStore - توسعه دهندگان اندروید

پروژه: /architecture/_project.yaml کتاب: /architecture/_book.yaml کلمات کلیدی: datastore، معماری، api:JetpackDataStore توضیحات: این راهنمای معماری برنامه را در مورد کتابخانه‌های لایه داده بررسی کنید تا در مورد تنظیمات DataStore و Proto DataStore، تنظیمات و موارد دیگر اطلاعات کسب کنید. hide_page_heading: true

بخشی از DataStore در Android Jetpack .

با کاتلین چند پلتفرمی امتحان کنید
کاتلین چند پلتفرمی امکان اشتراک‌گذاری لایه داده با سایر پلتفرم‌ها را فراهم می‌کند. یاد بگیرید چگونه DataStore را در KMP راه‌اندازی و کار کنید.

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

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

تنظیمات DataStore و Proto DataStore

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

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

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

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

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

  2. نوع عمومی DataStore<T> باید تغییرناپذیر باشد. تغییر نوع استفاده شده در DataStore، ثباتی را که DataStore ارائه می‌دهد، نامعتبر می‌کند و باعث ایجاد اشکالات جدی و دشوار می‌شود. توصیه می‌کنیم از بافرهای پروتکل استفاده کنید که به تضمین تغییرناپذیری، یک API واضح و سریال‌سازی کارآمد کمک می‌کنند.

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

راه‌اندازی

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

داده‌های برگزیده

شیار

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

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

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

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

کاتلین

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

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

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

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

Proto DataStore

شیار

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

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

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

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

کاتلین

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

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

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

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

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

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

ایجاد یک پایگاه داده تنظیمات (Preferences DataStore)

از نماینده‌ی ویژگی ایجاد شده توسط preferencesDataStore برای ایجاد یک نمونه از DataStore<Preferences> استفاده کنید. آن را یک بار در سطح بالای فایل Kotlin خود فراخوانی کنید و از طریق این ویژگی در بقیه‌ی برنامه به آن دسترسی داشته باشید. این کار باعث می‌شود DataStore شما به عنوان یک singleton آسان‌تر نگه داشته شود. به عنوان یک جایگزین، اگر از 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, "settings").build();

خواندن از یک پایگاه داده تنظیمات برگزیده

از آنجا که 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));

نوشتن در یک پایگاه داده تنظیمات (Preferences DataStore)

DataStore تنظیمات، تابعی edit() ارائه می‌دهد که داده‌ها را در DataStore به صورت تراکنشی به‌روزرسانی می‌کند. پارامتر transform این تابع، بلوکی از کد را می‌پذیرد که در آن می‌توانید مقادیر را در صورت نیاز به‌روزرسانی کنید. تمام کدهای موجود در بلوک 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 به یک طرحواره از پیش تعریف شده در یک فایل proto در دایرکتوری app/src/main/proto/ نیاز دارد. این طرحواره، نوع اشیایی را که در Proto DataStore خود ذخیره می‌کنید، تعریف می‌کند. برای کسب اطلاعات بیشتر در مورد تعریف یک طرحواره proto، به راهنمای زبان protobuf مراجعه کنید.

syntax = "proto3";

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

message Settings {
  int32 example_counter = 1;
}

ایجاد یک Proto DataStore

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

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

کاتلین

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() {
    return Settings.getDefaultInstance();
  }

  @Override
  public Settings readFrom(@NotNull InputStream input) {
    try {
      return Settings.parseFrom(input);
    } catch (InvalidProtocolBufferException exception) {
      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 ناهمزمان ارائه نمی‌دهد.

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

کاتلین

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

جاوا

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

انجام عملیات ورودی/خروجی همزمان در نخ رابط کاربری می‌تواند باعث بروز ANR یا عدم پاسخگویی رابط کاربری شود. می‌توانید با پیش‌بارگذاری ناهمزمان داده‌ها از 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 موارد زیر را ارائه می‌دهد:

  • عملیات خواندن فقط داده‌هایی را که روی دیسک ذخیره شده‌اند، برمی‌گرداند.
  • سازگاری خواندن پس از نوشتن.
  • نوشتن‌ها سریالی می‌شوند.
  • خواندن‌ها هرگز توسط نوشتن‌ها مسدود نمی‌شوند.

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

  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): Settings =
       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() یا در متد factory 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، به منابع اضافی زیر مراجعه کنید:

نمونه‌ها

وبلاگ‌ها

کدلبز

{% کلمه به کلمه %} {% فعل کمکی %} {% کلمه به کلمه %} {% فعل کمکی %}