در طول سالها من با پستها و مقالات بسیاری که به ابزار یا افزونههای مفیدی اختصاص داده شدهاند روبرو شدهام. "5 تا از بهترین افزونههای مفید کاتلین "، "10 ابزار برتر کاتلین "، "20 افزونه برتر کاتلین "، "100 چیز مورد نیاز برتر". همچنین شما میتوانید مقالات زیادی در سرتاسر اینترنت پیدا کنید: اینجا، اینجا، این، آن و اینجا.
اینجا نمیخواهم در مورد این مقالات صحبت کنم و آنها را بررسی کنم. در عوض تلاشی را که نویسندگان آنها برای سهولت ما انجام دادند را دوست دارم. و این به ما بستگی دارد که چه تجربهای رو قبول کنیم. و همچنین به من ایدهای برای به اشتراک گذاشتن افزونهها و ابزارهایی که در توسعه پروژه به ما کمک میکند داد.
مقداری چایی یا قهوه آماده کنید زیرا من فقط 5 یا 20 متد برای شما به اشتراک نمیگذارم، بلکه راه حلهایی برای بهبود کد در زمینههای مختلفی ارائه میدهم.
1- Resource Provider روشی برای بسته بندی متابع در اندروید
بیان مشکل
اندروید راهی برای کار با منابع دارد تا آنها را از پوشه res خارج کند. شما از context یا resource برای گرفتن عدد صحیح، رشتهها و ... استفاده میکنید. اما موارد زیر را تصور کنید:
- شما از ViewHolder استفاده میکنید و در مرحله اتصال، میخواهید ابعاد را به عنوان یک عدد صحیح دریافت کنید، بنابراین باید چیزی شبیه به کد زیر بنویسید.
viewHolder.itemView.context.resources.getDimensionPixelSize(R.dimen.my_dimen)
//which is quite long :)
- فرض کنید منطق شما در لایه presentation قرار دارد، و میخواهید منطق از Ui جدا شود، و میخواهید منابع را بستهبندی کنید تا مستقیماً از context یا resource استفاده نکنید. برای مثال شما یک mapper دارید که Ui را براساس مدل دامنه شما ایجاد میکند. و میخواهید آن را با تست واحد پوشش دهید.
هر دو مورد میتواند:
- نادیده گرفته شوند. شما لزوماً یک خط مشی برای تست واحد در پروژه ندارید. و لزوماً مشکلی با دیدن کدهای تکراری ندارید.
- اولین مشکل را میتوان با ایجاد BaseViewHolder و یک متد برای دریافت int dimen حل کرد. مشکل دوم را میتوان با mock(تقلید) context یا Resource حل کرد.
راه حل
ما روشی را ارائه میدهیم که هر دو مشکل را حل میکند و همزمان با ساختار قدیمی "پیاده سازی اینترفیس" برای تست واحد بدون نیاز به mock کردن چیزی در اندروید کار کنیم.
interface ResourcesProvider {
val isRtl: Boolean
@ColorInt
fun getColor(@ColorRes resId: Int): Int
fun getString(@StringRes resId: Int): String
fun getString(@StringRes resId: Int, vararg args: Any): String
fun getDimen(@DimenRes resId: Int): Float
fun getDimenInt(@DimenRes resId: Int): Int
//... other things, above methods just for the example
}
@Suppress("TooManyFunctions")
inline class AppResourcesProvider(
private val context: Context
) : ResourcesProvider {
override val isRtl: Boolean get() = context.isRtl
@ColorInt
override fun getColor(resId: Int) = context.getColorCompat(resId)
override fun getColorStateList(resId: Int): ColorStateList? = context.getColorStateListCompat(resId)
override fun getString(resId: Int) = context.getString(resId)
override fun getString(resId: Int, vararg args: Any) = context.getString(resId, *args)
override fun getDimen(resId: Int): Float = context.resources.getDimension(resId)
override fun getDimenInt(resId: Int) = context.resources.getDimensionPixelSize(resId)
//implementation for another methods
}
لطفا توجه داشته باشید که ما AppResourceProvider را به عنوان یک کلاس inline ایجاد کردیم، این یعنی که در برخی شرایط میتوان از تخصیص اشاء اضافی جلوگیری کرد. اما در مورد mapper اینطور نیست، زیرا شما مرجع را به اینترفیس منتقل میکنید، بنابراین پیاده سازی واقعی فراهم میشود.
لطفا برای سناریو فوق با ViewHolder به یک مقاله قدیمی در مورد declarative adapter نگاه کنید. بخشی به نام مدیریت منابع وجود دارد. BaseViewHolder بعدی به صورت زیر خواهد بود:
abstract class ResViewHolder(
itemView: View,
) : RecyclerView.ViewHolder(itemView), ResourcesProvider by AppResourcesProvider(itemView.context)
که بسیار خلاصهتر از این است که همه متدها را در ViewHolder تعریف کرد.
مزایای بیشتر
چند مزیت مهم این روش علاوه بر تقسیم به موجودی جداگانه، استفاده مختصرتر و تست واحد است.
- همانطور که ذکر کردم بعضی جاها نمونهای از مدیر منابع ایجاد نمیکنید، که میتواند در سایر افزونهها و رویکردهای دیگر در کد مفید باشد. مثال بعدی را در نظر بگیرید که شما یک کتابخانه view-binding دارید و میخواهید به راحتی به منابع دسترسی داشته باشید، میتوانید به راحتی با یک افزونه این کار را انجام دهید.
val ViewBinding.res get() = AppResourcesProvider(root.context)
//now compare
//1. root.context.resources.getColor(R.color.white)
//2. res.getColor(R.color.white)
//And no object allocation, since you using `AppResourcesProvider` directly
//Bonus, you can also write that extension for View
val View.res get() = AppResourcesProvider(context)
- میتوانید متدهای بیشتری را در ResourceProvider قرار دهید. چند وقت پیش مقالهای در مورد declarative work with spannable با کمک کاتلین نوشتم. زمان گذشت و رویکرد تکامل یافت. بنابراین با یک ResourceProvider جدید میتوانید متدهای مورد نیاز را اضافه کنید. و بلافاصله در همه جا ظاهر میشوند: mapperها، view holderها، bindingها. بدون هیچ کار اضافی.
interface ResourcesProvider {
//...
fun buildSpannable(separator: CharSequence = "", reverse: Boolean = isRtl, init: SpannableResCreator.() -> Unit): CharSequence
fun getSpannable(text: CharSequence, spanInit: ResSpans.() -> Unit): CharSequence
fun <T : Any> getSpannable(@StringRes resId: Int, vararg spanArgs: SpannableResCreator.Span<T>): CharSequence
fun getSpannable(@StringRes resId: Int, vararg spanArgs: Pair<Any, Iterable<Any>>): CharSequence
}
inline class AppResourcesProvider(
private val context: Context
) : ResourcesProvider {
//...put your implementation details here
}
//You can even add extensions to the interface, if you don't want to make some methods vitual 🤔
fun ResourcesProvider.spans(spanInit: ResSpans.() -> Unit) //...
infix fun <T> T.withSpans(spanInit: ResSpans.() -> Unit) //...
2- پیدا کردن enumها – حذف همه static/companion متدها
بیان مشکل
در پروژه ما، یعنی همه پروژههایی که من کار کردم، هر از چندگاهی سناریوهایی وجود دارد که شما کد یا id دارید و میخواهید با آن enum را پیدا کنید.
enum class Company(val code: String) {
GOOGLE("ggl"),
FACEBOOK("fcbk"),
SIDENIS("sdns"), //small "Hello" to my first company
UNKNOWN("let's say somebody uses it instead of null");
companion object {
fun get(code: String): Company =
values().find { it.code == code } ?: UNKNOWN
}
}
قبلاً نیاز بود که فیلتر را برای هر enum انجام دهم. اما دیگه کافیه!
راه حل
inline fun <reified T : Enum<T>> requireEnumBy(
fallback: EnumSet<T>.() -> T = EnumSet<T>::defFallback,
predicate: (T) -> Boolean
): T = requireEnumBy(T::class.java, fallback, predicate)
inline fun <reified T : Enum<T>> findEnumBy(
predicate: (T) -> Boolean
): T? = with(EnumSet.allOf(T::class.java)) { find(predicate) }
inline fun <T : Enum<T>> requireEnumBy(
enumCls: Class<T>,
fallback: EnumSet<T>.() -> T = { defFallback(enumCls) },
predicate: (T) -> Boolean
): T = with(EnumSet.allOf(enumCls)) { find(predicate) ?: fallback() }
inline fun <reified T : Enum<T>> find(predicate: (T) -> Boolean): T? = EnumSet.allOf(T::class.java).find(predicate)
inline fun <reified T : Enum<T>> EnumSet<T>.defFallback(): T = defFallback(T::class.java)
fun <T : Enum<T>> EnumSet<T>.defFallback(enumCls: Class<T>): T = firstOrNull()
?: throw IllegalStateException("In the enum ${enumCls.simpleName} there should be at least one enumeration")
به طور کلی ما دو روش داریم:
- requireEnumBy، یک enum از نوع T برمیگرداند که با predicate که شما به آن دادهاید مطابقت دارد یا با استفاده از fallback، enum پیش فرض را برمیگرداند.
- findEnumBy، یک enum از نوع T برمیگرداند، اما اگر با هیچ کدام مطابقت نداشته باشد null برمیگرداند.
بیایید ببینیم که چگونه میتوانیم از این روش با مثال بالا استفاده کنیم.
enum class Company(val code: String) {
GOOGLE("ggl"),
FACEBOOK("fcbk"),
SIDENIS("sdns"), //small "Hello" to my first company
UNKNOWN("let's say somebody uses it instead of null");
}
fun example() {
requireEnumBy<Company> { it.code == "fcbk" } //returns Company.FACEBOOK
requireEnumBy<Company> { it.code == "ppl" } //returns Company.GOOGLE as def fallback just takes first element, but you can change that :)
requireEnumBy<Company>({ Company.UNKNOWN }) { it.code == "ppl" } //returns Company.UNKNOWN
findEnumBy<Company> { it.code == "ggl" } //returns Company.GOOGLE
findEnumBy<Company> { it.code == "ppl" } //returns null
}
معایب
تنها عیب این روش این است که val code: String یا فیلدهای enum دیگر، که در فیلتر شرکت میکنند باید عمومی باشند.
ما میتوانیم یک راه حل جدید ایجاد کنیم:
enum class Company(private val code: String) {
GOOGLE("ggl"),
FACEBOOK("fcbk"),
SIDENIS("sdns"), //small "Hello" to my first company
UNKNOWN("let's say somebody uses it instead of null");
companion object {
fun get(code: String): Company = requireEnumBy({ UNKNOWN }) { it.code == code }
}
}
اما در این صورت، مزیت روش enum زیاد نیست. در پروژه ما استفاده از public val برای enumها مشکلی ندارد، زیرا اکثر آنها به عنوان مدل در نظر گرفته میشوند و ما میتوانیم فیلدهای مدل را عمومی در نظر بگیریم. در اینجا فقط خواستم که به شما هشدار بدم.
3- View Inline Classes – روش declarative برای انجام کارها
بیان مشکل
بعضی اوقات میخواهیم برخی offsetهای View را تغییر دهیم. اضافه کردن padding، کاهش margin. و گاهی انجام این کار از طریق xml دشوار است، زیرا ممکن است به هردلیلی بخواهید آن را به صورت پویا سازگار کنید.
معمولاً با padding همه چیز کم و بیش آسان میشود، اگر از متد get استفاده کنیم. فقط با استفاده از view.paddingStart, view.paddingTop و ... .اما برای اضافه کردن padding جدید باید همانند زیر عمل کنید.
view.setPaddingRelative(
newPadding,
view.paddingTop,
view.paddingEnd,
view.paddingBottom
)
که این روش بسیار ناخوشایند است. با استفاده از KTX وضعیت شما خیلی بهتر نمیشود.
view.updatePaddingRelative(start = newPadding)
اوضاع با margin بدتر است زیرا شما نمیتوانید به راحتی آن را get یا set کنید زیرا این اطلاعات مربوط به MarginLayoutParam است. دوباره KTX یک راه حل کاملاً عجیب ارائه میدهد. از یک طرف میگویند "در اینجا دستهای از متدهای get مفید وجود دارد": view.marginStart, view.marginEnd و... .از طرفی چیزی مانند view.marginStart = وجود ندارد، در عوض باید چیزی شبیه به کد زیر بنویسید:
view.updateLayoutParams {
updateMarginsRelative(start = newMargin)
}
که برای کاربر نهایی(مهندسی که فقط میخواهد margin را بروز کند) نیز مناسب نیست.
راه حل
راه حل اساساً دستیابی به حداکثر رویکرد declarative ممکن است. چیزی شبیه به:
view.margin.start = newMargin
view.padding.total = newPadding
view2.padding.bottom = view1.padding.top
کاتلین با معرفی کلاسهای inline به ما کمک میکند،و این امکان را میدهد که از تخصیص شئ جلوگیری کرده و کد شما را به همان شیوه ساختار ببخشد.
val View.margin get() = ViewMargin(this)
val View.padding get() = ViewPadding(this)
inline class ViewMargin(private val view: View) {
var start: Int
get() = view.marginStart
set(value) = applyMargin { marginStart = value }
var top: Int
get() = view.marginTop
set(value) = applyMargin { topMargin = value }
var end: Int
get() = view.marginEnd
set(value) = applyMargin { marginEnd = value }
var bottom: Int
get() = view.marginBottom
set(value) = applyMargin { bottomMargin = value }
var horizontal: Int
get() = start + end
set(value) {
start = value
end = value
}
var vertical: Int
get() = top + bottom
set(value) {
top = value
bottom = value
}
var total: Int
@Deprecated("No getter for that field", level = DeprecationLevel.HIDDEN)
get() = throw UnsupportedOperationException("No getter for such field")
set(value) {
horizontal = value
vertical = value
}
private inline fun applyMargin(body: ViewGroup.MarginLayoutParams.() -> Unit) {
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
view.updateLayoutParams(body)
} else {
throw IllegalStateException("Parent layout doesn't support margins")
}
}
}
inline class ViewPadding(private val view: View) {
var start: Int
get() = view.paddingStart
set(value) = view.setPaddingRelative(value, top, end, bottom)
var top: Int
get() = view.paddingTop
set(value) = view.setPaddingRelative(start, value, end, bottom)
var end: Int
get() = view.paddingEnd
set(value) = view.setPaddingRelative(start, top, value, bottom)
var bottom: Int
get() = view.paddingBottom
set(value) = view.setPaddingRelative(start, top, end, value)
var horizontal: Int
get() = start + end
set(value) {
start = value
end = value
}
var vertical: Int
get() = top + bottom
set(value) {
view.updatePaddingRelative(start = newPadding)
top = value
bottom = value
}
var total: Int
@Deprecated("No getter for that field", level = DeprecationLevel.HIDDEN)
get() = throw UnsupportedOperationException("No getter for such field")
set(value) {
horizontal = value
vertical = value
}
}
اکنون میتوانید به یک روش کاملاً declarative دست یابید که بسیار راحتتر از راه حلهای KTX است.
//Just compare
view.padding.top = 9
//vs
view.updatePaddingRelative(top = 9)
view.margin.end = 6
//vs
view.updateLayoutParams {
updateMarginsRelative(end = 6)
}
مزایا
نه تنها راه حل ما خواناتر و گویاتر است، بلکه میتوان همانند xml، از view.padding.vertical یا view.margin.horizental که سادهتر است استفاده کرد.
نسخه بعدی این راه حل کلاسهایinline ا اضافی است که مسئول تنظیم منابع هستند.
فرض کنید که میخواهید dimen خاصی که در پوشه res تعریف شده است را get کنید:
view.margin.start = view.context.resources.getDimensionPixelSize(R.dimen.small)
یا با AppResourceProvider که در ابتدای مقاله گفتیم میتوانید این کار را بکنید:
view.margin.start = view.res.getDimenInt(R.dimen.small)
بنابراین بیایید یک راه حل دیگر ارائه دهیم تا رویکرد ما حتی از این هم سادهتر شود:
val View.marginRes get() = ViewMarginRes(this)
val View.paddingRes get() = ViewPaddingRes(this)
inline class ViewMarginRes(val view: View) {
var start: Int
@Deprecated("No getter for that field", level = DeprecationLevel.ERROR) get () = throw IllegalStateException("No getter for such field")
set(@DimenRes value) { view.margin.start = dimen(value) }
var top: Int
@Deprecated("No getter for that field", level = DeprecationLevel.ERROR) get () = throw IllegalStateException("No getter for such field")
set(@DimenRes value) { view.margin.top = dimen(value) }
var end: Int
@Deprecated("No getter for that field", level = DeprecationLevel.ERROR) get () = throw IllegalStateException("No getter for such field")
set(@DimenRes value) { view.margin.end = dimen(value) }
var bottom: Int
@Deprecated("No getter for that field", level = DeprecationLevel.ERROR) get () = throw IllegalStateException("No getter for such field")
set(@DimenRes value) { view.margin.bottom = dimen(value) }
var horizontal: Int
@Deprecated("No getter for that field", level = DeprecationLevel.ERROR) get () = throw IllegalStateException("No getter for such field")
set(@DimenRes value) {
start = value
end = value
}
var vertical: Int
@Deprecated("No getter for that field", level = DeprecationLevel.ERROR) get () = throw IllegalStateException("No getter for such field")
set(@DimenRes value) {
top = value
bottom = value
}
var total: Int
@Deprecated("No getter for that field", level = DeprecationLevel.ERROR) get () = throw IllegalStateException("No getter for such field")
set(@DimenRes value) {
horizontal = value
vertical = value
}
private fun dimen(@DimenRes res: Int) =
view.context.resources.getDimensionPixelSize(res)
}
inline class ViewPaddingRes(val view: View) {
var start: Int
@Deprecated("No getter for that field", level = DeprecationLevel.ERROR) get () = throw IllegalStateException("No getter for such field")
set(@DimenRes value) { view.padding.start = dimen(value) }
var top: Int
@Deprecated("No getter for that field", level = DeprecationLevel.ERROR) get () = throw IllegalStateException("No getter for such field")
set(@DimenRes value) { view.padding.top = dimen(value) }
var end: Int
@Deprecated("No getter for that field", level = DeprecationLevel.ERROR) get () = throw IllegalStateException("No getter for such field")
set(@DimenRes value) { view.padding.end = dimen(value) }
var bottom: Int
@Deprecated("No getter for that field", level = DeprecationLevel.ERROR) get () = throw IllegalStateException("No getter for such field")
set(@DimenRes value) { view.padding.bottom = dimen(value) }
var horizontal: Int
@Deprecated("No getter for that field", level = DeprecationLevel.ERROR) get () = throw IllegalStateException("No getter for such field")
set(@DimenRes value) {
start = value
end = value
}
var vertical: Int
@Deprecated("No getter for that field", level = DeprecationLevel.ERROR) get () = throw IllegalStateException("No getter for such field")
set(@DimenRes value) {
top = value
bottom = value
}
var total: Int
@Deprecated("No getter for that field", level = DeprecationLevel.ERROR) get () = throw IllegalStateException("No getter for such field")
set(@DimenRes value) {
horizontal = value
vertical = value
}
private fun dimen(@DimenRes res: Int) =
view.context.resources.getDimensionPixelSize(res)
}
و حالا فقط کافیست کد زیر را بنویسید:
view.marginRes.start = R.dimen.small
که این روش بسیار مختصرتر است و خوانایی را هم از دست نداده است.
4- view Attributes – چیزهای کوچک برای راحتتر کردن کار
بیان مشکل
بعضی اوقات همه ما viewهای سفارشی ایجاد میکنیم. و گاهی attributeهای سفارشی به آنها اضافه میکنیم (یا به جای آن از برخی ویژگیهای اندروید دوباره استفاده میکنیم). اما کار با آنها همیشه باعث دردسر است. شما باید به خاطر بسپارید که با استفاده از obtainStyledAttribute همه attributeها را بدست بیاورید و سپس TypedArray دریافت شده را بازیافت کنید.
خب درواقع خیلی هم بد نیست، اما میتواند که این فکر را از سر برنامهنویس پاک کند. ابتدا میخواستم بگویم که یک متد خاص برای آن ایجاد کنیم، اما متوجه شدم که بچههای KTX قبلاً آن را برای ما ایجاد کردهاند. اگرچه بهترین راه حل برای مشکل ما نیست.
مشکل دیگر گرفتن typeface از attributeها است. یک متد getFont وجود دارد که فقط از Api نسخه 26 در دسترس است (بنابراین ما نیز باید این را به نوعی انجام دهیم).
راه حل
برای حل این مشکل، ما چندین متد معرفی کردیم:
@UseExperimental(ExperimentalContracts::class)
inline fun AttributeSet.parseAttrs(context: Context, @StyleableRes attrs: IntArray, parser: TypedArray.() -> Unit) {
contract { callsInPlace(parser, InvocationKind.EXACTLY_ONCE) }
context.obtainStyledAttributes(this, attrs).use(parser)
}
@UseExperimental(ExperimentalContracts::class)
inline fun TypedArray.use(block: TypedArray.() -> Unit) {
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
try {
block()
} finally {
recycle()
}
}
/**
* Unfortunatelly we need to pass context, as there is no proper way to parse font from fonts resource.
* There is variant to parse raw resource and get it as InputStream, then convert it into file and then pass to Typeface.
* But it is much easier to pass context.
*/
fun TypedArray.getTypeface(context: Context, @StyleableRes index: Int) =
runCatching { getFontCompat(context, index) }.getOrNull() ?:
getString(index)?.let { Typeface.create(it, Typeface.NORMAL) }
fun TypedArray.getFontCompat(context: Context, @StyleableRes index: Int): Typeface? {
return if (atLeastOreo()) {
getFont(index)
} else {
context.getFontCompat(getResourceId(index, -1))
}
}
fun Context.getFontCompat(@FontRes fontId: Int): Typeface? =
runCatching { ResourcesCompat.getFont(this, fontId) }.getOrNull()
اول از همه بیایید درباره متد use بحث کنیم. بچههای KTX راه حل خود را ارائه دادهاند، اما آنها contract برای آن قرار ندادهاند. بنابراین با متد آنها نوشتن چیزی مانند کد زیر امکان پذیر نیست.
val str: String
context.obtainStyledAttributes(attrSet, R.styleable.SomeView).use {
str = getString(R.styleable.some_text) ?: ""
}
شما با چنین اروری مواجه میشوید:
دوم اینکه در اینجا parseAttrs را معرفی میکنیم، بنابراین شما نیازی به نوشتن هر بار متد use ندارید. همانطور که مشخص است پیشرفتی کوچک اما مفید است.
و سوم امکان دریافت فونت از AttributeSet است.
هنگامی که یک view سفارشی دارید که متن را با Canvas و paint نشان میدهد، میتوانید متن را از attributeSet تجزیه کنید که بسیار مفید است.
val TEXT_APPEARANCE_SUPPORTED_ATTRS = intArrayOf(
android.R.attr.textSize,
android.R.attr.textColor,
androidx.appcompat.R.attr.fontFamily,
android.R.attr.fontFamily
)
@SuppressWarnings("Recycle")
@UseExperimental(ExperimentalContracts::class)
inline fun Context.parseTextAppearance(@StyleRes taStyle: Int, parser: TextAppearance.() -> Unit) {
contract { callsInPlace(parser, InvocationKind.EXACTLY_ONCE) }
obtainStyledAttributes(taStyle, TEXT_APPEARANCE_SUPPORTED_ATTRS).use { TextAppearance(this@parseTextAppearance, this).apply(parser) }
}
/**
* Unfortunately we can not make this class inline (and it should be inline), since
* we need to pass context for getting Typeface from TypedArray for pre Oreo devices
*
* We can split this class into two implementations and make one of them inline, but this will not
* help, since we will work with it through the interface, so inline implementation will transform
* into ordinary implementations. This is sad. :(
*/
@Suppress("ResourceType")
class TextAppearance(
private val context: Context,
private val typedArray: TypedArray
) {
val size: Float get() = typedArray.getDimension(0, 0f)
@get:ColorInt
val color: Int get() = typedArray.getColor(1, 0)
val font: Typeface? get() = typedArray.getTypeface(context, 2) ?: typedArray.getTypeface(context, 3)
}
سپس در view سفارشی میتوانید موارد زیر را انجام دهید:
private fun AttributeSet.init() = parseAttrs(context, R.styleable.MyView) {
parseTextAppearance(R.styleable.MyView_titleAppearance, titlePaint)
parseTextAppearance(R.styleable.MyView_labelAppearance, labelPaint)
//...parse other attrs
}
private fun TypedArray.parseTextAppearance(@StyleableRes index: Int, target: Paint) {
context.parseTextAppearance(getResourceId(index, R.style.Text_Primary)) {
target.textSize = size
target.color = color
target.typeface = font
}
}
5- Result<T> افزونه کوچک – آخرین و کوچک اما نه کمترین
99 درصد از مقاله تمام شده است. بخش آخر یک بخش کوچک است، زیرا فقط شامل چند افزونه کوچک است که هنوز برای توسعه ما بسیار مهم است.
بیان مشکل
ما از Result<T> در کد خود بسیار استفاده میکنیم. ما با کوروتینها و جریانها کار میکنیم، اما ایده نوشتن بلوکهای try/catche را همیشه دوست نداریم. برای همراهانی مانند ما، تیم کاتلین متد عالی runCatching{} را ایجاد کرده است که نتیجه را برمیگرداند. این کلاس inline شامل نتیجه یا exception + گروهی از متدهای مفید برای کنترل جریان شما پس از دریافت نتیجه (خطا) است.
اما چند موقعیت نادیده گرفته شده است:
- هنگامی که شما یک فراخوانی پی در پی دارید و فراخوانی دوم به نتیجه فراخوانی اول بستگی دارید، اما هنوز هم میتواند موارد exception را ایجاد کند، یک متد عالی به اسم mapCatching وجود دارد. اما اگر متد دوم اکنون Result<T2> را بازگرداند چه باید به جای T2 گذاشت؟ در این صورت mapCatching، Result<Result<T2>> را برمیگرداند.
- وقتی متد شما یک exception را ایجاد میکند، میتوانید آن را در متد onFailure کنترل کنید. اما فرض کنید شما باید exception را به مورد دیگری تبدیل کنید. در این صورت ما برای مثال همه Retrofit Exceptionها را با ساختار OurCompanyException با اطلاعات اضافی تبدیل میکنیم، مانند backendErrorCode و... . چگونه میتوان این کار را انجام داد؟
راه حل
inline fun <T, R> Result<T>.then(transform: (T) -> Result<R>): Result<R> =
mapCatching { transform(it).getOrThrow() }
inline fun <T> Result<T>.mapError(action: (Throwable) -> Throwable): Result<T> {
return when(val exception = exceptionOrNull()) {
null -> runCatching { getOrThrow() }
else -> Result.failure(action(exception))
}
}
در اینجا دو متد کوچک اما قدرتمند آورده شده است که مشکلات ما را برطرف میکند.
فرض کنید کد زیر را داریم:
interface Api {
suspend fun getUser(id: Int): Result<User>
suspend fun getAllUsers(name: String): Result<List<User>>
}
class MyCompanyException(
//with a lot of cool stuff inside
) : Exception(...)
با کمک دو متد فوق میتوانید به راحتی چیزی مانند کد زیر را بنویسید:
fun findAllSimilarUsers(userId: Int): Result<List<User>> =
api.getUser(id)
.then { api.getAllUsers(it) }
.mapError { MyCompanyException(it) }
//or something like that
دیدگاه و پرسش
در حال دریافت نظرات از سرور، لطفا منتظر بمانید
در حال دریافت نظرات از سرور، لطفا منتظر بمانید