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 -> {}
            }
        }
    }

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