探究MVVM

原文链接

MVVM模式的意思是Model-View-ViewModel,这是另一种可以将View层与逻辑层代码分开的软件开发模式,尤其在Android上,可以将Android代码和其他代码分开。
Model的职责是业务逻辑和数据的实体模型,应该做到易于替换,易于测试,并且独立于用户界面。
ViewModel作为一个中间人,职责是将数据和请求从Model层转换成可以成View层可以直接显示的东西。
View层的职责就是布局文件。

其实就是MVP中的Presenter 变成了ViewModel

相对于MVP来说,其实就是Presenter变成了ViewModel,使用观察者提供给View层异步的结果。
以下是两种实现MVVM的主要方式:

  1. 使用RxJava Observables
  2. 使用Android Data Binding

我更喜欢第一种。这里我会尽量仔细的解释如何使用RxJava Observables实现MVVM。

##初始代码
下面的Activity包含了:
调用了Google Books API
将数据展示在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();
    }
}

这是一个典型的里面实现了很多东西的MainActivity。

MainActivity里面东西太多了

我们可以注意到:

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

实现MVVM的目标:

  1. 将Retrofit的回调切换成RxJava的Observable。
  2. 将Retrofit的逻辑代码移到Model里,这样子MainAcitivity可以尽量的少去实现接口和将连接API的职责交给Model就可以了。
  3. 把View 订阅给ViewModel的结果。

##切换RxJava
在你将MainActivity里的逻辑迁移出去之前,你需要先将Retrofit切换成RxJava。这个步骤要求你先在你的项目里引入RxJava的库。

不是所有人都喜欢RxJava,但是我喜欢

要知道RxJava被很多人定义为“怪物”,因为他是一个巨大的库(超过5k的方法数量),你的leader可能会不高兴。相反,MVP不需要你添加更多的library到你的项目里。
添加下面这段到你的build.gradle里:

//In app/build.gradle
compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0'
compile 'io.reactivex:rxandroid:1.2.1'
compile 'io.reactivex:rxjava:1.2.1'

现在是时候迁移逻辑了。在Retrofit的服务层将GoogleBooksService 改成Observable:

Call<BookSearchResult> search(@Query("q") String search);
// change it to
Observable<BookSearchResult> search(@Query("q") String search);

注意:我们引用的是 rx.Observable而不是java.util.Observable。
现在我们就要使用Observable来改写MainActivity了。
旧代码:

service.search("search+" + formatUserInput)
        .enqueue(new Callback<BookSearchResult>() {

            @Override
            public void onResponse(Call<BookSearchResult> call,
                           Response<BookSearchResult> books) {
                updateUi(books.body().getBooks());
            }

            @Override
            public void onFailure(Call<BookSearchResult> call, 
                                  Throwable t) {
                t.printStackTrace();
            }
        });

改变成

subscription = service.search("search+" + formatUserInput)
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Observer<BookSearchResult>() {
            @Override
            public void onCompleted() {
                
            }

            @Override
            public void onError(Throwable e) {
                e.printStackTrace();
            }

            @Override
            public void onNext(BookSearchResult bookSearchResult) {
                updateUi(bookSearchResult.getBooks());
            }
        });

这段代码看上去类似回调函数,但是是不同的:

  • 搜索(字符串)返回一个会返回结果的Observable。
  • 你需要“订阅”Observable,所以你将会收到一个包括BookSearchResult的“下一步”事件。
  • 如果出现错误,您将收到一个“错误”事件。
  • 指定Rxjava我们想要使用的IO线程执行网络呼叫subscribeon(不是主线程)。
  • 指定要处理Rxjava Android的主线程接收我们的事件observeon(UI)。
  • 保持“订阅”的地方,因为你会想退订离开Activity时。

这是一个观察者模式:当你做的时候订阅退订的事件。

##创建Model
我们的模型将是一个java的接口,可以访问Google Books API提供了一种方法。
首先创建一个接口,将为我们提供数据。然后创建一个该接口的实现。将所有的改造代码都移动到它。他界面是这里的重要组成部分。它只包含模型中可用的方法的定义,并且应尽可能保持简单。

public interface BooksInteractor {
    Observable<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())
                // Retrofit Call to RxJava Observable
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .build();
        // Create the Google Book API Service
        service = retrofit.create(GoogleBooksService.class);
    }

    @Override
    public Observable<BookSearchResult> search(String search) {
        return service.search("search+" + search).subscribeOn(Schedulers.io());
    }
}

重点:

  • 将重构配置代码移入构造函数。
  • 搜索方法返回一个可观察booksearchresults。
  • 我把“搜索+”部分的交互实现。
  • Observable已经订阅了他。
  • 指定调用适配器工厂能够使用Rxjava。

##创建ViewModel
现在讲的就是跟MVP最大的区别了,我们的ViewModel非常简单,它只做一件事情,确保我们使用的是正确的线程。
如果格式化的数据,这是做它的地方,你可以用Rxjava map的方法转化到另一个物体,还有一个巨大的名单Rxjava运营商可以使用所有可能的行动。
BooksViewModel的代码:

public class BooksViewModel {

    private BooksInteractor interactor;
    private Scheduler scheduler;

    public BooksViewModel(BooksInteractor interactor, Scheduler scheduler) {
        this.interactor = interactor;
        this.scheduler = scheduler;
    }

    public Observable<BookSearchResult> search(String search) {
        return interactor.search(search).observeOn(scheduler);
    }
}

这个问题需要两个参数:关联和主线程调度器。因为AndroidSchedulers是一个Android的依赖,我决定搬出来,将它作为参数传递。
“只是想关联的搜索方法,确保项目在正确的线程观察。

##更新View

public class MainActivity extends AppCompatActivity {

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

    private CompositeSubscription subscriptions = new CompositeSubscription();
    private BooksViewModel booksViewModel;

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

        booksViewModel = new BooksViewModel(new BooksInteractorImpl(), 
                                            AndroidSchedulers.mainThread());

        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();
            }
        });
    }
  
    @Override
    protected void onDestroy() {
        subscriptions.unsubscribe();
        super.onDestroy();
    }

    private void performSearch() {
        String formatUserInput = getUserInput().trim().replaceAll("\\s+", "+");
        subscription = booksViewModel.search(formatUserInput)
                .subscribe(new Observer<BookSearchResult>() {
                    @Override
                    public void onCompleted() {

                    }

                    @Override
                    public void onError(Throwable e) {
                        e.printStackTrace();
                    }

                    @Override
                    public void onNext(BookSearchResult bookSearchResult) {
                        updateUi(bookSearchResult.getBooks());
                    }
                });
    }

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

重要的几点:

  • 使用compositesubscription保持所有的订阅,并在onDestroy方法的时候调用unsubscribe()。
  • subscribeon和observeon方法现在是Model和ViewModel的一部分。
  • ViewModel是在OnCreate初始化(就像MVP的Presenter)。
  • 通过androidschedulers,mainthread()将作为ViewModel的一个依赖。

##so哪里更优秀了呢?
在原有的MainActivity里没有太多的业务逻辑,因此很难很快看到更好的了。
其中一个最大的改进就是测试。如果我们要考验我们的用户提供自己的图书列表,我们只需要创建一个booksinteractor:

public class BooksInteractorMock implements BooksInteractor {
    @Override
    public Observable<BookSearchResult> search(String search) {
        return Observable.just(getMockedBookSearchResult());
    }

    private BookSearchResult getMockedBookSearchResult() {
        BookSearchResult bookSearchResult = new BookSearchResult();
//        bookSearchResult.setBooks(myListOfBooks);
        return bookSearchResult;
    }
}

与Observable的just()方法相比,我们可以创建一个可将自己的图书列表发出我们自己的booksearchresult。
所以在MainActivity我们改变它使用模拟:

booksViewModel = new BooksViewModel(new BooksInteractorMock(), AndroidSchedulers.mainThread());

更MVP相比,类跟接口变少了

对MVP的最大改进是,我们不需要一个视图界面了。“不知道看什么。这使得代码小于MVP和尺度更好当你有多个视图和ViewModels,频繁的在别人,以及,如果视图是无效的或不在主持人你不需要检查。
其次,但不是那么重要,你可以有多个视图模型视图。有时问题与MVP这帽子的主持人成为神的对象太多的责任,与MVVM可以有多个视图模型,每一个执行你的UX的一部分。
对MVP的最大缺点,除了要求rxjava,是MainActivity仍然持有一些职责:处理订阅和错误。
在我看来,MVP或MVVM之间的选择归结为这些不同的因素:

  • 重构为MVP模式使遗留代码更容易
  • MVP不需要RxJava
  • MVVM能更好的服务于更大的项目
  • 你可以有多个ViewModel服务于Model,而在MVP的关系是一对一
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值