给传统MVP/MVVM加上状态管理

1,157 阅读5分钟

先上个演示图

库地址:github.com/landscapesi…

首先问题是什么

传统MVP或者MVVM架构对于页面状态的管理处于散乱状态,用代码来表示:

    // 状态在view层里面
    
    class TestActivity:Activity(),TestView{
        
        var name:String = ""
        var phone:String = ""
        
        override fun setName(name:String){
            tv_name.text = name
        }
        
        override fun setPhone(phone:String){
            tv_phone.text = phone
        }
    }

或者另外一种形式:

    // 状态在presenter层里面
    
    class TestPresenterImpl:BasePresenter(),TestPresenter{
        var name:String = ""
        var phone:String = ""
        
        fun submit(){
            val result = Http.submit(name,phone)
            view?.setResult(result)
        }
    }
    
    class TestActivity:Activity(),TestView{
        
        var presenter = TestPresenterImpl()
        
        override fun setName(name:String){
            presenter.name = name
        }
        
        override fun setPhone(phone:String){
            presenter.phone = phone
        }
        
        fun submitClick(view:View){
            presenter.submit()
        }
    }

可以看见,即使将所有状态声明在一处(比如类开始位置)依然是离散混乱的,因此状态管理概念就出现了,可以百度redux(状态管理的典型代表)

安卓的状态实现的一般做法

这里就不重复描述了,推荐一些文章吧

从状态管理(State Manage)到MVI(Model-View-Intent)

[译]为什么使用MVI模式(MVI编写响应式安卓APP入门系列第一部分MODEL)

使用MVI打造响应式APP(一):Model到底是什么

MVI的改进

看了上面这些文章后,要应用到实际项目中特别是递进式改造现有传统MVP项目的话,就有一些痛苦的地方:

  • 这些实现都需要对底层架构大动,改了基类presenter或者基类viewmodel后,所有子类全部需要修改,而且往往view实现也要修改,emmm......
  • 这些文章里面都讲了状态合并(reduce操作),但是实际开发中为了尽可能保持view的函数功能单一,往往会分解尽可能职责单一的方法,请看下面两种方式作为对比:
    fun render(state:TotalState){
        if(state.name.isNotEmpty(){
            tv_name.text = state.name
        }
        if(state.phone.isNotEmpty()){
            tv_phone.text = state.phone
        }
        if(state.isError){
            toast("出错了")
        }
    }
    fun setName(name:String){
        tv_name.text = name
    }
    
    fun setPhone(phone:String){
        tv_phone.text = phone
    }
    
    fun showErr(){
        toast("出错了")
    }

可见,不论是从维护角度还是从单元测试用例编写来说,第2种都要明显优于第一种实现

改进思路

  1. 想要不变动基础传统架构基础上给页面加上状态管理的能力,常见的一般是加注解或者AOP,笔者这里选择APT编译期生成代码的方式

  2. 要想尽量细化view层函数职责,那么就需要对整个页面状态分解并单独监听

编译期生成状态管理

想法是这样:给activity或者fragment通过注解方式添加代表页面状态的类和状态处理的类,大概这样子:


@BindState(state = MainState::class, agent = MainAgent::class)
class MainActivity : AppCompatActivity(),MainView{
    lateinit var presenter: MainPresenter

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    ShadowState.bind(this)
    presenter = MainPresenter()
  }
}

data class MainState(
    val name: String,
    val amendInternal: Int = 0,
    val childState:ChildState = ChildState(),
    val listStates:List<String> = listOf()
){
    data class ChildState(
        val gender:String = "man"
    )
}


class MainAgent : StateAgent<MainState, MainView>() {
    override fun initState(bundle: Bundle?): MainState =
        MainState(name = "hahah",listStates = listOf("1","2"))

    override fun conf() {
        //监听状态变化,调用view相关方法
    }
}

然后在相应的create生命周期bind一下,即将view与对应的状态类关联起来,然后在presenter或者viewModel里面直接注入对应的状态代理,即可直接操作状态:

class MainPresenter {
    init {
        ShadowState.injectDispatcher(this)
    }

    @InjectAgent
    lateinit var agent: MainAgent

    fun changeName(){
        agent.setState { it.copy(name = it.name+"++") }
    }
}

这里笔者思路分为三步:

  1. 通过编译期代码生成,解析出所有BindState注解,将其state属性和agent属性与其对应的View类(比如MainActivity)形成关系图
  2. 通过bind方法,对view实例进行事件流监听,当然因为还涉及安卓里的生命周期相关问题,这里只接受类型为LifecycleOwner的view
  3. 通过injectDispatcher方法,注入关系图中对应的代理实例

这里就展示下编译期生成的代码,其他部分直接阅读源码即可


public final class MainStateManager implements StateManager {
  Map<Class<?>, Class<?>> stateMap = new HashMap<>();

  Map<Class<?>, StateAgent> stateAgentMap = new HashMap<>();

  Map<Class<?>, StateBinder> stateBinderMap = new HashMap<>();

  public MainStateManager() {
    stateMap.put(MainActivity.class,MainState.class);
    stateAgentMap.put(MainState.class,new MainAgent());
    stateBinderMap.put(MainState.class,new MainStateBinder());
    stateMap.put(SecondActivity.class,SecondState.class);
    stateAgentMap.put(SecondState.class,new SecondAgent());
    stateBinderMap.put(SecondState.class,new SecondStateBinder());
  }

  @Override
  public void bind(LifecycleOwner lifecycleOwner) {
    if (getStateClass(lifecycleOwner) == null) {
          return;
        };
    stateBinderMap.get(getStateClass(lifecycleOwner)).observe(lifecycleOwner, getStateAgent(lifecycleOwner));
  }

  @Override
  public void injectAgent(Object instance) {
    List<Class<?>> agentClasses = AgentInjection.INSTANCE.getAgents(instance);
    for (Class<?> cls : agentClasses) {
         for (Map.Entry<Class<?>, StateAgent> entry :
             stateAgentMap.entrySet()) {
                if (entry.getValue().getClass() == cls) {
                    AgentInjection.INSTANCE.inject(
                     instance,
                     entry.getValue()
                   );
                }
             }
         };
  }

  @Override
  public StateAgent getStateAgent(LifecycleOwner lifecycleOwner) {
    if (getStateClass(lifecycleOwner) == null) {
          return null;
        };
    return stateAgentMap.get(getStateClass(lifecycleOwner));
  }

  @Override
  public Class getStateClass(LifecycleOwner lifecycleOwner) {
    if (stateMap.get(lifecycleOwner.getClass()) == null) {
          return null;
        };
    return stateMap.get(lifecycleOwner.getClass());
  }
}

public final class MainStateBinder implements StateBinder {
  @Override
  public void observe(LifecycleOwner owner, StateAgent agent) {
    StateObserver observer = agent.createObserver();
    if (owner instanceof FragmentActivity) {
          observer.getLiveData().setValue((MainState) agent.initState(((FragmentActivity) owner).getIntent().getExtras()));
        } else if (owner instanceof Fragment) {
          observer.getLiveData().setValue((MainState) agent.initState(((Fragment) owner).getArguments()));
        } else {
          throw new IllegalArgumentException("");
        };
    observer.getLiveData().observe(owner, observer);
    agent.init(observer);
    agent.bindView((MainActivity) owner,observer);
  }
}

状态分解单独监听

直接看例子:


class MainAgent : StateAgent<MainState, MainView>() {
    override fun initState(bundle: Bundle?): MainState =
        MainState(name = "hahah",listStates = listOf("1","2"))

    override fun conf() {
        listen({ it.name }, {view?.setName(it)})
        listen({ it.phone },{view?.setPhone(it)})
        ....
    }
}

通过简单的listen方法,第一个function表达你想监听整个页面状态的哪个属性,第二个function里面表达你拿着这个属性想做什么操作

这里笔者用rxjava的map操作符来实现状态类的分解,具体请参考源码

最后

开局演示图上那个风骚的悬浮窗就是检查器,为了开发过程中随时调试当前页面的状态,有点像chrome浏览器开发者工具里面的那个修改变量的工具,直接ShadowState.openWatcher()即可唤起,可以最小化也可以移动,改了某个值之后不要忘记点保存图标才会反应到页面上

PS

未来考虑加上时光旅行,虽然感觉用的地方不大?emmm......