谈谈 Android MVP 架构
JewelSecres
7年前
<h3>MVP 架构简介</h3> <p>说起 MVP 架构,相信很多朋友都看过,网上也有很多这方面的资料。博主使用 MVP 架构搭建项目也有一段时间了。简单谈一谈心得。说到 MVP 架构,很多人都拿它跟 MVC 去对比。这里我就不过多重复说了,单刀直入。</p> <p>什么是 MVP 架构</p> <p>MVP 架构由 Model(模型)、View(视图)、Presenter(主持者)构成,下面我们一起来了解它们:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/2049c60abbe2668525591dea7a55dd6d.png"></p> <p>MVP 架构图</p> <ul> <li>Model 负责业务逻辑以及数据的处理,主要通过接口实现</li> <li>View 负责 UI 显示以及与用户之间的交互</li> <li>Presenter 起到一个衔接桥梁的作用,负责 Model 跟 View 之间的交互</li> </ul> <h3>MVP 架构的利弊</h3> <p>优点</p> <ol> <li> <p>耦合度低</p> <p>View 跟 Model 之间由 Presenter 负责两者之间的交互,低了其耦合度,使其更加关注自身逻辑,结构清晰</p> </li> <li> <p>可维护性高</p> <p>每个 View 都有其对应的 Presenter,容易进行区分,哪个模块出现了问题,或者接口出现了问题,可以迅速的确定。模型与视图之间完全分离,修改视图不影响模型</p> </li> <li> <p>方便单元测试</p> <p>因其业务逻辑都在 Presenter 里,进行单元测试的时候,可以直接写个测试接口,由 Presenter 去继承</p> </li> </ol> <p>缺点</p> <ol> <li> <p>类数量暴涨</p> <p>每个 View 都有 Presenter ,跟其对应的接口,类的数量会明显变多,在某些场景下 Presenter 的复用会产生接口冗余。</p> </li> <li> <p>额外的学习曲线</p> <p>需要花费额外的时间去学习,学习理解成本高,开始编写代码之前需要时间成本(项目的架构)</p> </li> </ol> <h3>实战演练</h3> <p>前面讲述了一堆的理论知识,下面一步步解剖 MVP 架构,下图是 Demo 的目录结构(看起来比较复杂,勿怪),实现模拟网络获取图书数据并将其显示的功能</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/4e7d1dea022d6327b6659f2807b38a6b.png"></p> <p>MVP Demo 目录图</p> <p>下面我们来看项目实现效果,功能比较简单,gif图就不弄了</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/07ee25acfbd7589962a17e0cbe2af48c.png"></p> <p>demo 运行图1</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/0ebab9923294c70de4893ff494e207b2.png"></p> <p>demo 运行图2</p> <p>Model</p> <p>创建实体类 Book</p> <pre> <code class="language-java">public class Book { private int book_id; private String book_name; private String book_author; private String book_tag; public Book() { } public Book(int book_id, String book_name, String book_author, String book_tag) { this.book_id = book_id; this.book_name = book_name; this.book_author = book_author; this.book_tag = book_tag; } public int getBook_id() { return book_id; } public void setBook_id(int book_id) { this.book_id = book_id; } public String getBook_name() { return book_name; } public void setBook_name(String book_name) { this.book_name = book_name; } public String getBook_author() { return book_author; } public void setBook_author(String book_author) { this.book_author = book_author; } public String getBook_tag() { return book_tag; } public void setBook_tag(String book_tag) { this.book_tag = book_tag; } }</code></pre> <p>创建一个接口,用于获取回调实体类 Book 携带的数据</p> <pre> <code class="language-java">public interface BooksDataSource { interface LoadBooksCallback{ void loadBooks(List<Book> bookList); void dataNotAvailable(); } void getBooks(@NonNull LoadBooksCallback loadBooksCallback); }</code></pre> <p>继承 BooksDataSource 接口,实现模拟数据获取</p> <pre> <code class="language-java">public class BooksLocalDataSource implements BooksDataSource{ private static BooksLocalDataSource INSTANCE; public static BooksLocalDataSource getInstance() { if (INSTANCE == null) { INSTANCE = new BooksLocalDataSource(); } return INSTANCE; } private BooksLocalDataSource(){} @Override public void getBooks(@NonNull LoadBooksCallback loadBooksCallback) { //模拟数据 List<Book> bookList = new ArrayList<>(); Book book1 = new Book(1,"《第一行代码:Android (第2版) 》","郭霖","编程"); Book book2 = new Book(2,"《Android开发艺术探索》","任玉刚","编程"); Book book3 = new Book(3,"《Android群英传》","徐宜生","编程"); bookList.add(book1); bookList.add(book2); bookList.add(book3); loadBooksCallback.loadBooks(bookList); } }</code></pre> <p>数据回调业务处理,网络数据和本地数据(这里仅模拟本地数据)</p> <pre> <code class="language-java">public class BooksRepository implements BooksDataSource{ private static BooksRepository INSTANCE = null; private final BooksDataSource mBooksRemoteDataSource; private final BooksDataSource mBooksLocalDataSource; private BooksRepository(@NonNull BooksDataSource booksRemoteDataSource, @NonNull BooksDataSource booksLocalDataSource) { mBooksRemoteDataSource = booksRemoteDataSource; mBooksLocalDataSource = booksLocalDataSource; } public static void destroyInstance() { INSTANCE = null; } public static BooksRepository getInstance() { if (INSTANCE == null) { INSTANCE = new BooksRepository(BooksRemoteDataSource.getInstance(), BooksLocalDataSource.getInstance()); } return INSTANCE; } @Override public void getBooks(@NonNull final LoadBooksCallback loadBooksCallback) { //数据回调 mBooksLocalDataSource.getBooks(new LoadBooksCallback() { @Override public void loadBooks(List<Book> bookList) { loadBooksCallback.loadBooks(bookList); } @Override public void dataNotAvailable() { loadBooksCallback.dataNotAvailable(); } }); } }</code></pre> <p>到这里,Model的任务算是结束了,得到了所需要的数据源。</p> <p>View</p> <p>Presenter 与 View 是通过接口进行交互,所以这里可以定义一个接口,用于进行交互,此处的难点在于,要清楚需要哪些方法。</p> <p>这里建立两个Base基类,用于初始化</p> <pre> <code class="language-java">public interface BaseView<T> { void setPresenter(T presenter); }</code></pre> <pre> <code class="language-java">public interface BasePresenter { void start(); }</code></pre> <p>从上面的demo运行演示图看,这里 View 的工作主要有2个,一个是显示无数据时的状态,第二个是显示书籍的列表</p> <pre> <code class="language-java">void showBookList(List<Book> bookList); void showNoBooks();</code></pre> <p>本demo演示的是本地模拟数据,关于网络数据方面,可以模拟开启一个线程,通过 Thread.sleep( long ) 充当耗时操作,使用 ProgressBar,给用户一个友好提示,同时需要在 View 的接口中定义相关方法</p> <p>小结:在 View 的方法定义上,需要观察功能上的操作,接着考虑:</p> <ul> <li>该操作需要做什么?( loadBooks )</li> <li>操作后的结果反馈?(showBookList,showNoBooks)</li> <li>操作中的友好交互?(显示正在加载,加载完成)</li> </ul> <p>经过上面的思考后,接下来就是 View 的实现,也就是 Activity ,MVP 中的 View 主要对应的是 Activity</p> <pre> <code class="language-java">public class MainActivity extends AppCompatActivity implements BooksContract.View { private BooksContract.Presenter mPresenter; private Button showBooksBtn; private TextView noDataText; private ListView bookListView; private BooksAdapter booksAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initView(); mPresenter = new BooksPresenter(BooksRepository.getInstance(),this); } private void initView() { showBooksBtn = (Button) findViewById(R.id.show_books_btn); noDataText = (TextView) findViewById(R.id.no_data_text); bookListView = (ListView) findViewById(R.id.books_list_view); showBooksBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mPresenter.loadBooks(); } }); } @Override protected void onResume() { super.onResume(); } @Override public void setPresenter(BooksContract.Presenter presenter) { mPresenter = presenter; } @Override public void showBookList(List<Book> bookList) { if (!bookList.isEmpty()) { noDataText.setVisibility(View.INVISIBLE); } booksAdapter = new BooksAdapter(getApplicationContext(), bookList); bookListView.setAdapter(booksAdapter); } @Override public void showNoBooks() { noDataText.setVisibility(View.VISIBLE); } }</code></pre> <p>从上面的代码看 Activity 实现还是比较简单的,接口引导我们去实现对应的功能,下面是 BooksAdapter 用于显示书籍列表</p> <pre> <code class="language-java">public class BooksAdapter extends BaseAdapter { private List<Book> mBookList; private Context mContext; private LayoutInflater inflater; public BooksAdapter(Context context, List<Book> bookList) { inflater = LayoutInflater.from(context); mBookList = bookList; mContext = context; } @Override public int getCount() { return mBookList.isEmpty() ? 0 : mBookList.size(); } @Override public Object getItem(int position) { return position; } @Override public long getItemId(int position) { return mBookList.get(position).getBook_id(); } @Override public View getView(int position, View convertView, ViewGroup parent) { BookViewHolder bookViewHolder; if (convertView == null) { convertView = inflater.inflate(R.layout.book_item, parent, false); bookViewHolder = new BookViewHolder(); bookViewHolder.itemBookName = (TextView) convertView.findViewById(R.id.item_book_name); bookViewHolder.itemBookAuthor = (TextView) convertView.findViewById(R.id.item_book_author); bookViewHolder.itemBookTag = (TextView) convertView.findViewById(R.id.item_book_tag); convertView.setTag(bookViewHolder); } else { bookViewHolder = (BookViewHolder) convertView.getTag(); } bookViewHolder.itemBookName.setText(mBookList.get(position).getBook_name()); bookViewHolder.itemBookAuthor.setText(mBookList.get(position).getBook_author()); bookViewHolder.itemBookTag.setText(mBookList.get(position).getBook_tag()); return convertView; } public class BookViewHolder { private TextView itemBookName; private TextView itemBookAuthor; private TextView itemBookTag; } }</code></pre> <p>到这里,View 所需要的工作我们都已经实现了,下面我们来看 Presenter</p> <p>Presenter</p> <p>上面讲过,Presenter 是 View 跟 Model 之间的桥梁,那它要做的工作是什么,需要有哪些方法呢?</p> <p>这里主要看功能有什么操作,比如,上面的运行图是通过按钮去显示书籍列表,那么这里我们需要一个方法用于 Presenter 去跟 Model 拿数据</p> <pre> <code class="language-java">interface Presenter extends BasePresenter{ void loadBooks(); }</code></pre> <pre> <code class="language-java">public class BooksPresenter implements BooksContract.Presenter { private BooksRepository mBooksRepository; private BooksContract.View mBookView; public BooksPresenter(@NonNull BooksRepository booksRepository, @NonNull BooksContract.View bookView) { mBooksRepository = booksRepository; mBookView = bookView; mBookView.setPresenter(this); } @Override public void start() { } @Override public void loadBooks() { mBooksRepository.getBooks(new BooksDataSource.LoadBooksCallback() { @Override public void loadBooks(List<Book> bookList) { mBookView.showBookList(bookList); } @Override public void dataNotAvailable() { mBookView.showNoBooks(); } }); } }</code></pre> <p>Presenter 要完成二者之间的交互,必须实现它们,从上面的代码看,得到按钮点击的通知,也就是 loadBooks() 方法,去跟 Model 拿数据,交由 BooksRepository 去处理数据的业务逻辑,最后通过 mBookView 去通知,View 进行对应的视图显示,交互。</p> <p>源码的解析到这一步,就比较清晰了,更多 MVP 相关的 demo 可以去看官方的 demo</p> <p> </p> <p>来自:https://juejin.im/post/590065a0570c350058fb8d4a</p> <p> </p>