وقتی به راه حلی برای ذخیره اطلاعات در اندروید فکر میکنیم در اکثر مواقع عبارت SharedPreferences به ذهنمان میآید. یک راه حل بسیارساده و سریع برای رفع نیازمان اما اگر به درستی مدیریت نشود ممکن است باعث بروز مشکلات بسیار زیادی در برنامه شود دقیقا هنگامی که برنامه شما از نظر اندازه و پیچیدگی افزایش می یابد، حل آن می تواند بسیار دشوار باشد.
حالا Jetpack DataStore آمده تا یک جایگزین بسیار مناسب به جای SharedPreferences باشد و خیلی از مشکلات ما را برطرف کند.
ـDataStore یک راه حل جدید و بهبود داده شده برای ذخیره اطلاعات میباشد که با هدف جایگزینی برای SharedPreferences توسعه داده شده است. بر روی coroutin و flow ساخته شده و دارای ۲ روش پیاده سازی میباشد( Proto DataStore وPreferences DataStore) .داده ها به صورت ناهمزمان، پیوسته و به صورت تراکنشی ذخیره می شوند و بر برخی از معایب SharedPreferences غلبه می کنند
گاهی اوقات شما متوجه نیاز خود به ذخیره اطلاعات کوچک یا ساده میشوید در گذشته ممکن بوده شما از SharedPreferences استفاده کنید اما این API چندین مشکل جدی را دارا هستش.
هدف کتابخانه Jetpack DataStore برطرف نمودن این مشکلات- پیاده سازی راحتتر-امن تر و API ناهمزمان برای ذخیره اطلاعات میباشد.
دارای ۲ نوع مختلف پیادهسازی میباشد
ـSharedPreferences (1 دارای API همزمان میباشد که به نظر میرسد برای صدا زده شدن درUI thread امن باشد.
اما در واقع عملیات I/O دیسک را انجام میدهد.علاوه بر این متد ()apply متد ()fsync در
UI thread را بلاک میکند.
متدهای معلق مانده ()fsync
در هر زمان که هر سرویس start/stop یا هر اکتیوتی start/stop میشود دوباره راه اندازی میشود.
2)ـSharedPreferences خطاهای parsing را به عنوان استثناهای زمان اجرا فرض میکند .
یکی از نقاط ضعف SharedPreferences و Preferences DataStore این است که راهی برای تعریف schema یا اطمینان از دسترسی به کلیدها با نوع صحیح وجود ندارد. Proto DataStore این مشکل را با استفاده از Protocol buffers برای تعریف schema برطرف نموده
استفاده از protosDataStore میداند که کدام نوع متغییر ذخیره شده و فقط آنها را تولید میکند و استفاده از کلید را حذف کرده است.
plugins { ... id "com.google.protobuf" version "0.8.17" } dependencies { implementation "androidx.datastore:datastore-core:1.0.0" implementation "com.google.protobuf:protobuf-javalite:3.18.0" implementation "androidx.datastore:datastore-preferences:1.0.0" ... } protobuf { protoc { artifact = "com.google.protobuf:protoc:3.14.0" } // 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 ساخته شود.
ـProtocol bufferها یک مکانیزم سریالسازی برای دادههای ساختار یافته شده هستند.
شما فقط یکبار هر آنچه از دادهای ساختار یافته را میخواهید ایجاد میکنید و کامپایلر کدهای مورد نیاز برای خواندن و نوشتن دادها را ایجاد میکند.
فایل schema را در مسیر app/src/main/User_store.proto ایجاد میکنیم(پکیج proto و فایلی با پسوند proto.)
اگر فایل مورد نظر قابل رویت نبود میتوانید با تغییر ساختار از Android به Project view آن را ایجاد کنید.
در protobufs ، هر ساختار با استفاده از یک کلمه کلیدی Message تعریف میشود و هر یک از اعضای ساختار بر اساس نوع و نام، درون Message تعریف میشوند و یک ترتیب مبتنی بر 1 به آن اختصاص مییابد.
proto file
syntax = "proto3" option java_package = "com.codelab.android.datastore" option java_multiple_files = true; message UserPreferences { // filter for showing / hiding completed tasks bool show_completed = 1; }
این کلاس در زمان کامپایل ساخته میشود.
(به منظور راحتی کار با این زبان میتوانید از قسمت plugin →protocol buffer editor را نصب کنید)
برای گفتن چگونگی خواندن و نوشتن اطلاعات در فایل 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("Cannot read proto.", exception) } } override suspend fun writeTo(t: UserStore, output: OutputStream) = t.writeTo(output) }
برای تغییر اطلاعات و امکان انجام عملیات باید یک شی از کلاس DataStore ایجاد کنید که در ورودی آن ۲ پارامتر را دریافت میکند
const val USER_DATA_STORE_FILE_NAME = "user_store.pb" protected val Context.userDataStore: DataStore<UserStore> by dataStore( USER_DATA_STORE_FILE_NAME , UserSerializer )
ـProto DataStore یک نمونه ازFlow دادهی ذخیره شده را ایجاد میکند
val userStoreFlow : Flow<UserStore> = ruserDataStore.data .map { it }
همانطور که 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, "Error reading sort order preferences.", exception) emit(UserStore.getDefaultInstance()) } else { throw exception } }
ـDataStore پیشنهاد میکند برای نوشتن از متد های Suspend استفاده کنیم.
fun saveProtoData(value: UserDataModel) { lifecycleScope.launch { requireContext().userDataStore.updateData { it.toBuilder() .setName(value.name) .setId(value.id) .build() } } }
برای کمک به مهاجرت، 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 = "user_store.pb", serializer = UserSerializer, migrations = listOf(sharedPrefsMigration) )
یک شی نمونه از PreferencesDataStore ایجاد میکنیم.
ک بار آن را در سطح بالای فایل kotlin خود تماس بگیرید و از طریق این ویژگی در بقیه برنامه خود به آن دسترسی داشته باشید.
protected val Context.userPreferencesStore: DataStore<Preferences> by preferencesDataStore( AppContainer.USER_PREFERENCES_NAME)
از آنجایی که Preferences DataStore از طراحی schema استفاده نمی کند، باید از تابع نوع کلید مربوطه برای تعریف یک کلید برای هر مقداری که باید در نمونه <DataStore<Preferences ذخیره کنید استفاده کنید.
fun readUserPreferences() { val userFlow: Flow<String> = requireContext().userPreferencesStore.data .map { preferences-> preferences[PreferencesKeys.USER_NAME].toString() } lifecycleScope.launch { userFlow.collect { Toast.makeText(requireContext(), "Preferences DataStore: $it", Toast.LENGTH_SHORT) .show() } } }
ـPreferences DataStore یک تابع ()edit را ارائه می دهد که به صورت تراکنشی داده ها را در یک DataStore به روزمیکند.
پارامتر تابع یک بلاک کد را می پذیرد که در آن می توانید مقادیر را در صورت نیاز به روز کنید. تمام کدهای موجود در بلاک تبدیل به عنوان یک تراکنش واحد در نظر گرفته می شود
fun saveUserPreferences(value: String){ lifecycleScope.launch { requireContext().userPreferencesStore.edit { it[PreferencesKeys.USER_NAME] = value } } }
یکی از مزایای اصلی DataStore API ناهمزمان بودن آن است، اما ممکن است همیشه امکان تغییر کد اطراف خود به ناهمزمان وجود نداشته باشد. اگر با یک پایگاه کد موجود کار میکنید که ازI/O دیسک همزمان استفاده میکند یا اگر وابستگی دارید که یک API ناهمزمان ارائه نمیدهد، ممکن است این مورد صدق کند.
ـKotlin coroutines متد ()runBlocking را برای کمک به پر کردن شکاف بین کدهای همزمان و ناهمزمان ارائه می کنند. برای خواندن همزمان داده ها از DataStore می توانید از ()runBlocking استفاده کنید.
val exampleData = runBlocking { context.dataStore.data.first() }
و در آخر میتونید در قسمت data دستگاه فایل های ذخیره رو ببینید.
میتونید کدهای مربوط با این مقاله رو در گیت هاب من مشاهده کنید .
من رو در لینکدین و اینستاگرام دنبال کنید .