آموزش DataStore در اندروید: جایگزین SharedPreferences

برنامه نویسی اندروید
آموزش DataStore در اندروید: جایگزین SharedPreferences

در چرخه حیات توسعه نرم افزار اندروید، ذخیره سازی پایدار تنظیمات کاربر، توکن های احراز هویت و داده های موقت سبک، از ابتدایی ترین و در عین حال حیاتی ترین نیازمندی های مهندسی به شمار می رود.

برای بیش از یک دهه، کلاس SharedPreferences به عنوان تنها ابزار پیش فرض و مرجع سیستم عامل برای نگه داری مقادیر کلید-مقدار (Key-Value) در فایل های XML مورد استفاده قرار می گرفت.

اما با گذر زمان و رشد استانداردهای مهندسی، نقایص ساختاری عمیق SharedPreferences نظیر مسدود کردن ترد اصلی (Main Thread)، عدم ایمنی در برابر خطای نوع داده (Type Safety)، فقدان مکانیزم های تراکنشی و خطاهای پیش بینی نشده زمان اجرا آشکار گردید.

برای پایان دادن قطعی به این چالش های تاریخی، تیم توسعه اندروید در شرکت گوگل، کتابخانه مدرن و قدرتمند جت پک دیتا استور (Jetpack DataStore) را به عنوان بخشی از معماری مدرن اندروید معرفی کرد.

دیتا استور بر پایه کاتلین و کوروتین طراحی شده و با ارائه دو رویکرد مجزای Preferences DataStore و Proto DataStore، پردازش های دیسک را به صورت کاملاً غیرمسدودکننده، واکنشی و با تضمین ۱۰۰ درصدی صحت داده ها مدیریت می کند.

در این راهنمای جامع و مرجع در حوزه برنامه نویسی اندروید، کالبدشکافی معماری دیتا استور، مقایسه با روش های سنتی، پیاده سازی عمیق هر دو رویکرد، اتصال به جت پک کامپوز و پایپ لاین مهاجرت بدون قطعی از SharedPreferences را گام به گام بررسی می کنیم.

نتیجه گیری مهندسی در یک نگاه (Executive Summary): جت پک دیتا استور (Jetpack DataStore) جایگزین رسمی و بدون نقص SharedPreferences در اندروید است. دیتا استور با اجرای عملیات های خواندن و نوشتن دیسک به صورت کاملاً ناهمگام روی ترد پس زمینه (Dispatchers.IO)، ارسال داده ها به صورت استریم زنده Flow، پشتیبانی از اسکیماهای تایپ سیف پروتکل بافر و ارائه تضمین های تراکنشی ACID، فریز رابط کاربری (ANR) و کرش های ناشی از خطای پارس را به صفر می رساند.

دستاوردهای کلیدی و آموزه های فنی این راهنما

  • حذف قطعی خطاهای ANR: دیتا استور با ممنوعیت دسترسی همگام، کلیه پردازش های دیسک را به صورت امن در پس زمینه اجرا می کند.
  • جریان داده های واکنشی با Flow: هرگونه تغییر در دیسک بلافاصله از طریق استریم های کاتلین فلو به لایه رابط کاربری ارسال می شود.
  • ایمنی مطلق نوع داده با Proto: رویکرد Proto DataStore با تکیه بر Protocol Buffers، خطاهای تطابق نوع داده را در زمان کامپایل مسدود می سازد.
  • تضمین پایداری تراکنش ها: کلیه عملیات های نوشتن به صورت اتمیک انجام شده و خطر خرابی فایل (Data Corruption) از بین می رود.
  • مهاجرت بدون دردسر: با ابزار داخلی SharedPreferencesMigration، انتقال داده های پروداکشن کاربران در اولین اجرا بدون وقفه انجام می شود.

۱. چرا دوره SharedPreferences به پایان رسید؟ کالبدشکافی معایب تاریخی

برای درک ضرورت مهاجرت به دیتا استور، باید نقایص پنهان و خطاهای فاجعه بار معماری SharedPreferences را در پروژه های بزرگ تجاری بررسی کنیم.

نخستین و خطرناک ترین نقص SharedPreferences، شیوه بارگذاری داده ها در حافظه رم است. هنگامی که یک شیء SharedPreferences ساخته می شود، سیستم کل محتوای فایل XML را به صورت مسدودکننده (Blocking) از دیسک خوانده و در یک مپ حافظه قرار می دهد.

اگر کاربر در صفحه اصلی یا زمان اجرای اپلیکیشن (Application.onCreate) مقداری را فراخوانی کند، ترد اصلی تا زمان اتمام خواندن دیسک فریز می شود که این رخداد علت اصلی هشدارهای کندی و خطاهای فریز رابط کاربری (ANR) در گوگل پلی کنسول است.

مقایسه معماری شیرد پرفرنسز با دیتا استور در اندروید
مقایسه ساختاری جریان همگام و مسدودکننده SharedPreferences در برابر معماری ناهمگام و امن دیتا استور با کاتلین فلو

دومین ایراد اساسی، فقدان مکانیسم گزارش خطای قابل اعتماد است. متد apply() تغییرات را در پس زمینه روی دیسک می نویسد اما در صورت بروز خطای کمبود فضا یا مجوز دیسک، هیچ اکسپشن یا خطایی صادر نمی کند و داده های کاربر بی صدا نابود می شوند.

متد commit() نیز با نوشتن همگام روی دیسک، ترد اجراکننده را مسدود می نماید. علاوه بر این، ذخیره سازی داده ها در SharedPreferences بر پایه رشته های بدون تایپ امن انجام می گیرد.

چنانچه توسعه دهنده کلیدی را که قبلاً به عنوان عدد صحیح (Integer) ذخیره شده بود با متد getString() بخواند، برنامه با خطای وحشتناک ClassCastException در زمان اجرا کرش می کند.

جت پک دیتا استور با هدف ریشه کن ساختن تمامی این معضلات طراحی شده است و با ادغام طبیعی با زبان کاتلین، ذخیره سازی محلی را به بالاترین سطح اطمینان مهندسی ارتقا داده است.

۲. ماتریس مقایسه فنی: SharedPreferences در برابر دو مدل DataStore

دیتا استور در قالب دو پیاده سازی مستقل اما هماهنگ ارائه می شود: Preferences DataStore برای ذخیره کلید-مقدار ساده و Proto DataStore برای ذخیره اشیاء تایپ سیف با ساختار معین.

جدول زیر مقایسه ای همه جانبه میان این سه فناوری ذخیره سازی محلی در اکوسیستم اندروید ۲۰۲۶ ارائه می نماید:

شاخص ارزیابی فنی ابزار سنتی SharedPreferences مدل Preferences DataStore مدل پیشرفته Proto DataStore
مدل فراخوانی (Threading) همگام و مسدودکننده ترد اصلی (خطر خطای ANR) کاملاً ناهمگام روی ترد پس زمینه (Dispatchers.IO) کاملاً ناهمگام روی ترد پس زمینه (Dispatchers.IO)
شیوه بازگرداندن داده مقدار ایستا با متدهای گتر سنتی استریم زنده واکنشی از جنس Flow<Preferences> استریم زنده واکنشی از جنس Flow<T>
ایمنی نوع داده (Type Safety) خیر (خطر کرش با خطای ClassCastException) بررسی از طریق کلیدهای اختصاصی PreferencesKey ۱۰۰ درصد تایپ سیف در زمان کامپایل با Protocol Buffers
تضمین تراکنش ها (ACID) خیر؛ امکان ذخیره داده های نیمه کاره و ناقص بله؛ عملیات نوشتن به صورت اتمیک و تراکنشی بله؛ ذخیره سازی ترتیبی و اتمیک تضمین شده
گزارش خطا (Error Handling) فاقد خطا در متد apply و پرتاب خطاهای ناشناخته پرتاب صریح خطای IOException و CorruptionHandler پشتیبانی پیشرفته از CorruptionHandler و بازسازی خودکار
فرمت فایل روی دیسک فایل متنی XML با ساختار باز و بدون بهینه سازی فایل باینری بهینه شده بر پایه پروتکل بافر فایل باینری فوق فشرده با اسکیما (Protobuf Schema)
سهولت مهاجرت پایگاه سنتی پروژه های قدیمی مهاجرت با یک خط کد از طریق SharedPreferencesMigration مهاجرت از طریق تعریف مایگریشن دستی و مپ کردن داده ها

انتخاب میان دو مدل دیتا استور وابسته به پیچیدگی داده ها است: برای تنظیمات ساده نظیر وضعیت تم دارک و توکن اهراز هویت، Preferences DataStore کافی است؛ اما برای پروفایل های پیچیده با فیلدهای متعدد، Proto DataStore بی رقیب خواهد بود.

۳. پیش نیازها، افزودن وابستگی های گریدل و پیکربندی پروژه

برای افزودن دیتا استور به پروژه های مدرن اندروید، ابتدا باید وابستگی های مورد نیاز را در فایل build.gradle.kts ماژول اپلیکیشن یا ماژول دیتای پروژه تعبیه کنید.

بسته به اینکه مایل به استفاده از Preferences DataStore یا Proto DataStore هستید، قطعه کدهای زیر را به بلوک وابستگی ها اضافه نمایید:

// build.gradle.kts (Module: app)
dependencies {
    val dataStoreVersion = "1.1.1"

    // وابستگی اصلی برای ذخیره کلید-مقدار (Preferences DataStore)
    implementation("androidx.datastore:datastore-preferences:$dataStoreVersion")

    // وابستگی برای ذخیره داده های تایپ سیف (Proto DataStore)
    implementation("androidx.datastore:datastore:$dataStoreVersion")

    // کتابخانه پروتکل بافر برای Proto DataStore
    implementation("com.google.protobuf:protobuf-javalite:3.25.3")
}

در صورت استفاده از Proto DataStore، باید پلاگین کامپایلر پروتوباف را نیز در بالای فایل گریدل فعال کرده و مسیر فایل های proto. را در دایرکتوری سورس پروژه مشخص سازید. این کار فرآیند کامپایل را خودکار می کند.

۴. آموزش گام به گام Preferences DataStore: ذخیره کلید-مقدار مدرن

برای آغاز کار با Preferences DataStore، اولین گام ایجاد یک نمونه سینگلتون از طریق نماینده ویژگی (Property Delegate) در سطح بالای فایل است.

تعریف دیتا استور به صورت شیء واحد و در سطح کانتکست اپلیکیشن برای جلوگیری از خطای همزمانی و دسترسی همزمان دو پروسه الزامی است:

// تعریف سینگلتون دیتا استور در کاتلین
val Context.userPreferencesDataStore: DataStore by preferencesDataStore(
    name = "user_app_preferences"
)

پس از تعریف دیتا استور، کلیدهای دسترسی به مقادیر با استفاده از توابع نوع دار نظیر stringPreferencesKey، booleanPreferencesKey و intPreferencesKey تعریف می شوند:

// تعریف کلیدهای با تایپ امن
object PreferenceKeys {
    val USER_TOKEN = stringPreferencesKey("auth_user_token")
    val DARK_MODE_ENABLED = booleanPreferencesKey("is_dark_mode")
    val NOTIFICATION_COUNT = intPreferencesKey("unread_notifications")
}
تفاوت پریفرنسز دیتا استور و پروتو دیتا استور
مقایسه دو مدل معماری دیتا استور: ساختار سبک کلید-مقدار Preferences در برابر اسکیمای تایپ سیف کامپایلر با پروتوباف در Proto DataStore

به لطف معماری نوین مبتنی بر کروتین و فلو در کاتلین، تمامی درخواست های خواندن داده ها از دیتا استور از طریق ویژگی data که یک استریم از جنس Flow<Preferences> است انجام می پذیرد.

به دلیل ماهیت واکنشی کاتلین فلو، در صورت بروز هرگونه خطا در زمان خواندن دیسک، می توان استثنای IOException را با اپراتور catch مدیریت کرده و مقدار پیش فرض بازگرداند:

// خواندن امن استریم داده با مدیریت استثنای دیسک
val userTokenFlow: Flow = context.userPreferencesDataStore.data
    .catch { exception ->
        if (exception is IOException) {
            emit(emptyPreferences())
        } else {
            throw exception
        }
    }
    .map { preferences ->
        preferences[PreferenceKeys.USER_TOKEN]
    }

برای نوشتن یا ویرایش داده ها در Preferences DataStore، از تابع تعلیق پذیر (Suspend Function) و اتمیک edit استفاده می شود:

// ذخیره ناهمگام و تراکنشی مقدار در دیتا استور
suspend fun saveAuthToken(context: Context, token: String) {
    context.userPreferencesDataStore.edit { preferences ->
        preferences[PreferenceKeys.USER_TOKEN] = token
    }
}

متد edit به صورت یک تراکنش اتمیک عمل می کند؛ بدین معنا که یا تمام تغییرات در یک گام روی دیسک ذخیره می شوند یا در صورت وقوع خطا، هیچ تغییری اعمال نخواهد شد.

۵. آموزش پیشرفته Proto DataStore: تعریف اسکیما با Protocol Buffers

در پروژه های سطح بالای معماری که با مدل های داده ای ساختاریافته سر و کار داریم، نگه داری جداگانه ده ها کلید رشته ای و عددی منطقی نیست.

رویکرد Proto DataStore راهکاری ارائه می دهد که داده ها در قالب اشیاء کاتلین با نوع کاملاً مشخص و بدون نیاز به تبدیل دستی سریالایز و ذخیره شوند.

نخستین گام، ایجاد یک فایل با پسوند proto. در دایرکتوری app/src/main/proto است:

// app/src/main/proto/user_prefs.proto
syntax = "proto3";

option java_package = "ir.vahidhoseini.datastore";
option java_multiple_files = true;

message UserSettings {
  string user_id = 1;
  string display_name = 2;
  bool is_premium = 3;
  int32 app_launch_count = 4;
}

پس از بیلد پروژه، کامپایلر پروتوباف کلاسی با نام UserSettings تولید می کند. اکنون باید یک کلاس سریالایزر (Serializer) اختصاصی پیاده سازی کنیم:

// پیاده سازی سریالایزر برای خواندن و نوشتن بایت ها در Proto DataStore
object UserSettingsSerializer : Serializer {
    override val defaultValue: UserSettings = UserSettings.getDefaultInstance()

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

    override suspend fun writeTo(t: UserSettings, output: OutputStream) {
        t.writeTo(output)
    }
}

اکنون می توان نمونه دیتا استور را با سریالایزر مربوطه رجیستر کرد و اشیاء را به طور مستقیم و با ایمنی نوع کامل در زمان کامپایل ذخیره و بازخوانی نمود:

// تعریف دیتا استور تایپ سیف با سریالایزر
val Context.userSettingsDataStore: DataStore by dataStore(
    fileName = "user_settings.pb",
    serializer = UserSettingsSerializer
)

// بروزرسانی شیء با متد updateData
suspend fun upgradeToPremium(context: Context) {
    context.userSettingsDataStore.updateData { currentSettings ->
        currentSettings.toBuilder()
            .setIsPremium(true)
            .build()
        }
}

یکی از برتری های بنیادین پروتکل بافر نسبت به سریالایز کردن دستی JSON با کتابخانه هایی نظیر Gson یا Moshi، پشتیبانی مقتدرانه از تکامل اسکیما (Schema Evolution) و سازگاری رو به عقب است.

در نسخه های جدید اپلیکیشن، شما می توانید بدون ترس از شکستن نسخه های قبلی، فیلدهای جدیدی با تگ های عددی مجزا (مثلاً شماره ۵) به فایل پروتوباف اضافه کنید. کلاینت های قدیمی فیلدهای ناشناخته جدید را نادیده می گیرند و کلاینت های جدید برای فیلدهای ثبت نشده گذشته از مقادیر پیش فرض پروتوباف نسخه ۳ بهره مند می شوند.

۶. مدیریت خطای خرابی دیسک با CorruptionHandler

یکی از ترسناک ترین رخدادها در نگه داری داده های محلی، خاموش شدن ناگهانی تلفن یا پرتاب استثنای خرابی بایت ها بر اثر خطاهای سخت افزاری دیسک است.

دیتا استور مکانیزم بی نظیری به نام CorruptionHandler ارائه می دهد که در صورت مواجهه با فایل های معیوب، به جای کرش دادن نرم افزار، واکنشی هوشمند نشان می دهد:

// پیکربندی بازسازی خودکار در صورت خرابی فایل دیتا استور
val Context.safePreferencesDataStore: DataStore by preferencesDataStore(
    name = "secure_user_prefs",
    corruptionHandler = ReplaceFileCorruptionHandler(
        produceNewData = { emptyPreferences() } // بازنشانی امن داده ها
    )
)
قاعده حیاتی معماری غیرمسدودکننده: هرگز و تحت هیچ شرایطی از متد runBlocking برای استخراج داده ها از Flow دیتا استور در ترد اصلی استفاده نکنید. این کار مزیت غیرمسدودکنندگی را از بین برده و دقیقاً همان گلوگاه خطای ANR در SharedPreferences را ایجاد می نماید.

۷. ادغام در معماری تمیز: تلفیق دیتا استور با Repository و Jetpack Compose

در یک معماری استاندارد بر پایه اصول معماری تمیز، اکتیویتی ها و کدهای کامپوز نباید مستقیماً با کانتکست دیتا استور تعامل داشته باشند.

لایف سایکل و فرآیند ارتباط باید از طریق یک لایه ریپازیتوری (Repository) ایزوله شده و سپس در ویومدل (ViewModel) به صورت StateFlow به UI ارائه شود.

جریان واکنشی دیتا استور با کاتلین فلو و کامپوز
پایپ لاین واکنشی اند به اند: جریان داده از ذخیره ساز دیسک دیتا استور به استریم Flow، تبدیل به StateFlow در ویومدل و بازترکیب تمیز در کامپوز

کلاس ریپازیتوری زیر الگوی تفکیک دغدغه ها را به شیوه ای بی نقص پیاده سازی می کند:

// اینترفیس ریپازیتوری تنظیمات در لایه دامنه
interface UserPreferencesRepository {
    val isDarkMode: Flow
    suspend fun setDarkMode(enabled: Boolean)
}

// پیاده سازی ریپازیتوری با تزریق دیتا استور
class UserPreferencesRepositoryImpl(
    private val dataStore: DataStore
) : UserPreferencesRepository {

    private object Keys {
        val DARK_THEME = booleanPreferencesKey("theme_dark_mode")
    }

    override val isDarkMode: Flow = dataStore.data
        .map { preferences -> preferences[Keys.DARK_THEME] ?: false }

    override suspend fun setDarkMode(enabled: Boolean) {
        dataStore.edit { preferences ->
            preferences[Keys.DARK_THEME] = enabled
        }
    }
}

در لایه ویومدل، این جریان به یک StateFlow پایدار و مجهز به چرخه حیات با اپراتور stateIn تبدیل می شود:

// تبدیل استریم دیتا استور به StateFlow در ویومدل
class SettingsViewModel(
    private val repository: UserPreferencesRepository
) : ViewModel() {

    val isDarkMode: StateFlow = repository.isDarkMode
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = false
        )

    fun toggleTheme(enabled: Boolean) {
        viewModelScope.launch {
            repository.setDarkMode(enabled)
        }
    }
}

پارامتر SharingStarted.WhileSubscribed(5000) یک نکته کلیدی در عملکرد است؛ این پارامتر موجب می شود که اگر کاربر برنامه را مینیمایز کرد یا صفحه چرخش پیدا نمود، ارتباط با دیسک تا ۵ ثانیه فعال بماند و سپس برای بهینه سازی باتری قطع گردد.

در کامپوننت رابط کاربری جت پک کامپوز، با بهره گیری از تابع collectAsStateWithLifecycle()، به محض تغییر داده در دیسک، صفحه کامپوز بدون نیاز به رفرش دستی بازترکیب (Recompose) می شود:

// صفحه تنظیمات مدرن در کامپوز با گوش دادن به StateFlow
@Composable
fun SettingsScreen(
    viewModel: SettingsViewModel = hiltViewModel()
) {
    val isDarkMode by viewModel.isDarkMode.collectAsStateWithLifecycle()

    Scaffold(
        topBar = { TopAppBar(title = { Text("تنظیمات برنامه") }) }
    ) { paddingValues ->
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(paddingValues)
                .padding(16.dp),
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically
        ) {
            Text(text = "حالت شب (Dark Mode)", style = MaterialTheme.typography.bodyLarge)
            Switch(
                checked = isDarkMode,
                onCheckedChange = { viewModel.toggleTheme(it) }
            )
        }
    }
}

۸. تزریق وابستگی DataStore با ابزار Hilt

برای تضمین سینگلتون بودن نمونه دیتا استور و تسهیل تست نویسی واحد، نمونه سازی دیتا استور باید توسط فریم ورک تزریق وابستگی Hilt مدیریت گردد.

ماژول زیر در پکیج DI پروژه نمونه DataStore را تامین می نماید:

// ماژول Hilt برای تزریق امن دیتا استور
@Module
@InstallIn(SingletonComponent::class)
object DataStoreModule {

    @Provides
    @Singleton
    fun providePreferencesDataStore(
        @ApplicationContext context: Context
    ): DataStore {
        return context.userPreferencesDataStore
    }

    @Provides
    @Singleton
    fun provideUserPreferencesRepository(
        dataStore: DataStore
    ): UserPreferencesRepository {
        return UserPreferencesRepositoryImpl(dataStore)
    }
}

۹. امنیت داده ها و رمزنگاری پیشرفته در DataStore با Google Tink

به صورت پیش فرض، فایل های Preferences DataStore و Proto DataStore به عنوان داده های غیررمزگذاری شده روی دیسک ذخیره می شوند.

چنانچه تلفن همراه کاربر روت شده باشد یا نسخه پشتیبان برنامه استخراج شود، داده های حساسی نظیر توکن های احراز هویت JWT و کلیدهای دسترسی در معرض سرقت قرار می گیرند.

برای ایجاد یک دیتا استور امنیتی در رده سازمانی، می توان از کتابخانه رسمی رمزنگاری گوگل یعنی Google Tink بهره برد:

// الگوی رمزنگاری بایت های Proto DataStore با کلید سخت افزاری Tink
class EncryptedProtoSerializer(
    private val aead: Aead
) : Serializer {

    override val defaultValue: UserSettings = UserSettings.getDefaultInstance()

    override suspend fun readFrom(input: InputStream): UserSettings {
        val encryptedBytes = input.readBytes()
        val decryptedBytes = aead.decrypt(encryptedBytes, null)
        return UserSettings.parseFrom(decryptedBytes)
    }

    override suspend fun writeTo(t: UserSettings, output: OutputStream) {
        val rawBytes = t.toByteArray()
        val cipherText = aead.encrypt(rawBytes, null)
        output.write(cipherText)
    }
}

برای تامین شیء Aead، از کلاس امنیتی AndroidKeysetManager استفاده می شود که کلید اصلی رمزنگاری را در ماژول امنیتی سخت افزاری دستگاه (Android KeyStore) نگه داری می نماید:

// مقداردهی اولیه سیستم امنیتی Tink با استفاده از Android KeyStore
fun getAeadPrimitive(context: Context): Aead {
    AeadConfig.register()
    val keysetHandle = AndroidKeysetManager.Builder()
        .withSharedPref(context, "secure_keyset_sp", "keyset_prefs")
        .withKeyTemplate(KeyTemplates.get("AES256_GCM"))
        .withMasterKeyUri("android-keystore://_master_key_datastore_")
        .build()
        .keysetHandle
    return keysetHandle.getPrimitive(Aead::class.java)
}

این استراتژی پیشرفته تضمین می کند که داده ها حتی در صورت دسترسی مستقیم به سیستم فایل اندروید، بدون کلید مستقر در سخت افزار امنیتی غیرقابل خواندن باشند.

۱۰. پایپ لاین مهاجرت بدون قطعی از SharedPreferences به DataStore

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

تیم مهندسی اندروید مکانیزم هوشمند و بدون نقصی به نام SharedPreferencesMigration را به طور پیش فرض درون دیتا استور تعبیه کرده است:

پایپ لاین مهاجرت از شیرد پرفرنسز به دیتا استور
فرآیند مهاجرت اتمیک: خواندن خودکار داده ها از فایل XML شیرد پرفرنسز، انتقال به ساختار باینری دیتا استور و حذف ایمن کدهای لگاسی

پیاده سازی این فرآیند مهاجرت تنها به یک خط کد در زمان سازنده نیاز دارد:

// تعریف پایپ لاین مهاجرت خودکار از SharedPreferences
val Context.migratedDataStore: DataStore by preferencesDataStore(
    name = "user_preferences",
    produceMigrations = { context ->
        listOf(
            SharedPreferencesMigration(
                context = context,
                sharedPreferencesName = "legacy_user_settings_sp" // نام فایل قدیمی XML
            )
        )
    }
}

هنگامی که اپلیکیشن برای نخستین بار پس از آپدیت اجرا می شود، دیتا استور به صورت خودکار تمامی جفت های کلید-مقدار موجود در فایل XML قدیمی را استخراج کرده، در قالب یک تراکنش اتمیک وارد دیتا استور می کند و سپس فایل قدیمی XML را از حافظه پاک می سازد.

اگر مایلید تنها بخشی از کلیدها منتقل شوند یا در حین مهاجرت عملیات تغییر نوع انجام دهید، می توانید از پارامتر keysToMigrate بهره ببرید:

// مهاجرت گزینشی کلیدهای مشخص از SharedPreferences
SharedPreferencesMigration(
    context = context,
    sharedPreferencesName = "old_app_prefs",
    keysToMigrate = setOf("auth_token", "user_display_name")
)

۱۱. تست نویسی واحد و راستی آزمایی عملکرد DataStore

در مهندسی نرم افزار تمیز، امکان تست نویسی مستقل و ایزوله بدون وابستگی به سخت افزار فیزیکی موبایل الزامی است.

با تکیه بر دیسپچرهای تستی کاتلین و ایجاد فایل موقت، می توان تست های واحد بسیار سریعی برای ریپازیتوری های مبتنی بر دیتا استور نوشت:

// تست واحد ریپازیتوری دیتا استور با دیسپچر تستی
class UserPreferencesRepositoryTest {

    private val testDispatcher = StandardTestDispatcher()
    private val testScope = TestScope(testDispatcher + Job())

    @Test
    fun saveDarkMode_emitsUpdatedValueCorrectly() = testScope.runTest {
        val testContext = ApplicationProvider.getApplicationContext()
        val testDataStore = PreferenceDataStoreFactory.create(
            scope = testScope,
            produceFile = { testContext.preferencesDataStoreFile("test_prefs") }
        )

        val repository = UserPreferencesRepositoryImpl(testDataStore)
        repository.setDarkMode(true)

        val result = repository.isDarkMode.first()
        assertEquals(true, result)
    }
}

۱۲. اشتباهات مهلک و خطاهای رایج پروداکشن در استفاده از DataStore

حتی پیشرفته ترین ابزار ها در صورت استفاده نادرست می توانند زمینه ساز باگ های ناشناخته و رفتارهای غیرعادی در اپلیکیشن شوند.

رایج ترین خطاهایی که در بررسی کدهای پروژه های تجاری مشاهده می شوند عبارتند از:

  • ایجاد بیش از یک نمونه DataStore برای یک فایل مشخص: هرگز در دو کلاس مجزا نماینده preferencesDataStore با یک نام مشترک نسازید؛ این کار خطای وحشتناک IllegalStateException: There are multiple DataStores active for the same file را ایجاد می کند. دیتا استور باید همواره به صورت Singleton مدیریت شود.
  • فراخوانی ناایمن کلیدهای رشته ای با املا های متفاوت: برای جلوگیری از خطای انسانی، تمامی کلیدها را درون یک شیء object متمرکز قرار دهید.
  • مسدود کردن ترد UI با کدهای شتابزده: از تبدیل فلو به لیست در صفحه آغازین برنامه با متدهایی نظیر runBlocking اکیداً اجتناب نمایید.
توصیه طلایی عملکردی: دیتا استور برای مقادیر سبک و رکوردهای زیر صدها کیلوبایت طراحی شده است. چنانچه نیاز به ذخیره جداول پیچیده، روابط چند به چند یا کوئری های سنگین دیتابیسی دارید، باید از دیتابیس رسمی SQLite و دیتابیس Room در اندروید استفاده نمایید و از تحمیل بار اضافی به دیتا استور بپرهیزید.

۱۳. چک لیست ارزیابی نهایی و توصیه راهبردی تیم های مهندسی

برای انتخاب ابزار ذخیره سازی بهینه در سال ۲۰۲۶، درخت تصمیم گیری مهندسی زیر مسیر را به وضوح روشن می سازد:

  • تنظیمات کلید-مقدار سبک (تم، پرچم های وضعیت، توکن ها): استفاده بدون درنگ از Preferences DataStore.
  • داده های ساختاریافته با مقادیر از پیش مشخص (پروفایل کاربر، تنظیمات اعلان پیشرفته): استفاده از Proto DataStore با Protocol Buffers.
  • داده های رابطه ای، تراکنش های مالی و جداول بزرگ: بهره گیری از Room Database.
  • کدهای باستانی SharedPreferences: اجرای فوری پایپ لاین SharedPreferencesMigration و بایگانی کردن کدهای منسوخ گذشته.

پرسش های متداول و نکات کلیدی

۱. مهم ترین تفاوت میان SharedPreferences و DataStore در اندروید چیست؟

شیرد پرفرنسز داده ها را به صورت همگام می خواند و می تواند باعث قفل شدن ترد اصلی (ANR) شود، در حالی که دیتا استور به صورت کاملاً ناهمگام روی ترد پس زمینه بر بستر کاتلین فلو کار می کند و خطاهای مسدودکنندگی را ریشه کن می سازد.

۲. تفاوت میان Preferences DataStore و Proto DataStore چیست؟

مدل Preferences DataStore برای مقادیر کلید-مقدار بدون اسکیما (مشابه SharedPreferences) به کار می رود، در حالی که Proto DataStore از پروتکل بافر برای تعریف اشیاء ساختاریافته با ایمنی کامل نوع در زمان کامپایل بهره می برد.

۳. چگونه می توان داده های قبلی SharedPreferences را به DataStore منتقل کرد؟

با افزودن پارامتر SharedPreferencesMigration در زمان تعریف نماینده دیتا استور، سیستم عامل در اولین بارگذاری تمام داده ها را به صورت خودکار و اتمیک منتقل کرده و فایل قدیمی XML را پاک می کند.

۴. چرا استفاده از runBlocking برای خواندن داده های DataStore اشتباه است؟

زیرا متد runBlocking ترد جاری (معمولاً ترد اصلی رابط کاربری) را تا زمان اتمام عملیات دیسک قفل می کند و دقیقاً خطرات فریز و کرش های SharedPreferences سنتی را دوباره احیا می نماید.

۵. چگونه می توان وضعیت DataStore را در Jetpack Compose مشاهده کرد؟

با تبدیل فلو دیتا استور به StateFlow در ویومدل با متد stateIn و سپس گوش دادن به آن در صفحه کامپوز با متد collectAsStateWithLifecycle، صفحه بدون پرش یا مصرف بی رویه پردازنده به روز می شود.

۶. آیا DataStore داده ها را به صورت رمزگذاری شده روی حافظه ذخیره می کند؟

به صورت پیش فرض خیر؛ داده ها به شکل باینری غیررمز ذخیره می شوند. اما برای امنیت اطلاعات بانکی و حساس، می توان سریالایزر را با کلیدهای کتابخانه Google Tink به راحتی رمزگذاری نمود.

۷. علت خطای Multiple DataStores active for the same file چیست؟

این خطا زمانی رخ می دهد که دو نمونه مجزا از دیتا استور برای یک فایل دیسک یکسان تعریف شوند. برای حل مشکل، دیتا استور باید فقط یک بار و به صورت سینگلتون در سطح برنامه یا ماژول Hilt تعریف شود.

۸. در صورت خرابی فایل دیسک DataStore، چه اتفاقی رخ می دهد؟

در حالت عادی خطای CorruptionException پرتاب می شود؛ اما با تنظیم پارامتر ReplaceFileCorruptionHandler می توان فایل معیوب را به صورت خودکار با مقادیر پیش فرض جایگزین و بازسازی کرد.

۹. چه زمانی باید از Room Database به جای DataStore استفاده کنیم؟

دیتا استور صرفاً برای تنظیمات و داده های سبک مناسب است؛ اگر به کوئری های جستجوی پیچیده، روابط جدولی، صفحه بندی (Paging) و حجم بالای دیتا نیاز دارید، باید از دیتابیس روم استفاده نمایید.

۱۰. آیا یادگیری DataStore برای مصاحبه های کاری برنامه نویسی اندروید الزامی است؟

بله؛ شرکت های مدرن تسلط بر معماری ناهمگام کاتلین، مدیریت وضعیت با فلو و تسلط بر جایگزینی SharedPreferences با DataStore را به عنوان یکی از مهم ترین سوالات مصاحبه های استخدامی مطرح می کنند.

امتیاز و نظرات

۰.۰ (۰ نظر)
برای ثبت دیدگاه باید وارد حساب کاربری شوید. ورود

هنوز نظری ثبت نشده است. اولین نفر باشید.