談談 Android MVP 架構 | 掘金技術徵文

PassersHowe發表於2017-04-26

前言:本文所寫的是博主的個人見解,如有錯誤或者不恰當之處,歡迎私信博主,加以改正!原文連結demo連結

MVP 架構簡介

說起 MVP 架構,相信很多朋友都看過,網上也有很多這方面的資料。博主使用 MVP 架構搭建專案也有一段時間了。簡單談一談心得。說到 MVP 架構,很多人都拿它跟 MVC 去對比。這裡我就不過多重複說了,單刀直入。

什麼是 MVP 架構

MVP 架構由 Model(模型)、View(檢視)、Presenter(主持者)構成,下面我們一起來了解它們:

談談 Android MVP 架構 | 掘金技術徵文
MVP 架構圖

  • Model 負責業務邏輯以及資料的處理,主要通過介面實現
  • View 負責 UI 顯示以及與使用者之間的互動
  • Presenter 起到一個銜接橋樑的作用,負責 Model 跟 View 之間的互動

MVP 架構的利弊

優點

  1. 耦合度低
    View 跟 Model 之間由 Presenter 負責兩者之間的互動,低了其耦合度,使其更加關注自身邏輯,結構清晰

  2. 可維護性高
    每個 View 都有其對應的 Presenter,容易進行區分,哪個模組出現了問題,或者介面出現了問題,可以迅速的確定。模型與檢視之間完全分離,修改檢視不影響模型

  3. 方便單元測試
    因其業務邏輯都在 Presenter 裡,進行單元測試的時候,可以直接寫個測試介面,由 Presenter 去繼承

缺點

  1. 類數量暴漲
    每個 View 都有 Presenter ,跟其對應的介面,類的數量會明顯變多,在某些場景下 Presenter 的複用會產生介面冗餘。

  2. 額外的學習曲線
    需要花費額外的時間去學習,學習理解成本高,開始編寫程式碼之前需要時間成本(專案的架構)

實戰演練

前面講述了一堆的理論知識,下面一步步解剖 MVP 架構,下圖是 Demo 的目錄結構(看起來比較複雜,勿怪),實現模擬網路獲取圖書資料並將其顯示的功能

談談 Android MVP 架構 | 掘金技術徵文
MVP Demo 目錄圖

下面我們來看專案實現效果,功能比較簡單,gif圖就不弄了
談談 Android MVP 架構 | 掘金技術徵文
demo 執行圖1
談談 Android MVP 架構 | 掘金技術徵文
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;
    }
}複製程式碼

建立一個介面,用於獲取回撥實體類 Book 攜帶的資料

public interface BooksDataSource {

    interface LoadBooksCallback{
        void loadBooks(List<Book> bookList);
        void dataNotAvailable();
    }

    void getBooks(@NonNull LoadBooksCallback loadBooksCallback);
}複製程式碼

繼承 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);

    }
}複製程式碼

資料回撥業務處理,網路資料和本地資料(這裡僅模擬本地資料)

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();
            }
        });
    }

}複製程式碼

到這裡,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);

    }
}複製程式碼

從上面的程式碼看 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;
    }
}複製程式碼

到這裡,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();
            }
        });
    }

}複製程式碼

Presenter 要完成二者之間的互動,必須實現它們,從上面的程式碼看,得到按鈕點選的通知,也就是loadBooks()方法,去跟 Model 拿資料,交由 BooksRepository 去處理資料的業務邏輯,最後通過 mBookView 去通知,View 進行對應的檢視顯示,互動。

原始碼的解析到這一步,就比較清晰了,更多 MVP 相關的 demo 可以去看官方的 demo

相關文章