谈谈 Android MVP 架构

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

MVP 架构简介

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

什么是 MVP 架构

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


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

MVP Demo 目录图
MVP Demo 目录图

下面我们来看项目实现效果,功能比较简单,gif图就不弄了


demo 运行图1
demo 运行图1

demo 运行图2
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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,568评论 25 707
  • 转载至:http://www.jianshu.com/p/9a6845b26856 “Android MVP 详解...
    SnowDragonYY阅读 10,298评论 5 241
  • 作者:李旺成 时间:2016年4月3日 “Android MVP 详解(下)”已经发布,欢迎大家提建议。 MVP ...
    diygreen阅读 128,662评论 86 1,321
  • 2016-7-25 周一 圣杯Ⅴ 昨晚的梦,依然清晰可见。是自己回到原单位工作的场景。发现自己仍然待在过去的...
    艾妈潘潘阅读 369评论 0 1