از الگوهای رایج Kotlin در اندروید استفاده کنید

این مبحث بر روی برخی از مفیدترین جنبه‌های زبان کاتلین هنگام توسعه اندروید تمرکز دارد.

کار با قطعات

بخش‌های بعدی از مثال‌های Fragment برای برجسته کردن برخی از بهترین ویژگی‌های کاتلین استفاده می‌کنند.

وراثت

شما می‌توانید در کاتلین یک کلاس را با کلمه کلیدی class تعریف کنید. در مثال زیر، LoginFragment یک زیرکلاس از Fragment است. می‌توانید با استفاده از عملگر : بین زیرکلاس و والد آن، وراثت را نشان دهید:

class LoginFragment : Fragment()

در این اعلان کلاس، LoginFragment مسئول فراخوانی سازنده‌ی کلاس پایه‌ی خود، Fragment ، است.

درون LoginFragment ، می‌توانید تعدادی از توابع فراخوانی چرخه عمر را برای پاسخ به تغییرات حالت در Fragment خود، بازنویسی کنید. برای بازنویسی یک تابع، از کلمه کلیدی override استفاده کنید، همانطور که در مثال زیر نشان داده شده است:

override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
): View? {
    return inflater.inflate(R.layout.login_fragment, container, false)
}

برای ارجاع به یک تابع در کلاس والد، از کلمه کلیدی super استفاده کنید، همانطور که در مثال زیر نشان داده شده است:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
}

قابلیت تهی‌سازی و مقداردهی اولیه

در مثال‌های قبلی، برخی از پارامترها در متدهای override شده دارای پسوند ? هستند. این نشان می‌دهد که آرگومان‌های ارسالی برای این پارامترها می‌توانند null باشند. حتماً nullability آنها را با خیال راحت مدیریت کنید .

در کاتلین، هنگام تعریف شیء، باید ویژگی‌های آن را مقداردهی اولیه کنید. این بدان معناست که وقتی نمونه‌ای از یک کلاس را به دست می‌آورید، می‌توانید بلافاصله به هر یک از ویژگی‌های قابل دسترسی آن ارجاع دهید. با این حال، اشیاء View در یک Fragment تا زمان فراخوانی Fragment#onCreateView آماده‌ی inflate شدن نیستند، بنابراین به روشی برای به تعویق انداختن مقداردهی اولیه‌ی ویژگی برای یک View نیاز دارید.

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

مثال زیر استفاده از lateinit را برای اختصاص دادن اشیاء View در onViewCreated نشان می‌دهد:

class LoginFragment : Fragment() {

    private lateinit var usernameEditText: EditText
    private lateinit var passwordEditText: EditText
    private lateinit var loginButton: Button
    private lateinit var statusTextView: TextView

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        usernameEditText = view.findViewById(R.id.username_edit_text)
        passwordEditText = view.findViewById(R.id.password_edit_text)
        loginButton = view.findViewById(R.id.login_button)
        statusTextView = view.findViewById(R.id.status_text_view)
    }

    ...
}

تبدیل SAM

شما می‌توانید با پیاده‌سازی رابط OnClickListener در اندروید، به رویدادهای کلیک گوش دهید. اشیاء Button شامل یک تابع setOnClickListener() هستند که یک پیاده‌سازی از OnClickListener را دریافت می‌کند.

OnClickListener یک متد انتزاعی واحد onClick() دارد که باید آن را پیاده‌سازی کنید. از آنجا که setOnClickListener() همیشه یک OnClickListener به عنوان آرگومان می‌گیرد، و از آنجا که OnClickListener همیشه یک متد انتزاعی واحد دارد، این پیاده‌سازی را می‌توان با استفاده از یک تابع ناشناس در کاتلین نمایش داد. این فرآیند به عنوان تبدیل متد انتزاعی واحد یا تبدیل SAM شناخته می‌شود.

تبدیل SAM می‌تواند کد شما را به طور قابل توجهی تمیزتر کند. مثال زیر نحوه استفاده از تبدیل SAM برای پیاده‌سازی OnClickListener برای یک Button را نشان می‌دهد:

loginButton.setOnClickListener {
    val authSuccessful: Boolean = viewModel.authenticate(
            usernameEditText.text.toString(),
            passwordEditText.text.toString()
    )
    if (authSuccessful) {
        // Navigate to next screen
    } else {
        statusTextView.text = requireContext().getString(R.string.auth_failed)
    }
}

کد درون تابع ناشناس ارسال شده به setOnClickListener() زمانی اجرا می‌شود که کاربر روی loginButton کلیک کند.

اشیاء همراه

اشیاء همراه، مکانیزمی برای تعریف متغیرها یا توابعی ارائه می‌دهند که از نظر مفهومی به یک نوع مرتبط هستند اما به یک شیء خاص وابسته نیستند. اشیاء همراه مشابه استفاده از کلمه کلیدی static جاوا برای متغیرها و متدها هستند.

در مثال زیر، TAG یک ثابت String است. شما برای هر نمونه از LoginFragment به یک نمونه منحصر به فرد از String نیاز ندارید، بنابراین باید آن را در یک شیء همراه تعریف کنید:

class LoginFragment : Fragment() {

    ...

    companion object {
        private const val TAG = "LoginFragment"
    }
}

شما می‌توانید TAG در سطح بالای فایل تعریف کنید، اما فایل ممکن است تعداد زیادی متغیر، تابع و کلاس نیز داشته باشد که آنها نیز در سطح بالا تعریف شده‌اند. اشیاء همراه به اتصال متغیرها، توابع و تعریف کلاس بدون ارجاع به هیچ نمونه خاصی از آن کلاس کمک می‌کنند.

واگذاری املاک

هنگام مقداردهی اولیه ویژگی‌ها، ممکن است برخی از الگوهای رایج‌تر اندروید، مانند دسترسی به یک ViewModel درون یک Fragment ، را تکرار کنید. برای جلوگیری از کد تکراری اضافی، می‌توانید از سینتکس واگذاری ویژگی کاتلین استفاده کنید.

private val viewModel: LoginViewModel by viewModels()

واگذاری ویژگی (Property Delegation) یک پیاده‌سازی رایج ارائه می‌دهد که می‌توانید در سراسر برنامه خود از آن استفاده مجدد کنید. اندروید KTX برخی از نماینده‌های ویژگی (property delegates) را برای شما فراهم می‌کند. برای مثال، viewModels یک ViewModel را بازیابی می‌کند که به Fragment فعلی محدود شده است.

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

قابلیت ابطال

کاتلین قوانین سختگیرانه‌ای برای nullability ارائه می‌دهد که type-safety را در سراسر برنامه شما حفظ می‌کند. در کاتلین، ارجاعات به اشیاء به طور پیش‌فرض نمی‌توانند حاوی مقادیر null باشند. برای اختصاص مقدار null به یک متغیر، باید با اضافه کردن ? به انتهای نوع پایه، یک نوع متغیر nullable تعریف کنید.

به عنوان مثال، عبارت زیر در کاتلین غیرمجاز است. name از نوع String است و nullable نیست:

val name: String = null

برای مجاز دانستن مقدار null، باید از یک نوع String nullable به String? استفاده کنید، همانطور که در مثال زیر نشان داده شده است:

val name: String? = null

قابلیت همکاری

قوانین سختگیرانه کاتلین، کد شما را ایمن‌تر و مختصرتر می‌کند. این قوانین احتمال بروز NullPointerException که باعث خرابی برنامه شما می‌شود را کاهش می‌دهند. علاوه بر این، تعداد بررسی‌های null که باید در کد خود انجام دهید را نیز کاهش می‌دهند.

اغلب، هنگام نوشتن یک برنامه اندروید، باید کد غیر کاتلین را نیز فراخوانی کنید، زیرا اکثر API های اندروید با زبان برنامه نویسی جاوا نوشته شده اند.

قابلیت تهی‌پذیری (nullability) یکی از حوزه‌های کلیدی است که جاوا و کاتلین در رفتار با آن تفاوت دارند. جاوا در مورد سینتکس قابلیت تهی‌پذیری (nullability) سخت‌گیری کمتری دارد.

به عنوان مثال، کلاس Account چند ویژگی دارد، از جمله یک ویژگی String به نام name . جاوا قوانین کاتلین در مورد nullability را ندارد، در عوض برای اعلام صریح اینکه آیا می‌توانید مقدار null اختصاص دهید یا خیر، به حاشیه‌نویسی‌های nullability اختیاری متکی است.

از آنجا که چارچوب اندروید عمدتاً با جاوا نوشته شده است، ممکن است هنگام فراخوانی APIها بدون حاشیه‌نویسی‌های nullability با این سناریو مواجه شوید.

انواع پلتفرم

اگر از کاتلین برای ارجاع به یک عضو name بدون حاشیه‌نویسی که در کلاس Account جاوا تعریف شده است استفاده کنید، کامپایلر نمی‌داند که آیا String به یک String یا یک String? در کاتلین نگاشت می‌شود. این ابهام از طریق یک نوع پلتفرم ، String! نمایش داده می‌شود.

String! هیچ معنی خاصی برای کامپایلر کاتلین ندارد. String! می‌تواند نمایانگر یک String یا یک String? باشد و کامپایلر به شما اجازه می‌دهد مقداری از هر دو نوع را اختصاص دهید. توجه داشته باشید که اگر نوع را به صورت String نمایش دهید و مقدار null را اختصاص دهید، خطر بروز NullPointerException وجود دارد.

برای حل این مشکل، باید هر زمان که در جاوا کد می‌نویسید از حاشیه‌نویسی‌های nullability استفاده کنید. این حاشیه‌نویسی‌ها هم به توسعه‌دهندگان جاوا و هم به توسعه‌دهندگان کاتلین کمک می‌کنند.

برای مثال، کلاس Account به شکلی که در جاوا تعریف شده است، به صورت زیر است:

public class Account implements Parcelable {
    public final String name;
    public final String type;
    private final @Nullable String accessId;

    ...
}

یکی از متغیرهای عضو، accessId ، با @Nullable حاشیه‌نویسی شده است که نشان می‌دهد می‌تواند مقدار null را در خود نگه دارد. در این صورت، کاتلین accessId به عنوان یک String?

برای اینکه نشان دهید یک متغیر هرگز نمی‌تواند تهی (null) باشد، از حاشیه‌نویسی @NonNull استفاده کنید:

public class Account implements Parcelable {
    public final @NonNull String name;
    ...
}

در این سناریو، name در کاتلین یک String non-nullable در نظر گرفته می‌شود.

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

مدیریت تهی بودن

اگر در مورد یک نوع جاوا مطمئن نیستید، باید آن را nullable در نظر بگیرید. به عنوان مثال، عضو name از کلاس Account حاشیه‌نویسی نشده است، بنابراین باید آن را یک String? با nullable در نظر بگیرید.

اگر می‌خواهید name طوری برش دهید که مقدار آن شامل فضای خالی قبل یا بعد از آن نباشد، می‌توانید از تابع trim در کاتلین استفاده کنید. می‌توانید با خیال راحت یک String? به چند روش مختلف برش دهید. یکی از این روش‌ها استفاده از عملگر اعلان غیر تهی ، !! ، است، همانطور که در مثال زیر نشان داده شده است:

val account = Account("name", "type")
val accountName = account.name!!.trim()

عملگر !! هر چیزی که در سمت چپ آن باشد را غیر تهی (non-null) در نظر می‌گیرد، بنابراین در این حالت، شما name به عنوان یک رشته غیر تهی (non-null String رفتار می‌کنید. اگر نتیجه عبارت سمت چپ آن تهی (null) باشد، برنامه شما یک NullPointerException صادر می‌کند. این عملگر سریع و آسان است، اما باید به مقدار کم استفاده شود، زیرا می‌تواند نمونه‌هایی از NullPointerException دوباره به کد شما وارد کند.

یک انتخاب امن‌تر، استفاده از عملگر فراخوانی امن ، ?. ، است، همانطور که در مثال زیر نشان داده شده است:

val account = Account("name", "type")
val accountName = account.name?.trim()

با استفاده از عملگر فراخوانی امن، اگر name برابر با null نباشد، نتیجه‌ی name?.trim() یک مقدار نام بدون فاصله‌ی خالی در ابتدا یا انتهای آن خواهد بود. اگر name برابر با null باشد، نتیجه‌ی name?.trim() برابر با null خواهد بود. این بدان معناست که برنامه‌ی شما هرگز نمی‌تواند هنگام اجرای این دستور، NullPointerException صادر کند.

اگرچه عملگر فراخوانی امن شما را از NullPointerException احتمالی نجات می‌دهد، اما مقدار null را به دستور بعدی ارسال می‌کند. در عوض، می‌توانید موارد null را فوراً با استفاده از عملگر Elvis ( ?: :) مدیریت کنید، همانطور که در مثال زیر نشان داده شده است:

val account = Account("name", "type")
val accountName = account.name?.trim() ?: "Default name"

اگر حاصل عبارت سمت چپ عملگر Elvis تهی (null) باشد، مقدار سمت راست به accountName اختصاص داده می‌شود. این تکنیک برای ارائه یک مقدار پیش‌فرض که در غیر این صورت تهی خواهد بود، مفید است.

همچنین می‌توانید از عملگر Elvis برای بازگشت زودهنگام از یک تابع استفاده کنید، همانطور که در مثال زیر نشان داده شده است:

fun validateAccount(account: Account?) {
    val accountName = account?.name?.trim() ?: "Default name"

    // account cannot be null beyond this point
    account ?: return

    ...
}

تغییرات API اندروید

APIهای اندروید به طور فزاینده‌ای با کاتلین سازگار می‌شوند. بسیاری از APIهای رایج اندروید، از جمله AppCompatActivity و Fragment ، حاوی حاشیه‌نویسی‌های nullability هستند و فراخوانی‌های خاصی مانند Fragment#getContext جایگزین‌های سازگارتری با کاتلین دارند.

برای مثال، دسترسی به Context یک Fragment تقریباً همیشه غیر تهی است، زیرا بیشتر فراخوانی‌هایی که در یک Fragment انجام می‌دهید در حالی رخ می‌دهد که Fragment به یک Activity (یک زیرکلاس از Context ) متصل است. با این حال، Fragment#getContext همیشه مقداری غیر تهی برنمی‌گرداند، زیرا سناریوهایی وجود دارد که در آنها یک Fragment به یک Activity متصل نیست. بنابراین، نوع بازگشتی Fragment#getContext قابل تهی شدن است.

از آنجایی که Context برگردانده شده از Fragment#getContext قابل null شدن است (و به صورت @Nullable حاشیه‌نویسی می‌شود)، باید در کد کاتلین خود با آن به عنوان یک Context? رفتار کنید. این به معنای اعمال یکی از عملگرهای ذکر شده قبلی برای رسیدگی به nullability قبل از دسترسی به ویژگی‌ها و توابع آن است. برای برخی از این سناریوها، اندروید شامل APIهای جایگزینی است که این راحتی را فراهم می‌کنند. به عنوان مثال، Fragment#requireContext یک Context غیر null را برمی‌گرداند و در صورت فراخوانی در زمانی که Context تهی است، یک IllegalStateException صادر می‌کند. به این ترتیب، می‌توانید Context حاصل را بدون نیاز به عملگرهای فراخوانی ایمن یا راه‌حل‌های جایگزین، غیر null در نظر بگیرید.

مقداردهی اولیه ویژگی

ویژگی‌ها در کاتلین به طور پیش‌فرض مقداردهی اولیه نمی‌شوند. آن‌ها باید هنگام مقداردهی اولیه کلاس محصورکننده‌شان، مقداردهی اولیه شوند.

شما می‌توانید ویژگی‌ها را به چند روش مختلف مقداردهی اولیه کنید. مثال زیر نحوه مقداردهی اولیه یک متغیر index را با اختصاص دادن مقداری به آن در اعلان کلاس نشان می‌دهد:

class LoginFragment : Fragment() {
    val index: Int = 12
}

این مقداردهی اولیه را می‌توان در یک بلوک مقداردهی اولیه نیز تعریف کرد:

class LoginFragment : Fragment() {
    val index: Int

    init {
        index = 12
    }
}

در مثال‌های بالا، index هنگام ساخت یک LoginFragment مقداردهی اولیه می‌شود.

با این حال، ممکن است برخی از ویژگی‌ها داشته باشید که نمی‌توانند در طول ساخت شیء مقداردهی اولیه شوند. برای مثال، ممکن است بخواهید از درون یک Fragment به یک View ارجاع دهید، به این معنی که ابتدا باید طرح‌بندی (layout) پر شود. پر شدن (inflation) هنگام ساخت یک Fragment رخ نمی‌دهد. در عوض، هنگام فراخوانی Fragment#onCreateView پر می‌شود.

یک راه برای حل این مشکل، تعریف ویو به عنوان nullable و مقداردهی اولیه آن در اسرع وقت است، همانطور که در مثال زیر نشان داده شده است:

class LoginFragment : Fragment() {
    private var statusTextView: TextView? = null

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)

            statusTextView = view.findViewById(R.id.status_text_view)
            statusTextView?.setText(R.string.auth_failed)
    }
}

اگرچه این روش طبق انتظار کار می‌کند، اما اکنون باید هر زمان که به View ارجاع می‌دهید، nullability View را مدیریت کنید. یک راه حل بهتر، استفاده از lateinit برای مقداردهی اولیه View است، همانطور که در مثال زیر نشان داده شده است:

class LoginFragment : Fragment() {
    private lateinit var statusTextView: TextView

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)

            statusTextView = view.findViewById(R.id.status_text_view)
            statusTextView.setText(R.string.auth_failed)
    }
}

کلمه کلیدی lateinit به شما این امکان را می‌دهد که از مقداردهی اولیه یک ویژگی هنگام ساخت یک شیء جلوگیری کنید. اگر قبل از مقداردهی اولیه، به ویژگی شما ارجاع داده شود، کاتلین خطای UninitializedPropertyAccessException را صادر می‌کند، بنابراین حتماً در اسرع وقت ویژگی خود را مقداردهی اولیه کنید.