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

    ...
}

إحالة ناجحة من خلال مدير حسابات المبيعات

يمكنك الاستماع إلى أحداث النقر في 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.

الكائنات المصاحبة

توفّر الكائنات المصاحبة آلية لتحديد المتغيرات أو الدوال المرتبطة بشكل مفاهيمي بنوع معيّن ولكنها غير مرتبطة بكائن معيّن. تتشابه عناصر Companion مع استخدام الكلمة الرئيسية 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، لا يمكن أن تحتوي مراجع الكائنات على قيم فارغة تلقائيًا. لتعيين قيمة فارغة لمتغيّر، يجب تعريف نوع متغيّر يقبل القيم الفارغة من خلال إضافة ? إلى نهاية النوع الأساسي.

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

val name: String = null

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

val name: String? = null

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

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

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

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

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

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

أنواع المنصات

إذا كنت تستخدم Kotlin للإشارة إلى عنصر name غير مزوّد بتعليق توضيحي ومحدّد في فئة Account Java، لن يعرف المترجم ما إذا كان 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;

    ...
}

تمت إضافة التعليق التوضيحي @Nullable إلى أحد المتغيرات التابعة، وهو accessId، ما يشير إلى أنّه يمكن أن يتضمّن قيمة فارغة. ستتعامل لغة 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? بأمان بعدة طرق مختلفة. إحدى هذه الطرق هي استخدام عامل تأكيد عدم القيمة الفارغة، !!، كما هو موضّح في المثال التالي:

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

يتعامل عامل التشغيل !! مع كل ما يقع على يمينه على أنّه قيمة غير فارغة، لذا في هذه الحالة، أنت تتعامل مع name على أنّه String غير فارغ. إذا كانت نتيجة التعبير على يسار NullPointerException هي قيمة فارغة، سيُظهر تطبيقك الخطأ. هذا العامل سريع وسهل، ولكن يجب استخدامه باعتدال، لأنّه قد يعيد إدخال حالات NullPointerException إلى الرمز.

والخيار الأكثر أمانًا هو استخدام عامل التشغيل safe-call، ?.، كما هو موضّح في المثال التالي:

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

باستخدام عامل التشغيل safe-call، إذا كانت قيمة name غير فارغة، ستكون نتيجة name?.trim() هي قيمة اسم بدون مسافة بيضاء بادئة أو لاحقة. إذا كانت قيمة name فارغة، تكون نتيجة name?.trim() هي null. وهذا يعني أنّه لا يمكن لتطبيقك عرض الخطأ NullPointerException عند تنفيذ هذه العبارة.

في حين أنّ عامل الاستدعاء الآمن يجنّبك حدوث NullPointerException محتمل، إلّا أنّه يمرّر قيمة فارغة إلى العبارة التالية. يمكنك بدلاً من ذلك التعامل مع الحالات التي تكون فيها القيمة فارغة على الفور باستخدام عامل 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

    ...
}

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