أرسلت بواسطة سيرجي يشين ، مطور أندرويد القوي الأوسط ، DataArtلقد مر أكثر من عام ونصف على إعلان Google عن الدعم الرسمي لشركة Kotlin في نظام أندرويد ، وبدأ معظم المطورين المحنكين في تجربته في قتالهم وليس مشاريعهم منذ أكثر من ثلاث سنوات.
تم استقبال اللغة الجديدة بحرارة في مجتمع Android ، وستبدأ الغالبية العظمى من مشاريع Android الجديدة مع Kotlin. من المهم أيضًا أن تجمع Kotlin في رمز JVM ، وبالتالي ، فهي متوافقة تمامًا مع Java. لذلك ، في مشاريع Android الموجودة المكتوبة بلغة Java ، هناك أيضًا فرصة (علاوة على ذلك ، حاجة) لاستخدام جميع ميزات Kotlin ، بفضله اكتسب الكثير من المعجبين.
في هذا المقال ، سأتحدث عن تجربة ترحيل تطبيق Android من Java إلى Kotlin ، والصعوبات التي كان لا بد من التغلب عليها في هذه العملية ، وأشرح لماذا لم يكن كل هذا سدى. يستهدف المقال بشكل أكبر مطوري Android الذين بدأوا لتعلم Kotlin ، بالإضافة إلى التجربة الشخصية ، يعتمد على مواد أعضاء المجتمع الآخرين.
لماذا كوتلين؟
صف بإيجاز ميزات Kotlin ، التي تحولت إليها بسببها في المشروع ، تاركةً عالم Java "المريح والمألوف":
- توافق جافا الكامل
- سلامة خالية
- اكتب الاستدلال
- طرق التمديد
- وظائف ككائنات من الدرجة الأولى ولامدا
- الوراثة
- Coroutines
- لا يوجد استثناء محدد
تطبيق DISCO
هذا تطبيق صغير الحجم لتبادل بطاقات الخصم ، يتكون من 10 شاشات. باستخدام مثاله ، سننظر في الهجرة.
باختصار عن الهندسة المعمارية
يستخدم التطبيق بنية MVVM مع Google Architecture Components تحت الغطاء: ViewModel، LiveData، Room.
أيضًا ، وفقًا لمبادئ العمارة النظيفة من العم بوب ، اخترت 3 طبقات في التطبيق: البيانات ، والمجال ، والعرض التقديمي.
من أين تبدأ؟ لذلك ، فإننا نتخيل الملامح الرئيسية لكوتلين ولدينا فكرة بسيطة عن المشروع الذي يجب ترحيله. السؤال الطبيعي هو "من أين نبدأ؟"
تقول صفحة وثائق
الشروع في العمل مع Kotlin Android الرسمية إنه إذا كنت تريد نقل تطبيق موجود إلى Kotlin ، فعليك فقط البدء في كتابة اختبارات الوحدات. عندما تكتسب القليل من الخبرة مع هذه اللغة ، اكتب رمزًا جديدًا في Kotlin ، فأنت تحتاج فقط إلى تحويل كود Java الحالي.
ولكن هناك واحد "لكن". في الواقع ، يسمح لك التحويل البسيط عادة (وإن لم يكن دائمًا) بالحصول على كود عمل على Kotlin ، ومع ذلك ، فإن تعبيره يترك الكثير مما هو مرغوب فيه. علاوة على ذلك ، سوف أخبرك بكيفية القضاء على هذه الفجوة بسبب الميزات المذكورة (وليس فقط) للغة Kotlin.
ترحيل الطبقة
نظرًا لأن التطبيق تم الطبقات بالفعل ، فمن المنطقي الترحيل بواسطة الطبقات ، بدءًا من الأعلى.
يظهر تسلسل الطبقات أثناء الترحيل في الصورة التالية:
ليس من قبيل الصدفة أننا بدأنا الهجرة بالتحديد من الطبقة العليا. وبذلك ننقذ أنفسنا من استخدام كود Kotlin في كود Java. على العكس من ذلك ، نجعل كود Kotlin للطبقة العليا يستخدم فئات Java في الطبقة السفلى. الحقيقة هي أن Kotlin قد صُمم في الأصل مع مراعاة الحاجة إلى التفاعل مع Java. يمكن استدعاء كود Java الحالي من Kotlin بطريقة طبيعية. يمكننا أن نرث بسهولة من فئات Java الحالية ، والوصول إليها وتطبيق تعليقات توضيحية Java على فئات Kotlin والأساليب. يمكن أيضًا استخدام كود Kotlin في Java دون حدوث الكثير من المتاعب ، لكنه غالبًا ما يتطلب بذل جهد إضافي ، مثل إضافة تعليق JVM. ولماذا التحويلات غير الضرورية في شفرة جافا ، إذا كان في النهاية لا يزال سيتم إعادة كتابتها في Kotlin؟
على سبيل المثال ، دعنا ننظر إلى توليد الحمل الزائد.
عادة ، إذا كتبت وظيفة Kotlin بقيم المعلمة الافتراضية ، فستكون مرئية فقط في Java كتوقيع كامل مع جميع المعلمات. إذا كنت ترغب في توفير أحمال زائدة متعددة لمكالمات Java ، يمكنك استخدام التعليق التوضيحيJvmOverloads:
class Foo @JvmOverloads constructor(x: Int, y: Double = 0.0) { @JvmOverloads fun f(a: String, b: Int = 0, c: String = "abc") { ... } }
لكل معلمة ذات قيمة افتراضية ، سيؤدي ذلك إلى إنشاء حمل زائد واحد إضافي ، والذي يحتوي على هذه المعلمة وجميع المعلمات إلى يمينها ، في قائمة المعلمات عن بعد. في هذا المثال ، سيتم إنشاء ما يلي:
هناك العديد من الأمثلة على استخدام التعليقات التوضيحية JVM للتشغيل الصحيح لـ Kotlin. تفاصيل
هذه الصفحة وثائق الدعوة إلى Kotlin من جافا.
الآن نحن تصف عملية طبقة الهجرة طبقة.
طبقة العرض التقديمي
هذه طبقة واجهة مستخدم تحتوي على شاشات بها طرق عرض و ViewModel ، بدورها ، تحتوي على خصائص في شكل LiveData مع بيانات من النموذج. بعد ذلك ، ننظر إلى الحيل والأدوات التي تبين أنها مفيدة عند ترحيل طبقة التطبيق هذه.
1. Kapt معالج الشرح
كما هو الحال مع أي MVVM ، يرتبط View بخصائص ViewModel من خلال ربط البيانات. في حالة Android ، نتعامل مع Android Databind Library ، التي تستخدم معالجة التعليقات التوضيحية. لذلك لدى Kotlin
معالج التعليقات التوضيحية الخاص بها ، وإذا لم تقم بإجراء تغييرات على ملف build.gradle المطابق ، فسيتوقف المشروع عن البناء. لذلك ، سنقوم بإجراء هذه التغييرات:
apply plugin: 'kotlin-kapt' android { dataBinding { enabled = true } } dependencies { api fileTree(dir: 'libs', include: ['*.jar'])
من المهم أن تتذكر أنه يجب استبدال جميع تكرارات تكوين المعالج التوضيحي بالكامل في build.gradle الخاص بك باستخدام kapt.
على سبيل المثال ، إذا كنت تستخدم مكتبات Dagger أو Room في المشروع ، والتي تستخدم أيضًا معالج التعليقات التوضيحية أسفل غطاء الشفرة ، فيجب عليك تحديد kapt كمعالج التعليقات التوضيحية.
2. وظائف مضمنة
عند وضع علامة على وظيفة مضمن ، نطلب من المترجم وضعه في مكان الاستخدام. يصبح نص الوظيفة مضمنًا ، بمعنى آخر ، يتم استبداله للاستخدام المعتاد للوظيفة. بفضل هذا ، يمكننا التحايل على تقييد محو الكتابة ، أي محو النوع. عند استخدام وظائف مضمّنة ، يمكننا الحصول على النوع (الفئة) في وقت التشغيل.
تم استخدام ميزة Kotlin هذه في الكود الخاص بي "لاستخراج" فئة النشاط الذي تم إطلاقه.
inline fun <reified T : Activity> Context?.startActivity(args: Bundle) { this?.let { val intent = Intent(this, T::class.java) intent.putExtras(args) it.startActivity(intent) } }
reified - تعيين نوع reified.
في المثال الموضح أعلاه ، تطرقنا أيضًا إلى ميزة لغة Kotlin مثل الامتدادات.
3. ملحقات
إنها امتدادات. تم استخدام طرق المنفعة في الملحقات ، مما ساعد على تجنب المرافق الصفية المتضخمة والوحشية.
سأقدم مثالًا على الامتدادات المشاركة في التطبيق:
fun Context.inflate(res: Int, parent: ViewGroup? = null): View { return LayoutInflater.from(this).inflate(res, parent, false) } fun <T> Collection<T>?.isNotNullOrEmpty(): Boolean { return this != null && isNotEmpty(); } fun Fragment.hideKeyboard() { view?.let { hideKeyboard(activity, it.windowToken) } }
فكر مطورو Kotlin في إضافات Android المفيدة مسبقًا من خلال تقديم المكون الإضافي Kotlin Android Extensions. من بين الميزات التي يقدمها عرض دعم ملزمة وطرود. يمكن العثور على معلومات مفصلة حول ميزات هذا البرنامج المساعد
هنا .
4. وظائف لامدا وظائف النظام العالي
باستخدام وظائف lambda في كود Android ، يمكنك التخلص من ClickListener و callback ، اللذين تم تنفيذهما في Java من خلال واجهات مكتوبة ذاتيا.
مثال على استخدام lambda بدلاً من onClickListener:
button.setOnClickListener({ doSomething() })
تُستخدم Lambdas أيضًا في وظائف الترتيب العالي ، على سبيل المثال ، لوظائف التجميع.
خذ
الخريطة كمثال:
fun <T, R> List<T>.map(transform: (T) -> R): List<R> {...}
يوجد مكان في الكود الخاص بي حيث أحتاج إلى "تعيين" هوية البطاقات لإزالتها لاحقًا.
باستخدام تعبير lambda الذي تم تمريره إلى الخريطة ، أحصل على صفيف المعرف المطلوب:
val ids = cards.map { it.id }.toIntArray() cardDao.deleteCardsByIds(ids)
يرجى ملاحظة أنه يمكن حذف الأقواس على الإطلاق عند استدعاء الوظيفة ، إذا كانت lambda هي الوسيطة الوحيدة ، والكلمة الرئيسية هي الاسم الضمني للمعلمة الوحيدة.
5. أنواع المنصات
سيكون عليك حتماً العمل مع SDKs المكتوبة بلغة Java (بما في ذلك ، Android SDK في الواقع). هذا يعني أنه يجب أن تكون دائمًا متيقظًا مع Kotlin و Java Interop مثل أنواع الأنظمة الأساسية.
نوع النظام الأساسي هو نوع لا تستطيع Kotlin العثور على معلومات صلاحية خالية منه. والحقيقة هي أنه ، افتراضيًا ، لا يشتمل كود Java على معلومات حول صلاحية null ، ولا يتم استخدام التعليقات التوضيحية
NotNull و @
Nullable دائمًا. عند عدم وجود تعليق توضيحي في Java ، يصبح النوع نظامًا أساسيًا. يمكنك التعامل معها كنوع يسمح بالقيمة الخالية ، ونوع لا يسمح بالقيمة الخالية.
هذا يعني أنه تمامًا كما في Java ، يكون المطور مسؤولًا تمامًا عن العمليات بهذا النوع. لا يضيف المحول البرمجي وقت تشغيل تحقق فارغًا وسيسمح لك بالقيام بكل شيء.
في المثال التالي ، فإننا نتجاوز onActivityResult في نشاطنا:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent{ super.onActivityResult(requestCode, resultCode, data) val randomString = data.getStringExtra("some_string") }
في هذه الحالة ، البيانات هي نوع نظام أساسي قد يحتوي على قيمة خالية. ومع ذلك ، من وجهة نظر كود Kotlin ، لا يمكن أن تكون البيانات خالية تحت أي ظرف من الظروف ، وبغض النظر عما إذا كنت تحدد نوع النية على أنه لاغٍ ، فلن تتلقى تحذيرًا أو خطأ من المترجم ، حيث أن كلا الإصدارين صالحين . ولكن نظرًا لأن تلقي بيانات غير فارغة غير مضمون ، لأنه في حالات SDK لا يمكنك التحكم في ذلك ، سيؤدي الحصول على قيمة خالية في هذه الحالة إلى NPE.
أيضًا ، على سبيل المثال ، يمكننا سرد الأماكن التالية لظهور أنواع الأنظمة الأساسية المحتملة:
- Service.onStartCommand () ، حيث قد تكون النية فارغة.
- BroadcastReceiver.onReceive ().
- Activity.onCreate () و Fragment.onViewCreate () وغيرها من الطرق المشابهة.
علاوة على ذلك ، يحدث أن يتم شرح معلمات الأسلوب ، ولكن لسبب ما يفقد الاستوديو Nullability عند إنشاء تجاوز.
طبقة المجال
تتضمن هذه الطبقة كل منطق الأعمال ؛ فهي مسؤولة عن التفاعل بين طبقة البيانات وطبقة العرض التقديمي. لعبت الدور الرئيسي هنا من قبل مستودع. في المستودع ، نقوم بتنفيذ عمليات معالجة البيانات الضرورية ، من جانب الخادم والمحلية. في الطابق العلوي ، إلى طبقة العرض التقديمي ، نقدم فقط طريقة واجهة مستودع التخزين ، والتي تخفي تعقيد عمليات البيانات.
كما ذكر أعلاه ، تم استخدام RxJava للتنفيذ.
1. RxJava
Kotlin متوافق تمامًا مع RxJava وأكثر إيجازًا مع جافا. ومع ذلك ، حتى هنا اضطررت إلى مواجهة مشكلة واحدة غير سارة. هذا يبدو كالتالي: إذا
نجحت في استخدام lambda
كمعلمة للأسلوب
andThen ، فلن تعمل هذه lambda!
للتحقق من ذلك ، فقط اكتب اختبارًا بسيطًا:
Completable .fromCallable { cardRepository.uploadDataToServer() } .andThen { cardRepository.markLocalDataAsSynced() } .subscribe()
وسوف تفشل المحتوى. هذا هو الحال مع معظم المشغلين (مثل
flatMap ، و
defer ، و
fromAction ، والعديد من الشركات الأخرى) ، ومن المتوقع lambda حقًا كحجج. ومع وجود مثل هذا السجل مع
andThen ،
يُتوقع اكتمال / ملاحظ / SingleSource . يتم حل المشكلة باستخدام أقواس عادية () بدلاً من مجعد {}.
تم وصف هذه المشكلة بالتفصيل في المقالة
"Kotlin و Rx2. كيف أضيع 5 ساعات بسبب الأقواس الخاطئة " .
2. إعادة الهيكلة
نتطرق أيضًا إلى بناء جملة Kotlin المثير للاهتمام مثل التدمير أو
التخصيص . يسمح لك بتعيين كائن لعدة متغيرات في وقت واحد ، وتقسيمه إلى أجزاء.
تخيل أن لدينا طريقة في واجهة برمجة التطبيقات تُرجع عدة كيانات مرة واحدة:
@GET("/foo/api/sync") fun getBrandsAndCards(): Single<BrandAndCardResponse> data class BrandAndCardResponse(@SerializedName("cards") val cards: List<Card>?, @SerializedName("brands") val brands: List<Brand>?)
طريقة مدمجة لإرجاع النتيجة من هذه الطريقة هي التدمير ، كما هو موضح في المثال التالي:
syncRepository.getBrandsAndCards() .flatMapCompletable {it-> Completable.fromAction{ val (cards, brands) = it syncCards(cards) syncBrands(brands) } } }
تجدر الإشارة إلى أن الإعلانات المتعددة تستند إلى اصطلاح: يجب أن تحتوي الفئات التي يُفترض أن يتم تدميرها على وظائف componentN () ، حيث N هو رقم المكون المقابل - عضو في الفصل. وهذا هو ، المثال أعلاه يترجم إلى التعليمات البرمجية التالية:
val cards = it.component1() val brands = it.component2()
يستخدم مثالنا فئة بيانات تعلن تلقائيًا عن وظائف componentN (). لذلك ، تعمل التصريحات المتعددة معه خارج الصندوق.
سنتحدث أكثر عن فئة البيانات في الجزء التالي ، المكرس لطبقة البيانات.
طبقة البيانات
تتضمن هذه الطبقة POJO للبيانات من الخادم والقاعدة ، واجهات للعمل مع كل من البيانات المحلية والبيانات الواردة من الخادم.
للعمل مع البيانات المحلية ، تم استخدام Room ، والتي توفر لنا غلافًا ملائمًا للعمل مع قاعدة بيانات SQLite.
الهدف الأول للهجرة ، الذي يوحي بحد ذاته ، هو POJOs ، والتي هي في تعليمات Java البرمجية القياسية فئات مجمعة تحتوي على العديد من الحقول وطريقة get / set الخاصة بها. يمكنك جعل POJO أكثر إيجازًا بمساعدة فئات البيانات. يكفي سطر واحد من الكود لوصف كيان به عدة حقول:
data class Card(val id:String, val cardNumber:String, val brandId:String,val barCode:String)
بالإضافة إلى الإيجاز ، نحصل على:
- يتجاوز طرق يساوي () ، hashCode () ، و toString () تحت غطاء محرك السيارة. يعد إنشاء تساوي لجميع خصائص فئة البيانات مناسبًا للغاية عند استخدام DiffUtil في محول يولد طرق عرض لـ RecyclerView. الحقيقة هي أن DiffUtil يقارن مجموعتي البيانات ، قائمتين: القديم والجديد ، ومعرفة التغييرات التي حدثت ، واستخدام طرق الإخطار بتحديث المحول على النحو الأمثل. وعادة ، تتم مقارنة عناصر القائمة باستخدام يساوي.
وبالتالي ، بعد إضافة حقل جديد إلى الفصل ، لا نحتاج إلى إضافته إلى يساوي بحيث يأخذ DiffUtil الحقل الجديد في الاعتبار. - الطبقة الثابتة
- دعم القيم الافتراضية ، والتي يمكن استبدالها باستخدام نمط البناء.
مثال:
data class Card(val id : Long = 0L, val cardNumber: String="99", val barcode: String = "", var brandId: String="1") val newCard = Card(id =1L,cardNumber = "123")
خبر جيد آخر: مع تكوين kapt (كما هو موضح أعلاه) ، تعمل فئات البيانات بشكل جيد مع التعليقات التوضيحية للغرفة ، والتي تتيح لك ترجمة جميع كيانات قواعد البيانات إلى فئات البيانات. الغرفة تدعم أيضا خصائص لاغية. صحيح ، لا يدعم Room حتى الآن القيم الافتراضية من Kotlin ، ولكن تم إنشاء الخطأ المقابل بالفعل لهذا الغرض.
الاستنتاجات
درسنا فقط بعض المزالق التي قد تنشأ أثناء الترحيل من Java إلى Kotlin. من المهم ، على الرغم من أن المشاكل تنشأ ، خاصة مع نقص المعرفة النظرية أو الخبرة العملية ، إلا أنها جميعها قابلة للحل.
ومع ذلك ، فإن متعة كتابة رمز معبر وآمن موجزة في Kotlin ستدفع أكثر من كل الصعوبات التي تنشأ على المسار الانتقالي. أستطيع أن أقول بكل ثقة أن مثال مشروع DISCO يؤكد هذا بالتأكيد.
كتب ، روابط مفيدة ، موارد
- إن الأساس النظري لمعرفة اللغة سوف يسمح بوضع كتاب Kotlin in Action من المبدعين للغة Svetlana Isakova و Dmitry Zhemerov.
تجعل اللايقونية والمعلوماتية والتغطية الواسعة للموضوعات والتركيز على مطوري جافا وتوافر إصدار روسي من أفضل الأدلة الممكنة في بداية تعلم اللغة. لقد بدأت معها. - مصادر Kotlin مع developer.android.
- دليل Kotlin باللغة الروسية
- مقالة ممتازة لكونستانتين ميخائيلوفسكي ، مطور أندرويد من جينيسيس ، عن تجربة التحول إلى كوتلين.