سجاد رودی
سجاد رودی
خواندن ۳ دقیقه·۷ ماه پیش

استفاده از String Resource در View Model

در برنامه‌نویسی native اندروید و در معماری MVVM گاهی نیاز داریم در ViewModel به String Resourceها دسترسی داشته باشیم. در این صورت شاید با خودتون بگین «خب اینکه چالشی نداره که، همونجا R.string میزنیم و اینجوری به اون String Resourceای که میخوایم دسترسی پیدا میکنیم و پاسش میدیم به UI. اون ور هم با context تابع getString رو فراخوانی میکنیم و رشته‌ی نهایی رو میگیریم.». بله این راه‌حل اوکیه و مشکلی نداره. اما اگه اون منبع، placeholder هم داشته باشه که باید بهش پاس بدیم چی؟

در این صورت یکم کار پیچیده میشه. بنابراین به راه‌حل انعطاف‌پذیرتری نیاز داریم. راه‌حل‌های مختلفی ممکنه به ذهنمون برسه. یکی از اون راه‌حل‌ها که به نظر خیلی تمیز و خوبه اینه که از AndroidViewModel استفاده کنیم که به نظر راه‌حل خوبی میاد ولی بهتره سمتش نریم مگر اینکه واقعا مجبور باشیم. در ادامه به معایبش میپردازیم و بعدش میریم سراغ راه‌حل پیشنهادی من.



معایب استفاده از روش AndroidViewModel

- تخطی از قاعده‌ی Separation of Concerns

در واقع concern هندل کردن resourceها بر عهده‌ی Activity و Fragment هست و نه ViewModel. ما تا حد امکان باید سعی کنیم که از هندل resourceها در ViewModel پرهیز کنیم و این کار رو به همون Activity و Fragment بسپریم.

- سخت‌تر شدن تست‌نویسی

چون در صورت استفاده از AndroidViewModel، کلاس ViewModelمون رو به کامپوننت‌های اندرویدی وابسته کردیم و موقع Unit Test نوشتن، مجبوریم که Context رو هم پاس بدیم و این تست نوشتن برای این کلاس رو میتونه سخت‌تر کنه.

- هنگام تغییرات Locale، ویومدل آپدیت نمیشه

از اونجایی که ViewModel مثل کامپوننت‌های Activity و Fragment هنگام تغییر Configuration دوباره ایجاد نمیشن، بنابراین وقتی ViewModelمون در قید حیاته (!) و Locale تغییر کنه، در این صورت زبان متن‌های نوشته‌شده در صفحه‌ی مربوطه، آپدیت نمیشه و به همون زبان قبلی میمونه و این UX بدی خواهد داشت.



راه‌حل پیشنهادی: کلاس DynamicString

پیشنهاد من استفاده از کلاسی به اسم DynamicString هست. این کلاس به این صورت هست:

class DynamicString( @StringRes private val resId: Int, private vararg val args: Any ) { fun asString(context: Context): String { return context.getString(resId, *args) } }

فیلدها رو هم private تعریف میکنیم، چون واقعا نیاز نداریم از بیرون بهشون دسترسی داشته باشیم (encapsulation یادتون نره).

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

private val _message = MutableStateFlow<DynamicString?>(null) val message : StateFlow<DynamicString?> = _message

و توی همون ViewModel میتونیم اینجوری پرش کنیم:

_message.value = DynamicString(R.string.some_message, someArg)

فقط یه مشکلی داره این راه‌حل. اون هم اینه که اگه بخوایم رشته‌ای رو استفاده کنیم که placeholder نمیگیره، چیکار کنیم؟

واسه حل این مشکل، یه راه قشنگش اینه که یه String Resource تعریف کنیم به این شکل:

<string name=&quotplaceholder_text&quot translatable=&quotfalse&quot>%s</string>

حالا با توجه به String Resource بالا، میتونیم DynamicString رو به این شکل تغییر بدیم:

class DynamicString( @StringRes private val resId: Int, private vararg val args: Any ) { constructor(arg: Any) : this(R.string.placeholder_text, arg) fun asString(context: Context): String { return context.getString(resId, *args) } }

اگه هم بخوایم کاری کنیم که بشه به شکل تمیزی، یه رشته‌ی خالی ساخت،‌ میتونیم یه constructor دیگه هم اضافه کنیم:

class DynamicString( @StringRes private val resId: Int, private vararg val args: Any ) { constructor(arg: Any) : this(R.string.placeholder_text, arg) constructor() : this(&quot&quot) fun asString(context: Context): String { return context.getString(resId, *args) } }

اگه دقت کنین متد getString در کلاس Context چند تا امضاء داره. برای اینکه روش استفاده‌ی ما از کلاس خودمون و استخراج String ازش، از همین الگو پیروی کنه تا کد یکدستی داشته باشیم، میتونیم یه extension function روی کلاس Context تعریف کنیم و در این صورت میتونیم متد asString رو حذف کنیم.

class DynamicString( @StringRes private val resId: Int, private vararg val args: Any ) { constructor(arg: Any) : this(R.string.placeholder_text, arg) constructor() : this(&quot&quot) companion object { fun Context.getString(dynamicString: DynamicString?): String? { dynamicString ?: return null return getString(dynamicString.resId, dynamicString.args) } } }

حالا در لایه‌ی UI که در Activity یا Fragment وجود داره، میشه همچین کدی زد:

val message = Context.getString(viewModel.message.value) // Show message

و تمام.

امیدوارم این راه‌حل براتون مفید باشه و به کارتون بیاد. اگه پیشنهادی، بهبودی چیزی سراغ دارین حتما مطرح کنین. همچنین حس میکنم اسم DynamicString خیلی جالب نباشه، اگه اسم بهتری سراغ دارین، خوشحال میشم بگین :)


string resourceviewmodelandroidاندرویدmvvm
توسعه دهنده‌ی اندروید
شاید از این پست‌ها خوشتان بیاید