
نحوه بکارگیری Model View Presenter در اندروید
در مطلب قبلی از Model View Presenter، نحوه اعمال آن در اندروید و مزایایی که به همراه دارد سخن گفتیم. در این مطلب قصد داریم تا با جزئیات بیشتری به الگوی Model View Presenter پرداخته و آن را در یک اپلیکیشن اندروید پیاده سازی کنیم.
در این مطلب:
- یک اپلیکیشن نمونه با استفاده از الگوی MVP می سازیم.
- نحوه پیاده سازی الگوی MVP در اندروید را شرح می دهیم.
- به بحث درباره نحوه رفع مشکلاتی که به سبب معماری اندروید پیش آمده اند خواهیم پرداخت.
1. Model View Presenter
الگوی Model View Presenter یک الگوی معماری بر پایه الگوی Model View Controller می باشد که جداسازی وظایف را افزایش داده و فرآیند تست واحد را تسهیل می بخشد. این الگو سه لایه به نام های Model، View و Presenter را می سازد که هرکدام دارای وظایف از پیش تعریف شده می باشند.
لایه Model دربرگیرنده منطق کاری اپلیکیشن است و نحوه ساخت، ذخیره سازی و اعمال تغییرات بر روی داده ها را کنترل می کند. View یک رابط غیرفعال است که داده ها را به نمایش می گذارد و عملیات کاربر را به سمت Presenter هدایت می کند. Presenter در حقیقت در نقش واسط ظاهر می شود و با بازیابی داده ها از Model، آن را در View به نمایش می گذارد، علاوه بر این، لایه مذکور آن دسته از عملیات کاربر را که توسط View برای آن ارسال شده است پردازش می کند.
2. برنامه ریزی پروژه و راه اندازی
به منظور نمایش نحوه عملکرد MVP یک اپلیکیشن notes ساده می سازیم. این اپلیکیشن کاربر را قادر به یادداشت برداری می کند و یادداشت ها را در یک پایگاه داده لوکال ذخیره می نماید، در صورت تمایل کاربر می تواند یادداشت های نوشته شده را پاک کند، برای سادگی کار اپلیکیشن تنها یک اکتیویتی خواهد داشت.
در این مطلب آموزشی تمرکز ما بر روی پیاده سازی الگوی MVP است، سایر عملکردها مانند راه اندازی یک پایگاه داده SQLite، ساخت یک DAO و یا مدیریت تعاملات کاربر در این مطلب شرح داده نخواهند شد.
نمودار اکشن و لایه های MVP
کار را با ساخت یک یادداشت جدید آغاز می کنیم، در صورتی که این عملیات را به عملکردها کوچک تر بشکنید نمودار جریان کاری با استفاده از الگوی معماری MVP به صورت زیر خواهد بود:
- کاربر یک یادداشت را تایپ کرده و بر روی دکمه افزودن یادداشت کلیک می کند.
- Presenter آبجکت Note را با استفاده از متنی که کاربر وارد کرده می سازد و از لایه Model درخواست می کند تا آن را در پایگاه داده درج نماید.
- Model یادداشت را در پایگاه داده درج نموده و Presenter را از بابت تغییر اعمال شده در فهرست یادداشت ها مطلع می سازد.
- Presenter فیلد متنی را پاک کرده و از View درخواست بازنشانی فهرست خود و نمایش یادداشت جدید را می کند.
رابط های MVP
در این مرحله تمامی عملیات و فرآیندهای موردنیاز برای انجام این کار و جداسازی آنها با استفاده از MVP مد نظر ما قرار دارد. به منظور برقراری ارتباط بین این لایه ها به رابط هایی نیاز داریم، چهار رابط به شرح زیر مورد استفاده قرار می گیرند:
RequiredViewOps: عملیات موردنیاز View که در دسترس Presenter قرار دارد.
ProvidedPresenterOps: عملیاتی که به View به منظور برقراری ارتباط با View پیشنهاد شده است.
RequiredPresenterOps: عملیات موردنیاز Presenter که در دسترس Model قرار دارند.
ProvidedModelOps: عملیاتی که به Model جهت برقراری ارتباط با Presenter پیشنهاد شده است.
3. پیاه سازی MVP در اندروید
در این مرحله که ایده کلی سازماندهی متدهای مختلف را در ذهن داریم می توانیم عملیات ساخت اپلیکیشن را آغاز نماییم. به منظور ساده سازی فرآیند، تمرکز خود را تنها بر روی عملیات ساخت یک یادداشت جدید می گذاریم. فایل های منبع این مطلب آموزشی در GitHub در دسترس می باشند.
برای این کار از یک اکتیویتی با لی اوتی دربرگیرنده موارد زیر استفاده می کنیم:
- EditText برای یادداشت های جدید
- Button برای افزودن یک یادداشت جدید
- RecyclerView برای فهرست بندی تمامی یادداشت ها
- دو عنصر TextView و یک Button در داخل یک هولدر 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();
}
}
لایه View
در این مرحله لازم است تا لایه های Model، View و Presenter را بسازیم.
MainActivity در نقش View ظاهر می شود، بنابراین باید به پیاده سازی رابط 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);
}
}
}
لایه Presenter
همانطور که پیشتر نیز اشاره شد، Presenter در نقش واسطه ای عمل می کند و باید به پیاده سازی دو رابط بپردازد.
- ProvidedPresenterOps به منظور پذیرفتن فراخوانی های صورت گرفته از سمت View
- RequiredPreesnterOps به منظور بازیابی نتایج از Model
به رابط لایه View توجه داشته باشید، باید <WeakReference<MVP_Main.RequiredViewOps را مورد استفاده قرار دهیم، زیرا MainActivity ممکن است هر زمانی از بین برود و باید از نشتی های حافظه جلوگیری به عمل آید. علاوه بر این، لایه Model هنوز ساخته نشده و زمانی که لایه های 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;
}
}
لایه Model
لایه Model مسئول مدیریت منطق کاری اپلیکیشن است و 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;
}
}
4. آزمودن همه چیز با هم
حال که تمامی لایه های MVP را در اختیار داریم، لازم است تا نمونه سازی کرده و مراجع موردنیاز را درج نماییم. پیش از این کار باید به برخی از مشکلاتی که مستقیما به اندروید مرتبط اند بپردازیم.
نمونه سازی لایه ها
از آنجایی که نمونه سازی یک اکتیویتی در اندروید امکان پذیر نمی باشد، لایه View آن را نمونه سازی خواهد کرد. اما نمونه سازی لایه های Presenter و Model بر عهده ما قرار دارد. متاسفانه نمونه سازی این لایه ها در خارج از اکتیویتی می تواند مشکل ساز باشد.
پیشنهاد می شود تا از یک فرم از تزریق وابستگی برای انجام این کار استفاده گردد. از آنجایی که هدف ما بر روی پیاده سازی MVP نهاده شده، رویکرد ساده تری را در پیش خواهیم گرفت. این رویکرد بهترین گزینه ممکن نیست، اما درک آن ساده تر صورت می پذیرد.
- نمونه سازی Presenter و Model در اکتیویتی با استفاده از متغیرهای محلی
- راه اندازی RequiredViewOps و ProvidedModelOps در Presenter
- راه اندازی RequiredPresenterOps در Model
- ذخیره سازی ProvidedPresenterOps به عنوان مرجعی جهت استفاده در View
/**
* 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;
}
مدیریت تغییرات پیکربندی
چرخه حیات اکتیویتی مورد دیگری است که باید مد نظر قرار گیرد، هر زمانی قادر به از بین بردن اکتیویتی اندروید هستید و در این زمان لایه های Presenter و Model نیز همراه آن از بین می روند. به منظور رفع این مشکل باید از یک ماشین حالت استفاده کنیم که حالت ها را در طول تغییرات پیکربندی ذخیره کند. علاوه بر این لازم است تا سایر لایه ها را نیز از حالت اکتیویتی مطلع گردانیم، برای این کار یک کلاس مجزا را مورد استفاده قرار می دهیم که StateMaintainer نام دارد، این کلاس دربرگیرنده یک فرگمنت است که حالت را نگهداری کرده و از این فرگمنت برای ذخیره سازی و بازیابی آبجکت های ما استفاده می کند. می توانید در فایل های منبع این مطلب آموزشی نگاهی به نحوه پیاده سازی این کلاس بیندازید.
باید یک متد Ondestroy را به Presenter و Model افزوده و آنها را از حالت کنونی اکتیویتی مطلع گردانیم. علاوه بر این لازم است تا یک متد setView را به Presenter بیفزاییم، این متد مسئول دریافت یک مرجع جدید View از اکتیویتی ساخته شده می باشد.
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 می باشید. هم اکنون عملکرد این لایبرری ها برای شما قابل درک تر و ملموس تر از گذشته شده است.
در مطلب آینده به فرآیند تست واحد و ادغام و سازگار کردن کد به منظور استفاده از تزریق وابستگی با کمک Dagger خواهیم پرداخت.