探究MVP

原文链接

这是一篇写给那些听过MVP但是不是很清楚怎么去选择正确的使用方法的朋友。我们将从一个实现了API接口的MainActivity开始,将MainActivity上的逻辑迁移到MVP模式中。

免责申明:MVP模式不仅仅是一个层次性很优秀的模式

正如很多人说的,MVP不仅仅是一个层次性很优秀的模式:他只是一种分层次去写代码的方式,MVP很优秀,所以不管是大型APP还是小的一个demo,我们都可以使用并且很容易上手。但是,如果你想要对整个APP的代码进行规划,根据功能模块来写代码,而不是根据MVP那种定死的框架来写代码的话,那就一起往下看吧。

##准备##

下面的Activity包含了:

  1. 调用了Google Books API
  2. 将数据展示在ListView上,用Adapter的形式
public class MainActivity extends AppCompatActivity {

    EditText editText;
    ImageButton imageButton;
    BooksAdapter adapter;
    ListView listView;
    TextView textNoDataFound;
    GoogleBooksService service;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Configure Retrofit
        Retrofit retrofit = new Retrofit.Builder()
                // Base URL can change for endpoints (dev, staging, live..)
                .baseUrl("https://www.googleapis.com")
                // Takes care of converting the JSON response into java objects
                .addConverterFactory(GsonConverterFactory.create())
                .build();
        // Create the Google Book API Service
        service = retrofit.create(GoogleBooksService.class);


        editText = (EditText) findViewById(R.id.editText);
        imageButton = (ImageButton) findViewById(R.id.imageButton);
        textNoDataFound = (TextView) findViewById(R.id.text_no_data_found);

        adapter = new BooksAdapter(this, -1);

        listView = (ListView) findViewById(R.id.listView);
        listView.setAdapter(adapter);

        imageButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                performSearch();
            }
        });
    }

    private void performSearch() {
        String formatUserInput = getUserInput().trim().replaceAll("\\s+", "+");
        // Just call the method on the GoogleBooksService
        service.search("search+" + formatUserInput)
                // enqueue runs the request on a separate thread
                .enqueue(new Callback<BookSearchResult>() {

                    // We receive a Response with the content we expect already parsed
                    @Override
                    public void onResponse(Call<BookSearchResult> call, Response<BookSearchResult> books) {
                        updateUi(books.body().getBooks());
                    }

                    // In case of error, this method gets called
                    @Override
                    public void onFailure(Call<BookSearchResult> call, Throwable t) {
                        t.printStackTrace();
                    }
                });
    }

    private void updateUi(List<Book> books) {
        if (books.isEmpty()) {
            // if no books found, show a message
            textNoDataFound.setVisibility(View.VISIBLE);
        } else {
            textNoDataFound.setVisibility(View.GONE);
        }
        adapter.clear();
        adapter.addAll(books);
    }

    private String getUserInput() {
        return editText.getText().toString();
    }
}

这是一个典型的,在Activity里面实现了所有东西而不是根据MVP来写的Activity类。

MainActivity知道太多接口实现逻辑

我们需要注意到以下几点:

  1. MainActivity知道太多接口实现的东西。ps:在这里我们网络请求使用的是Retrofit,用一个回调来获取数据的。
  2. 这种写法违背了单一职责原理:MainActivity不仅负责获取数据,还要负责展示数据。
  3. 测试不同的Google Book数据将会意味着你需要修改MainActivity的代码,注释掉不需要没用的代码等。如果把它搞成一个对象那就会方便很多了。

我们的目标是在这个Activity里实现MVP,先说下思路:

  • 将Retrofit代码移到Model里面,这样子MainAcitivity可以尽量的少去实现接口和将连接API的职责交给Model就可以了。
  • 将performSearch方法移动到Presenter里面,这样就可以把处理响应的责任交给Presenter了。
  • 在Presenter里调updateUi,这样更新UI的责任就交给了Presenter而不是MainActivity了。

##创建Model##
我们的Model是个接口,提供了一个方法去获取Google Books API。
首先,创建一个能提供给我们数据的接口,然后去实现它,具体实现的方法就是上面的使用Retrofit的方法。

public interface BooksInteractor {
    Call<BookSearchResult> search(String search);
}
public class BooksInteractorImpl implements BooksInteractor {

    private GoogleBooksService service;

    public BooksInteractorImpl() {
        // Configure Retrofit
        Retrofit retrofit = new Retrofit.Builder()
                // Base URL can change for endpoints (dev, staging, live..)
                .baseUrl("https://www.googleapis.com")
                // Takes care of converting the JSON response into java objects
                .addConverterFactory(GsonConverterFactory.create())
                .build();
        // Create the Google Book API Service
        service = retrofit.create(GoogleBooksService.class);
    }

    @Override
    public Call<BookSearchResult> search(String search) {
        return service.search("search+" + search);
    }
}

现在我们就有Model了,上面的BooksInteractorImpl就是我们的Model。
这里我把Retrofit的配置也放到Model里了。其实最合适的方法是使用Dagger注入进去。(没了解过的同学可以看这里
现在我们就需要在MainActivity的onCreate方法里创建我们的BookInteractor 对象了。

//...
BooksInteractor interactor;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    interactor = new BooksInteractorImpl();
    //...

##创建Presenter
根据上面的分析,Presenter需要以下三样东西:

  1. Model
  2. View
  3. performSearch方法

首先创建Presenter类:

public class BooksPresenter {
}

添加一个Model的引用和一个构造方法。

public class BooksPresenter {
    private BooksInteractor interactor;
    public BooksPresenter(BooksInteractor interactor) {
        this.interactor = interactor;
    }
}

现在我们需要一个View的引用,和方法去绑定和取消绑定。

public class BooksPresenter {
    BooksView view;
    private BooksInteractor interactor;

    public BooksPresenter(BooksInteractor interactor) {
        this.interactor = interactor;
    }

    public void bind(BooksView view) {
        this.view = view;
    }

    public void unbind() {
        view = null;
    }
}

绑定和解除绑定View是MVP模式的一个关键点,因为View是Android类的关键部分。
有些开发者喜欢在onCreate方法里绑定,在onDestroy方法里取消绑定,有些开发者则喜欢分别在onResume和onPause方法里绑定和取消绑定,这取决于你的使用场景。但是一般都是在最后的时候去取消绑定的。

记得remove View的引用(设置为空)

当然也有一些开发者使用不同的命名约定,比如attach/detach或者set/unset等。
最后将performSearch方法移动到Presenter里面。

public class BooksPresenter {
    BooksView view;
    private BooksInteractor interactor;

    public BooksPresenter(BooksInteractor interactor) {
        this.interactor = interactor;
    }

    public void bind(BooksView view) {
        this.view = view;
    }

    public void unbind() {
        view = null;
    }

    public void performSearch(String userInput) {
        String formatUserInput = userInput.trim().replaceAll("\\s+", "+");
        // Just call the method on the GoogleBooksService
        interactor.search("search+" + formatUserInput)
                // enqueue runs the request on a separate thread
                .enqueue(new Callback<BookSearchResult>() {

                    // We receive a Response with the content we expect already parsed
                    @Override
                    public void onResponse(Call<BookSearchResult> call,
                                           Response<BookSearchResult> books) {
                        if (view != null)
                            view.updateUi(books.body().getBooks());
                    }

                    // In case of error, this method gets called
                    @Override
                    public void onFailure(Call<BookSearchResult> call, Throwable t) {
                        t.printStackTrace();
                    }
                });
    }
}

我们需要在performSearch方法里改变几个地方:

  1. 首先我添加了一个用户输入的参数,而不是使用getUserInput。
  2. 我在onResponse回调里也做了些改变,updateUi由BooksView实施,但是因为View层是分开的,所以我们要去判断下是否不为空。

注意判断Presenter里的View是否为空

##创建View
我们移动了很多代码到了Model和Presenter里面,所以我们现在的MainActivity就很清晰了,但是还没实现View层的交接。
BooksView是一个包含了updateUi方法的小接口,所以我们需要让MainActivity去实现BooksView接口。


public interface BooksView {
    void updateUi(List<Book> books);
}
public class MainActivity extends AppCompatActivity implements BooksView {

现在updateUi方法就在MainActivity里被重写了:

@Override
public void updateUi(List<Book> books) {

为了让所有东西都连接起来,我们就需要在onCreate方法里创建Presenter对象,并且绑定View层。

BooksInteractor interactor = new BooksInteractorImpl();
presenter = new BooksPresenter(interactor);
presenter.bind(this);

现在imageButton的点击事件就是调用presenter的performSearch方法。

imageButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        presenter.performSearch(getUserInput());
    }
});

附上MainActivity的全部代码:

public class MainActivity extends AppCompatActivity implements BooksView {

    EditText editText;
    ImageButton imageButton;
    BooksAdapter adapter;
    ListView listView;
    TextView textNoDataFound;
    private BooksPresenter presenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);


        editText = (EditText) findViewById(R.id.editText);
        imageButton = (ImageButton) findViewById(R.id.imageButton);
        textNoDataFound = (TextView) findViewById(R.id.text_no_data_found);

        adapter = new BooksAdapter(this, -1);

        listView = (ListView) findViewById(R.id.listView);
        listView.setAdapter(adapter);

        imageButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                presenter.performSearch(getUserInput());
            }
        });
        
        // Note: Don't do this on production code, use Dependency Injection instead
        // to provide the BooksInteractor and the BooksPresenter to the View
        // Learn how to use Dagger 2 here: 
        // https://medium.com/@Miqubel/understanding-dagger-2-367ff1bd184f#.s2jza32df
        BooksInteractor interactor = new BooksInteractorImpl();
        presenter = new BooksPresenter(interactor);
        presenter.bind(this);
    }
        
    @Override
    protected void onDestroy() {
        presenter.unbind();
        super.onDestroy();
    }

    @Override
    public void updateUi(List<Book> books) {
        if (books.isEmpty()) {
            // if no books found, show a message
            textNoDataFound.setVisibility(View.VISIBLE);
        } else {
            textNoDataFound.setVisibility(View.GONE);
        }
        adapter.clear();
        adapter.addAll(books);
    }

    private String getUserInput() {
        return editText.getText().toString();
    }
}

不要在View层写代码的时候将Model和Presenter实例化,最好使用依赖注入的方式提供Presenter和Model

##总结

  1. MainActivity不关心Model里逻辑是怎么实现的,它不知道是使用Retrofit还是其他的。我们可以通过改变Presenter里的方法列表来使测试更加方便,各个方法模块之间也很独立
  2. MainActivity也不关心Presenter的结果是成功还是错误的
  3. 回调回来的逻辑放到Presenter里。如果有一天,你想把Retrofit的回调方法都改成用RxJava或者其他方式来实现,你根本不用动View层。
  4. 你现在可以对Model和Presenter进行分开测试了
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。
1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值