نحوه بکارگیری Model View Presenter در اندروید

نحوه بکارگیری 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 درخواست بازنشانی فهرست خود و نمایش یادداشت جدید را می کند.

blog_17892_3

رابط های 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 خواهیم پرداخت.

 

http://code.tutsplus.com برگرفته از

اینها را هم بخوانید