Jetpack DataStore جایگزین مناسبی برای SharedPreferences

وقتی به راه حلی برای ذخیره اطلاعات در اندروید فکر میکنیم در اکثر مواقع عبارت SharedPreferences به ذهنمان میآید. یک راه حل بسیارساده و سریع برای رفع نیازمان اما اگر به درستی مدیریت نشود ممکن است باعث بروز مشکلات بسیار زیادی در برنامه شود دقیقا هنگامی که برنامه شما از نظر اندازه و پیچیدگی افزایش می یابد، حل آن می تواند بسیار دشوار باشد.حالا Jetpack DataStore آمده تا یک جایگزین بسیار مناسب به جای SharedPreferences باشد و خیلی از مشکلات ما را برطرف کند.What is DataStoreـDataStore یک راه حل جدید و بهبود داده شده برای ذخیره اطلاعات میباشد که با هدف جایگزینی برای SharedPreferences توسعه داده شده است. بر روی coroutin و flow ساخته شده و دارای ۲ روش پیاده سازی میباشد( Proto DataStore وPreferences DataStore) .داده ها به صورت ناهمزمان، پیوسته و به صورت تراکنشی ذخیره می شوند و بر برخی از معایب SharedPreferences غلبه می کنندWhat you'll learnـDataStore چیست و دلیل استفاده ازش چیه ؟چگونگی اضافه نمودن DataStore در پروژهتفاوت بین Preferences DataStore وProto DataStore و مزیت های هر کدامنحوه استفاده از Proto DataStoreنحوه مهاجرت از SharedPreferences به Proto DataStoreDataStoreگاهی اوقات شما متوجه نیاز خود به ذخیره اطلاعات کوچک یا ساده میشوید در گذشته ممکن بوده شما از SharedPreferences استفاده کنید اما این API چندین مشکل جدی را دارا هستش.هدف کتابخانه Jetpack DataStore برطرف نمودن این مشکلات- پیاده سازی راحتتر-امن تر و API ناهمزمان برای ذخیره اطلاعات میباشد.دارای ۲ نوع مختلف پیاده‌سازی میباشدPreferences DataStoreProto DataStoreـSharedPreferences (1 دارای API همزمان میباشد که به نظر میرسد برای صدا زده شدن درUI thread امن باشد.اما در واقع عملیات I/O دیسک را انجام میدهد.علاوه بر این متد ()apply متد ()fsync درUI thread را بلاک میکند.متدهای معلق مانده ()fsync در هر زمان که هر سرویس start/stop یا هر اکتیوتی start/stop میشود دوباره راه اندازی میشود.2)ـSharedPreferences خطاهای parsing را به عنوان استثناهای زمان اجرا فرض میکند .Preferences vs Proto DataStoreـPreference DataStore : شبیه به SharedPreferences میباشد و روش پایه‌ی برای دسترسی به دیتا بر مبنای کلید میباشد(بدون تعریف schema)ـProto DataStore : تعریف schema با استفاده از Protocol buffers استفاده از آن اجازه دسترسی به strongly typed data را میدهد. آنها سریعتر، کوچکتر، ساده تر و کمتر از XML و سایر فرمت های داده مبهم هستند. در حالی که Proto DataStore شما را ملزم به یادگیری یک مکانیسم سریال سازی جدید می کند.Proto DataStoreیکی از نقاط ضعف SharedPreferences و Preferences DataStore این است که راهی برای تعریف schema یا اطمینان از دسترسی به کلیدها با نوع صحیح وجود ندارد. Proto DataStore این مشکل را با استفاده از Protocol buffers برای تعریف schema برطرف نمودهاستفاده از protosDataStore میداند که کدام نوع متغییر ذخیره شده و فقط آنها را تولید میکند و استفاده از کلید را حذف کرده است.Adding dependencies plugins { ... id &quotcom.google.protobuf&quot version &quot0.8.17&quot } dependencies { implementation &quotandroidx.datastore:datastore-core:1.0.0&quot implementation &quotcom.google.protobuf:protobuf-javalite:3.18.0&quot implementation &quotandroidx.datastore:datastore-preferences:1.0.0&quot ... } protobuf { protoc { artifact = &quotcom.google.protobuf:protoc:3.14.0&quot } // Generates the java Protobuf-lite code for the Protobufs in this project. See // https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation // for more information. generateProtoTasks { all().each { task -> task.builtins { java { option 'lite' } } } } } پروژه خود را Rebuild کنید تا فایل UserStore.java ساخته شود.Defining and using protobuf objectsـProtocol bufferها یک مکانیزم سریالسازی برای داده‌های ساختار یافته شده هستند.شما فقط یکبار هر آنچه از دادهای ساختار یافته را میخواهید ایجاد میکنید و کامپایلر کدهای مورد نیاز برای خواندن و نوشتن دادها را ایجاد میکند.Create the proto fileفایل schema را در مسیر app/src/main/User_store.proto ایجاد میکنیم(پکیج proto و فایلی با پسوند proto.)اگر فایل مورد نظر قابل رویت نبود میتوانید با تغییر ساختار از Android به Project view آن را ایجاد کنید.در protobufs ، هر ساختار با استفاده از یک کلمه کلیدی Message تعریف می‌شود و هر یک از اعضای ساختار بر اساس نوع و نام، درون Message تعریف می‌شوند و یک ترتیب مبتنی بر 1 به آن اختصاص می‌یابد.proto file syntax = &quotproto3&quot option java_package = &quotcom.codelab.android.datastore&quot option java_multiple_files = true; message UserPreferences { // filter for showing / hiding completed tasks bool show_completed = 1; } این کلاس در زمان کامپایل ساخته میشود.(به منظور راحتی کار با این زبان میتوانید از قسمت plugin →protocol buffer editor را نصب کنید)Create the serializerبرای گفتن چگونگی خواندن و نوشتن اطلاعات در فایل Proto ایجاد شده ما نیاز به پیاده‌سازی Serializer داریم. object UserSerializer : Serializer<UserStore> { override val defaultValue: UserStore = UserStore.getDefaultInstance() override suspend fun readFrom(input: InputStream): UserStore { try { return UserStore.parseFrom(input) } catch (exception: InvalidProtocolBufferException) { throw CorruptionException(&quotCannot read proto.&quot, exception) } } override suspend fun writeTo(t: UserStore, output: OutputStream) = t.writeTo(output) } Creating the DataStoreبرای تغییر اطلاعات و امکان انجام عملیات باید یک شی از کلاس DataStore ایجاد کنید که در ورودی آن ۲ پارامتر را دریافت میکندنام فایلی که DataStore روی آن کار میکندSerializer برای نوع داده مورد استفاده با DataStore const val USER_DATA_STORE_FILE_NAME = &quotuser_store.pb&quot protected val Context.userDataStore: DataStore<UserStore> by dataStore( USER_DATA_STORE_FILE_NAME , UserSerializer ) Reading data from Proto DataStoreـProto DataStore یک نمونه ازFlow داده‌ی ذخیره شده را ایجاد میکند val userStoreFlow : Flow<UserStore> = ruserDataStore.data .map { it } Handling exceptions while reading dataهمانطور که DataStore داده ها را از یک فایل می خواند، IOException ها زمانی که خطایی در هنگام خواندن داده ها رخ می دهد ظاهر میشوند. ما می توانیم با استفاده از تبدیل catch Flow این موارد را مدیریت کنیم و فقط خطا را ثبت کنیم val userStoreFlow: Flow<UserStore> = dataStore.data .catch { exception -> // dataStore.data throws an IOException when an error is encountered when reading data if (exception is IOException) { Log.e(TAG, &quotError reading sort order preferences.&quot, exception) emit(UserStore.getDefaultInstance()) } else { throw exception } } Writing data to Proto DataStoreـDataStore پیشنهاد میکند برای نوشتن از متد های Suspend استفاده کنیم. fun saveProtoData(value: UserDataModel) { lifecycleScope.launch { requireContext().userDataStore.updateData { it.toBuilder() .setName(value.name) .setId(value.id) .build() } } } SharedPreferences to Proto DataStoreMigrating from SharedPreferencesبرای کمک به مهاجرت، DataStore کلاس SharedPreferencesMigration را تعریف می کندمتد by dataStore که DataStore را ایجاد می‌کند (که در TasksActivity استفاده می‌شود)، یک پارامتر productMigrations را نیز نمایش می‌دهد. در این بلاک ما لیستی از DataMigrations را ایجاد می کنیم که باید برای این نمونه DataStore اجرا شوند.هنگام پیاده سازی SharedPreferencesMigration بلاک migrate ۲ پارامتر به ما میدهد:SharedPreferencesView : اجازه بازگزدانی اطلاعات از SharedPreferences را به ما میدهدUserStore : اطلاعات درست و صحیح private val Context.userPreferencesStore: DataStore<UserStore> by dataStore( fileName = USER_DATA_STORE_FILE_NAME, serializer = UserSerializer, produceMigrations = { context -> listOf( SharedPreferencesMigration( context, USER_PREFERENCES_NAME ) { sharedPrefs: SharedPreferencesView, currentData: UserStore-> // Define the mapping from SharedPreferences to UserStore if (currentData.sortOrder == SortOrder.UNSPECIFIED) { currentData.toBuilder().setSortOrder( SortOrder.valueOf( sharedPrefs.getString(SORT_ORDER_KEY, SortOrder.NONE.name)!! ) ).build() } else { currentData } } ) } ) حالا که ما منطق مهاجرت را تعریف کرده‌ایم زمان آن رسیده که DataStore ازش استفاده کند.برایاین کار، سازنده DataStore را به روز کنید و به پارامتر migrations یک لیست جدید که حاوی نمونه ای از SharedPreferencesMigration است اختصاص دهید. private val dataStore: DataStore<UserStore> = context.createDataStore( fileName = &quotuser_store.pb&quot, serializer = UserSerializer, migrations = listOf(sharedPrefsMigration) ) Create a Preferences DataStoreیک شی نمونه از PreferencesDataStore ایجاد میکنیم.ک بار آن را در سطح بالای فایل kotlin خود تماس بگیرید و از طریق این ویژگی در بقیه برنامه خود به آن دسترسی داشته باشید. protected val Context.userPreferencesStore: DataStore<Preferences> by preferencesDataStore( AppContainer.USER_PREFERENCES_NAME) از آنجایی که Preferences DataStore از طراحی schema استفاده نمی کند، باید از تابع نوع کلید مربوطه برای تعریف یک کلید برای هر مقداری که باید در نمونه <DataStore<Preferences ذخیره کنید استفاده کنید.Read from a Preferences DataStore fun readUserPreferences() { val userFlow: Flow<String> = requireContext().userPreferencesStore.data .map { preferences-> preferences[PreferencesKeys.USER_NAME].toString() } lifecycleScope.launch { userFlow.collect { Toast.makeText(requireContext(), &quotPreferences DataStore: $it&quot, Toast.LENGTH_SHORT) .show() } } } Write to a Preferences DataStoreـPreferences DataStore یک تابع ()edit را ارائه می دهد که به صورت تراکنشی داده ها را در یک DataStore به روزمیکند.پارامتر تابع یک بلاک کد را می پذیرد که در آن می توانید مقادیر را در صورت نیاز به روز کنید. تمام کدهای موجود در بلاک تبدیل به عنوان یک تراکنش واحد در نظر گرفته می شود fun saveUserPreferences(value: String){ lifecycleScope.launch { requireContext().userPreferencesStore.edit { it[PreferencesKeys.USER_NAME] = value } } } Use DataStore in synchronous codeیکی از مزایای اصلی DataStore API ناهمزمان بودن آن است، اما ممکن است همیشه امکان تغییر کد اطراف خود به ناهمزمان وجود نداشته باشد. اگر با یک پایگاه کد موجود کار می‌کنید که ازI/O دیسک همزمان استفاده می‌کند یا اگر وابستگی دارید که یک API ناهمزمان ارائه نمی‌دهد، ممکن است این مورد صدق کند.ـKotlin coroutines متد ()runBlocking را برای کمک به پر کردن شکاف بین کدهای همزمان و ناهمزمان ارائه می کنند. برای خواندن همزمان داده ها از DataStore می توانید از ()runBlocking استفاده کنید. val exampleData = runBlocking { context.dataStore.data.first() } و در آخر میتونید در قسمت data دستگاه فایل های ذخیره رو ببینید.میتونید کدهای مربوط با این مقاله رو در گیت هاب من مشاهده کنید .من رو در لینکدین و اینستاگرام دنبال کنید .