談談 Android MVP 架構
MVP 架構簡介
說起 MVP 架構,相信很多朋友都看過,網上也有很多這方面的資料。博主使用 MVP 架構搭建項目也有一段時間了。簡單談一談心得。說到 MVP 架構,很多人都拿它跟 MVC 去對比。這里我就不過多重復說了,單刀直入。
什么是 MVP 架構
MVP 架構由 Model(模型)、View(視圖)、Presenter(主持者)構成,下面我們一起來了解它們:
MVP 架構圖
- Model 負責業務邏輯以及數據的處理,主要通過接口實現
- View 負責 UI 顯示以及與用戶之間的交互
- Presenter 起到一個銜接橋梁的作用,負責 Model 跟 View 之間的交互
MVP 架構的利弊
優點
-
耦合度低
View 跟 Model 之間由 Presenter 負責兩者之間的交互,低了其耦合度,使其更加關注自身邏輯,結構清晰
-
可維護性高
每個 View 都有其對應的 Presenter,容易進行區分,哪個模塊出現了問題,或者接口出現了問題,可以迅速的確定。模型與視圖之間完全分離,修改視圖不影響模型
-
方便單元測試
因其業務邏輯都在 Presenter 里,進行單元測試的時候,可以直接寫個測試接口,由 Presenter 去繼承
缺點
-
類數量暴漲
每個 View 都有 Presenter ,跟其對應的接口,類的數量會明顯變多,在某些場景下 Presenter 的復用會產生接口冗余。
-
額外的學習曲線
需要花費額外的時間去學習,學習理解成本高,開始編寫代碼之前需要時間成本(項目的架構)
實戰演練
前面講述了一堆的理論知識,下面一步步解剖 MVP 架構,下圖是 Demo 的目錄結構(看起來比較復雜,勿怪),實現模擬網絡獲取圖書數據并將其顯示的功能
MVP Demo 目錄圖
下面我們來看項目實現效果,功能比較簡單,gif圖就不弄了
demo 運行圖1
demo 運行圖2
Model
創建實體類 Book
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>
創建一個接口,用于獲取回調實體類 Book 攜帶的數據
public interface BooksDataSource {
interface LoadBooksCallback{
void loadBooks(List<Book> bookList);
void dataNotAvailable();
}
void getBooks(@NonNull LoadBooksCallback loadBooksCallback);
}</code></pre>
繼承 BooksDataSource 接口,實現模擬數據獲取
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>
數據回調業務處理,網絡數據和本地數據(這里僅模擬本地數據)
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>
到這里,Model的任務算是結束了,得到了所需要的數據源。
View
Presenter 與 View 是通過接口進行交互,所以這里可以定義一個接口,用于進行交互,此處的難點在于,要清楚需要哪些方法。
這里建立兩個Base基類,用于初始化
public interface BaseView<T> {
void setPresenter(T presenter);
}
public interface BasePresenter {
void start();
}
從上面的demo運行演示圖看,這里 View 的工作主要有2個,一個是顯示無數據時的狀態,第二個是顯示書籍的列表
void showBookList(List<Book> bookList);
void showNoBooks();
本demo演示的是本地模擬數據,關于網絡數據方面,可以模擬開啟一個線程,通過 Thread.sleep( long ) 充當耗時操作,使用 ProgressBar,給用戶一個友好提示,同時需要在 View 的接口中定義相關方法
小結:在 View 的方法定義上,需要觀察功能上的操作,接著考慮:
- 該操作需要做什么?( loadBooks )
- 操作后的結果反饋?(showBookList,showNoBooks)
- 操作中的友好交互?(顯示正在加載,加載完成)
經過上面的思考后,接下來就是 View 的實現,也就是 Activity ,MVP 中的 View 主要對應的是 Activity
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>
從上面的代碼看 Activity 實現還是比較簡單的,接口引導我們去實現對應的功能,下面是 BooksAdapter 用于顯示書籍列表
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>
到這里,View 所需要的工作我們都已經實現了,下面我們來看 Presenter
Presenter
上面講過,Presenter 是 View 跟 Model 之間的橋梁,那它要做的工作是什么,需要有哪些方法呢?
這里主要看功能有什么操作,比如,上面的運行圖是通過按鈕去顯示書籍列表,那么這里我們需要一個方法用于 Presenter 去跟 Model 拿數據
interface Presenter extends BasePresenter{
void loadBooks();
}
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>
Presenter 要完成二者之間的交互,必須實現它們,從上面的代碼看,得到按鈕點擊的通知,也就是 loadBooks() 方法,去跟 Model 拿數據,交由 BooksRepository 去處理數據的業務邏輯,最后通過 mBookView 去通知,View 進行對應的視圖顯示,交互。
源碼的解析到這一步,就比較清晰了,更多 MVP 相關的 demo 可以去看官方的 demo
來自:https://juejin.im/post/590065a0570c350058fb8d4a