در برنامهنویسی native اندروید و در معماری MVVM گاهی نیاز داریم در ViewModel به String Resourceها دسترسی داشته باشیم. در این صورت شاید با خودتون بگین «خب اینکه چالشی نداره که، همونجا R.string میزنیم و اینجوری به اون String Resourceای که میخوایم دسترسی پیدا میکنیم و پاسش میدیم به UI. اون ور هم با context تابع getString رو فراخوانی میکنیم و رشتهی نهایی رو میگیریم.». بله این راهحل اوکیه و مشکلی نداره. اما اگه اون منبع، placeholder هم داشته باشه که باید بهش پاس بدیم چی؟
در این صورت یکم کار پیچیده میشه. بنابراین به راهحل انعطافپذیرتری نیاز داریم. راهحلهای مختلفی ممکنه به ذهنمون برسه. یکی از اون راهحلها که به نظر خیلی تمیز و خوبه اینه که از 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 هست. این کلاس به این صورت هست:
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="placeholder_text" translatable="false">%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("") 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("") 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 خیلی جالب نباشه، اگه اسم بهتری سراغ دارین، خوشحال میشم بگین :)