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

این موضوع بر برخی‌از مفیدترین جنبه‌های زبان Kotlin هنگام توسعه برای Android تمرکز دارد.

کار با قطعه‌ها

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

میراث

می‌توانید کلاسی را در Kotlin با کلیدواژه 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)
}

مقدارپذیری و مقداردهی اولیه

در مثال‌های قبلی، برخی‌از پارامترها در روش‌های لغو‌شده دارای انواعی هستند که با علامت سؤال ? پسوند شده‌اند. این نشان می‌دهد که آرگومان‌های ارسال‌شده برای این پارامترها می‌تواند تهی باشد. حتماً تهی‌پذیری آن‌ها را به‌طور ایمن مدیریت کنید.

در Kotlin، باید هنگام اعلام شیء، ویژگی‌های آن را مقداردهی اولیه کنید. این یعنی وقتی نمونه‌ای از کلاس را دریافت می‌کنید، می‌توانید فوراً به هرکدام از ویژگی‌های دردسترس آن ارجاع دهید. بااین‌حال، اشیای View در Fragment تا زمانی که Fragment#onCreateView فراخوانی نشود برای باد شدن آماده نیستند، بنابراین به روشی برای به‌تعویق انداختن مقداردهی اولیه ویژگی برای 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 می‌توانید به رویدادهای کلیک در Android گوش دهید. اشیاء Button حاوی عملکرد setOnClickListener() هستند که در پیاده‌سازی OnClickListener استفاده می‌شود.

‫OnClickListener یک متد انتزاعی واحد، onClick()، دارد که باید آن را پیاده‌سازی کنید. چون setOnClickListener() همیشه OnClickListener را به‌عنوان متغیر مستقل می‌گیرد، و چون OnClickListener همیشه همان روش انتزاعی واحد را دارد، این پیاده‌سازی را می‌توان بااستفاده از تابع ناشناس در Kotlin نشان داد. این فرایند به‌عنوان تبدیل روش انتزاعی تکی یا تبدیل 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 Java برای متغیرها و متدها هستند.

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

class LoginFragment : Fragment() {

    ...

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

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

واگذار کردن دارایی

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

private val viewModel: LoginViewModel by viewModels()

واگذاری ویژگی پیاده‌سازی مشترکی ارائه می‌دهد که می‌توانید در سراسر برنامه‌تان از آن مجدداً استفاده کنید. Android KTX چند واگذاری ویژگی برای شما ارائه می‌دهد. برای مثال، viewModels یک ViewModel را که به Fragment فعلی محدود شده است بازیابی می‌کند.

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

Nullability

Kotlin قوانین تهی‌پذیری سخت‌گیرانه‌ای ارائه می‌دهد که ایمنی تایپی را در سراسر برنامه شما حفظ می‌کند. در Kotlin، ارجاع به اشیاء به‌طور پیش‌فرض نمی‌تواند حاوی مقادیر تهی باشد. برای اختصاص دادن مقدار تهی به متغیر، باید نوع متغیر تهی‌پذیر را با افزودن ? به انتهای نوع پایه اعلام کنید.

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

val name: String = null

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

val name: String? = null

هم‌کنش‌پذیری

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

اغلب، هنگام نوشتن برنامه Android، باید کد غیرKotlin را نیز فراخوانی کنید، زیرا اکثر میاناهای برنامه‌سازی کاربردی Android به زبان برنامه‌نویسی Java نوشته شده‌اند.

تهی‌پذیری یکی از حوزه‌های کلیدی است که رفتار Java و Kotlin در آن متفاوت است. جاوا در نحو تهی‌پذیری سخت‌گیری کمتری دارد.

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

چون چارچوب Android عمدتاً به زبان Java نوشته شده است، ممکن است هنگام فراخوانی میاناهای برنامه‌سازی کاربردی بدون حاشیه‌نویسی‌های تهی‌پذیری با این سناریو مواجه شوید.

انواع پلاتفرم

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

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

برای رفع این مشکل، باید هروقت در Java کد می‌نویسید از حاشیه‌نویسی‌های تهی‌پذیری استفاده کنید. این حاشیه‌نویسی‌ها هم به توسعه‌دهندگان Java و هم به توسعه‌دهندگان Kotlin کمک می‌کند.

برای مثال، در اینجا کلاس Account همان‌طور که در Java تعریف شده است آورده شده است:

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

    ...
}

یکی از متغیرهای عضو، accessId، با @Nullable حاشیه‌نویسی شده است، که نشان می‌دهد می‌تواند مقدار تهی داشته باشد. سپس Kotlin با accessId به‌عنوان String? رفتار می‌کند.

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

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

در این سناریو، name در Kotlin به‌عنوان String غیرتهی‌پذیر درنظر گرفته می‌شود.

حاشیه‌نویسی‌های تهی‌پذیری در همه میاناهای برنامه‌سازی کاربردی جدید Android و بسیاری از میاناهای برنامه‌سازی کاربردی Android موجود گنجانده شده است. بسیاری از کتابخانه‌های Java حاشیه‌نویسی‌های تهی‌پذیری را برای پشتیبانی بهتر از توسعه‌دهندگان Kotlin و Java اضافه کرده‌اند.

مدیریت تهی‌پذیری

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

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

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

عملگر !! با هر چیزی که در سمت چپ آن قرار دارد به‌عنوان غیرتهی رفتار می‌کند، بنابراین در این مورد، شما با name به‌عنوان یک String غیرتهی رفتار می‌کنید. اگر نتیجه عبارت سمت چپ آن تهی باشد، برنامه‌تان NullPointerException را برمی‌گرداند. این عملگر سریع و آسان است، اما باید به‌ندرت استفاده شود، زیرا می‌تواند نمونه‌های NullPointerException را دوباره به کد شما وارد کند.

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

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

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

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

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

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

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

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

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

    ...
}

تغییرات Android API

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

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

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

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

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

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

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

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

class LoginFragment : Fragment() {
    val index: Int

    init {
        index = 12
    }
}

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

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

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

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 ارجاع می‌دهید، قابلیت تهی بودن آن را مدیریت کنید. راهکار بهتر این است که از 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 به شما امکان می‌دهد هنگام ساخته شدن شیء، از مقداردهی اولیه خصوصیت اجتناب کنید. اگر قبل‌از مقداردهی اولیه به ویژگی‌تان ارجاع داده شود، Kotlin UninitializedPropertyAccessException را برمی‌گرداند، بنابراین حتماً در اسرع وقت به ویژگی‌تان مقداردهی اولیه کنید.