خیلی سخت میشود پروژهای را گیر آورد که تمام مفاهیم جدید در توسعهی اندروید را در بر داشته باشد؛ به همین دلیل آقای ملادن راکونجاک (Mladen Rakonjac) آستین بالا زده و اغلب مفاهیم جدید در برنامهنویسی اندروید را در قالب یک اپلیکیشن ساده آموزش داده است. ما نیز از زبان نویسنده ترجمهاش را تقدیم شما میکنیم. در این درس و درسهای بعدی سراغ اندروید استودیو ۳ (جدیدترین نسخه) میرویم. کمی بیشتر با زبان کاتلین که پیشتر شایستگیاش بر زبان جاوا و دلایل مهاجرت به این زبان را توضیح دادهایم آشنا خواهیم شد. طرز استفاده از ConstraintLayout را توضیح میدهم. با کاربرد Build Variant ها آشنا خواهید شد. چگونگی بایند کردن دادهها (Data binding) به المانهای داخل صفحه را توضیح میدهم. با معماری MVVM که منطق و یوزر اینترفیس برنامه را از هم جدا میکند آشنا خواهید شد. یاد میگیرید چگونه از RxJava2 استفاده کنید و اینکه چطور به ساماندهی معماری برنامه کمک میکند. توضیحاتی در رابطه با تزریق وابستگی (Dependency Injection) و چگونگی استفاده از آن میدهم. دربارهی کتابخانهی مفید و کاربردی Retrofit و Room نیز توضیحاتی خواهم داد. همه این مفاهیم در قالب یک برنامهی کاربردی آموزش داده میشود.
حالا این برنامهای که میخواهیم بسازیم چیست؟
این برنامه یک فیچر بیشتر ندارد: تمام ریپوها را از آدرس googlesamples دریافت میکند، سپس در یک دیتابیس محلی ذخیره میکند و نهایتاً اطلاعات جمعآوری شده را به کاربر نمایش میدهد. همین. سعی میکنم تا حد امکان تمام کدها را توضیح دهم. این کدها را در گیتهاب منتشر کردهام بنابراین اگر با مشکلی مواجه شدید به آنجا مراجعه کنید.
خب شروع میکنیم.
۰- اندروید استودیو
برای دانلود و نصب اندروید استودیو ۳ به این صفحه مراجعه کنید. در صورتی که تمایل دارید نسخهی پیشین را هم داشته باشید در سیستم عامل مک باید به فولدر Application مراجعه کرده و نسخهی قبلی را تغییرنام دهید (مثلاً Android Studio Old). برای ویندوز و لینوکس هم طبق راهنمای این صفحه عمل کنید. نسخهی جدید اندروید استودیو به صورت کامل از زبان کاتلین ساپورت میکند. در هنگام ساخت پروژهی جدید (Create Android Project) شاهد چکباکس جدیدی با نام Include Kotlin support هستید که به صورت پیشفرض تیک خورده است. دوبار دکمهی Next را بزنید و در صفحهی بعد Empty Activity را انتخاب کنید، سپس finish کنید. خب این هم اولین برنامهی اندرویدی به زبان کاتلین 🙂
۱- کاتلین
فایل MainActivity.kt را باز کنید:
package me.mladenrakonjac.modernandroidapp import android.support.v7.app.AppCompatActivity import android.os.Bundle class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) } }
پسوند kt. یعنی فایل از نوع کاتلین است.
()MainActivity:AppCompatActivity یعنی کلاس AppCompatActivity را اکستند کردهایم.
بعلاوه همهی متدها در کاتلین باید از کلمهی fun استفاده کنند. ضمناً به جای annotation باید کلمهی override را به کار ببرید. حالا معنی ?savedInstantState: Bundle چیست؟ این یعنی پارامتر savedInstantState میتواند از نوع Bundle یا null باشد. همانطور که در پستهای قبلی گفتیم، کاتلین یک زبان null safety است یعنی دیگر خبری از NullPointerExceptionها نیست چون این زبان طوری طراحی شده که قبل از اجرای برنامه و در زمان کامپایل شما را وادار میکند تا تکلیف nullها را مشخص کنید.
مثلاً وقتی چنین متغیری در کاتلین تعریف میکنید:
var a : String
با خطای کامپایل مواجعه میشوید چرا؟ چون a مقداردهی نشده و تهی (null) است. اگر کاتلین null safety نبود و شما بدون مقداردهی a از آن استفاده میکردید در زمان اجرا با استثنای NPE: NullPointerException مواجه میشدید. از آنجایی که بخش عمدهای از زمان دیباگینگ صرف ریشهیابی این خطا میشود، حتی تونی هواری – مخترع NPE – هم به خاطر اختراعش عذرخواهی کرد. کاتلین اینگونه طراحی شد تا جلوی مشکل را از همان ابتدا بگیرد. این یعنی برای رفع خطای کامپایل، باید متغیر a را مقداردهی کنیم به این صورت:
var a : String = "Init value"
همچنین برای a=null هم با خطای کامپایل مواجه میشوید. برای null کردن یک متغیر در کاتلین باید اینگونه عمل کنید:
var a : String?
یک nameTextView را در نظر بگیرید. اگر مقدارش تهی باشد، کد پایین به خطای NPE منجر میشود:
nameTextView.setEnabled(true)
اما کاتلین اجازه انجام چنین کاری را به ما نمیدهد چون احتمال میدهد که nameTextView تهی باشد. برای حل مشکل یا باید از عملگر ؟ استفاده کنیم:
nameTextView?.setEnabled(true)
یا با مسئولیت خودمان از عملگر !! :
nameTextView!!.setEnabled(true)
وقتی از ؟ استفاده میکنیم کاتلین تنها وقتی این خط را پردازش میکند که مقدار nameTextView نال یا تهی نباشد. و عملگر !! یعنی کاتلین جان میدانم تهی است ولی دوست دارم استفاده کنم. دردسر NPE هم به عهدهی خودم!
خب فعلاً با همین مقدمه مطلب را ادامه میدهیم، هر جایی لازم بود توضیحات بیشتری میدهم.
۲- Build Variants
معمولاً در فرایند تولید و توسعهی نرمافزار، با محیطهای مختلفی سروکار دارید. رایجترینشان محیط تست و تولید است. این محیطها به لحاظ آدرس، آیکون، نام و api و … با هم تفاوتهایی دارند. من خودم در شروع هر پروژه معمولاً موارد زیر را دارم:
- finalProduction: همان apk نهایی است که در پلیاستور منتشر میشود؛
- demoProduction: این نسخهی بتای برنامه است با url مستقل که حاوی قابلیتهای جدیدی است. کلاینتها با دریافت این نسخه، بازخوردشان را به ما میدهند؛
- demoTesting: همانند demoProduction است فقط url آن فرق میکند؛
- mock: این برای توسعهدهندگان و طراحان مفید است. گاهی اوقات طراحی آماده است ولی API هنوز کامل نشده است. در چنین شرایطی نمیتوان منتظر آمادهسازی API ماند. این build variant دادههای جعلی یا fake تولید میکند و تیم طراحی میتواند با همین دادهها تستهای لازم را انجام دهند و نتیجه را گزارش دهند. همینکه کار معطل نمیشود نعمت بزرگی است. با کامل شدن API خیلی سریع میتوانید به محیط demoTesting سوئیچ کنید.
در این برنامه همهی این build variantها را داریم. تفاوتشان در applicationId و نام آنهاست. در gradle 3.0.0 قابلیت جدید flavorDimension وجود دارد که اجازه میدهد flavorهای مختلف را میکس کنید ولی در این برنامه ما فقط از default استفاده میکنیم. فایل build.gradle را باز نموده و این کدها را داخل {}android قرار دهید:
flavorDimensions "default" productFlavors { finalProduction { dimension "default" applicationId "me.mladenrakonjac.modernandroidapp" resValue "string", "app_name", "Modern App" } demoProduction { dimension "default" applicationId "me.mladenrakonjac.modernandroidapp.demoproduction" resValue "string", "app_name", "Modern App Demo P" } demoTesting { dimension "default" applicationId "me.mladenrakonjac.modernandroidapp.demotesting" resValue "string", "app_name", "Modern App Demo T" } mock { dimension "default" applicationId "me.mladenrakonjac.modernandroidapp.mock" resValue "string", "app_name", "Modern App Mock" } }
به فایل strings.xml مراجعه کرده و رشتهی app_name_string را حذف کنید تا تداخلی نداشته باشیم. خب حالا دکمه Sync Now را بزنید. اگر به قسمت Build Variants که در سمت چپ صفحه قرار دارد مراجعه کنید شاهد ۴ تا Build Variant خواهید بود که هر یک دو build type دارند یکی Debug و دیگری Release. به حالت demoProduction سوئیچ کرده و اجرایش کنید. سپس به دیگری سوئیچ و اجرا کنید. با این کار بایستی دو اپلیکیشن با دو نام متفاوت ببینید.
۳- ConstraintLayout
اگر activity_main.xml را باز کنید متوجه میشوید که layout از نوع ConstrintLayout است. در صورتی که برنامههای iOS ساخته باشید با AutoLayout آشنا هستید. ConstrintLayout دقیقاً چیزی مشابه آن است. جالب است بدانید که هر دوی این layoutها از الگوریتم یکسان Cassowary استفاده میکنند.
<?xml version=”۱٫۰″ encoding=”utf-8″?> <android.support.constraint.ConstraintLayout xmlns:android=”http://schemas.android.com/apk/res/android” xmlns:app=”http://schemas.android.com/apk/res-auto” xmlns:tools=”http://schemas.android.com/tools” android:layout_width=”match_parent” android:layout_height=”match_parent” tools:context=”me.mladenrakonjac.modernandroidapp.MainActivity”> <TextView android:layout_width=”wrap_content” android:layout_height=”wrap_content” android:text=”Hello World!” app:layout_constraintBottom_toBottomOf=”parent” app:layout_constraintLeft_toLeftOf=”parent” app:layout_constraintRight_toRightOf=”parent” app:layout_constraintTop_toTopOf=”parent” /> </android.support.constraint.ConstraintLayout>
Constraintها به ما کمک میکند تا رابطهی بین ویوها را تعیین کنیم. برای هر ویو، چهار constraint یا محدودیت وجود دارد که هر کدام در یک سمت از آن ظاهر میشود. در مثال ما، ویو از چهار سو به والد خود محدود شده است.
با اندکی حرکتِ Hello World در مُد طراحی (Design tab)،این کد در مد متنی ظاهر میشود:
app:layout_constraintVertical_bias="0.28"
تب طراحی و متنی هر دو هماهنگ هستند. تغییر در مد طراحی روی کدهای xml تولیدی تاثیر میگذارد و بالعکس. Vertical Bias گرایش عمودی ویو به محدودیت اعمال شده را نشان میدهد. اگر میخواهید ویو در ستون وسط قرار گیرد از این کد استفاده کنید:
app:layout_constraintVertical_bias="0.28"
خب بیایید طرز نمایش یک ریپو (Repository یا مخزن کد) را تعریف کنیم. میخواهیم نام ریپو، تعداد ستارهها، مالک و اینکه issue دارد یا نه را به کاربر نشان دهیم.
برای ساخت چین چیزی از این کدها استفاده میکنیم:
<?xml version=”۱٫۰″ encoding=”utf-8″?> <android.support.constraint.ConstraintLayout xmlns:android=”http://schemas.android.com/apk/res/android” xmlns:app=”http://schemas.android.com/apk/res-auto” xmlns:tools=”http://schemas.android.com/tools” android:layout_width=”match_parent” android:layout_height=”match_parent” tools:context=”me.mladenrakonjac.modernandroidapp.MainActivity”> <TextView android:id=”@+id/repository_name” android:layout_width=”wrap_content” android:layout_height=”wrap_content” android:layout_marginEnd=”۱۶dp” android:layout_marginStart=”۱۶dp” android:textSize=”۲۰sp” app:layout_constraintBottom_toBottomOf=”parent” app:layout_constraintHorizontal_bias=”۰٫۰″ app:layout_constraintLeft_toLeftOf=”parent” app:layout_constraintRight_toRightOf=”parent” app:layout_constraintTop_toTopOf=”parent” app:layout_constraintVertical_bias=”۰٫۰۸۳″ tools:text=”Modern Android app” /> <TextView android:id=”@+id/repository_has_issues” android:layout_width=”wrap_content” android:layout_height=”wrap_content” android:layout_marginEnd=”۱۶dp” android:layout_marginStart=”۱۶dp” android:layout_marginTop=”۸dp” android:text=”@string/has_issues” android:textStyle=”bold” app:layout_constraintBottom_toBottomOf=”@+id/repository_name” app:layout_constraintEnd_toEndOf=”parent” app:layout_constraintHorizontal_bias=”۱٫۰″ app:layout_constraintStart_toEndOf=”@+id/repository_name” app:layout_constraintTop_toTopOf=”@+id/repository_name” app:layout_constraintVertical_bias=”۱٫۰″ /> <TextView android:id=”@+id/repository_owner” android:layout_width=”۰dp” android:layout_height=”wrap_content” android:layout_marginBottom=”۸dp” android:layout_marginEnd=”۱۶dp” android:layout_marginStart=”۱۶dp” android:layout_marginTop=”۸dp” app:layout_constraintBottom_toBottomOf=”parent” app:layout_constraintEnd_toEndOf=”parent” app:layout_constraintStart_toStartOf=”parent” app:layout_constraintTop_toBottomOf=”@+id/repository_name” app:layout_constraintVertical_bias=”۰٫۰″ tools:text=”Mladen Rakonjac” /> <TextView android:id=”@+id/number_of_starts” android:layout_width=”wrap_content” android:layout_height=”wrap_content” android:layout_marginBottom=”۸dp” android:layout_marginEnd=”۱۶dp” android:layout_marginStart=”۱۶dp” android:layout_marginTop=”۸dp” app:layout_constraintBottom_toBottomOf=”parent” app:layout_constraintEnd_toEndOf=”parent” app:layout_constraintHorizontal_bias=”۱″ app:layout_constraintStart_toStartOf=”parent” app:layout_constraintTop_toBottomOf=”@+id/repository_owner” app:layout_constraintVertical_bias=”۰٫۰″ tools:text=”۰ stars” /> </android.support.constraint.ConstraintLayout>
tools:textها شما را گیج نکند. این متن فقط برای شما نشان داده میشود و در اجرای واقعی وجود ندارد. از این قابلیت استفاده میکنیم تا نمای بصری بهتری شاهد باشیم. احتمالاً متوجه طراحی تخت این صفحه شدهاید. هیچ layout تودرتویی وجود ندارد. تا حد امکان از طراحی تودرتوی layoutها اجتناب کنید چون به شدت روی پرفرمنس برنامه تاثیر میگذارد. اینجا میتوانید اطلاعات بیشتری کسب کنید. ConstrintLayout با صفحاتی در اندازههای مختلف نیز سازگار است.
برای کسب اطلاعات بیشتر در رابطه با این layout به آزمایشگاه گوگل کد مراجعه کنید یا مستندات آن را در گیتهاب مطالعه کنید.
۴- کتابخانهی Data Binding
اولین چیزی که بعد از شنیدن اسم data binding از خودم پرسیدم این بود که با وجود Butterknife چه نیازی به دیتا بایندینگ هست. به راحتی از طریق یک پلاگین به ویوها دسترسی دارم. چرا سراغ چیز دیگری بروم؟ اما با تحقیق بیشتر دربارهی این کتابخانه، همان حسی به من دست داد که برای اولین بار از Butterknige گرفتم.
ButterKnife چگونه به ما کمک میکند؟
این کتابخانه ما را از شر findViewByIdهای خستهکننده نجات میدهد. ButterKnife در واقع یک view injection در اندروید است. این کتابخانه تنها با استفاده از annotationها دسترسی به المانهای گرافیکی یا همان ویوها را فراهم میکند و دیگر نیازی به استفاده از findViewById نیست.
مشکل ButterKnife چیست؟
مشکلات نگهداری کد با استفاده از این کتابخانه حل نمیشود. در هنگام استفاده از این ButterKnife خطاهای زمان اجرا وجود دارند. ممکن است ویوی بایند شدهای داخل فایل xml وجود داشته باشد ولی کدش از اکتیویتی یا فرگمنت حذف نشده باشد. ضمناً با اضافه کردن هر ویو به فایل xml بایستی کدهای بایندینگش را هم اضافه کنید که بعد از مدتی واقعاً خستهکننده میشود. در واقع وقت زیادی سر همین بایند کردن ویوها هدر میرود.
اینجاست که کتابخانهی Data Binding به کمک ما میآید.
این کتابخانه منافع زیادی دارد. تنها با یک خط کد میتوانید تمامی ویوها را بایند کنید. اجازه دهید با مثال توضیح دهم. ابتدا لازم است این کتابخانه را به پروژهمان اضافه کنیم:
// at the top of file apply plugin: 'kotlin-kapt' android { //other things that we already used dataBinding.enabled = true } dependencies { //other dependencies that we used kapt "com.android.databinding:compiler:3.0.0-beta1" }
دقت کنید، ورژن کامپایلر Data Binding همان ورژن گریدل است که در فایل build.gradle وجود دارد:
classpath 'com.android.tools.build:gradle:3.0.0-beta1'
خب، حالا دکمه Sync Now را بزنید. به activity_main.xml رفته و محتویاتش را داخل یک تگ layout قرار دهید.
<?xml version=”۱٫۰″ encoding=”utf-8″?> <layout xmlns:android=”http://schemas.android.com/apk/res/android” xmlns:app=”http://schemas.android.com/apk/res-auto” xmlns:tools=”http://schemas.android.com/tools”> <android.support.constraint.ConstraintLayout android:layout_width=”match_parent” android:layout_height=”match_parent” tools:context=”me.mladenrakonjac.modernandroidapp.MainActivity”> <TextView android:id=”@+id/repository_name” android:layout_width=”wrap_content” android:layout_height=”wrap_content” android:layout_marginEnd=”۱۶dp” android:layout_marginStart=”۱۶dp” android:textSize=”۲۰sp” app:layout_constraintBottom_toBottomOf=”parent” app:layout_constraintHorizontal_bias=”۰٫۰″ app:layout_constraintLeft_toLeftOf=”parent” app:layout_constraintRight_toRightOf=”parent” app:layout_constraintTop_toTopOf=”parent” app:layout_constraintVertical_bias=”۰٫۰۸۳″ tools:text=”Modern Android app” /> <TextView android:id=”@+id/repository_has_issues” android:layout_width=”wrap_content” android:layout_height=”wrap_content” android:layout_marginEnd=”۱۶dp” android:layout_marginStart=”۱۶dp” android:layout_marginTop=”۸dp” android:text=”@string/has_issues” android:textStyle=”bold” app:layout_constraintBottom_toBottomOf=”@+id/repository_name” app:layout_constraintEnd_toEndOf=”parent” app:layout_constraintHorizontal_bias=”۱٫۰″ app:layout_constraintStart_toEndOf=”@+id/repository_name” app:layout_constraintTop_toTopOf=”@+id/repository_name” app:layout_constraintVertical_bias=”۱٫۰″ /> <TextView android:id=”@+id/repository_owner” android:layout_width=”۰dp” android:layout_height=”wrap_content” android:layout_marginBottom=”۸dp” android:layout_marginEnd=”۱۶dp” android:layout_marginStart=”۱۶dp” android:layout_marginTop=”۸dp” app:layout_constraintBottom_toBottomOf=”parent” app:layout_constraintEnd_toEndOf=”parent” app:layout_constraintStart_toStartOf=”parent” app:layout_constraintTop_toBottomOf=”@+id/repository_name” app:layout_constraintVertical_bias=”۰٫۰″ tools:text=”Mladen Rakonjac” /> <TextView android:id=”@+id/number_of_starts” android:layout_width=”wrap_content” android:layout_height=”wrap_content” android:layout_marginBottom=”۸dp” android:layout_marginEnd=”۱۶dp” android:layout_marginStart=”۱۶dp” android:layout_marginTop=”۸dp” app:layout_constraintBottom_toBottomOf=”parent” app:layout_constraintEnd_toEndOf=”parent” app:layout_constraintHorizontal_bias=”۱″ app:layout_constraintStart_toStartOf=”parent” app:layout_constraintTop_toBottomOf=”@+id/repository_owner” app:layout_constraintVertical_bias=”۰٫۰″ tools:text=”۰ stars” /> </android.support.constraint.ConstraintLayout> </layout>
توجه کنید که همهی xmlnsها را به تگ layout منتقل کنید. حالا دکمهی build یا کلیدهای Ctrl+F9 را بزنید. این کار برای استفاده از کلاس ActivityMainBinding که توسط کتابخانه Data Binding تولید میشود و بعدها در کلاس MainActivity از آن استفاده میکنیم ضروری است.
https://media.giphy.com/media/xT39CX3yiZb8IaTpjq/giphy.gif
در صورتی که پروژه را build نکنید در هنگام کامپایل ActivityMainBinding در لیست پیشنهادی وجود ندارد. کار بایندینگ هنوز تمام نشده، تا الان فقط گفتهایم یک متغیر غیرتهی (non-null) داریم که ActivityMainBinding است. همچنین اگر دقت کنید، من از علامت ؟ در انتهای ActivityMainBinding استفاده نکردم و مقدار دهی اولیه هم نکردم. مگر نگفتیم که چنین کاری در کاتلین مجاز نیست. چرا ولی اینجا از مودیفایر lateinit استفاده کردهایم که اجازه میدهد متغیرهایی غیرتهی و در انتظار مقداردهی داشته باشیم. مانند ButterKnife، مقداردهی متغیر binding بایستی در متد onCreate صورت بگیرد یعنی زمانی که layout ما رندر شده و آماده است. بعلاوه binding را نباید داخل این متد تعریف کنید چرا که شاید خارج از متد onCreate هم به آن نیاز داشته باشیم. binding نباید null باشد؛ به همین خاطر از lateinit استفاده کردهایم. با استفاده از مودیفایر lateinit دیگر نیازی نیست با هر بار استفاده از binding عملیات null check را انجام دهیم.
بیایید متغیر binding را مقداردهی کنیم. به جای :
setContentView(R.layout.activity_main)
از این استفاده کنید:
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
تمام شد. شما با موفقیت توانستید ویوها را بایند کنید. اکنون به راحتی میتوانید به این ویوها دسترسی داشته باشید و مقدارشان را تغیر دهید. مثلاً برای تغییر repositoryName به کد زیر نیاز دارید:
binding.repositoryName.text = "Modern Android Medium Article"
همانطور که میبینید به همهی ویوهای activity_main.xml از طریق متغیر binding دسترسی داریم (البته آنهایی که id دارند). این دلیل برتری Data binding بر ButterKnife است.
Getter و Setter در کاتلین
احتمالاً متوجه شدهاید که از متد ()setText برای مقداردهی المانها استفاده نکردهایم. بهتر میدانم قبل از ادامهی بحث اندکی در رابطه با getter و setter در کاتلین و مقایسهی آن با جاوا صحبت کنم.
ابتدا باید بدانید که چرا از getterو setter استفاده میکنیم. ما از این قابلیت برای مخفی کردن متغیرهای کلاس و مقداردهی آنها از طریق متدها استفاده میکنیم. یعنی جزئیات کلاس را از دید مصرفکنندهها پنهان میکنیم و جلوی هر گونه مقداردهی مستقیم را میگیریم. کلاس Square را در زبان جاوا در نظر بگیرید:
public class Square { private int a; Square(){ a = 1; } public void setA(int a){ this.a = Math.abs(a); } public int getA(){ return this.a; } }
با کمک متد ()setA هرگونه دسترسی مستقیم کلاینتها به متغیر a و مقداردهی منفی آن گرفته میشود. برای این منظور باید متغیر a را private تعریف کنید تا از خارج کلاس امکان دسترسی به آن نباشد. private بودن a جلوی دسترسی به مقدار آن خارج از کلاس را هم میگیرد پس یک متد Getter نیز که اینجا اسمش ()getA است نیاز داریم. در جاوا اگر ۱۰ متغیر دیگر هم داشته باشید برای هر کدام به یک Getter و یک Setter نیاز دارید. این کار خستهکننده و زمانبر است. کاتلین کار را بسیار آسان کرده است. در این زبان کد پایین:
var side: Int = square.a
به این معنی نیست که مستقیماً به متغیر a دسترسی دارید. به زبان کد معنیاش این است:
int side = square.getA();
در واقع کاتلین خودش Getter و Setterهای لازم را جنریت میکند. در این زبان تنها وقتی که به Getter یا Setter خاصی نیاز باشد تعریفش میکند در غیر این صورت همهی انها به صورت پیشفرض توسط کاتلین تولید میشود. بنابراین وقتی مینویسید a=somthing در واقع متد set وارد عمل میشود.
برویم سراغ یکی دیگر از ویژگیهای جالب زبان کاتلین، apply:
class MainActivity : AppCompatActivity() { lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.apply { repositoryName.text = "Medium Android Repository Article" repositoryOwner.text = "Mladen Rakonjac" numberOfStarts.text = "1000 stars" } } }
کاملاً گویا است. apply اجازه میدهد تا چندین متد را تنها با یک نمونه فراخوانی کنید. کارمان هنوز با data Binding تمام نشده است. خب حالا میخواهیم برای ریپوزیتوری یک کلاسِ UI Model ایجاد کنیم (این UI Model برای ریپوهای گیتهاب است و دادههایی که باید نمایش داده شود را نگهداری میکند، آن را با الگوی طراحی Repository اشتباه نگیرید.) برای ساخت یک کلاس جدید در کاتلین به این مسیر بروید: New -> Kotlin File/Class :
class Repository(var repositoryName: String?,var repositoryOwner: String?,var numberOfStars: Int? ,var hasIssues: Boolean = false)
در کاتلین، سازندهی اولیه بخشی از هیدر کلاس است. در صورتی که نیازی به سازندهی دوم ندارید همینجا کارتان با ساخت این کلاس تمام میشود. هیچ پارامتر و getter و setterای وجود ندارد. کل کلاس همین یک خط است.
به کلاس MainActivity.kt مراجعه کنید . نمونهای از کلاس Repository بسازید:
var repository = Repository("Medium Android Repository Article", "Mladen Rakonjac", 1000, true)
همانطور که میبینید در کاتلین برخلاف جاوا، ساخت یک نمونهی جدید از کلاس نیازی به کلیدواژهی New ندارد.
خب برویم سراغ activity_main.xml و تگ data را اضافه کنیم:
<data> <variable name=”repository” type=”me.mladenrakonjac.modernandroidapp.uimodels.Repository” /> </data>
ما میتوانیم به متغیر repository از داخل layout دسترسی داشته باشیم. برای مثال:
android:text="@{repository.repositoryName}"
تکست ویویی با آی دی repository_name پراپرتی repositoryName که مقدارش را از متغیر repository دریافت میکند نمایش میدهد. تنها چیزی که باقی مانده اتصال متغیر repository در فایل xml به repository واقع در فایل MainActivity.kt است. دکمهی build را بزنید تا کتابخانهی دیتا بایندینگ، کلاسهای مورد نیاز را ایجاد کند. خب حالا به Main Activity برگردید و این دو خط را اضافه کنید:
binding.repository = repository binding.executePendingBindings()
اگر برنامه را اجرا کنید مقدار Medium Android Repository Article را مشاهده خواهید کرد. قابلیت جالبی است نه؟
حالا اگر از کد پایین استفاده کنیم چه میشود:
Handler().postDelayed({repository.repositoryName="New Name"}, 2000)
آیا مقدار متنی New Name بعد از ۲۰۰۰ میلیثانیه نمایش داده میشود؟ خیر. این اتفاق نمیافتد. برای این کار باید repository را دوباره تنظیم کنید. چیزی شبیه این:
Handler().postDelayed({repository.repositoryName="New Name" binding.repository = repository binding.executePendingBindings()}, 2000)
اما انجام این کار هر وقت تغییری در پراپرتی ایجاد شود کار خستهکنندهای است. برای این کار راهکار بهتری تحت عنوان Property Observer وجود دارد. ایتدا اجازه دهید الگوی Observer را که بعداً در بخش rxJava هم به آن نیاز داریم توضیحج دهیم:
احتمالاً با سایت androidweekly.net که به صورت هفتگی اخبار جدید توسعهی اندروید را منتشر میکند آشنا هستید. برای دریافت این اخبار لازم است تا در خبرنامهی آن عضو شوید. با این کار هر هفته اطلاعات جدید برای شما ایمیل میشود. هر زمانی که بخواهید میتوانید درخواست لغو عضویت دهید.
این یک مثال ساده از الگوی Observer/Observable بود. در این مثال Android Weekly همان Observable است که هر هفته اخبار جدید را منتشر میکند. خوانندهها یا افرادی که در خبرنامه عضو شدهاند نیز Observer هستند که اخبار دریافتی را به محض انتشار دریافت میکنند. هر کسی که مایل به ادامهی دریافت اخبار نباشد عضویت خود را لغو میکند.
Property Observer در مثال ما xml Layout است که به محض تغییر Repository مقدار جدید را نشان میدهد. بنابراین Repository معادل Observable است. مثلاً وقتی مقدار پراپرتی repository name تغییر کند، فایل xml بایستی بدون فراخوانی کد پایین بروزرسانی شود:
binding.repository = repository binding.executePendingBindings()
چطور میتوانیم این کار را با کتابخانهی Data Binding انجام دهیم؟ این کتابخانه کلاس BaseObservable را تدارک دیده است که باید کلاس Repository آن را پیادهسازی کند:
class Repository(repositoryName : String, var repositoryOwner: String?, var numberOfStars: Int? , var hasIssues: Boolean = false) : BaseObservable(){ @get:Bindable var repositoryName : String = "" set(value) { field = value notifyPropertyChanged(BR.repositoryName) } }
BR کلاسی است که به محض استفاده از انوتیشین Bindable به صورت خودکار ساخته میشود. همانطور که میبینید با تغییر مقدار پراپرتی اطلاعرسانی میشود. حالا برنامه را اجرا کنید. خواهید دید repository name بعد از ۲ ثانیه به صورت خودکار تغییر میکند آن هم بدون فراخوانی مجدد ()executePendingBindings.
خب به پایان بخش اول رسیدیم. در قسمت بعدی راجع الگوی MVVM، الگوی طراحی Repository و Android Wrapper Manager خواهم نوشت. تمامی کدهای استفاده شده را میتوانید در مخزن گیتهاب مشاهده کنید.
4 دیدگاه
ممنون از مطالب خوبتون
چطور میتوان یک build variant برای mock دیتا داشت؟
ممنون به خاطر مطالب عالیتون. چند مشکل در توضیحات و کد ها وجود داره (در منبع اصلی هم این مشکلات هست)
برای قرار دادن ویو در ستون وسط از استفاده میکنیم (یا کلا استفاده نمی کنیم! به طور پیشفرض این مقدار ست شده)
app:layout_constraintVertical_bias=”0.5″
نیازی به استفاده افزودن این لایبرری نیست
kapt “com.android.databinding:compiler:3.0.0-beta1”
کد پایین به تنهایی کافی است
dataBinding.enabled = true
کد زیر جا مانده
lateinit var binding: ActivityMainBinding
نیازی به کد زیر در هیچ کجا نیست
binding.executePendingBindings()
کد پایین به شکل عجیبی با توجه به حرفهای بودن نویسنده اشتباه است
var repositoryName : String = “”
از این استفاده کنید تا مقدار دهی اولیه انجام شود
var repositoryName : String = repositoryName
برای رفع خطای شگفت انگیز
BR.repositoryName
کد زیر را به اول فایل گردلی اضافه کنید
apply plugin: “kotlin-kapt”
(پیر شدم بخاطرش)
موفق باشید
سلام
تشکر از شما
سلام واقعا دم شما گرم
عالی بود
منتظر ادامه آموزش هستم…