اگه به برنامه نویسی برای موبایل، مخصوصاً سیستم عامل اندروید آشنا یا بهش علاقه داشته باشین، شاید اسم Android Jetpack به گوشتون خورده باشه. اگه نخورده شاید براتون جالب باشه که بدونین:
به مجموعه ای از کامپوننتها، ابزار و راهنمایی ها برای ساخت اپلیکیشن های بروز، مدرن و بهینه در اندروید، Android Jetpack گفته میشه. جالبه با اینکه پیشوند این کامپوننت ها *.androidx هست ولی اصطلاحاً unbundled هستن (به این معنی که بصورت پیشفرض روی پلت فرم اندروید موجود نیستن! بنابراین برای استفاده باید به پروژه اضافه بشن). از مهمترین ویژگی هاشون هم میشه به این موارد اشاره کرد:
اکثر این کامپوننت ها جوری طراحی شدن که کنار همدیگه استفاده بشن (و چه کد معرکه ای میشه وقتی از این مجموعه استفاده کنیم)، اما در عین حال میتونیم هرکدوم که خواستیم رو به پروژه اضافه کنیم. اگه توسعه دهنده ی اندروید هستین، شاید باورش راحت نباشه ولی خواسته یا ناخواسته حتماً حدأقل از چندتا از این موارد استفاده کردین.
خب حالا که با کلیت این موجوعه آشنا شدیم، بریم سراغ اینکه ببینیم شامل چه مواردی میشن (سعی میکنم خلاصه ترین حالتی که بشه کلیتشون رو معرفی کرد بیان کنم. همچنین دقت کنیم که این مقاله فقط برای آشنایی با این موارد هست نه بررسی موشکافانه ی همشون! و اینکه برخی از این کامپوننت ها رو میشه توی سایر ideها هم استفاده کرد ولی اکثرشون به اندروید استدیو ورژن 3.2 یا بالاتر نیاز دارن):
شامل موارد زیر:
برخی موارد مانند طراحی متریال، از نسخه های جدیدتر اندروید معرفی شدن یا استفاده میشن (برای مثال طراحی متریال دیزاین که با اندروید لالیپاپ معرفی شد)، و در نسخه های پایین تر، یا قابلیت استفاده ندارن و یا دچار خطا میشن. با توجه به این موضوع، ما با این library میتونیم در نسخه های قدیمی تر هم رفتار مشابهی ببینیم.
اگه از زبان کاتلین برای توسعه ی اندروید استفاده میکنین (که اگه نمیکنین بشدت توصیه میکنم حتما برین سراغش)، این لایبرری بشدت میتونه کمکتون بکنه. به عبارت دیگه، کدهای شما بهینه تر و دارای حجم کمتری میشن. بذارین با دو تا مثال بهتر تفاوتش رو حس کنیم:
این قطعه کد به زبان کاتلین هست:
view.viewTreeObserver.addOnPreDrawListener( object : ViewTreeObserver.OnPreDrawListener { override fun onPreDraw(): Boolean { viewTreeObserver.removeOnPreDrawListener(this) actionToBeTriggered() return true } } )
و تفاوت فاحشش رو با استفاده از Android KTX ببینیم:
view.doOnPreDraw { actionToBeTriggered() }
مثال دیگه رو با جابجایی فرگمنت ها داریم:
supportFragmentManager .beginTransaction() .replace(R.id.my_fragment_container, myFragment, FRAGMENT_TAG) .commitAllowingStateLoss()
و با کمک Android KTX تبدیل به کد زیر میشه:
supportFragmentManager.transaction(allowStateLoss = true) { replace(R.id.my_fragment_container, myFragment, FRAGMENT_TAG) }
برای استفاده از این قابلیت بعد از اطمینان از وجود ریپازیتوری گوگل توی پروژتون:
repositories{ google() }
در بخش وابستگی های پروژتون ktx رو اضافه کنین:
dependencies { implementation 'androidx.core:core-ktx:$KTX_VERSION' }
(که در زمان نگارش این مقاله KTX_VERSION برابر 1.0.0هست)
اگه برنامه های حجیم و بزرگ با اندروید نوشته باشین شاید به مشکلی برخورده باشن که به 64K refrence limit مصطلح هست. به معنی اینکه کل رفرنس هایی که توی کد خودتون، لایبرری هایی که استفاده کردین و یا موارد موجود توی خود فریم ورک اندروید، نباید از تعداد 65536 (64x2^10) تجاوز کنه. اگه این اتفاق افتاد و شما با ارورهای زیر مواجه شدین:
trouble writing output: Too many field references: 131000; max is 65536. You may try using --multi-dex option.
یا در ورژن های پایینتر:
Conversion to Dalvik format failed: Unable to execute dex: method ID not in [0, 0xffff]: 65536
باید بجای حالت پیشفرض اندروید که single Dalvik Executable (DEX) هست از multidex استفاده کنین. برای این منظور اگه minSdkVersion شما بالای 21 هست، بصورت پیشفرض از این قابلیت پیشتیبانی میکنه و فقط کافیه کد زیر رو به کانفیگ پروژتون اضافه کنین:
android { defaultConfig { ... multiDexEnabled true } ... }
اگه از ورژن 20 یا پایین تر استفاده میکنین میتونین به این لینک یا این یکی توی سایت استک اور فلو سری بزنین.
از اونجایی که توی این مقاله نمیخوایم صرفاً ترجمه داشته باشیم، و با توجه به اینکه قضیه ی تست مقداری طولانی هست از رفتن سراغ جزییات این قسمت خودداری میکنیم (در آینده مقاله ای جدا در این زمینه نوشته خواهد شد).
شامل موارد زیر:
کسانی که هنگام برنامه سازی برای اندروید، ویو رو با xml پیاده سازی میکنن، به کمک دیتابایندینگ دیگه نیازی نیست هربار findViewById رو برای تک تک عناصر ویوشون فراخوانی کنن.
همچنین دیتاباندینگ این قابلیت رو میده شما مدلی رو بدین به xmlتون و اون خودش دیتا رو ست کنه (بجای اینکه تک تک دیتارو خودتون داخل کد ست کنید). برای پیاده سازی دیتابایندینگ xml بصورت زیر تغییر میکنه:
<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> <variable name="viewmodel" type="com.myapp.data.ViewModel" /> </data> <!-- UI layout's root element --> </layout>
توی اعضای این xml میتونیم از viewmodel استفاده کنیم. برای مثال برای یه TextView پارامتر textش رو از viewmodel بگیریم:
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{viewmodel.text}" />
یکی دیگه از مزایای استفاده از دیتابایندینگ قابلیت ایجاد BindingAdapter هست. در حقیقت ما میتونیم تابعی پیاده سازی و توی xml مستقیماً ازش استفاده کنیم. برای مثال برای فرض کنیم که میخوایم TextViewمون html رو هم ساپورت کنه. میتونیم برای این کار تابع زیر رو تعریف کنیم:
@BindingAdapter("htmlText") public static void setText(TextView textView, String text) { if (!TextUtils.isEmpty(text)) { textView.setText(Html.fromHtml(text)); } else { textView.setText(text); } }
و کافیه توی xml بصورت زیر استفادش کنیم:
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" bind:htmlText="@{@string/html_text}" />
دقت کنیم که bind (یا هر اسم دیگه ای که علاقه داشتین بذارین) باید بصورت زیر به xml معرفی شده باشه:
xmlns:bind="http://schemas.android.com/apk/res-auto"
گرچه این عمل رو مستقیما بصورت زیر و بدون تعریف BindingAdapter هم میتونیم انجام بدیم (البته برای جلوگیری از خطا باید مطمئن باشیم نوشته ای که بهش میدیم تبدیل به html میشه و خطایی نداشته باشه):
android:text="@{Html.fromHtml(@string/my_html)}"
و البته اینم بگیم که دیتاباندینگ رو هم میشه توی اکتیویتی و فرگمنت، هم توی RecyclerView و هم توی جاهای دیگه هم استفاده کرد. ولی توی این مثالی که ما زدیم برای اینکه viewmodel رو ست کنیم بصورت زیر عمل میکنیم:
binding = DataBindingUtil.setContentView(this, R.layout.activity_main); binding.setViewModel(ourViewModelObject);
دقت کنیم که بجای setContentView که قبلا استفاده میکردیم از خط اول کد بالا برای ست کردن ویو استفاده میکنم. کار با دیتابایندینگ جزییات زیاد داره حتماً در موردش مطالعه کنید چون مزایای خیلی زیادی داره و وقتی ازش استفاده کنید متوجه میشید که چقدر کمکتون خواهد کرد.
دیتابایندینگ رو باید بصورت زیر به پروژتون اضافش کنید (باید از gradle ورژن 1.5.0 یا بالاتر استفاده کرده باشین):
android { .... dataBinding { enabled = true } }
برای ذخیره سازی اطلاعات توی اندروید چند راهکار وجود داره که یکی از اونها استفاده از دیتابیس و معروفترین دیتابیس موجود sqlite هست. کامپوننت Room کار با دیتابیس رو ساده تر و بهینه تر میکنه. روم علاوه بر اینکه دستورات پایه ای مثل اضافه، حذف، و بروزرسانی رو ساپورت میکنه، امکان اجرای دستورات پیچیده ی sqlite رو هم در صورت نیاز برای کاربر فراهم میکنه. برای استفاده از روم بعد از اینکه اطمینان حاصل کردین که قسمت repositories داخل gradle پروژتون google() رو داره وابستگی زیر رو به قسمت مرتبط به زیرپروژه (یا اصطلاحاً app level) اضافه کنید:
implementation "android.arch.persistence.room:runtime:1.0.0" annotationProcessor "android.arch.persistence.room:compiler:1.0.0"
اگه میخواین از تستش هم استفاده کنید خط زیر رو هم اضافه کنید:
androidTestImplementation “android.arch.persistence.room:testing:1.0.0”
(طبیعتاً هر ورژنی بخواید میتونید استفاده کنید ولی در زمان نگارش ورژن 1.0.0 بود)
یه مثال خیلی مختصر از نحوه پیاده سازی ببینیم. اول یه کلاس entity ایجاد میکنیم:
@Entity(tableName = "users") public class User { @PrimaryKey @ColumnInfo(name = "userid") private String mId; @ColumnInfo(name = "username") private String mUserName; @ColumnInfo(name = "last_update") private Date mDate; @Ignore public User(String userName) { mId = UUID.randomUUID().toString(); mUserName = userName; mDate = new Date(System.currentTimeMillis()); } public User(String id, String userName, Date date) { this.mId = id; this.mUserName = userName; this.mDate = date; } ... }
با @Entity به روم میفهمونیم که این کلاس یه entity هست و با tableName میگیم که اسم تیبلمون چی باشه. با @PrimaryKey کلید اولیه یا اصلی رو معرفی میکنیم و با استفاده از @ColumnInfo اسم هر ستون داخل تیبل رو مشخص میکنیم. با @Ignore هم به روم میگیم که از کدوم تابع سازنده استفاده نکنه. روم موارد دیگه هم داره مثل @TypeConvertors (برای تبدیل نوع داده ها) که وارد معرفی یا بیان جزییاتشون نمیشم.
خب حالا که تیبلمون رو معرفی کردیم باید یه DAO یا همون Data Access Object بسازیم. برای مثال:
@Dao public interface UserDao { @Insert User insertUser (User user); @Update void updateUser(User user); @Delete void deleteUser (User user); @Query(“SELECT * FROM users”) List<User> getUsers(); }
همونطور که گفتیم روم عملیات اضافه، حذف و بروزرسانی رو خودش ساپورت میکنه و این کار رو میشه با این annotationها انجام داد @Insert برای اضافه @Delete برای حذف و @Update برای بروزرسانی.
خب نوبت میرسه به ساخت خود دیتابیس:
@Database(entities = {User.class}, version = 1) @TypeConverters(DateConverter.class) public abstract class UsersDatabase extends RoomDatabase { private static UsersDatabase INSTANCE; public abstract UserDao userDao(); }
با انوتیشن @Database نشون میدیم که این کلاس دیتابیسمون هست و ویژگیهای entities (تیبل های دیتابیسمون) و version (نسخه ی دیتابیسمون که زمان تغییر نسخه از migration استفاده میکنم) رو برای تعریف مکنیم.
و قطعه کد زیر نمونه ایه از استفاده از کوئری های دلخواه خودمون:
@Query("SELECT * FROM users " + "WHERE username LIKE '% :like %' " + "GROUP BY userid " + "ORDER BY last_update " + "LIMIT : limit") List<User> customUsers(String like, int limit);
برای استفاده از دیتابیس بصورت زیر میتونیم تعریفش کنیم:
database = Room.databaseBuilder(context.getApplicationContext(), UsersDatabase.class, "Sample.db") .addMigrations(MIGRATION_1_2) .build();
خب وقت استفاده از روم میرسه. برای این کار یه userDao تعریف میکنیم و میتونیم بصورت زیر لیست کل کاربرها رو از تیبلمون بگیریم:
userDao.getUsers();
اما با اجرای این دستور پیغام خطای زیر رو میگیریم:
java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
به دلیل اینکه روم اجازه نمیده روی نخ اصلی ارتباط با دیتابیس برقرار بشه. برای رفع این مشکل میشه تعریف دیتابیس رو بصورت زیر تغییر داد:
database = Room.databaseBuilder(context.getApplicationContext(), UsersDatabase.class, "Sample.db") .addMigrations(MIGRATION_1_2) .allowMainThreadQueries() .build();
(خط .allowMainThreadQueries() رو اضافه کنیم)
از اسمش مشخصه چی میگه، و این کارِ خیلی بدیه. برای همین میتونیم با روش هایی مثل استفاده از AsynTask یا ReactiveX programming ریکوئست ها رو توی بک گراند یا روی نخ io انجام بدیم.
شامل موارد زیر:
شامل موار دزیر:
منتشر شده در ویرگول توسط محمد قدسیان https://virgool.io/@mohammad.ghodsian
https://virgool.io/@mohammad.ghodsian/androidjetpack-lloycecccb7w