上一篇博客寫了《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 -> {}
}
}
}
}