MvRx + Epoxy —— 简单封装/数据传递/监听

上一篇博客写了《MvRx的基本使用》,简单介绍了以下MvRx的优缺点和基本使用。这篇博客主要分享下MvRx的一些简单使用技巧。


博客中的项目地址:https://github.com/RDSunhy/MvRxSample
本文的代码效果

简单封装

根据官方的WiKi先封装一些基类,方便后面的使用,首先封装的就是ViewModel,代码如下:

BaseViewModel

abstract class BaseViewModel<S : MvRxState>(initialState: S) :
    BaseMvRxViewModel<S>(initialState, debugMode = true)

BaseViewModel代码很简单,debugMode 是控制日志输出,可以在BaseViewModel中进行控制,子类集成BaseViewModel,debugMode需要改变时不用在每个子类里进行改变了。

BaseFragment

abstract class BaseFragment : BaseMvRxFragment(){

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return super.onCreateView(inflater, container, savedInstanceState)
    }

    protected fun navigateTo(@IdRes actionId: Int, arg: Serializable? = null) {
    	/**注意!要传递的数据类型必须是可序列化的,也就是必须实现Serializable接口**/
        val bundle = arg?.let { Bundle().apply { putSerializable(MvRx.KEY_ARG, it) } }
        findNavController().navigate(actionId, bundle)
    }

}

BaseFragment的封装也很简单,上面代码仅仅添加了一个 navigateTo()方法,里面的两行代码,分别是处理MvRx中的数据传递、以及Navigation的跳转,这个方法在我们跳转页面传递数据时会用到。

数据传递

MvRx的一大优势就在于页面间的数据传递非常简单,Fragment和Fragment之间的数据传递常常让我们头疼,来看一下MvRx是如何传递数据的,效果图:
在这里插入图片描述
主要由两个Fragment构成,FirstFragment输入数据,单击按钮将输入框中的数据传递到SecondFragment中,SecondFragment接受数据并且展示。

代码如下:
传递数据的实体类Person注意:一定要实现Serializable

data class Person(
    val name: String,
    val age: String,
    val sex: String
):Serializable

FirstFragment代码:

data class FirstState(
    val name: String = "--"
) : MvRxState

class FirstViewModel(firstState: FirstState, private val apiService: ApiService) :
    BaseViewModel<FirstState>(firstState) {

    init {
        logStateChanges()
    }

    /**
     * MvRx提供的依赖注入,通过MvRxViewModelFactory中的create,可以给
     * viewModel中的构造参数实现依赖注入 下面代码实例化了一个retrofit的请求类
     * 代码在Http包中
     */
    companion object : MvRxViewModelFactory<FirstViewModel, FirstState> {

        override fun create(
            viewModelContext: ViewModelContext,
            state: FirstState
        ): FirstViewModel {
            val service: ApiService by lazy {
                HttpUtils.retrofit.create(ApiService::class.java)
            }
            return FirstViewModel(state, service)
        }
    }
}

class FirstFragment : BaseFragment(){

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_first,container,false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        bnIntent.setOnClickListener{
            if(etName.text.toString().isNotEmpty()
                ||etAge.text.toString().isNotEmpty()
                ||etSex.text.toString().isNotEmpty()){
                var person = Person(
                    etName.text.toString(),
                    etAge.text.toString(),
                    etSex.text.toString()
                )
                //传递数据
                navigateTo(R.id.action_firstFragment_to_secondFragment,person)
            }else{
                Toast.makeText(context,"请输入要传递的数据",Toast.LENGTH_SHORT)
            }

        }
    }

    override fun invalidate() {

    }

}

传递数据时很简单,利用我们在BaseFragment中的navigateTo()指定跳转的Fragment (Fragment跳转我采用的是Navigation中的action,Deom中有对应代码就不贴出来了),重点是如何接受数据,下面贴出SecondFragment代码,代码中提供了两种接受传递数据的方法,代码中有大量注释:

SecondFragment

/**
 * 方法二:
 * 通过State的构造器也可以接受传递的数据
 * 同时,如果你的state中的属性需要初始值(例如:分页加载的页数),都可以在构造器中赋值
 */
data class SecondState(
    val name: String = "",
    val state_person: Person? = null
) : MvRxState {
    constructor(person: Person) : this(
        name = "Sunhy",//给state中的属性赋初始值
        state_person = person
    )
}

class SecondViewModel(secondState: SecondState, private val apiService: ApiService) :
    BaseViewModel<SecondState>(secondState) {

    init {
        logStateChanges()
    }

    companion object : MvRxViewModelFactory<SecondViewModel, SecondState> {

        override fun create(
            viewModelContext: ViewModelContext,
            state: SecondState
        ): SecondViewModel {
            val service: ApiService by lazy {
                HttpUtils.retrofit.create(ApiService::class.java)
            }
            return SecondViewModel(state, service)
        }
    }
}

class SecondFragment : BaseFragment(){

    /**
     * 方法一:
     * 接受传递过来的数据  只需要指定类型 从 args()中取出
     *  在BaseFragment中 我们把数据存在了 MvRx.KEY_ARG 里
     *  @see [BaseFragment]
     */
    val person :Person by args()
    val secondViewModel by fragmentViewModel(SecondViewModel::class)

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_second,container,false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
    }

    override fun invalidate() {
        /**
         * 通过 args() 取到的 person 数据
         */
        if (person != null){
            tvName.text = person.name
            tvAge.text = person.age
            tvSex.text = person.sex
        }

        /**
         * 通过 State 构造器 取到的 state_person 数据
         */
        withState(secondViewModel){
            if(it.state_person != null){
                tvName2.text = it.state_person.name
                tvAge2.text = it.state_person.age
                tvSex2.text = it.state_person.sex
            }

            /**
             * 通过 State 构造器 赋初始值
             */
            tvName3.text = it.name
        }
    }

}

上面的代码中共有两种接受数据的方式:
1.通过val person :Person by args()只需要指定数据类型,从args()中取需要的数据即可
2.通过state的构造器去赋值,同时也可以给其他属性赋予初始值

监听

监听,同样也是MvRx的一大优势,本质上就是观察者模式,通过viewModel我们可以在任何地方监听State中的属性变化,也就是说当State中的属性发生变化时,就会回调,执行对应的代码
这一点和我们使用retrofit + rxjava进行网络请求时非常的相似,rxjava会在请求成功或者失败时回调不同的方法,处理对应代码。
MvRx中的监听分为两种,一种是监听普通的State属性,另一种就是监听Async<T>包裹的网络请求接受实体类。前者通过selectSubscribe监听并且可以监听多个(最多四个),后者通过asyncSubscribe监听提供了onSuccess和onFail回调

效果图:
在这里插入图片描述
代码中有大量注释,代码如下:

SubscribeFragment

data class SubscribeState(
    val name: String = "Shy",
    val age: Int = 21,
    val articleData: Async<ArticleData> = Uninitialized
) : MvRxState

class SubscribeViewModel(state: SubscribeState, private val apiService: ApiService) :
    BaseViewModel<SubscribeState>(state) {

    init {
        logStateChanges()
    }

    fun changeName(newName: String){
        withState { state ->
            setState { copy(name = newName) }
        }
    }

    fun changeAge(newAge: Int){
        withState {
            setState { copy(age = newAge) }
        }
    }

    fun getArticleData(){
        withState {
            /**
             *  Loading 表示 正在请求中 防止重复请求
             */
            if(it.articleData is Loading) return@withState
            Api.api.getArticleList()
                .execute { data ->
                    copy(articleData = data)
                }
        }
    }

    companion object : MvRxViewModelFactory<SubscribeViewModel, SubscribeState> {

        override fun create(
            viewModelContext: ViewModelContext,
            state: SubscribeState
        ):  SubscribeViewModel{
            val service: ApiService by lazy {
                HttpUtils.retrofit.create(ApiService::class.java)
            }
            return SubscribeViewModel(state, service)
        }
    }
}

class SubscribeFragment : BaseFragment(){

    val subscribeViewModel by fragmentViewModel(SubscribeViewModel::class)

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_subscribe,container,false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        /**
         * 通过两个按钮的单击事件 触发 State 的改变
         */
        bnChangeName.setOnClickListener {
            if(tvName.text == "Shy"){
                subscribeViewModel.changeName("Sunhy")
            }else{
                subscribeViewModel.changeName("Shy")
            }
        }

        bnChangeAge.setOnClickListener {
            if(tvAge.text == "21"){
                subscribeViewModel.changeAge(99)
            }else{
                subscribeViewModel.changeAge(21)
            }
        }

        bnRequest.setOnClickListener {
            subscribeViewModel.getArticleData()
        }

        /**
         *  通过两种监听 监听 State"普通"属性 和 State中Async<T>属性
         *
         *  selectSubscribe 监听"普通
         */

        subscribeViewModel.selectSubscribe(SubscribeState::name, SubscribeState::age,
            subscriber = { name, age ->
                Toast.makeText(context,"name:-> ${name},age:-> ${age}",Toast.LENGTH_SHORT).show()
            })

        /**
         * asyncSubscribe 监听网络请求接受bean的成功或者失败
         */
        subscribeViewModel.asyncSubscribe(SubscribeState::articleData,
            onSuccess = {
                //请求成功
                Toast.makeText(context,"请求成功",Toast.LENGTH_SHORT).show()
            },
            onFail = {
                //请求失败
                Toast.makeText(context,"请求失败->${it}",Toast.LENGTH_SHORT).show()
            })

    }

    override fun invalidate() {
        withState(subscribeViewModel){
            tvName.text = it.name
            tvAge.text = it.age.toString()
            when(it.articleData){
                is Success -> {
                    /**
                     *  因为 Async 是在 Observable 上封装了一层 所以 需要 invoke() 之后 获取到的才是 实体类 (也就是响应数据)
                     */
                    tvData.text = it.articleData.invoke().data.toString()
                }
                is Fail -> {
                    tvData.text = "请求失败"
                    Log.e("网络请求失败","网络请求失败")
                }
                is Loading -> {
                    tvData.text = "请求中..."
                    Log.e("网络请求中","网络请求中")
                }
                else -> {}
            }
        }
    }

}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章