ساخت اپلیکیشن های مدرن با استفاده از معماری کامپوننت ها در اندروید

توسعه دهندگان اندروید در حین ساختن اپلیکیشن های اندرویدی با چالش های رایجی مانند مدیریت چرخه حیات رویدادها، مرحله نگهداری و ذخیره سازی داده ها مواجه می شوند. این عوامل باعث سختی توسعه اپلیکیشن اندرویدی می شوند. بنابراین در این مقاله ساخت اپلیکیشن های مدرن با استفاده از معماری کامپوننت ها در اندروید را بررسی خواهیم کرد.

ساخت اپلیکیشن های مدرن با استفاده از معماری کامپوننت ها در اندروید

توسعه دهندگان اندروید در حین ساختن اپلیکیشن های اندرویدی با چالش های رایجی مانند مدیریت چرخه حیات رویدادها، مرحله نگهداری و ذخیره سازی داده ها مواجه می شوند. این عوامل باعث سختی توسعه اپلیکیشن اندرویدی می شوند. علاوه بر این ها اعضای یک تیم اندروید هیچگاه دیدگاه های خود را در مورد اینکه از کدام معماری استفاده کنیم مطرح نمی کنند. آنها همیشه پیشنهاد می کنند که از معماری ای استفاده شود که بهترین سازگاری رو با اپلیکیشن دارد، که در بیشتر مواقع این پیشنهاد باعث گنگی بیشتر میشود تا روشن تر کردن قضیه.

خوشبختانه تیم اندروید، معماری کامپوننت ها را همراه با دستورالعمل ها و راهنمایی ها برای حل مشکلات رایج در حین ساخت اپلیکیشن اندرویدی دراختیار توسعه دهندگان گذاشته است.

معماری کامپوننت ها چیست؟

باتوجه به گفته های تیم توسعه دهنده این معماری، "معماری کامپوننت ها یک مجموعه جدید از کتابخانه هایی است که کمک میکنند تا شما یک اپلیکیشن قدرتمند که قابلیت تست و نگهداری دارد را طراحی کنید. آنها با کلاس هایی که وظیفه مدیریت چرخه حیات اجزای رابط کاربری شما ومدیریت ماندگاری داده ها را بر عهده دارند شروع کردند."

درادامه کامپوننت های ماژولاری که می توانند با یکدیگر یا جدا از هم استفاده شوند آورده شده است:

1- چرخه حیات (LifeCycle)

2- داده زنده (LiveData)

3- مدل نما (ViewModel)

4- فضا (Room)

نکته: چند کامپوننت دیگر هم هستند که به تازگی منتشر شده اند ولی ما درباره آنها صحبت نمیکنیم چون در مرحله اولیه توسعه هستند.

چرخه حیات (Lifecycle)

مدیریت چرخه حیات یکی از بزگترین چالش های ساخت یک اپلیکیشن اندرویدی است. اگر از فرگمنت ها استفاده کنید چرخه حیات حتی می تواند پیچیده تر شود ، به این دلیل که چرخه حیات آنها در نسخه های مختلف اندروید متفاوت است ( البته این مشکل الان توسط کتابخانه AppCompat حل شده است). بریم ببنیم که کامپوننت چرخه حیات چگونه به ما کمک میکند تا از سد این چالش عبور کنیم.

Lyfecycle، کلاس ها و اینترفیس هایی را اماده کرده است که اجازه ساخت کامپوننت های مطلع از چرخه حیات (lifecycle-aware) را میدهند که به صورت خودکار رفتار خودشان را با چرخه حیات فعلی یک اکتیویتی یا فرگمنت سازگار میکنند. این امکان اجازه میدهد که چرخه حیات برای بقیه ابجکت ها قابل مشاهده باشد.

این کامپوننت برای اضافه کردن چرخه های حیات غیر مطلع(lifecycle-awareness) استفاده میشود. این مورد برای کسانی که کتابخانه می سازند مفید خواهد بود به این دلیل که میتوانند منابع را باتوجه به چرخه حیات ازاد کنند.

بریم ببینیم چگونه کامپوننت Lyfecycle به ما کمک میکند. اینجا یک مثال از اینکه چگونه یک کد شبکه بنویسیم اورده شده است:

override fun onCreate(savedInstanceState: Bundle?) {
 
super.onCreate(savedInstanceState)
 
setContentView(R.layout.activity_main)
 
// Network call
 
api.get().onResponse { data ->
 
textView.text = data
 
}
 
}

نکته: کد بالا به زبان کاتلین است، که الان به عنوان زبان رسمی ساخت اپلیکیشن های اندروید استفاده می شود.

این مثال یک کد ساده شبکه است که یک درخواست GET به وب ارسال میکند و پاسخ ان را در یک TextView تنظیم میکند.

ساده به نظر می رسد، ولی اینطور نیست، زیرا هنگامی که شما دستگاه اندرویدی خود را بچرخانید یا اینکه از برنامه خارج شوید، درخواستی که به وب فرستاده بودید همچنان در بک گراند در حال اجراست و وقتی که پاسخ خود را دریافت میکند برنامه کرش میکند چون دیگر TextView وجود ندارد زیرا اکتویتی از بین رفته است.

توسعه دهندگان اندروید از این مشکل باخبر هستند. برای حل این مشکل، نیاز داریم وقتی که اکتیویتی از بین میرود درخواست را لغو کنیم مانند کد زیر:

override fun onCreate(savedInstanceState: Bundle?) {
 
///....
 
// Network call
 
request = api.get()
 
request.onResponse { data ->
 
textView.text = data
 
request = null
 
}
 
}
 
 
override fun onDestroy() {
 
if (request != null) request.cancel()
 
}

هرچند، با افزایش پیچیدگی اپلیکیشن، مدیریت این مساله به یک چالش تبدیل می شود و به راحتی ممکن است جاهایی را فراموش کنیم.

کامپوننت Lyfecycle برای حل چنین مشکلاتی طراحی شده است که با ایجاد کامپوننت های مطلع از چرخه حیات (lifecycle-aware) ( که از وضعیت اکتیویتی با خبر هستند) مشکل را حل میکنند.

بیاید این بار با استفاده از کتابخانه Lifecyle نگاهی به همان تکه کد کنیم:

override fun onCreate(savedInstanceState: Bundle?) {
 
super.onCreate(savedInstanceState)
 
setContentView(R.layout.activity_main)
 
// Network call
 
api.get().onResponse(getLifecycle()) { data ->
 
textView.text = data
 
}
 
}

نکته: این یک کد شبکه فرضی است برای اینکه نشان دهد چگونه کتابخانه های دیگر میتوانند چرخه حیات را مشاهده کنند و به صورت خودکار انها را از بین ببرند.

ساختمان کامپوننت چرخه حیات (Lyfecycle) :

این ساختمان از دو بخش اصلی تشکیل شده است – صاحبان چرخه حیات (Lifecycle owners) و ناظران چرخه حیات (Lifecycle owners).

1- صاحبان چرخه حیات (Lifecycle owners) کنترل کنندگان رابط کاربری هستند، که چرخه حیات خودشان را دارند مثل اکتیویتی و فرگمنت.

2- ناظران چرخه حیات (Lifecycle owners) مشاهده گر صاحبان چرخه حیات (Lifecycle owners) هستند و هر تغییر در مورد چرخه حیات انها را اعلان میکنند. ما از LifecycleObserver برای ایجاد کامپوننت های مطلع از چرخه حیات (lifecycle-aware) استفاده میکنیم.

LiveData

LiveData برای این استفاده می شود که فورا تغییرات ایجاد شده در داده ها را به رابط کاربری خبر دهد. لیست زیر توصیفی است از LiveData :

-یک نگهدارنده داده قابل مشاهده است.

-ناظران را از تغییرات داده ها با خبر میکند بنابراین میتواند رابط کاربری را بروزرسانی کند.

-به چرخه حیات احترام میگذارد.

-خیلی شبیه به RxJava است.

جریان کلی (Overall flow) LiveData

کامپوننت های رابط کاربری تغییرات را در LiveData مشاهده می کنند که به نوبه خود در واقع صاحبان چرخه حیات را مشاهده میکنند برای مثال اکتیویتی یا فرگمنت برای چرخه حیات.

اینجا می گوییم که LiveData چگونه کار میکند. ابتدا بر روی بروزرسانی ها و تغییرات در داده ها نظارت می کند.

و وقتی که مقدار داده ها بروز شد کنترل گر های رابط کاربری را بررسی می کند برای مثال اکتیویتی یا فرگمنت در حالت شروع (started) یا ادامه (Resumed) باشند.(یعنی در واقع رابط کاربری در حال نمایش به کاربران هست) که به ناظران اطلاع دهد که رابط کاربری را بروز کند.

هنوز چیزهایی بیشتری وجود دارد ولی برای ساده تر شدن من فقط بروی قسمت های مهم که در شکل 3 امده است تمرکز کرده ام.

مزیت های کلیدی LiveData :

-همیشه داده ها را بروز می کند.

-یک چرخه حیات مطلع است.

-استفاده از پیکربندی های مناسب با هر تغییراتی.

-جلوگیری از هدررفت حافظه.

مدل نما (ViewModel)

ViewModel یک محفظه برای ذخیره سازی داده های مربوط به رابط کاربریست. که شامل داده های موردنیاز رابط کاربری می شود. برای مثال، اگر رابط کاربری شامل لیستی از ایتم های خبری است، پس   ViewModel اخبار را در خود نگه میدارد/ذخیره میکند و در اختیار رابط کاربری میگذارد اگر نیاز باشد.

این باعث می شود که قسمت های مختلف  کد از هم جدا شوند (separation of concerns) ، و کنترلگرهای رابط کاربری (Activity  و Fragment) به صورت خالص فقط شامل کدهای مختص به رابط کاربری مشوند مانند findViewById, click listeners, manipulating widgets ,... در حالی که ViewModel شامل داده هایی هست که باید در رابط کاربری به نمایش درایند.

این جداسازی مشکلات عمده ای از توسعه دهدگان اندروید را حل کرده است برای مثال کرش کردن برنامه یا از دست رفتن داده ها در هنگام چرخش دستگاه. ملخص کلام این است که، ViewModel ها تغییرات در پیکربندی را حفظ می کند.

یک مثال:میخواهیم یک رابط کاربری که یک لیست از مقاله های خبری را به نمایش میگذارد را بسازیم. اینجا داده های مورد نیاز، یک لیست از مقاله های خبریست. درحالی که ViewModel چیزی شبیه آنچه که در زیر امده است می باشد:

class NewsViewModel() : ViewModel() {
 
private var newsArticles: List<NewsArticles>
 
init {
 
// Load the data over here
 
newsArticles = ....
 
}
 
 
fun getNewsArticles(): List<NewsArticles> {
 
return newsArticles
 
}
 
}

کلاس NewsViewModel  که داده ها را نگه میدارد که در این قسمت "newsArticles" است، و از جایی گرفته می شود (در حالت ایده ال از یک repository، که در ادامه می بینیم). همانطور که قبلا بحث کردیم، ViewModel معمولا داده ها رو در یک فرم از LiveData نگه میدارد تا رابط کاربری را بلافاصله بعد از تغییر بروز کند.

class NewsViewModel() : ViewModel() {
 
private var newsArticles: LiveData<List<NewsArticles>>
 
 
init {
 
// Load the data over here
 
newsArticles = ....
 
}
 
 
fun getNewsArticles(): LiveData<List<NewsArticles>> {
 
return newsArticles
 
}
 
}

حال نگاهی به کنترلگر های رابط کاربری مان (Activity و Fragment) می اندازیم:

class NewsActivity: AppCompatActivity() {
 
override fun onCreate(savedInstanceState: Bundle?) {
 
super.onCreate(savedInstanceState)
 
setContentView(R.layout.activity_news)
 
// Get ViewModel
 
val newsViewModel = ViewModelProviders.of(this).get(NewsViewModel::class.java)
 
 
// Observing for data change
 
newsViewModel.getNewsArticles().observe(this, Observer<List<NewsArticles>> {
 
// Update the UI
 
})
 
}
 
}

در اکتیویتی یا فرگمنت ما یک نمونه از ViewModel با استفاده از ViewModelProviders می سازیم و سپس با استفاده از "getter " که تعریف کرده ایم داده ها را می گیریم. هنگامی که ما داده ها را در قالب یک بسته LiveData برگردانیم قادر خواهیم بود تغییرات داده را نظارت کنیم.

پشت صحنه:

ViewModel زمانی ایجاد میشود که اکتیویتی ایجاد شده باشد و زمانی از بین میرود که اکتیویتی به پایان رسیده باشد. در نتیجه، زمانی که صاحب اکتیویتی به پایان رسد، فریمورک به صورت خودکار متد  ViewModel’s onCleared() را صدا میزند که باعث میشود تمام منابع ازاد شوند.

لیست زیر شامل بعضی از مزیت های ViewModel  است:

-طراحی شده برای نگهداری و مدیریت داده های مربوط به رابط کاربری

-زنده ماندن تغییرات پیکربندی

-یک چرخه حیات مطلع است

-بعضی اوقات ابجکت های LiveData را ذخیره می کند

-کمک به برقراری ارتباط بین اکتیویتی و فرگمنت

-رها شدن از God Activities

فضا (Room)

Room یک کتابخانه پایدار است که طراحی شده برای کمک به ذخیره سازی داده های اپلیکیشن در یک پایگاه داده SQLite که در همه نسخه های اندروید وجود دارد.

ویژگی های آن شامل موارد زیر میشود:

-فراهم کردن local data

-یک لایه انتزاعی بر روی پایگاه داده SQLite موجود

-بررسی کوئری های SQL در هنگام کامپایل

-قابلیت مشاهده تغییرات در پایگاه داده با استفاده از LiveData

-رهاشدن از boilerplate code

-سازگازی خوب با LiveData و RxJava

ساختمان Room :کامپوننت های مختلفی از کتابخانه Room وجود دارد که در ادامه می بینیم.

موجودیت (Entity)

-برای هر موجودیت یک جدول در دیتابیس ایجاد شده است.

-نشان دهنده یک کلاس است، برای نگه داشتن یک سطر از پایگاه داده

ابجکت دسترسی پایگاه داده (Database access objects)

-این موارد کامپوننت های اصلی Room هستند زیرا انها مسئول تعریف متدهای دسترسی به پایگاه داده هستند.

-متد های تعامل با پایگاه داده را تعریف میکنند.

-پیاده سازی به صورت خودکار در زمان کامپایل

پایگاه داده (Database)

-تعریف نگهدارنده پایگاه داده

-نقطه دسترسی اصلی به ارتباط پایگاه داده.

-تعریف لیستی از موجودیت ها و ابجکت های دسترسی به پایگاه داده

بیایید اپلیکیشن خبری خودمان را که مقاله هارا در پایگاه داده SQLite  ذخیره میکرد درنظر بگیریم. با این فرض کاربران قادر به خواندن خبرها حتی به صورت افلاین نیز هستند.

موجودیت هایی که نماینده مقاله های خبری هستند در زیر نشان داده شده است:

@Entity(tableName = “news_articles”)
 
data class NewsArticle(
 
@PrimaryKey(autoGenerate = true)
 
var id: Int = 0,
 
var author: String? = null,
 
var title: String = “”,
 
var description: String = “”)

این قطعه کد جدولی با نام news_articles در پایگاه داده SQLite برنامه خواهد ساخت و از "ID" به عنوان کلید اصلی استفاده میکند. برای دسترسی به جدول news_articles ما به یک ابجکت دسترسی به پایگاه داده (DAO)  نیاز داریم تا یک فضای انتزاعی بر روی منطق پایگاه داده فراهم اورد و همچنین یک راه به اینترنت با لایه های زیرین پایگاه داده فراهم کند.

برای تعریف DAO احتیاج داریم که یک اینترفیس به همراه @Dao annotation ایجاد کنیم. و یک تابع برای کار با جدول ‘news_articles’ تعریف کنیم.

DAO برای ‘NewsArticle’ چیزی شبیه به کد پایین خواهد بود:

-
 
@Dao
 
interface NewsArticlesDao {
 
}

درج (Insert)

درج کردن یک رکورد بسیار ساده است. فقط احتیاج داریم یک تابع به DAO همراه با @Insert annotation اضافه کنیم.

@Dao
 
interface NewsArticlesDao {
 
@Insert
 
fun insertArticle(article: NewsArticle)
 
}

شما حتی میتوانید لیست یا ارایه ای از ‘NewsArticle’ ها نیز درج کنید:

@Insert
 
fun insertArticles(articles: List<NewsArticle>)

بروزرسانی (Update)

مشابه با درج کردن، برای بروزرسانی یک کوئری فقط یک تابع به همراه @Update annotation. نیاز است.

@Update
 
fun updateArticle(article: NewsArticle)

کتابخانه Room عملیات بروزرسانی را با استفاده از کلید اصلی موجودیت انجام می دهد.

حذف (Delete)

برای حذف یک کوئری کافیست یک تابع به همراه @Delete annotation تعریف کنید.

@Delete
 
fun deleteArticle(article: NewsArticle)

کتابخانه Room عملیات حذف را با استفاده از کلید اصلی موجودیت انجام می دهد.

خواندن/انتخاب (Read/select)

Select querieها با Room کمی متفاوت است. ما احتیاج به نوشتن یک SQL کوئری داریم که در زمان کامپایل تایید شود. برای اجرای Select querieها، نیاز داریم که از @Query annotation و یک کوئری خاص استفاده کنیم، سپس Room این کوئری ها را به کلاس های جاوا/کاتلین نگاشت میکند.

@Query(“SELECT * FROM news_articles”)
 
fun getNewsArticles(): List<NewsArticle>

شما حتی قادر خواهید بود از داده های مشاهده پذیر با استفاده از LiveData نیز استفاده کنید:

@Query(“SELECT * FROM news_articles”)
 
fun getNewsArticles(): LiveData<List<NewsArticle>>

با این روش شما میتوانید رابط کاربری را به هنگام تغییرات جدول بروز کنید.

از طرفی دیگر، اگر شما از طرفداران RxJava هستید میتواند از Maybe ، Single ، Flowable به جای LiveData استفاده کنید.

@Query(“SELECT * FROM news_articles”)
 
fun getNewsArticles(): Flowable<List<NewsArticle>>

فواید معماری کامپوننت ها

برخی از مزیت های اصلی معماری کامپوننت ها در زیر اورده شده است.

1- این کامپوننت ها باعث ماژولار شدن اپلیکیشن می شوند و تمرکز انها بر جداسازی قسمت های مختلف کدهای برنامه است.

2- کمک میکنند که چرخه حیات اپلیکیشن را مدیریت کنید و از کرش کردن برنامه در حین چرخش دستگاه رها شوید.

3- به شما کمک میکنند تا داده ها را به صورت ماندگار نگه دارید و برنامه های افلاین بسازید.

4- برای جلوگیری از هدر رفت حافظه و مشکلاتی از این دست کمک میکند.

5- جلوگیری از نوشتن کد خسته کننده (boring boilerplate code) .

معماری نهایی

گوگل راهنمایی هایی را برای معماری کامپوننت ها فراهم کرده است تا به ساخت اپلیکیشن های امروزی کمک کند و حتی پیشنهادهایی هم در مورد اینکه کدام معماری برای کدام هدف استفاده شود نیز می دهد.

شکل 6 یک دیاگرام از معماری پیشنهادی است که توسط تیم اندروید در قسمت دستورالعمل های این معماری گنجانده شده است.

این معماری پیشنهاد میکند که اکتویتی و فرگمنت را سبک نگه داریم به این صورت که فقط کدهای مربوط به رابط کاربری مثل click listeners و... را در آن نگه می داریم.

ViewModel- داده های موردنیاز کنترلگر های رابط کاربری مثل اکتیویتی و فرگمنت ها را فراهم می کند که این کمک میکند تغییرات در پیکربندی در امان بمانند.

ViewModel- داده ها را از repository می گیرد، که نقش یک منبع یکتا از داده های حقیقی را عمل می کند، به این معنا که هر زمان که اپلیکیشن به داده نیاز داشته باشد، داده ها همیشه از repository می آیند.

-این repository است که تصمیم میگیرد داده ها با استفاده از Room از پایگاه داده محلی گرفته شوند (واکشی شوند) یا با استفاده از Retrofit از وب سرویس گرفته شوند.

گوگل استفاده از Retrofit را برای کدهای مربوط به شبکه پیشنهاد می دهد وهمچنین استفاده از تزریق وابستگی (احتمالا Dagger) برای کنار هم چسباندن این کامپوننت ها را پیشنهاد می دهد.

جدول شماره 1 همه این ویژگی ها را در کنار هم آورده است:

شما میتوانید به repository گیت هاب برای دستسرسی به کدها و اپلیکیشن کامل ساخته شده با معماری کامپوننت ها و دستورالعمل ها در ادرس https://github.com/AkshayChordiya/android-arch-news-sample/ مراجعه کنید.

تست

دستورالمل های این معماری کمک میکند تا همه این کامپوننت ها را به صورت منزوی و ایزوله نگهدارید، که برای تست خیلی عالی میشود. این یک تمرین خوب برای تست اپلیکیشنی که میسازید می شود. جدول شماره 2 با استفاده از دستورالعمل های این معماری به شما نشان میدهد که چگونه عملیات تست را انجام دهید.

نکته:

ابزار test case اشاره به test case هایی دارد که بر روی یک دستگاه اندرویدی انجام می شوند.

معماری کامپوننت ها در اندروید یک مجموعه شگفت انگیز از کتابخانه های جدا از هم هستند که برای کمک به توسعه آسان تر اندروید ساخته شده اند. با استفاده از دستورالعمل های این معماری، ما راهنمایی هایی از تیم اندروید گرفتیم که چگونه اپلیکیشن های اندروید امروزی باید ساخته شوند.

نکته:

هیچ معماری کاملی برای هر موردی و هدفی وجود ندارد و معماری که در این دستورالعمل ها گنجانده شده است فقط به عنوان پیشنهاد هستند. به هر حال شما ازاد هستید که با هرچیزی که با مورد شما مناسب تراست کار کنید.

کتابخانه هایی مانند WorkManager و Navigation  اخیرابه عنوان بخشی از معماری کامپوننت ها اضافه شده اندوکتابخانه های بیشتری برای اینکه توسعه اندروید را راحت تر و سرگرم کننده تر کنند نیز اضافه خواهند شد.

دانلود نسخه ی PDF این مطلب