در بخش قبلی درباره معماری MVP و تفاوت آن با معماری پیشفرض اندروید و MVC صحبت کردیم. در این بخش میخواهیم درباره پیادهسازی معماری MVP در پروژههای اندروید صحبت کنیم.
۱- معماری MVP
الگوی معماری MVP یک الگوی طراحی بر مبنای MVC است که جدایی موضوعات را افزایش داده و آزمونشوندگی برنامه را تسهیل میکند. این الگو سه لایه مدل، نمایشگر و معرف با وظایف و نقشهای کاملاً تعریف شده ایجاد میکند.
مدل مسئول منطق کاری برنامه است و نحوه ایجاد، ذخیرهسازی و تغییر دادهها را کنترل میکند. نمایشگر (View) یک رابط کاربری منفعل است که دادهها را نمایش داده و عملیات کاربر را به معرف انتقال میدهد. معرف (Presenter) به عنوان میانجی عمل میکند و رابط بین مدل و نمایشگر است و همچنین عملیات کاربر را که نمایشگر فرستاده است پردازش میکند.
۲- برنامهریزی و آمادهسازی پروژه
میخواهیم یک پروژه ساده یادداشت نویسی بسازیم تا به کمک آن MVP را بهتر درک کنید. این برنامه به کاربر امکان میدهد تا یادداشت بنویسد، آنها را در یک پایگاه داده یا دیتابیس محلی (بر روی خود گوشی) ذخیره کند و همچنین بتواند یادداشتّا را حذف کند. برای ساده کردن کار، این برنامه فقط یک اکتیویتی دارد.
در این آموزش، ما به طور خاصی بر روی پیادهسازی MVP تمرکز میکنیم و سایر عملکردها، مانند تنظیم دیتابیس اسکیولایت SQLite)، ساخت DAO (اشیای دسترسی به دادهها یا Data Access Object) و پردازش عملیات کاربر را رها میکنیم. اگر در هر کدام از این موضوعات مشکل داشتید، اسمارتلب برای همه آنها آموزشهای مناسب را قبلاً تدارک دیده است!
نمودار فرایند کار و لایههای MVP
بیایید با ساخت یک یادداشت جدید شروع کنیم. اگر ما این کار را به اجزای کوچکتر بشکنیم، جریان کار بر اساس معماری MVP این شکلی خواهد شد:
- کاربر متن یادداشت را مینویسد و بر روی دکمه اضافه کردن تپ میکند.
- معرف یا Presenter یک شی Note با متنی که کاربر وارد کرده است میسازد و از مدل میخواهد که آن را در دیتابیس ذخیره کند.
- مدل یادداشت جدید را در دیتابیس ذخیره کرده و به معرف اطلاع میدهد که لیست یادداشتها تغییر کرده است.
- معرف فیلد مربوط به یادداشت جدید را خالی کرده و از نمایشگر میخواهد که لیست یادداشتها را تازه کند تا یادداشت جدید در آن نمایش داده شود.
حالا بیایید و عملیات مورد نیاز این کار را به کمک MVP از هم جدا کنیم. برای این که این اشیا را تا حد ممکن از هم جدا کنیم، ارتباط بین این اجزا را از طریق اینترفیسها انجام میدهیم. ما به چهار اینترفیس نیاز داریم:
- RequiredViewOps: عملیات ضروری برای نمایشگر که در اختیار معرف (Presenter) قرار میگیرد و معرف هر کاری که با نمایشگر داشته باشد باید از طریق این اینترفیس انجام دهد.
- ProvidedPresenterOps: عملیاتی که معرف در اختیار نمایشگر میگذارد تا نمایشگر از طریق آن با معرف ارتباط برقرار کند.
- RequiredPresenterOps: عملیات ضروری معرف که در اختیار مدل قرار میگیرد و مدل از طریق آن با معرف ارتباط برقرار میکند.
- ProvidedModelOps: عملیاتی که مدل در اختیار معرف میگذارد.
۳- پیادهسازی MVP در اندروید
حالا که میدانیم چطور باید متدهای مختلف را سازمان دهی کنیم، میتوانیم ساخت برنامه را آغاز کنیم. ما برای سادگی فقط بر روی پیادهسازی عملیات اضافه کردن یک یادداشت جدید متمرکز میشویم. کد برنامه در گیتهاب در اختیار شما قرار خواهد گرفت.
ما فقط از یک اکتیویتی استفاده میکنیم که شامل ویوهای زیر است:
- یک ادیت تکست برای یادداشت جدید
- دکمهای برای ذخیره یادداشت جدید
- یک ریسایکلر ویر برای نمایش فهرست همه یادداشتها
- دو تکستویو و یک دکمه برای حذف یادداشت در هر آیتم RecyclerView
اینترفیسها
با ساختن اینترفیسها شروع میکنیم. برای این که همه چیز منظم بماند همه اینترفیسها را یک جا تعریف میکنیم. باز هم یادآوری میکنیم که در این مثال فقط بر روی ایجاد یک یادداشت جدید تمرکز میکنیم.
public interface MVP_Main { /** * Required View methods available to Presenter. * A passive layer, responsible to show data * and receive user interactions */ interface RequiredViewOps { // View operations permitted to Presenter Context getAppContext(); Context getActivityContext(); void notifyItemInserted(int layoutPosition); void notifyItemRangeChanged(int positionStart, int itemCount); } /** * Operations offered to View to communicate with Presenter. * Processes user interactions, sends data requests to Model, etc. */ interface ProvidedPresenterOps { // Presenter operations permitted to View void clickNewNote(EditText editText); // setting up recycler adapter int getNotesCount(); NotesViewHolder createViewHolder(ViewGroup parent, int viewType); void bindViewHolder(NotesViewHolder holder, int position); } /** * Required Presenter methods available to Model. */ interface RequiredPresenterOps { // Presenter operations permitted to Model Context getAppContext(); Context getActivityContext(); } /** * Operations offered to Model to communicate with Presenter * Handles all data business logic. */ interface ProvidedModelOps { // Model operations permitted to Presenter int getNotesCount(); Note getNote(int position); int insertNote(Note note); boolean loadData(); } }
لایه نمایشگر
حالا وقت آن است که لایههای مدل، نمایشگر و معرف را بسازیم. از آنجایی که MainActivity نقش نمایشگر را بر عهده دارد، باید اینترفیس RequiredViewOps را پیادهسازی کند.
public class MainActivity extends AppCompatActivity implements View.OnClickListener, MVP_Main.RequiredViewOps { private MVP_Main.ProvidedPresenterOps mPresenter; private EditText mTextNewNote; private ListNotes mListAdapter; @Override public void onClick(View v) { switch (v.getId()) { case R.id.fab:{ // Adds a new note mPresenter.clickNewNote(mTextNewNote); } } } @Override public Context getActivityContext() { return this; } @Override public Context getAppContext() { return getApplicationContext(); } // Notify the RecyclerAdapter that a new item was inserted @Override public void notifyItemInserted(int adapterPos) { mListAdapter.notifyItemInserted(adapterPos); } // notify the RecyclerAdapter that items has changed @Override public void notifyItemRangeChanged(int positionStart, int itemCount){ mListAdapter.notifyItemRangeChanged(positionStart, itemCount); } // notify the RecyclerAdapter that data set has changed @Override public void notifyDataSetChanged() { mListAdapter.notifyDataSetChanged(); } // Recycler adapter // This class could have their own Presenter, but for the sake of // simplicity, will use only one Presenter. // The adapter is passive and all the processing occurs // in the Presenter layer. private class ListNotes extends RecyclerView.Adapter<NotesViewHolder> { @Override public int getItemCount() { return mPresenter.getNotesCount(); } @Override public NotesViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return mPresenter.createViewHolder(parent, viewType); } @Override public void onBindViewHolder(NotesViewHolder holder, int position) { mPresenter.bindViewHolder(holder, position); } } }
لایه معرف:
معرف میانجی است و باید دو تا اینترفیس را پیادهسازی کند:
- ProvidedPresenterOps تا به نمایشگر اجازه بدهد از این طریق با معرف ارتباط برقرار کند
- RequiredPresenterOps تا بتواند نتایج را از مدل بگیرد
به اینترفیس لایه نمایشگر توجه ویژه بکنید. ما باید از WeakReference<MVP_Main.RequiredViewOps> استفاده کنیم چون MainActivity ممکن است در هر لحظه نابود شود و ما باید جلوی نشت حافظه را بگیریم. همچنین لایه مدل هنوز ساخته نشده است. این کار را بعداً وقتی که لایههای مخالف MVP را به هم وصل میکنیم، انجام خواهیم داد.
public class MainPresenter implements MVP_Main.ProvidedPresenterOps, MVP_Main.RequiredPresenterOps { // View reference. We use as a WeakReference // because the Activity could be destroyed at any time // and we don't want to create a memory leak private WeakReference<MVP_Main.RequiredViewOps> mView; // Model reference private MVP_Main.ProvidedModelOps mModel; /** * Presenter Constructor * @param view MainActivity */ public MainPresenter(MVP_Main.RequiredViewOps view) { mView = new WeakReference<>(view); } /** * Return the View reference. * Throw an exception if the View is unavailable. */ private MVP_Main.RequiredViewOps getView() throws NullPointerException{ if ( mView != null ) return mView.get(); else throw new NullPointerException("View is unavailable"); } /** * Retrieves total Notes count from Model * @return Notes list size */ @Override public int getNotesCount() { return mModel.getNotesCount(); } /** * Creates the RecyclerView holder and setup its view * @param parent Recycler viewGroup * @param viewType Holder type * @return Recycler ViewHolder */ @Override public NotesViewHolder createViewHolder(ViewGroup parent, int viewType) { NotesViewHolder viewHolder; LayoutInflater inflater = LayoutInflater.from(parent.getContext()); View viewTaskRow = inflater.inflate(R.layout.holder_notes, parent, false); viewHolder = new NotesViewHolder(viewTaskRow); return viewHolder; } /** * Binds ViewHolder with RecyclerView * @param holder Holder to bind * @param position Position on Recycler adapter */ @Override public void bindViewHolder(final NotesViewHolder holder, int position) { final Note note = mModel.getNote(position); holder.text.setText( note.getText() ); holder.date.setText( note.getDate() ); holder.btnDelete.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { clickDeleteNote(note, holder.getAdapterPosition(), holder.getLayoutPosition()); } }); } /** * @return Application context */ @Override public Context getAppContext() { try { return getView().getAppContext(); } catch (NullPointerException e) { return null; } } /** * @return Activity context */ @Override public Context getActivityContext() { try { return getView().getActivityContext(); } catch (NullPointerException e) { return null; } } /** * Called by View when user clicks on new Note button. * Creates a Note with text typed by the user and asks * Model to insert it in DB. * @param editText EditText with text typed by user */ @Override public void clickNewNote(final EditText editText) { getView().showProgress(); final String noteText = editText.getText().toString(); if ( !noteText.isEmpty() ) { new AsyncTask<Void, Void, Integer>() { @Override protected Integer doInBackground(Void... params) { // Inserts note in Model, returning adapter position return mModel.insertNote(makeNote(noteText)); } @Override protected void onPostExecute(Integer adapterPosition) { try { if (adapterPosition > -1) { // Note inserted getView().clearEditText(); getView().notifyItemInserted(adapterPosition + 1); getView().notifyItemRangeChanged(adapterPosition, mModel.getNotesCount()); } else { // Informs about error getView().hideProgress(); getView().showToast(makeToast("Error creating note [" + noteText + "]")); } } catch (NullPointerException e) { e.printStackTrace(); } } }.execute(); } else { try { getView().showToast(makeToast("Cannot add a blank note!")); } catch (NullPointerException e) { e.printStackTrace(); } } } /** * Creates a Note object with given text * @param noteText String with Note text * @return A Note object */ public Note makeNote(String noteText) { Note note = new Note(); note.setText( noteText ); note.setDate(getDate()); return note; } }
لایه مدل
لایه مدل مسئول انجام منطق کاری برنامه است. این کلاس یک ArrayList با یادداشتهایی که به دیتابیس اضافه شدهاند و یک DAO برای انجام عملیات دیتابیس و یک ارجاع به معرف (Presenter) دارد.
۰۱
۰۲
۰۳
۰۴
۰۵
۰۶
۰۷
۰۸
۰۹
۱۰
۱۱
۱۲
۱۳
۱۴
۱۵
۱۶
۱۷
۱۸
۱۹
۲۰
۲۱
۲۲
۲۳
۲۴
۲۵
۲۶
۲۷
۲۸
۲۹
۳۰
۳۱
۳۲
۳۳
۳۴
۳۵
۳۶
۳۷
۳۸
۳۹
۴۰
۴۱
۴۲
۴۳
۴۴
۴۵
۴۶
۴۷
۴۸
۴۹
۵۰
۵۱
۵۲
۵۳
۵۴
۵۵
۵۶
۵۷
۵۸
۵۹
۶۰
۶۱
۶۲
|
public class MainModel implements MVP_Main.ProvidedModelOps { // Presenter reference private MVP_Main.RequiredPresenterOps mPresenter; private DAO mDAO; // Recycler data public ArrayList<Note> mNotes; /** * Main constructor, called by Activity during MVP setup * @param presenter Presenter instance */ public MainModel(MVP_Main.RequiredPresenterOps presenter) { this .mPresenter = presenter; mDAO = new DAO( mPresenter.getAppContext() ); } /** * Inserts a note on DB * @param note Note to insert * @return Note's position on ArrayList */ @Override public int insertNote(Note note) { Note insertedNote = mDAO.insertNote(note); if ( insertedNote != null ) { loadData(); return getNotePosition(insertedNote); } return - 1 ; } /** * Loads all Data, getting notes from DB * @return true with success */ @Override public boolean loadData() { mNotes = mDAO.getAllNotes(); return mNotes != null ; } /** * Gets a specific note from notes list using its array position * @param position Array position * @return Note from list */ @Override public Note getNote( int position) { return mNotes.get(position); } /** * Get ArrayList size * @return ArrayList size */ @Override public int getNotesCount() { if ( mNotes != null ) return mNotes.size(); return 0 ; } } |
۴- وصل کردن اجزا به هم
حالا که لایههای مختلف MVP را داریم وقت آن است که از روی آنها شی بسازیم و آنها را به هم وصل کنیم. اما قبل از آن باید چند موضوع را مستقیماً به اندروید ربط دارند بررسی کنیم.
نمونهسازی از لایهها
از آنجایی که اندروید اجازه نمیدهد یک شی از روی اکتیویتی ساخته شود، لایه نمایشگر توسط اندروید نمونهسازی خواهد شد. ما باید از لایههای معرف و مدلنمونه یا شی بسازیم. متأسفانه ساختن اشیا بیرون از اکتیویتی ممکن است مشکلاتی برای ما ایجاد کند.
توصیه میکنیم که از نوعی «تزریق وابستگی» (dependency injection) استفاده کنید. از آنجایی که تمرکز ما بر روی پیادهسازی MVP است، از روش سادهتری استفاده میکنیم، اما این سادگی فقط در درک مسأله است. در ادامه این مبحث به تزریق وابستگی هم خواهیم پرداخت.
- نمونهسازی از روی معرف و مدل در اکتیویتی به عنوان متغیرهای محلی
- تنظیم RequiredViewOps و ProvidedModelOps در معرف
- تنظیم RequiredPresenterOps در مدل
- نگهداری ProvidedPresenterOps به عنوان یک ارجاع (reference) در نمایشگر
/** * Setup Model View Presenter pattern */ private void setupMVP() { // Create the Presenter MainPresenter presenter = new MainPresenter(this); // Create the Model MainModel model = new MainModel(presenter); // Set Presenter model presenter.setModel(model); // Set the Presenter as a interface mPresenter = presenter; }
مدیریت تغییر تنظیمات
نکته دیگری که باید همیشه به آن توجه کنیم چرخه زندگی اکتیویتی است. اکتیویتیهای اندروید ممکن است در هر زمانی نابود شوند و لایههای معرف و مدل هم به همراه آن نابود میشوند. ما باید برای حل این مشکل و نگهداری حالت برنامه در زمان تغییر تنظیمات دستگاه (مانند تغییر جهت صفحه نمایش از عمودی به افقی و مانند آن) از نوعی ماشین نگهداری حالت کمک بگیریم.
برای این کار ما از یک کلاس دیگر به نام StateMaintainer استفاده میکنیم که شامل یک فرگمنت است که حالت خودش را نگهداری میکند و ما از این فرگمنت برای ذخیرهسازی و بازیابی اشیامان استفاده میکنیم. کد این کلاس در سورس کدهای پروژه هست و میتوانید آن را ببینید.
ما باید متد onDestroy را به معرف و مدل اضافه کنیم تا آنها را از وضعیت فعلی اکتیویتی مطلع کنیم. همچنین باید متد setView را به معرف اضافه کنیم که مسئول گرفتن ارجاع به یک نمایشگر جدید بعد از بازسازی اکتیویتی است.
public class MainActivity extends AppCompatActivity implements View.OnClickListener, MVP_Main.RequiredViewOps { // … private void setupMVP() { // Check if StateMaintainer has been created if (mStateMaintainer.firstTimeIn()) { // Create the Presenter MainPresenter presenter = new MainPresenter(this); // Create the Model MainModel model = new MainModel(presenter); // Set Presenter model presenter.setModel(model); // Add Presenter and Model to StateMaintainer mStateMaintainer.put(presenter); mStateMaintainer.put(model); // Set the Presenter as a interface // To limit the communication with it mPresenter = presenter; } // get the Presenter from StateMaintainer else { // Get the Presenter mPresenter = mStateMaintainer.get(MainPresenter.class.getName()); // Updated the View in Presenter mPresenter.setView(this); } } // … }
موخره
معماری MVP میتواند بعضی از مسائلی که در معماری پیشفرض اندروید وجود دارند را حل کند. این معماری نگهداری از کد و تست برنامه را آسان میکند. ممکن است در ابتدای کار پیادهسازی MVP مشکل به نظر برسد اما وقتی که منطق پشت آن را فهمیدید، کل ماجرا برایتان ساده میشود.
الان میتوانید کتابخانه MVP خودتان را بسازید یا از راهحلهای موجود مانند Mosby یا Simple-mvp استفاده کنید. حالا میفهمید که این کتابخانهها در پشت صحنه چه کار میکنند.
ما تقریباً به انتهای سفر خودمان در MVP رسیدیم. در بخش سوم و آخر این سری، تست واحد (Unit Test) و تزریق وابستگی (dependency Injection) را به برنامه اضافه میکنیم. همچنان ما را دنبال کنید.
سلام. لینک این StateMaintainer رو تو سورس گیت هاب میخاستم. نتونستم پیدا کنم
خوب نگشتید پس! اینجا رو ببینید.
سلام
ممنون از مقالتون
ولی بهتر نیس یه سری کلمات به فارسی ترجمه نشن – مثل معرف یا نمایشگر یا آزمونشوندگی
سلام .
من سورس این پروژه رو پیدا نکردم . لطفا لینک شو بذارین . ممنون
سلام . به نظرم این نوع پیاده سازی درست نیست شما حتی setOnClickListener ها رو هم در لایه presenter تعریف کردین و یا recyclerview و اینها از اساس view هستن و ربطی به منطق ندارن . میتونستین منطق رو بهشون تزریغ کنید از presenter و در view عملیات ساخت رو اضافه کنید .
مشکل بعدی که مشکل نمیشه گفت بهش این هست که از Callback داریم استفاده میکنیم بهتره به نظرم در ارتباط بین لایه های Model و Presenter از callback استفاده نشه چون نیازی نیست اما در ارتباط با View نیاز هست چون نمیتونیم از این کلاس نمونه سازی کنیم .
نکته بعدی این هست که اصلا activity رو نمیشه view حساب کرد .
سلام. ممنون از آموزش های خوبتون. اسمارت لب یکی از بهترین منابع آموزشی فارسی زبان اندروید هست که من خیلی اوقات بهش سر میزنم و ازش خیلی چیزا یاد گرفتم. در رابطه با این آموزشتون، با توجه به اینکه مدت زیادی ازش میگذره، بنظرتون الان با وجود الگوی معماری MVVM باز هم نیازی به MVP تو اندروید هست؟ و اگر هست چطور باید تشخیص بدیم کدوم یکی برای کار ما مناسبه؟
سلام
من یه سوال برام پیش اومده؛ چرا به جای این که توی اکتیویتی بیایم از presenter و model شی بسازیم یا تزریق وابستگی انجام بدیم، نمیایم از Singleton استفاده کنیم، یعنی presenter و model رو بیایم Singleton کنیم.
به دلیل اینکه ما یه شی از اونا بیشتر نیاز نداریم و یکی باشه بهتره و همینطور خودشون object خودشون رو نگه میدارن.
هر جا لازمه صداشون بزنیم getInstance میکنیم.