۲۲ اکستنشن کاتلین برای نوشتن کد تمیز | به زبان ساده

۶۲ بازدید
آخرین به‌روزرسانی: ۳ مهر ۱۴۰۲
زمان مطالعه: ۱۰ دقیقه

یکی از دلایل محبوبیت گسترده‌تر زبان کاتلین (Kotlin) در میان توسعه‌دهندگان موبایل، پشتیبانی آن از اکستنشن‌ها است. اکستنشن‌ها به برنامه‌نویس امکان می‌دهند که متدها را به هر کلاس موجود و یا حتی به نوع Any و Optional (مانند ?Int) اضافه کند. در این مقاله با 22 اکستنشن کاتلین برای نوشتن کد تمیز آشنا خواهیم شد.

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

در این مقاله از کاتلین 1.4.0 برای اندروید در اندروید استودیو نسخه 4.0.1 استفاده شده است. همه متدهای معرفی شده مربوط به کاتلین و نه جاوا هستند. با این حال غالب اکستنشن‌ها در نسخه‌های دیگر کاتلین و حتی برخی از آن‌ها در محیط‌های دیگر نیز کار می‌کنند.

()Int.toDate و Int.asDate

در اغلب موارد نیاز داریم که تاریخ و زمان را به صورت یک timestamp دریافت کنیم. منظور از timestamp تعداد ثانیه‌هایی (گاهی میلی‌ثانیه‌ها) است که از 1 ژانویه 1970 سپری شده است. این زمان در علم رایانه epoch نامیده می‌شود.

این اکستنشن ساده ثانیه‌ها را به یک شیء Date تبدیل می‌کند. این اکستنشن دو گزینه به صورت مشخصه‌های function و یک read-only دارد. این دو از نظر تابعی یکسان هستند و این که از کدام یک استفاده می‌کنید به سلیقه شما بستگی دارد.

import java.util.Date

fun Int.toDate(): Date = Date(this.toLong() * 1000L)

val Int.asDate: Date
get() = Date(this.toLong() * 1000L)

کاربرد

val json = JSONObject();
json.put("date", 1598435781)

val date = json.getIntOrNull("date")?.asDate

نکته: در این مثال از یک اکستنشن دیگر به نام getIntOrNull استفاده شده است. این اکستنشن در صورتی که JSON وجود داشته باشد یک مقدار int و در غیر این صورت null بازگشت می‌دهد.

(…)String.toDate و (…)Date.toString

یک تبدیل رایج دیگر برای شیء Date این است که آن را به یک رشته و یا از یک رشته تبدیل کنیم. توجه کنید که در مورد متد استاندارد toString() در جاوا/کاتلین صحبت نمی‌کنیم. در این مورد باید یک قالب را تعیین کنیم:

import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

fun String.toDate(format: String): Date? {
  val dateFormatter = SimpleDateFormat(format, Locale.US)
  return try {
    dateFormatter.parse(this)
  } catch (e: ParseException) {
    null
  }
}

fun Date.toString(format: String): String {
  val dateFormatter = SimpleDateFormat(format, Locale.US)
  return dateFormatter.format(this)
}

هشدار عملکردی: در این مثال ما هر بار یک شیء aSimpleDateFormat ایجاد می‌کنیم. اگر از این اکستنشن درون یک لیست استفاده می‌کنید، یا یک پاسخ بزرگ را از یک API تحلیل می‌کنید، باید کد زیر را از اکستنشن حذف کنید و آن را درون یک متغیر گلوبال یا یک عضو کلاس استاتیک قرار دهید:

theval dateFormatter = SimpleDateFormat(format, Locale.US)

کاربرد

val format = "yyyy-MM-dd HH:mm:ss"
val date = Date()
val str = date.toString(format)
val date2 = str.toDate(format)

()Int.centsToDollars و ()Int.centsToDollarsFormat

اگر با قیمت‌ها کار می‌کنید، ممکن است در زمان کار با انواع داده Float و Double با مشکل دقت اعشار مواجه شوید. یک روش رایج برای نمایش قیمت استفاده از مقادیر Int است. اما به جای ذخیره کردن مقادیر در وجه اصلی (مانند دلار یا یورو) می‌توانید آن‌ها را به صورت کمترین واحدهای پولی (یعنی سنت) نمایش دهید.

به این منظور نیاز به محاسباتی درون Int دارید و در نهایت تنها مقدار نهایی را به کاربر نمایش می‌دهید یعنی Double را مستقیماً به String تبدیل می‌کنید.

import java.util.Locale

fun Int.centsToDollars(): Double = this.toDouble() / 100.0

fun Int.centsToDollarsFormat(currency: String): String {
  val dollars = this / 100
  val cents = this % 100
  return String.format(Locale.US, "%s%d.%02d", currency, dollars, cents)
}

کاربرد

val amount = 4999
val doubleAmount = amount.centsToDollars()
val priceTag = amount.centsToDollarsFormat("\$")

()Double.toPrice

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

در اکستنشن زیر از دو رقم کسری استفاده شده و یک کاما بین ارقام عدد نمایش می‌یابد و عدد را سه رقم به سه رقم با نقطه جدا می‌کند تا راحت‌تر خوانده شود. این بار از نوع ()Double.toPrice استفاده خواهیم کرد. تبدیل Int به Double با استفاده از اکستنشن ()centsToDollars که در بخش قبل معرفی کردیم، ممکن است.

import java.text.DecimalFormat

fun Double.toPrice(): String {
  val pattern = "#,###.00"
  val decimalFormat = DecimalFormat(pattern)
  decimalFormat.groupingSize = 3

  return "€" + decimalFormat.format(this)
}

کاربرد

val price = 123456789.5.toPrice()

(…)String.toLocation

زمانی که با یک API کار می‌کنید تا مختصات جغرافیایی یک شیء را دریافت کنید، ممکن است این مختصات را با صورت دو فیلد مجزا دریافت کنید. اما گاهی اوقات تنها یک فیلد دریافت می‌شود که مقادیر طول و عرض جغرافیایی درون آن با کاما از هم جدا شده‌اند.

اکستنشن زیر این نوع از مختصات جغرافیایی را به موقعیت اندروید در یک خط تبدیل می‌کند:

import android.location.Location

fun String.toLocation(provider: String): Location? {
  val components = this.split(",")
  if (components.size != 2)
    return null

  val lat = components[0].toDoubleOrNull() ?: return null
  val lng = components[1].toDoubleOrNull() ?: return null
  val location = Location(provider);
  location.latitude = lat
  location.longitude = lng
  return location
}

کاربرد

val apiLoc = "41.6168, 41.6367".toLocation("API")

به طور مشابه، می‌توانید یک اکستنشن برای تبدیل String به LatLng از پکیج Google Maps بسازید. در این حالت، حتی نیازی به تعیین location provider نیز وجود ندارد.

22 اکستنشن کاتلین برای نوشتن کد تمیز

String.containsOnlyDigits و String.isAlphanumeric

در این بخش در مورد مشخصه‌های یک String صحبت می‌کنیم. رشته‌ها می‌توانند خالی یا غیر خالی باشند. برای نمونه ممکن است شامل تنها رقم و یا تنها کاراکترهای عددی-رقمی باشند. اکستنشن‌های زیر به ما امکان می‌دهند که یک رشته را در یک خط از کد اعتبارسنجی کنیم:

val String.containsDigit: Boolean
get() = matches(Regex(".*[0-9].*"))
val String.isAlphanumeric: Boolean
get() = matches(Regex("[a-zA-Z0-9]*"))

کاربرد

val digitsOnly = "12345".containsDigitOnly
val notDigitsOnly = "abc12345".containsDigitOnly
val alphaNumeric = "abc123".isAlphanumeric
val notAlphanumeric = "ab.2a#1".isAlphanumeric

Context.versionNumber و Context.versionCode

در اپلیکیشن‌های اندروید معمولاً بهتر است که شماره نسخه اپلیکیشن را در صفحه about یا support نمایش دهیم. این کار به کاربران کمک می‌کند تا از به‌روز بودن اپلیکیشن خود مطمئن شوند و یا اطلاعات ارزشمندی در زمان معرفی باگ ارائه نمایند. تابع‌های استاندارد اندروید برای اجرای این کار نیازمند چندین خط از کد و مدیریت استثنا‌ها هستند. اکستنشن زیر به ما امکان می‌دهد که شماره نسخه را در یک خط از کد دریافت کنیم:

import android.content.Context
import android.content.pm.PackageManager

val Context.versionName: String?
get() = try {
  val pInfo = packageManager.getPackageInfo(packageName, 0);
  pInfo?.versionName
} catch (e: PackageManager.NameNotFoundException) {
  e.printStackTrace()
  null
}

val Context.versionCode: Long?
get() = try {
  val pInfo = packageManager.getPackageInfo(packageName, 0)
  if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
    pInfo?.longVersionCode
  } else {
    @Suppress("DEPRECATION")
    pInfo?.versionCode?.toLong()
  }
} catch (e: PackageManager.NameNotFoundException) {
  e.printStackTrace()
  null
}

کاربرد

val vn = versionName ?: "Unknown"
val vc = versionCode?.toString() ?: "Unknown"
val appVersion = "App Version: $vn ($vc)"

Context.screenSize

یک اکستنشن مفید دیگر در اندروید اکستنشن Context نام دارد که به ما امکان می‌دهد تا اندازه صفحه دستگاه را دریافت کنیم. این اکستنشن اندازه صفحه را با مقیاس پیکسل بازگشت می‌دهد.

import android.content.Context
import android.graphics.Point
import android.view.WindowManager

@Suppress("DEPRECATION")
val Context.screenSize: Point
get() {
  val wm = getSystemService(Context.WINDOW_SERVICE) as WindowManager
  val display = wm.defaultDisplay
  val size = Point()
  display.getSize(size)
  return size
}

کاربرد

Log.d(TAG, "User's screen size: ${screenSize.x}x${screenSize.y}")

Any.deviceName

این که نوع Any را بسط دهیم موضوع بحث‌برانگیزی محسوب می‌شود. اگر چنین کنیم، تابع به صورت گلوبال طاهر می‌شود و از این رو اساساً معادل همان اعلان گلوبال تابع است. در اکستنشن زیر نام دستگاه اندرویدی را دریافت می‌کنیم. از آنجا که این کار نیازی به هیچ Any.deviceName ندارد، یک اکستنشن Any استفاده شده است:

import android.os.Build
import java.util.Locale

val Any.deviceName: String
get() {
  val manufacturer = Build.MANUFACTURER
  val model = Build.MODEL
  return if (model.startsWith(manufacturer))
    model.capitalize(Locale.getDefault())
  else
    manufacturer.capitalize(Locale.getDefault()) + " " + model
}

کاربرد

Log.d(TAG, "User's device: $deviceName")

T.weak

موقعیتی که در این بخش مطرح می‌کنیم کمی پیچیده است. فرض کنید یک Activity و یک ListView داریم که درون خود خانه‌های زیادی دارد. هر خانه می‌تواند نوعی بازخورد ارائه کند. فرض کنید یک اینترفیس delegate داریم و خود اکتیویتی را به یک خانه ارسال می‌کنیم، زیرا اینترفیس خانه را پیاده‌سازی کرده است:

interface CellDelegate {
  fun buttonAClicked()
  fun buttonBClicked()
}

class Cell(context: Context?) : View(context) {
  // ...

  var delegate: CellDelegate? = null

  fun prepare(arg1: String, arg2: Int, delegate: CellDelegate) {
    this.delegate = delegate
  }
}

class Act: Activity(), CellDelegate {
  // ...
  fun createCell(): Cell {
    val cell = Cell(this)
    cell.prepare("Milk", 10, this)
    return cell
  }

  override fun buttonAClicked() {
    TODO("Not yet implemented")
  }

  override fun buttonBClicked() {
    TODO("Not yet implemented")
  }
  // ...
}

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

22 اکستنشن کاتلین برای نوشتن کد تمیز

یکی از مشکلات این حالت آن است که Act و Cell ارجاع‌هایی به همدیگر دارند که منجر به نشت حافظه می‌شود. یک راه‌حل خوب در اینجا آن است که از WeakReference استفاده کنیم. متغیرهای Delegate که درون یک WeakReference قرار گیرند تأثیری روی شمارنده ارجاع در Act نخواهند داشت و از این رو به محض بسته شدن صفحه به همراه همه خانه‌های تخصیص‌یافته تخریب خواهند شد.

این اکستنشن ساده به ما امکان می‌دهد که یک ارجاع ضعیف را با افزودن.weak به هر شیئی به دست آوریم:

val <T> T.weak: WeakReference<T>
get() = WeakReference(this)

کاربرد

class Cell(context: Context?) : View(context) {
  // ...

  private var delegate: WeakReference<CellDelegate>? = null

  fun prepare(arg1: String, arg2: Int, delegate: CellDelegate) {
    this.delegate = delegate.weak
  }
  
  fun callA() {
    delegate?.get()?.buttonAClicked()
  }
  
  fun callB() {
    delegate?.get()?.buttonBClicked()
  }
}

باید تأکید کرد که این اکستنشن ژنریک است و با هر نوع داده‌ای کار می‌کند.

(…)Context.directionsTo

باز کردن ناوبری از هر اپلیکیشن اندروید یک قابلیت رایج است. اندروید همانند Google Maps یک محصول گوگل است. اپلیکیشن Google Maps از پیش روی اغلب گوشی‌ها و تبلت‌های اندروید نصب شده است. آسان‌ترین راهکار برای باز کردن ناوبری در یک اپلیکیشن اندروید، باز کردن اپلیکیشن Android Maps است. اگر این اپلیکیشن نصب نشده باشد، این لینک در یک مرورگر باز خواهد شد.

import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.location.Location
import android.net.Uri
import java.util.Locale

fun Context.directionsTo(location: Location) {
  val lat = location.latitude
  val lng = location.longitude
  val uri = String.format(Locale.US, "http://maps.google.com/maps?daddr=%f,%f", lat, lng)
  try {
    val intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri))
    intent.setClassName("com.google.android.apps.maps", "com.google.android.maps.MapsActivity")
    startActivity(intent)
  }
  catch (e: ActivityNotFoundException) {
    e.printStackTrace()

    val intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri))
    startActivity(intent)
  }
}

این یک اکستنشن Context است و از دیدگاه کدنویسی مشکلی ندارد. اما از دیدگاه منطقی باید آن را مشخص‌تر سازیم. این اکستنشن می‌تواند Activity یا AppCompatActivity را بسط دهد تا از برخی عوارض جانبی مانند استفاده از آن درون یک Service جلوگیری شود. شما می‌توانید کلاس قابل بسط را به هر چیزی که در اپلیکیشن خود استفاده می‌کنید، تغییر دهید.

(…)AppCompatActivity.callTo یا (…)Activity.callTo

در این اکستنشن هم از همان منطق اکستنشن قبلی استفاده کرده‌ایم. اما به جای ناوبری به یک شیء تلاش می‌کنیم تا یک تماس برقرار کنیم. این دو اکستنشن می‌توانند در کنار هم در یک اپلیکیشن مورد استفاده قرار گیرند.

پیچیدگی این راه‌حل در مسئله مجوز (permission) است. اندروید برای ایجاد یک تماس نیازمند کسب مجوز است. اما برخلاف آیفون این مجوز نه برای برقرار تماس؛ بلکه برای باز کردن صفحه تماس‌ها دریافت می‌شود.

این اکستنشن دو پارامتر دارد و به صورت مستقیم یک Activity یا کلاس مشابه را بسط می‌دهد. پارامتر اول یک شماره تلفن است و پارامتر دوم نیز کد درخواست است. در صورتی که نیاز به مجوز داشته باشیم، اما آن را کسب نکرده‌ باشیم به صورت زیر عمل می‌کنیم:

import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat

fun AppCompatActivity.callTo(phoneNumber: String, requestCode: Int) {
  val intent = Intent(Intent.ACTION_CALL)

  intent.data = Uri.parse("tel:$phoneNumber")
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
      val permissions = arrayOfNulls<String>(1)
      permissions[0] = Manifest.permission.CALL_PHONE
      requestPermissions(permissions, requestCode)
    } else {
      startActivity(intent)
    }
  } else {
    startActivity(intent)
  }
}

کاربرد

توجه کنید که کد زیر بخشی از AppCompatActivity است

private val phone: String = "+1234567890"

private fun call() {
  callTo(phone, callPermissionRequestCode)
}

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
  if (requestCode == callPermissionRequestCode) {
    if (permissions.isNotEmpty() && grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
      call()
    }
  } else {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
  }
}

companion object {
  const val callPermissionRequestCode = 2001
}

String.asUri

ما بر حسب عادت نشانی‌های اینترنتی را به صورت یک رشته تصور کنیم، چون می‌توانیم نشانی را تایپ کرده و درون گیومه قرار دهیم. اما اندروید برای کاربرد درونی خود یک نوع خاص برای نشانی‌های اینترنتی به نام Uri دارد. تبدیل Uri به رشته و برعکس کار آسانی است. اکستنشن زیر به ما این امکان را می‌دهد که یک string را به یک Uri تبدیل کنیم و این کار را با تأیید صحت کار انجام دهیم. یک Uri معتبر باشند، مقدار null بازگشت می‌یابد:

import android.net.Uri
import android.webkit.URLUtil
import java.lang.Exception

val String.asUri: Uri?
get() = try {
  if (URLUtil.isValidUrl(this))
    Uri.parse(this)
  else
    null
} catch (e: Exception) {
  null
}

کاربرد

val uri = "invalid_uri".asUri
val uri2 = "https://medium.com/@alex_nekrasov".asUri

(…)Uri.open(…) ،Uri.openInside و (…)Uri.openOutside

گاهی اوقات زمانی که یک Uri داریم، می‌خواهیم آن را در یک مرورگر باز کنیم. دو روش برای انجام این کار وجود دارد:

  • آن را درون اپلیکیشن باز کنیم.
  • آن را در یک مرورگر بیرونی باز کنیم.

ما معمولاً علاقه داریم که کاربر را درون اپلیکیشن حفظ کنیم، اما برخی اسکیماها امکان باز شدن درون اپلیکیشن را ندارند. برای نمونه ما نمی‌خواهیم //:instagram را درون مرورگر اپلیکیشن باز کنیم. در واقع ما تنها می‌خواهیم نشانی‌های //:http و //:https را درون اپلیکیشن خود باز کنیم.

بدین ترتیب سه اکستنشن مختلف اضافه می‌کنیم. یکی برای باز کردن Uri درون اپلیکیشن، دیگری برای باز کردن در مرورگر بیرونی و آخری برای تصمیم‌گیری به صورت دینامیک بر اساس نوع اسکیمای نشانی مورد استفاده قرار می‌گیرد.

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

در فایل گریدل اپلیکیشن خط زیر را بنویسید:

dependencies {
    implementation 'com.thefinestartist:finestwebview:1.2.7'
}

و در مانیفست نیز کد زیر را درج کنید:

<uses-permission android:name="android.permission.INTERNET" />

<activity
    android:name="com.thefinestartist.finestwebview.FinestWebViewActivity"
    android:configChanges="keyboardHidden|orientation|screenSize"
    android:screenOrientation="sensor"
    android:theme="@style/FinestWebViewTheme.Light" />

اکستنشن‌هایی که اشاره کردیم به صورت زیر هستند:

import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import com.thefinestartist.finestwebview.FinestWebView

fun Uri.open(context: Context): Boolean =
  if (this.scheme == "http" || this.scheme == "https") {
    openInside(context)
    true
  } else
    openOutside(context)

fun Uri.openInside(context: Context) =
  FinestWebView.Builder(context).show(this.toString())

fun Uri.openOutside(context: Context): Boolean =
  try {
    val browserIntent = Intent(Intent.ACTION_VIEW, this)
    context.startActivity(browserIntent)
    true
  } catch (e: ActivityNotFoundException) {
    e.printStackTrace()
    false
  }

کاربرد

val uri2 = "https://medium.com/@alex_nekrasov".asUri
uri2?.open(this)

(…)Context.vibrate

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

<uses-permission android:name="android.permission.VIBRATE" />

اکستنشن

import android.content.Context
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator

fun Context.vibrate(duration: Long) {
  val vib = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    vib.vibrate(VibrationEffect.createOneShot(duration, VibrationEffect.DEFAULT_AMPLITUDE))
  } else {
    @Suppress("DEPRECATION")
    vib.vibrate(duration)
  }
}

کاربرد

vibrate(500) // 500 ms
// Should be called from Activity or other Context

یا

context.vibrate(500) // 500 ms
// Can be called from any place having context variable

سخن پایانی

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

بر اساس رای ۰ نفر
آیا این مطلب برای شما مفید بود؟
شما قبلا رای داده‌اید!
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
better-programming

نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *