谈谈 Android MVP 架构 | 掘金技术征文

2,539 阅读6分钟

前言:本文所写的是博主的个人见解,如有错误或者不恰当之处,欢迎私信博主,加以改正!原文链接demo链接

MVP 架构简介

说起 MVP 架构,相信很多朋友都看过,网上也有很多这方面的资料。博主使用 MVP 架构搭建项目也有一段时间了。简单谈一谈心得。说到 MVP 架构,很多人都拿它跟 MVC 去对比。这里我就不过多重复说了,单刀直入。

什么是 MVP 架构

MVP 架构由 Model(模型)、View(视图)、Presenter(主持者)构成,下面我们一起来了解它们:

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 的目录结构(看起来比较复杂,勿怪),实现模拟网络获取图书数据并将其显示的功能

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

创建一个接口,用于获取回调实体类 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