Skip to content

MVP&MVVM: 让Pan按照你喜欢的模型工作

Pan的模型是MV*,意味着,只要达到了Pan的初心:分离Activity的代码,Pan可以被用来以任何你喜欢的模型来编写代码。例如,你可以把Pan当做一个MVP框架来使用,也可以当做一个MVVM的框架来使用,只需要很少、甚至不需要代码改动。

Pan当做MVP框架(复刻Mosby)

ViewModel本身理解为MVP中的View。ViewModel的render方法相当于全量刷新界面,而更局部刷新可以在子类提供诸如renderUser等方法。而Controller则可以类比到Presenter。因此,符合MVP框架的代码可以直接迁移成Pan框架的代码,你会发现是等价的。不仅如此,Pan框架由于强制代码分离,会使得原先仍然有很多代码的Activity,变的只需要寥寥几行。

例如,我们可以直接迁移最近比较火的MVP框架Mosby的Getting Started,你可以类似的写出下面的代码:

首先是View接口,定义View的渲染行为

// View interface
public interface HelloWorldView {

  // displays "Hello" greeting text in red text color
  void showHello(String greetingText);

  // displays "Goodbye" greeting text in blue text color
  void showGoodbye(String greetingText);
}

然后是View接口的实现。Mosby是把实现交给了Activity,而在Pan中,对应的渲染逻辑实现则是ViewModel,从而使得Activity没有更多的代码。 当然Mosby也是支持不交给Activity的,用别的实现类;而Pan则是强制分离。

public class HelloWorldViewImpl extends GeneralViewModel implements HelloWorldView{

    @BindView(R.id.greetingTextView) TextView vGreetingTextView;
    @BindView(R.id.helloButton) Button vHelloBtn;

    @Override void showHello(String greetingText){
        greetingTextView.setTextColor(Color.RED);
        greetingTextView.setText(greetingText);
    }

    @Override public void showGoodbye(String greetingText){
        greetingTextView.setTextColor(Color.BLUE);
        greetingTextView.setText(greetingText);
    }   

    @Override public void render(){
        //It is more clear that put 'greetingText' and 'isHelloOrGoodbye' as a member of ViewModel in Pan's philosophy,
        //and render method will use current state to choose show which color.

        //But anyway, this is the example for Mosby.
    }
}

接下来是Presenter,实际上Mosby的Presenter处理的是将异步请求的结果绑定到View的渲染方法上的作用,而这个作用在Pan中由Controller实现。这里可以直接将Mosby的Presenter的代码拷贝过来。 Mosby的点击事件绑定也交给了Activity,如果迁移到Pan,就需要放到Controller的bindEvents方法中来。你可以在bindEvents中使用Butterknife,这样就更像Mosby的做法。

public class HellowWorldPresenter extends GeneralController<HelloWorldViewImpl>{

    // Greeting Task is "business logic"
    private GreetingGeneratorTask greetingTask;

    private void cancelGreetingTaskIfRunning(){
        if (greetingTask != null){
            greetingTask.cancel(true);
        }
    }

    public void greetHello(){
        cancelGreetingTaskIfRunning();

        greetingTask = new GreetingGeneratorTask("Hello", new GreetingTaskListener(){
            public void onGreetingGenerated(String greetingText){
                if (isViewAttached())
                getView().showHello(greetingText);
            }
        });
        greetingTask.execute(); 
    }

    @Override public void bindEvents(){
        $vm.vHelloBtn.setOnClickListener(v -> {
            greetHello();
        });
    }

    //...more logic can be same way ported.
}

最后,在Activity中调用工厂方法。

Pan.with(this, HelloWorldViewImpl.class)
    .controlledBy(HelloWorldPresenter.class)
    .getViewModel();

Pan当做MVVM

当然,你也可以完全可以把Pan改造成一个MVVM框架,只需很少的代码。如果你阅读过AutoRenderViewModel的介绍,会发现这个类的用法已经在向MVVM上靠拢了。

MVVM的核心在于VM-V双向绑定。所以我们先搞定VM->V,意味着我们要限定ViewModel中使用的数据对象:

public interface ViewModelData{
    void notifyRender();
    void addViewModel(ViewModel viewModel)
}

public abstract class ViewModelDataBase implements ViewModelData{
    Set<ViewModel> mViewModels = Collections.newSetFromMap(new WeakHashMap<ViewModel, Boolean>());

    public void notifyRender(){
        for(ViewModel vm: mViewModels){
            if(vm != null) vm.render();
        }
    }

    public void addViewModel(ViewModel viewModel){
        mViewModels.add(new WeakReference(viewModel));
    }
}

上面的代码提供了一个数据类型的基类,他在合适的时机通知ViewModel去刷新界面,一个可能的实现类如下:

public class WeiboData extends ViewModelDataBase{
    String weiboText;

    public void setWeiboText(String weiboText){
        this.weiboText = weiboText;
        notifyRender();
    }
    //...
}

接下来编写一个ViewModel的基类,ViewModel需要至少提供单向的自动刷新,所以会有如下代码:

public abstract class TwoWayBindingViewModel<D extends ViewModelDataBase> extends GeneralViewModel{

    protected D mData;

    public TwoWayBindingViewModel setData(D data){
        mData = data;
        if(mData != null){
            mData.addViewModel(this);
        }
        return this;
    }

    public TwoWayBindingViewModel setDataAndRender(D data){
        setData(data);
        render();
        return this;
    }

}

在具体的场景中,直接使用TwoWayBindingViewModel作为基类,并传入ViewModelDataBase的子类作为数据,就可以实现VM->V自动刷新功能。

接下来,我们通过Controller来实现V->VM的绑定。首先需要一个注解标记绑定的field。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Binding{

}

public class WeiboViewModel extends TwoWayBindingViewModel<Weibo>{

    @Binding("weiboText")
    protected EditText vEditText;

}

接下来,是Controller的具体实现,为了避免反射代码影响阅读性,这里用伪代码编写:

public abstract class TwoWayBindingController<T extends TwoWayBindingViewModel> extends GeneralController<T>{

    protected void bindEvents(){
        super.bindEvents();

        for each field in $vm.getClass() {
            if(f.getName().startsWith("v")){
                bindField(f);
            }
        }
    }

    protected void bindField(Field f){

        final Method setter = find the setter binded, e.g. setWeiboText;

        switch f.getType():
            case EditText.class:
                 f.get($vm).addTextChangeListener(new TextWatcher(){
                        public void afterTextChanged(Editable s){

                            setter.invoke(mData, s.toString());
                        }
                    })
                 break;
            case ... //other controls
    }

}

这里用的是反射,考虑到一个界面上一般只需要绑定一次,以及需要绑定的控件其实有限,这样做不会对性能有太大的影响。当然,用Annotation Processing可以实现相同的效果,只是编写/测试起来相对麻烦。

Pan融合Android官方的Data-Binding

如果你对上面自己实现MVVM的方案不满意,官方的Data-Binding是避免了反射,而且可以直接使用。Pan也同样支持,接入后让使用DataBinding的也更加舒适,虽然这样做并不被推荐。可以直接参考集成Data-binding