如何在Android中采用MVP?
OPEN编辑
9年前
<p>在前面的教程中,我们讨论了MVP模式,它是如何应用于安卓的,以及它最重要的优点是什么。在本教程中,我们将通过在一个安卓应用程序中实现它,以更详细地探讨MVP模式。</p> <p>在本教程中:</p> <ul> <li>我们将构建了一个使用MVP模式的简单应用程序</li> <li>我们将探讨如何在Android上实现MVP模式</li> <li>并且我们将讨论如何克服一些由安卓构架所引起的困难 </li> </ul> <h2>1.MVP介绍</h2> <p>MVP模式是基于模型视图控制器(MVC)模式的构架模式。MVC模式增加了关注分离,便于单元测试。它创建了三层模型(Modle),视图(View)和展示器(Presenter),每一层都有各自的职责。</p> <p style="text-align:center"><img alt="" src="https://simg.open-open.com/show/a0277444aa01c4d25840a487b4690839.png"></p> <p>模型(Model)包含应用程序的逻辑。它控制着数据的创建,存储和修改。视图(View)是一个被动的界面,用于显示数据和发送用户动作到展示器(Presenter)。展示器扮演中间人的角色,它从Modle模型中检索数据,并在View视图中显示数据。展示器还处理由视图(View)转发的用户操作。</p> <h2>2.项目计划和设置</h2> <p>我们正在构建一个简单的笔记应用程序来阐明MVP。这个应用程序允许用户去做笔记,把笔记存储在一个本地数据库中,并且可以删除笔记。为了简化,程序只有一个活动。</p> <p style="text-align:center"><img alt="" src="https://simg.open-open.com/show/cec2c74042aed390532907501437b7f0.jpg"></p> <p>在本教程中,我们主要关注MVP模式的实现。其他的功能(比如创建一个SQLite数据库,构建一个<a href="/misc/goto?guid=4959670730991308751">DAO</a>,或者处理用户交互)就略过了。如果你需要这些功能方面的帮助,Envato Tuts+ 有一些很好的这方面的教程。 </p> <h3>活动图和MVP层</h3> <p>让我们开始一个新笔记的创建。如果我们将这个动作划分为更小的操作,那么使用MVP构架模式就像下面的样子:</p> <ul> <li>用户输入一个笔记并且点击添加按钮。</li> <li>展示器(Presenter)使用用户输入的文本创建一个笔记对象,并命令模型(Model)将其插入到数据库中。</li> <li>该模型(Model)将笔记插入到数据库中并通知展示器(Presenter)笔记列表已经更改。</li> <li>该展示器(Presenter)清除文本域,并要求视图(View)更新它的列表以显示新创建的笔记。</li> </ul> <p style="text-align:center"><img alt="" src="https://simg.open-open.com/show/112a3df14d889b612b415ca4819195fe.png"></p> <p style="text-align:center"><strong><sup>MVP 接口</sup></strong></p> <p>现在让我们考虑需要完成这些动作所需要的操作并且使用MVP分离他们。为了使各种对象解耦,各层之间的通信需要使用接口来进行。我们需要四个接口:</p> <ul> <li>requiredviewops:展示器(Presenter)所需的视图(View)操作</li> <li>providedpresenterops:提供给视图(View)的用于与展示器(Presenter)通信的操作</li> <li>requiredpresenterops:模式(Model)所需的展示器(Presenter)操作</li> <li>providedmodelops:提供给模式(Model)的用于与展示器(Presenter)通信的操作</li> </ul> <p style="text-align:center"><img alt="" src="https://simg.open-open.com/show/b6eda7cdfb8e57f3bb46cff3fe86e877.png"> </p> <h2>3.在安卓中实现MVP</h2> <p>现在我们有一个想法:各种方法应该怎么组织?我们怎么开始创建我们的app?我们通过仅仅关注添加新笔记的行为来简化实施。本教程的源文件在<a href="/misc/goto?guid=4959670731081821142">GitHub</a>上。</p> <p>我们仅使用一个<strong>Activity</strong>布局,其中包括:</p> <ul> <li>用于新笔记的编辑框(EditText )</li> <li>添加笔记的按钮(Button )</li> <li>列出所有笔记的RecylerView</li> <li>RecyclerView 的holder接口包含2个TextView和一个Button</li> </ul> <h3>接口</h3> <p>让我们开始创建这个接口。 为了有条理,我们把接口放在持有者里面。同样的,在这个例子里,我们主要关注添加一个新笔记的行为。</p> <pre> <code class="language-java">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(); } }</code></pre> <h3> 视图层</h3> <p>现在来创建模型(Modle),视图(View)和展示器(Presenter )层。因为MainActivity将会作为一个视图,它应该实现RequiredViewOps接口。</p> <pre> <code class="language-java">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); } } }</code></pre> <h3>展示层</h3> <p>展示器扮演中间人角色并且需要执行两个接口:</p> <ul> <li>ProvidedPresenterOps允许被视图(View)调用</li> <li>ProvidedPresenterOpss接收来自模型(Modle)的结果</li> </ul> <p>请特别注意视图(View)层参数。我们需要用一个WeakReference<MVP_Main.RequiredViewOps>。因为MainActivity 可能在任何时刻被销毁并且我们想要避免内存泄漏。此外,模型(Modle)层还没有被建立。我们之后在连接MVP层的时候也会这样做。</p> <pre> <code class="language-java">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; } }</code></pre> <h3>模型层</h3> <p>模型层负责处理业务逻辑。它带有一个ArrayList,用于把笔记添加到数据库中的;一个操作数据库的DAO,还有展示器(Presenter)的引用。</p> <pre> <code class="language-java">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; } }</code></pre> <h2>4.尝试将所有的组合在一起</h2> <p>随着MVP层就位了, 我们需要将他们实例化并且插入必要的引用。在我们做这个工作之前,我们需要解决一些直接和安卓相关的问题。</p> <h3>实例化各层</h3> <p>因为安卓不允许Activity实例化,视图(View)层将会做实例化工作。我们只需负责实例化展示器(Presenter)和模型(Model)层。不幸的是,实例化这两个层会带来问题。</p> <p>它推荐我们去使用一个依赖注入的形式来实现。因为我们的目标是去关注MVP的实现,我们将会采取一个更为简单的方法。虽然这个方法不是最为有效的,但是它却是最易理解的。我们之后将会在这一系列中讨论MVP和依赖注入。</p> <ul> <li>用本地变量在Activity中实例化展示器(Presenter)和模型(Modle)</li> <li>在展示器(Presenter)中建立RequiredViewOps 和ProvidedModelOps</li> <li>在模型(Modle)中建立RequiredPresenterOps </li> <li>存储ProvidedPresenterOps 作为一个引用,在View中使用</li> </ul> <pre> <code class="language-java">/** * 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; }</code></pre> <h3>处理配置变化</h3> <p>我们应该考虑的另一个事情是Activity的生命周期。安卓的Activity可以在任何时间被销毁,并且展示器(Presenter)和模型(Modle)层也随之被销毁。我们需要通过使用一种状态机来在配置变化时存储状态以便解决这一问题。我们也应该通知其他层关于Activity的状态。</p> <p>为了实现这一目标,我们将会使用一个独立类:StateMaintainer,,它具有一个包含它状态的Fragment,并且我们可以使用这个Fragment来存储或者检索对象。你可以看看这个类在本教程的<a href="/misc/goto?guid=4959670731081821142">源文件</a>中。</p> <p>我们需要给展示器(Presenter)和模型(Model)添加一个onDestroy方法,去通知他们有关Activity的当前状态。我们也需要给展示器(Presenter)添加一个setView方法,它的作用是负责接收一个Activity新创建的View引用。</p> <p style="text-align:center"><img alt="" src="https://simg.open-open.com/show/61edc0c926e300437f66e6dfd729531b.png"></p> <pre> <code class="language-java">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); } } // … }</code></pre> <h2>总结</h2> <p>MVP模式能够解决一些由安卓本身默认架构造成的问题。它使得你的代码容易维护和测试。采用MVP起初可能看起来复杂,但是一旦你理解了它背后的逻辑,整个的过程就非常简单了。</p> <p>现在你能够创建你自己的MVP库或者使用一个有效的解决方案,比如<a href="/misc/goto?guid=4959633174800373014">Mosby</a>或者simple-mvp。你现在最好应该理解这些库在背后做些什么。</p> <p>我们几乎快走完了MVP旅程。在这一系列的第三个和最后一部分,我们将会添加测试单元并且调整我们的代码来解决内存泄漏。我希望你们加油。</p> <div style="page-break-after: always"> <span style="display:none"> </span> </div> <p>英文原文:<a href="/misc/goto?guid=4959670731206693294">http://code.tutsplus.com/tutorials/how-to-adopt-model-view-presenter-on-android--cms-26206</a></p> <p>中文翻译:<a href="http://www.open-open.com/lib/view/open1460635515430.html">http://www.open-open.com/lib/view/open1460635515430.html</a></p> <p>译者:Liam 校对:OPEN编辑</p> <p>本译文仅用于学习和交流,转载请注明文章译者、出处、和本文链接</p> <p>我们遵照 CC 协议,如有侵犯到您的权益,请联系我们</p>