如何在Android中采用MVP?
在前面的教程中,我們討論了MVP模式,它是如何應用于安卓的,以及它最重要的優點是什么。在本教程中,我們將通過在一個安卓應用程序中實現它,以更詳細地探討MVP模式。
在本教程中:
- 我們將構建了一個使用MVP模式的簡單應用程序
- 我們將探討如何在Android上實現MVP模式
- 并且我們將討論如何克服一些由安卓構架所引起的困難
1.MVP介紹
MVP模式是基于模型視圖控制器(MVC)模式的構架模式。MVC模式增加了關注分離,便于單元測試。它創建了三層模型(Modle),視圖(View)和展示器(Presenter),每一層都有各自的職責。
模型(Model)包含應用程序的邏輯。它控制著數據的創建,存儲和修改。視圖(View)是一個被動的界面,用于顯示數據和發送用戶動作到展示器(Presenter)。展示器扮演中間人的角色,它從Modle模型中檢索數據,并在View視圖中顯示數據。展示器還處理由視圖(View)轉發的用戶操作。
2.項目計劃和設置
我們正在構建一個簡單的筆記應用程序來闡明MVP。這個應用程序允許用戶去做筆記,把筆記存儲在一個本地數據庫中,并且可以刪除筆記。為了簡化,程序只有一個活動。
在本教程中,我們主要關注MVP模式的實現。其他的功能(比如創建一個SQLite數據庫,構建一個DAO,或者處理用戶交互)就略過了。如果你需要這些功能方面的幫助,Envato Tuts+ 有一些很好的這方面的教程。
活動圖和MVP層
讓我們開始一個新筆記的創建。如果我們將這個動作劃分為更小的操作,那么使用MVP構架模式就像下面的樣子:
- 用戶輸入一個筆記并且點擊添加按鈕。
- 展示器(Presenter)使用用戶輸入的文本創建一個筆記對象,并命令模型(Model)將其插入到數據庫中。
- 該模型(Model)將筆記插入到數據庫中并通知展示器(Presenter)筆記列表已經更改。
- 該展示器(Presenter)清除文本域,并要求視圖(View)更新它的列表以顯示新創建的筆記。
MVP 接口
現在讓我們考慮需要完成這些動作所需要的操作并且使用MVP分離他們。為了使各種對象解耦,各層之間的通信需要使用接口來進行。我們需要四個接口:
- requiredviewops:展示器(Presenter)所需的視圖(View)操作
- providedpresenterops:提供給視圖(View)的用于與展示器(Presenter)通信的操作
- requiredpresenterops:模式(Model)所需的展示器(Presenter)操作
- providedmodelops:提供給模式(Model)的用于與展示器(Presenter)通信的操作
3.在安卓中實現MVP
現在我們有一個想法:各種方法應該怎么組織?我們怎么開始創建我們的app?我們通過僅僅關注添加新筆記的行為來簡化實施。本教程的源文件在GitHub上。
我們僅使用一個Activity布局,其中包括:
- 用于新筆記的編輯框(EditText )
- 添加筆記的按鈕(Button )
- 列出所有筆記的RecylerView
- RecyclerView 的holder接口包含2個TextView和一個Button
接口
讓我們開始創建這個接口。 為了有條理,我們把接口放在持有者里面。同樣的,在這個例子里,我們主要關注添加一個新筆記的行為。
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();
}
}
視圖層
現在來創建模型(Modle),視圖(View)和展示器(Presenter )層。因為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允許被視圖(View)調用
- ProvidedPresenterOpss接收來自模型(Modle)的結果
請特別注意視圖(View)層參數。我們需要用一個WeakReference<MVP_Main.RequiredViewOps>。因為MainActivity 可能在任何時刻被銷毀并且我們想要避免內存泄漏。此外,模型(Modle)層還沒有被建立。我們之后在連接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;
}
}
4.嘗試將所有的組合在一起
隨著MVP層就位了, 我們需要將他們實例化并且插入必要的引用。在我們做這個工作之前,我們需要解決一些直接和安卓相關的問題。
實例化各層
因為安卓不允許Activity實例化,視圖(View)層將會做實例化工作。我們只需負責實例化展示器(Presenter)和模型(Model)層。不幸的是,實例化這兩個層會帶來問題。
它推薦我們去使用一個依賴注入的形式來實現。因為我們的目標是去關注MVP的實現,我們將會采取一個更為簡單的方法。雖然這個方法不是最為有效的,但是它卻是最易理解的。我們之后將會在這一系列中討論MVP和依賴注入。
- 用本地變量在Activity中實例化展示器(Presenter)和模型(Modle)
- 在展示器(Presenter)中建立RequiredViewOps 和ProvidedModelOps
- 在模型(Modle)中建立RequiredPresenterOps
- 存儲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;
}
處理配置變化
我們應該考慮的另一個事情是Activity的生命周期。安卓的Activity可以在任何時間被銷毀,并且展示器(Presenter)和模型(Modle)層也隨之被銷毀。我們需要通過使用一種狀態機來在配置變化時存儲狀態以便解決這一問題。我們也應該通知其他層關于Activity的狀態。
為了實現這一目標,我們將會使用一個獨立類:StateMaintainer,,它具有一個包含它狀態的Fragment,并且我們可以使用這個Fragment來存儲或者檢索對象。你可以看看這個類在本教程的源文件中。
我們需要給展示器(Presenter)和模型(Model)添加一個onDestroy方法,去通知他們有關Activity的當前狀態。我們也需要給展示器(Presenter)添加一個setView方法,它的作用是負責接收一個Activity新創建的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。你現在最好應該理解這些庫在背后做些什么。
我們幾乎快走完了MVP旅程。在這一系列的第三個和最后一部分,我們將會添加測試單元并且調整我們的代碼來解決內存泄漏。我希望你們加油。
英文原文:http://code.tutsplus.com/tutorials/how-to-adopt-model-view-presenter-on-android--cms-26206
中文翻譯:http://www.baiduhome.net/lib/view/open1460635515430.html
譯者:Liam 校對:OPEN編輯
本譯文僅用于學習和交流,轉載請注明文章譯者、出處、和本文鏈接
我們遵照 CC 協議,如有侵犯到您的權益,請聯系我們