
مقدمه:
این معماری در بسیاری از اپلیکیشنهای بزرگ ایران استفاده شده و یکی از معماریهای متداول در ایران میباشد. مزایای آن سازگار بودن با ساختار توسعه اپلیکیشنهای اندرویدی و تست پذیری خوب میباشد. در این نوشتار پیشرو با شیوه پیادهسازی یک اپلیکیشن بر پایه معماری MVP آشنا میشوید. در معماری MVP ما سه لایهی پایهای با عناوین Model – View – Presenter داریم، که هر کدام وظیفهی خاصی را به عهده دارند. دلیل استفاده از این لایهها این است که ما بتوانیم مفاهیم را از همدیگر جدا کنیم و تست پذیری کدهای خود را بالا ببریم و در نتیجه کدهای تمیزتری بنویسیم.
ما دو نوع تست داریم:
Unit Test: تست کردن کوچکترین بخش نرم افزار
Integration Test یا UI Test: تست کردن تمام حالتهای اپلیکیشن، در واقع اینکار از پیچیدگی کد جلوگیری میکند.
فهرست محتوای این مقاله
ساخت پکیجهای Base, Home, Data
1 - View:
این لایه صرفاً وظیفهی نمایش اطلاعات را دارد و کاری ندارد که این اطلاعات از کجا و به چه شکلی آمدهاند.
2 - Presenter:
این لایه وظیفهی دریافت اطلاعات را دارد. اطلاعات از کجا دریافت میشوند؟ از لایهی مدل دریافت میشوند.
3 - Model:
این بخش رابط بین View و Presenter است. مدل خود نیز شامل زیربخشهای Data Model، Business Model و View Model است که وظایفی مانند دریافت و ذخیره اطلاعات، تعامل با دادهها، ارسال اطلاعات و شکلدهی منطق سیستم را به عهده دارند.
پیاده سازی:
ساخت پکیج Base:
این پکیج شامل تمامی کلاسها، اینترفیسهای پایه و لازم برای معماری MVP است که شامل کلاسها و اینترفیسهای زیر است:
1-1- ساخت اینترفیس BaseView:
این اینترفیس توسط هر View ای که میسازیم ایمپلمنت میشود. مثلاً HomeFragment یا MainActivity.
- داخل این اینترفیس یک متد با نام setProgressIndicator میسازیم. چرا؟ چون در تمام ویوها ما داریم با سرور ارتباط برقرار میکنیم پس نیاز است که این اینترفیس در تمام ویوها وجود داشته باشد.
- داخل این اینترفیس یک متد دیگر با اسم context میسازیم چرا؟ چون Context ویو را برمیگرداند. چرا برمیگرداند؟ چون ما به آن در خیلی جاها نیاز داریم. در ادامه دلیل اینکار را متوجه خواهید شد.
interface BaseView {
fun showError(errorMessage: String)
fun setPorogressIndicator(shouldShow: Boolean)
fun context(): Context
}
1-2- ساخت اینترفیس BasePresenter:
این اینترفیس از نوع جنریک است، که یک Generic Parameter به نام T دریافت میکند، که آن T باید BaseView را extends کرده باشد. در واقع با اینکار به خود و سایر برنامه نویسانی که از این اینترفیس میخواهند استفاده کنند اعلام میکنیم که این اینترفیس یک View که BaseView را extends کرده باشد را میخواهد. این اینترفیس دو متد با نامهای attachView و detachView دارد.
interface BasePresenter<T : BaseView> {
fun attachView(view : T) fun detachView()
}
متد attachView برای این به کار میرود که به Presenter اطلاع دهیم که ویو attach شده و آماده است که دادههای ما را نمایش دهد .
متد detachView برای این به کار میرود که به Presenter اطلاع دهیم که ویو detach شده و غیرقابل استفاده است. پروژهای که ما انجام میدهیم بر اساس package By feature است. بنابراین یک پکیج با نام home میسازیم چرا؟ چون میخواهیم فایلهای مربوط به فرگمنت خانه را انجام دهیم.
2 - ساخت پکیج home:
2-1- ساخت اینترفیس HomeContract: به معنی قرارداد
این اینترفیس دو اینترفیس درونی با نامهای View و Presenter دارد. View اینترفیس BaseView را ایمپلمنت میکند. در این اینترفیس چون ما در صفحهی اصلی برای نمایش خبرها به لیست اخبار نیاز داریم یک متد با نام showNews میسازیم. فرگمنت خانهی ما باید این رفتارها را داشته باشد یعنی باید لیست اخبار را نشان دهد. همچنین یک متد دیگر با نام showBanner میسازیم.
به صورت خلاصه کلاسی که بازیگر نقش ویو است باید دو متد showNews وshowBanner را ایمپلمنت کند.
به این دلیل که کلاس ویو اصلا نباید بداند که دیتا از کجا و به چه طریقی میآید. Presenter اینترفیس BasePresenter را ایمپلمنت میکند. این اینترفیس چون قرار است که لیست اخبار و بنرها را دریافت کند پس دو متد با نامهای getNewsList و getBanners میسازیم.
نکته: باید دیتامدل خود را در پکیج data بسازیم که در اینجا News است.
اینترفیس پرزنتر(Presente)، بیسپرزنتر(BasePresenter) را ایمپلمنت میکند. به صورت خلاصه کلاسی که بازیگر نقش Presenter است باید دو متد getNewsList و getBanners را ایمپلمنت کند. زمانی که متدهای کلاس HomeFragment یعنی onStart و onStop صدا زده شد به Presenter میگوییم که ویو آماده نمایش اطلاعات است.
interface HomeContract {
interface View : BaseView {
fun showNews(newsList: List<News>)
fun showBanner(banners : List<Any>)
}
interface Presenter : BasePresenter<View> {
fun getNewsList() fun getBanners()
}
}
2-2- ساخت کلاس HomeFragment:
این کلاس طبق دانستههای قبلی باید کلاس پدر خود یعنی Fragment را Extends کند؛ علاوه بر آن باید اینترفیس HomeContract.View را نیز ایملمنت کند.
class HomeFragment : Fragment(), HomeContract.View {
override fun showNews(newsList: List<News>) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun showBanner(newsList: List<News>) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun showError(errorMessage: String) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun setPorogressIndicator(shouldShow: Boolean) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun context(): Context {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
متدهايی که در بالا مشاهده میکنيد تماماً توسط پرزنتر کال (فراخوانی) میشوند. اما اگر کمی دقت کنيم متوجه میشويم که پياده سازی آن به عهدهی خود فرگمنت است، چرا که مثلا در متد ()context به Context خود فرگمنت نياز داريم.
2-3 - ساخت کلاس HomePresenter:
این کلاس نقش Presenter را برای View را بازی میکند.
class HomePresenter : HomeContract.Presenter {
override fun getNewsList() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun getBanners() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun attachView(view: HomeContract.View) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun detachView() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
زمانی که متد آناستارت (onStart)، هومفرگمنت (HomeFragment) اجرا شد، متد attachView را صدا میزنیم. زمانی که متد آناستاپ (onStop)، هوم فرگمنت (HomeFragment) اجرا شد، متد dettachView را صدا میزنیم. چرا؟
زمانی که Presenter از سرور یا دیتابیس محلی اطلاعات را دریافت میکند مدت زمانی طول میکشد تا ما نتیجه را دریافت کنیم. ممکن است View ما detach شده باشد، یعنی ویو عملاً null هست و نباید کاری کنیم. حالا داخل کلاس HomeFragment یه instance از Presenter نگه میداریم و به همین ترتیب داخل کلاس HomePresenter هم یه instance از view رو نگه میداریم. نکتهای که باید به آن توجه کنیم این است که ما دو Instance رو از جنس interface استفاده میکنیم تا در آینده اگر از اکتیویتی یا فرگمنت استفاده کردیم مشکلی در کارمان به وجود نیاید. سپس از لایهی Model اطلاعاتمان که همان لیست اخبار و بنرها است را دریافت کنیم. چطور باید اینکار را انجام دهیم؟
3- ساخت پکیج data:
ساخت اینترفیس NewsDataSource: اینجا مخزن اطلاعات هست. هر کسی مسئولیت فراهم کردن اطلاعات Banner، News هست باید این اینترفیس را ایمپلمنت کند. چرا این اینترفیس را میسازیم ؟ چرا برای درخواست مستقیم به سرور کلاس نمیسازیم؟
1 – مشخص نیست که اطلاعات قرار هست از سرور بیاید یا از دیتابیس، یا از یک فایل خوانده شود.
2 – Presenter نباید متوجه شود که دیتاها از کجا لود میشوند. پرزنتر دیتا را از لایهی مدل دریافت میکند.
3 - باعث میشود کانسپتها از هم جدا باشند و عملاً هر کسی وظیفهی خودش رو داشته باشد.
4 - تست پذیری راحت میشود
5 - الان اطلاعات ما از سرور دریافت میشود اگر زمانی تصمیم بگیریم اطلاعات را از دیتابیس لود کنیم چه اتفاقی خواهد افتاد؟
6 - استفاده از مفاهیم شی گرایی است و استفاده از Abstraction است.
interface NewsDataSource {
}
تعریف کتابخانههای استفاده شده:
RxJava چیست؟
برای دریافت اطلاعات باید متدهایی بسازیم اما اگر با استفاده از دانستههای قبلی این مورد را پیاده سازی کنیم با مشکلی روبرو میشویم مشکل این است که اگر تعداد متدها افزایش یابد باید به تعداد آنها Callback تعریف شود و عملاً این کار اشتباه است چرا که مقدار بسیار زیادی کد اضافی تولید میشود. در اینجا RxJava به ما کمک میکند. چطور کمک میکند؟ RxJava یکی از ابزارهای reactive programming است.
https://en.wikipedia.org/wiki/Event-driven_architecture
RxJava یک EDA است یعنی: معماری بر اساس رویداد است. به عنوان مثال ما یک درخواست به سمت سرور ارسال میکنیم. مدت زمانی طول میکشد تا نتیجهاش برگردد اینکه نتیجه برگردد یک رویداد محسوب میشود. پس از برگشت اگر داخل دیتابیس ذخیره کنیم این هم یک رویداد است. بعد از ذخیره شدن اگر داخل ویو نمایش بدهیم این هم یک رویداد هست. این رویدادها همزمان نیستند میتوانیم با RxJava هندل کنیم اگر از RxJava استفاده نکنیم این مشکل را داریم که برای هر رویداد باید یک Callback تعریف کنیم.
interface NewsDataSource {
fun getNews(onNewsReceived: OnNewsReceived)
interface OnNewsReceived {
fun onReceived(newsList: List<News>)
}
}
ما یک مفهومی تحت عنوان Observable و یک مفهوم دیگر با نام Observer در RxJava داریم. Observer: شنوندهی رویداد است و باید داخل لایه پرزنتر تعریف شود، به عنوان مثال وقتی لیست اخبار دریافت شد، به ما اطلاع میدهد ،اگر خبری بوکمارک شد به ما خبر میدهد.
Observable: منبعی است که به آن توجه میکنیم منبعی که رویدادها را ارسال میکند.
در اینجا میتوان گفت نتیجه getNews است.
RxJava کتابخانهای است که Observableهای مختلفی دارد.
interface NewsDataSource {
fun getNews(): Observable<List<News>>
}
Observable: این Observable متدهای مختلفی مانند onNext – onComplete و ... دارد. که ما از آن استفاده نمیکنیم و ما از Single استفاده میکنیم. چون دو حالت بیشتر ندارد یا success میشود یا error .
interface NewsDataSource {
fun getNews(): Single<List<News>>
fun getBanner(): Single<List<Banner>>
}
Completable نوع دیگری از Observable است: زمانی استفاده میشود که ما اکشنی داریم که یا success هست یا error اما وقتی success هست نتیجهای بر نمیگردد. ما دو منبع داده داریم:
1 – سرور آنلاین
2 – دیتابیس محلی
پس باید دو کلاس بسازیم که NewsDataSource را ایمپلنت کردند: این کلاس وظیفهی دریافت اطلاعات از سرور را به عهده دارد.
class CloudDataSource: NewsDataSource {
override fun getNews(): Single<List<News>> {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun getVideoNews(): Single<List<News>> {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun getBanner(): Single<List<Banner>> {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun bookmark(news: News) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun search(keyword: String): Single<News> {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
LocalDataSource این کلاس وظیفهی دریافت اطلاعات از دیتابیس محلی را به عهده دارد.
class LocalDataSource: NewsDataSource {
override fun getNews(): Single<List<News>> {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun getVideoNews(): Single<List<News>> {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun getBanner(): Single<List<Banner>> {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun bookmark(news: News) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun search(keyword: String): Single<News> {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
اما چطور تصمیمگیری میشود که اطلاعات را از کجا دریافت کنیم؟ باید یک کلاس به اسم NewsRepository بسازیم. و عملاً پرزنتر ما با NewsRepository کار میکند. NewsRepository دقیقاً همان مخزن اصلی اطلاعات ما هست.
class NewsRepository : NewsDataSource {
private var localDataSource = LocalDataSource()
private var cloudDataSource = CloudDataSource()
override fun getNews(): Single<List<News>> {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun getVideoNews(): Single<List<News>> {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun getBanner(): Single<List<Banner>> {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun bookmark(news: News) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun search(keyword: String): Single<News> {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
نکته: چون فعلاً میخواهیم از سرور دریافت کنیم از CloudDataSource استفاده میکنیم.
class NewsRepository : NewsDataSource {
private var localDataSource = LocalDataSource()
private var cloudDataSource = CloudDataSource()
override fun getNews(): Single<List<News>> {
return cloudDataSource.getNews()
}
override fun getVideoNews(): Single<List<News>> {
return cloudDataSource.getVideoNews()
}
override fun getBanner(): Single<List<Banner>> {
return cloudDataSource.getBanner()
}
override fun bookmark(news: News) {
return cloudDataSource.bookmark(news)
}
override fun search(keyword: String): Single<News> {
return cloudDataSource.search(keyword)
}
}
وارد کلاس CloudDataSource میشویم و کارهایی که لازم هست را انجام میدهیم: برای اینکه اطلاعات را از سرور دریافت کنیم، HttpClientهای مختلفی داریم، مانند:
ما داخل build.gradle کتابخانههای مختلفی را اضافه کردیم. برای همین منظور لازم دانستیم برخی از آنها را معرفی کنیم که دقیقا چه کاربردی دارند.
تفاوت RxJava و RxAndroid:
RxJava چیست؟ برای reactive programming در زبان جاوا است. برای اینکه داخل اندروید از آن استفاده کنیم با مشکلاتی مواجه میشویم مثلا چون main thread را نداریم. به همین منظور JakeWharton کتابخانهی RxAndroid را نوشت که یک Wrapper برای اینکه مفهوم MainThread را با آن بتوان هندل کرد بود، و کار را سادهتر کرد.
کتابخانه گوگل GSON
این لایبرری برای تبدیل Json به جاوا و بالاعکس استفاده میشود و در وقت برنامه نویسی بسیار صرفه جویی میکند. این کتابخانه نه فقط در اندروید بلکه برنامه نویسی سمت سرور با جاوا نیز استفاده میشود و یکی از معروفترین لایبرریها در این زمینه است. کتابخانهی Gson برای parse کردن اطلاعات json و تبدیل آن به کلاسهای جاوا است.
Retrofit:
کتابخانه Retrofit ساخت شرکت Square یکی از معروفترین Http Clientها در اندروید هست. دلیل محبوبیت این لایبرری سادگی استفاده و سرعت بسیار خوب آن هست. از دیگر مزیتهای Retrofit سازگار بودن با Gson و Rxjava میباشد. این کتابخانه برای ارتباط با سرور به کار میرود.
retrofit2:adapter-rxjava2:2.4.0:
رتروفیت از RxJava پشتیبانی نمیکند و زمانی که اطلاعات از سمت سرور میآید باید از callback استفاده کنیم ولی وقتی این کتابخانه را استفاده کنیم Observableها را میشناسد. retrofit2:converter-gson:2.4.0:
این کتابخانه هم برای این هست که اطلاعاتی که از سمت سرور میآید به صورت خودکار توسط رتروفیت به gson پاس داده شود و تبدیل به کلاس جاوایی شود.
دریافت اطلاعات از سرور - ساخت کلاس CloudDataSource:
در این کلاس قرار هست که با استفاده از رتروفیت به سرور درخواستهای خودمان را بفرستیم و اطلاعات را دریافت نمائیم. برای این کار باید یک اینترفیس به اسم EnglishApiService ایجاد نمائیم که رتروفیت این اینترفیست رو ایمپلمنت میکند.
News List: https://api.myjson.com/bins/p5gwn ENG Banners List: https://api.myjson.com/bins/el9en ENG Video List: https://api.myjson.com/bins/tdr8j ENG News List: https://api.myjson.com/bins/x4scj FA Banners List: https://api.myjson.com/bins/14vf7n FA Video List: https://api.myjson.com/bins/1359k3 FA
interface EnglishApiService {
@GET("p5gwn")
fun getNews(): Single<List<News>>
@GET("el9en")
fun getVideoNews(): Single<List<News>>
@GET("tdr8j") fun getBanners(): Single<List<News>>
}
حالا باید داخل متد سازنده یا کانستراکتور CloudDataSource :
class CloudDataSource : NewsDataSource {
var englishApiService: EnglishApiService init {
val retrofit = Retrofit.Builder()
.baseUrl("https://api.myjson.com/bins/")
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build() englishApiService = retrofit.create(EnglishApiService::class.java)
}
override fun getNews(): Single<List<News>> {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun getVideoNews(): Single<List<News>> {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun getBanner(): Single<List<Banner>> {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun bookmark(news: News) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun search(keyword: String): Single<News> {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
اکنون داخل کلاس HomeFragment میخواهیم که اطلاعات رو دریافت کنیم. پس یک نمونه از NewsDataSource نگهداری میکنیم. چرا از NewsRespository استفاده نمیکنیم؟ چون میخواهیم که از اینترفیس استفاده کنیم و اصطلاحا Dependency Injection رو رعایت کنیم.
class HomePresenter(newsDataSource: NewsDataSource) : HomeContract.Presenter {
private var view: HomeContract.View? = null
private var newsDataSource: NewsDataSource? = null
init {
this.newsDataSource = newsDataSource
}
override fun attachView(view: HomeContract.View) {
this.view = view
}
override fun detachView() {
this.view = null
}
override fun getNewsList() {
}
override fun getBanners() {
newsDataSource?.getBanner()
}
}
حالا میتوانیم داخل متدهای getNewsList و getBanners Observerهای خودمان را بسازیم.
class HomePresenter(newsDataSource: NewsDataSource) : HomeContract.Presenter { private var view: HomeContract.View? = null private var newsDataSource: NewsDataSource? = null init { this.newsDataSource = newsDataSource } override fun getNewsList() { newsDataSource!!.getNews().subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(object : SingleObserver<List<News>> { override fun onSuccess(newsList: List<News>) { view!!.showNews(newsList) } override fun onSubscribe(d: Disposable) { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } override fun onError(e: Throwable) { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } }) } override fun getBanners() { newsDataSource!!.getBanner().subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(object : SingleObserver<List<Banner>> { override fun onSuccess(banners: List<Banner>) { view!!.showBanner(banners) } override fun onSubscribe(d: Disposable) { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } override fun onError(e: Throwable) { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } }) } override fun attachView(view: HomeContract.View) { this.view = view } override fun detachView() { this.view = null }
subscribeOn : یعنی این درخواست روی چه تردی انجام شود؟ ما میخواهیم روی ترد جدید اجرا شود. observeOn : یعنی جواب در چه تردی برگردد؟ ما گفتیم در ترد اصلی برگردد. Subscribe : یعنی وقتی انجام شد چه کار کند؟ چون از نوع Single هست سه تا متد دارد برای سه حالت :
1- متد onSuccess : وقتی که عملیات با موفقیت انجام میشود این متد اجرا میشود که لیستی از آبجکت مد نظر ما را بر میگرداند.
2- متد onError : وقتی با خطایی (هر خطایی) مواجه شویم این متد اجرا میشود.
3- متد onSubscribe : ما وقتی داریم به این رویداد گوش میدهیم توسط این متد و مقداری که از جنس Disposable دارد میتوانیم مدیریت کنیم که الان ما میخواهیم گوش کنیم یا نه.
class HomePresenter(newsDataSource: NewsDataSource) : HomeContract.Presenter { private var view: HomeContract.View? = null private var newsDataSource: NewsDataSource? = null private var compositeDisposable = CompositeDisposable() init { this.newsDataSource = newsDataSource } override fun attachView(view: HomeContract.View) { if (!isViewRendered) { this.view = view getBannersList() getNewsList() } } override fun detachView() { this.view = null if (compositeDisposable != null && compositeDisposable.size() > 0) { compositeDisposable.clear() } if (subscription != null) { subscription!!.cancel() } } override fun getNewsList() { view!!.setPorogressIndicator(true) newsDataSource!!.getNews() .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(object : SingleObserver<List<News>> { override fun onSuccess(newsList: List<News>) { view!!.showNews(newsList) view!!.setPorogressIndicator(false) } override fun onSubscribe(d: Disposable) { compositeDisposable.add(d) } override fun onError(e: Throwable) { view!!.showError(view!!.context().getString(R.string.all_unkown_error)) } }) } override fun getBannersList() { view!!.setProgressIndicator(true) newsDataSource!!.getBanners().subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(object : SingleObserver<List<Banner>> { override fun onSuccess(banners: List<Banner>) { view!!.showBanners(banners) view!!.setProgressIndicator(false) isViewRendered = true } override fun onSubscribe(d: Disposable) { compositeDisposable.add(d) } override fun onError(e: Throwable) { view!!.showError(view!!.context().getString(R.string.all_unknown_error)) view!!.setProgressIndicator(false) } }) } }
ساخت ساختار اصلی - ساخت BaseFragment:
در فرگمنت Home میبینیم که Fragment را extends کرده است که این اصلا جالب نیست! چرا؟ چون اگه ما بخواهیم یک رفتار را برای همه فرگمنتها در نظر بگیریم باید کدهای تکراری بنویسیم مثلا همین متد onCreateView را باید برای هر فرگمنت بنویسیم برای همین یک کلاس BaseFragment میسازیم که کلاس Fragment را extends کرده است. و رفتارهای ثابت داخل تمام فرگمنتها را داخلش تعریف میکنیم این کلاس BaseFragment از نوع abstract هست چون بعضی متدهایش باید توسط فرزندش پیاده سازی شود مثل getLayoutRes.
abstract class BaseFragment : Fragment() { protected var rootView: View? = null override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { if (rootView == null) { rootView = inflater.inflate(getLayoutRes(), container, false) setupViews() } return rootView } abstract fun getLayoutRes(): Int abstract fun setupViews() fun getBaseActivity(): MainActivity { return activity as MainActivity } }
rootView برای این هست که وضعیت ویو رو نگه داری کنیم کش کنیم اگر ویو ساخته شده دوباره نسازیم. getLayoutRes و setupViews را باید در کلاس فرزند پیاده سازی کنیم مثلا لایه xml فرگمنت Home با category فرق دارد متد setupViews هم همینطور است.
ساخت ساختار اصلی - ساخت BaseActivity :
این کلاس هم میسازیم ولی بعدا متدهای داخلش رو میسازیم. سپس تغییرات کلاس HomeFragment را اعمال میکنیم در کلاس HomePresenter وقتی که ویو attach میشود دو متد getNewsList و getBanners رو فراخوانی میکنیم.
override fun attachView(view: HomeContract.View) {
this.view = view
getBanners()
getNewsList()
}
پی نوشت:
- برای درک بهتر MVP، تست نویسی مطالعه شود.
- پیاده سازی رابط کاربری: در داخل متدهای showNews و showBanner ریسایکلرویوهای (Recycler view) خود را مقدار دهی میکنیم و به نمایش میگذاریم.
پیاده سازی بخش ذخیره اطلاعات دریافت شده از سرور روی دیتابیس:
افزودن dependencyهای کتابخانه Room به پروژه: افزودن انوتیشن DAO به کلاس LocalDataSource:
@Dao
abstract class LocalDataSource : NewsDataSource {
@Query("SELECT * FROM `tbl_news`")
abstract override fun getNews(): Flowable<List<News>>
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract fun saveNewsList(newsList: List<News>)
override fun getVideoNews(): Single<List<News>> {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun getBanners(): Single<List<Banner>> {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun bookmark(news: News) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun search(keyword: String): Single<News> {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
دریافت اطلاعات از سرور - ساخت کلاس AppDatabase :
@Database(entities = arrayOf(News::class), version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun getLocalDataSource(): LocalDataSource
companion object {
@Volatile
private var instance: AppDatabase? = null
private const val DATABASE_NAME = "mvp-startup1.db"
private val LOCK = Object()
fun getInstance(context: Context): AppDatabase {
if (instance == null) {
synchronized(LOCK) {
instance =
Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME)
.setJournalMode(JournalMode.TRUNCATE)
.build()
}
}
return instance as AppDatabase
}
}
}
NewsRepository: حال باید Single را به Flowable تبدیل کنیم. چون میخواهیم همان لحظه اطلاعات را از سرور بگیرد و همزمان اطلاعات دیتابیس را هم نشان بدهد. و وقتی اطلاعات وارد دیتابیس شد باید ذخیره کند.
تغییرات در کلاسهای زیر رخ داده است :
class HomePresenter(newsDataSource: NewsDataSource) : HomeContract.Presenter {
private var view: HomeContract.View? = null
private var newsDataSource: NewsDataSource? = null
private var compositeDisposable = CompositeDisposable()
private var isViewRendered: Boolean = false
private var subscription: Subscription? = null
init {
this.newsDataSource = newsDataSource
}
override fun attachView(view: HomeContract.View) {
if (!isViewRendered) {
this.view = view
getBannersList()
getNewsList()
}
}
override fun detachView() {
this.view = null
if (compositeDisposable != null && compositeDisposable.size() > 0) {
compositeDisposable.clear()
}
if (subscription != null) {
subscription!!.cancel()
}
}
override fun getNewsList() {
view!!.setProgressIndicator(true)
newsDataSource!!.getNews()
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.doOnNext { news ->
news?.let {
view!!.showNews(it)
view!!.setProgressIndicator(false)
isViewRendered = true
}
}
.doOnError {
view!!.showError(view!!.context().getString(R.string.all_unknownError))
view!!.setProgressIndicator(false)
}
.doOnSubscribe {
this.subscription = it
}
.subscribe()
}
override fun getBannersList() {
view!!.setProgressIndicator(true)
newsDataSource!!.getBanners().subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : SingleObserver<List<Banner>> {
override fun onSuccess(banners: List<Banner>) {
view!!.showBanners(banners)
view!!.setProgressIndicator(false)
isViewRendered = true
}
override fun onSubscribe(d: Disposable) {
compositeDisposable.add(d)
}
override fun onError(e: Throwable) {
view!!.showError(view!!.context().getString(R.string.all_unknown_error))
view!!.setProgressIndicator(false)
}
})
}
}
تغییرات در کلاس NewsRepository :
class NewsRepository(var context: Context) : NewsDataSource {
private var cloudDataSource = CloudDataSource()
private lateinit var localDataSource: LocalDataSource
init {
localDataSource = AppDatabase.getInstance(context).getLocalDataSource()
}
override fun getNews(): Flowable<List<News>> {
cloudDataSource.getNews()
.subscribeOn(Schedulers.newThread())
.observeOn(Schedulers.newThread())
.doOnNext(object : Consumer<List<News>> {
override fun accept(newsList: List<News>?) {
localDataSource.saveNewsList(newsList!!)
}
})
.subscribe()
return localDataSource.getNews()
}
override fun getVideoNews(): Single<List<News>> {
return cloudDataSource.getVideoNews()
}
override fun getBanners(): Single<List<Banner>> {
return cloudDataSource.getBanners()
}
override fun bookmark(news: News) {
localDataSource.bookmark(news)
}
override fun search(keyword: String): Single<News> {
return localDataSource.search(keyword)
}
}
نتیجهگیری
MVP یک Pattern یا الگو از معماری MVC بوده و تفاوت اصلی آن در نحوهی کار Controller و Presenter است. زمانی که به کمک یک معماری مثل MVC یا MVP لایههای برنامه را از هم جدا میکنیم، تمرکز ما روی اجزای مختلف پروژه بیشتر میشود. شاید در پروژههای کوچک استفاده از ساختاری مثل MVP چندان به صرفه نباشد اما وقتی پروژهی ما بزرگتر شده و اعضای تیم، قابلیتهای محصول، حجم کدها و غیره بیشتر میشوند منطقی است از یک معماری مثل MVP استفاده کنیم. با این کار زمان زیادی برای خود و اعضای تیم میخریم و فرآیند توسعه، خطایابی و حل مشکلات پروژه کاهش مییابد. این آموزش بر پایه الگوی معماری MVP بوده و تمامی موارد ذکر شده به روزترین شیوهي پیاده سازی یک اپلیکیشن بر پایه معماری MVP میباشد. البته شایان ذکر است که MVP یک معماری میباشد و شما میتوانید با تکیه بر تجربه و دانش خود این معماری را پیاده سازی کنید. اگر تجربه کار با الگو MVP را دارید در بخش نظرات تجربیات خود را با ما و تیم سون لرن در میان بگذارید.
اساتید ما در سون لرن از شروع یادگیری برنامه نویسی اندروید، تا ورود به بازار کار در کنارتان هستند.
مبحث MVP بهطور گسترده و کاربردی در دوره متخصص اندروید تدریس میشود و در صورت تمایل به آشنایی بیشتر، میتوانید به صفحهي دوره آموزشی متخصص اندروید مراجعه نمایید.
من از مقالات سایتتون زیاد استفاده می کنم . واقعا دمتون گرم
ممنون که با ما همراه هستید.
خیلی عالی بود من هم اندروید و هم برنامه نویسی سمت وب با پی اچ پی کار می کنم و خیلی علاقه دارم لطفا یه نمنه سورس برنامه اندرویدی تحت mvp در سایت قرار بدهید ممنونم
سلام دوست من؛
خیلی خوشحالم که برات مفید بوده.
چشم در اولین فرصت اینکار رو انجام میدم.
هفته بعدی یه سر بزن
سلام ممنون از این مقاله عالی
لطفا سورس این آموزش رو هم توی سایت قرار بدید تا بتونیم درک بهتری داشته باشیم
سلام چشم در اولین فرصت قرار میدم
سلام
بسیار عالی و مفید بود
کامل و جامع، ممنون بابت مقاله🙏
سلام محمد عزیز
در حال آماده سازی مقالات، پادکست ها و آموزش های بسیار خوبی در این زمینه هستیم!
پس حتما مطالب سایت را دنبال کنید❤