استخدام أنماط لغة 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

يمكنك الاطّلاع على الأحداث الناتجة عن النقر في Android من خلال تنفيذ واجهة OnClickListener. تحتوي كائنات 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. ولست بحاجة إلى مثيل فريد من String لكل مثيل من LoginFragment، لذا عليك تعريفه في كائن مصاحب:

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 الحالي.

تستخدِم عملية تفويض الموقع الانعكاس، ما يضيف بعض النفقات العامة على الأداء. المقايضة هي بناء جملة موجز يوفر وقت التطوير.

قابلية إلغاء

توفّر لغة Kotlin قواعد صارمة بشأن إمكانية القيم الفارغة، وتحافظ على أمان النوع في تطبيقك. وفي لغة Kotlin، لا يمكن أن تحتوي الإشارات إلى الكائنات على قيم فارغة تلقائيًا. لتخصيص قيمة فارغة لمتغير، يجب إعلان نوع متغير null عن طريق إضافة ? إلى نهاية النوع الأساسي.

على سبيل المثال، التعبير التالي غير قانوني في Kotlin. name من النوع String ولا يمكن عرضه قيمة فارغة:

val name: String = null

للسماح بقيمة فارغة، يجب استخدام نوع String قابل للقيم، String?، كما هو موضّح في المثال التالي:

val name: String? = null

إمكانية التشغيل التفاعلي

تجعل القواعد الصارمة في Kotlin رمزك أكثر أمانًا واختصارًا. تقلل هذه القواعد من احتمالات وجود NullPointerException قد يؤدي إلى تعطُّل تطبيقك. علاوة على ذلك، فهي تقلل من عدد عمليات التحقق الفارغة التي تحتاج إلى إجرائها في التعليمات البرمجية الخاصة بك.

في كثير من الأحيان، يجب عليك أيضًا استدعاء تعليمات برمجية غير Kotlin عند كتابة تطبيق Android، لأنّ معظم واجهات برمجة تطبيقات Android مكتوبة بلغة برمجة Java.

تُعدّ القيم غير القابلة للاستبدال (Nullability) من المجالات الرئيسية التي تختلف فيها لغة Java ولغة Kotlin في السلوك. تعد Java أقل تعقيدًا مع بناء جملة إمكانية القيم الفارغة.

على سبيل المثال، تتضمّن الفئة Account بعض السمات، من بينها سمة String التي تُسمى name. لا تتضمَّن لغة Java قواعد لغة البرمجة Kotlin بشأن إمكانية إبطال القيم الفارغة، بدلاً من الاعتماد على التعليقات التوضيحية بشأن قابلية القيم الفارغة الاختيارية لتوضيح ما إذا كان بإمكانك تعيين قيمة فارغة.

بما أنّ إطار عمل Android مكتوب بشكلٍ أساسي بلغة Java، قد تواجه هذا السيناريو عند استدعاء واجهات برمجة التطبيقات بدون تعليقات توضيحية بشأن القيم الفارغة.

أنواع الأنظمة الأساسية

إذا كنت تستخدم لغة Kotlin للإشارة إلى عضو name بدون تعليقات توضيحية تم تعريفه في فئة Java Account، لن يعرف المحول البرمجي ما إذا كانت String يتم الربط بـ String أم String? في Kotlin. ويمثل هذا الغموض من خلال نوع النظام الأساسي، String!.

إنّ String! ليس له معنى خاص في المحول البرمجي بلغة Kotlin. وتستطيع اللغة String! أن تمثِّل String أو String?، وتتيح لك أداة التجميع تحديد قيمة من أيّ نوع. تجدر الإشارة إلى أنّك تخاطر بعرض السمة NullPointerException إذا كنت تمثل النوع على أنّه String وعيّنت قيمة فارغة.

لمعالجة هذه المشكلة، يجب عليك استخدام التعليقات التوضيحية لقيم القيم الفارغة عند كتابة تعليمة برمجية في 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 على أنّها String غير قابلة للقيم الفارغة في لغة Kotlin.

يتم تضمين التعليقات التوضيحية التي لا تتضمّن أي قيم في جميع واجهات برمجة تطبيقات Android الجديدة والعديد من واجهات برمجة تطبيقات Android الحالية. أضافت العديد من مكتبات Java تعليقات توضيحية بشأن القيم الفارغة لدعم مطوّري برامج Kotlin وJava بشكل أفضل.

التعامل مع القيم الفارغة

إذا لم تكن متأكدًا من نوع Java، فيجب أن تعتبره قابلاً للقيم الفارغة. على سبيل المثال، لم يتم إضافة تعليقات توضيحية إلى العنصر name في الفئة Account، وبالتالي يجب أن تفترض أنّه String قابل للقيم الفارغة.

إذا كنت تريد قطع name بحيث لا تشمل قيمتها مسافة بيضاء بادئة أو لاحقة، يمكنك استخدام دالة trim في Kotlin. يمكنك قصّ String? بأمان ببضع طرق مختلفة. تتمثل إحدى هذه الطرق في استخدام عامل تشغيل التأكيد not-null، !!، كما هو موضّح في المثال التالي:

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 محتمَل، إلّا أنّه يمرِّر قيمة فارغة إلى العبارة التالية. يمكنك بدلاً من ذلك التعامل مع الحالات الفارغة على الفور باستخدام عامل التشغيل Elvis (?:)، كما هو موضّح في المثال التالي:

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

أصبحت واجهات برمجة تطبيقات 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 غير فارغة وتعرِض IllegalStateException إذا تم استدعائها عندما تكون قيمة Context فارغة. بهذه الطريقة، يمكنك التعامل مع 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، لذا احرص على إعداد موقعك في أقرب وقت ممكن.