听说这套框架可以解决Android MVI
Posted 涂程
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了听说这套框架可以解决Android MVI相关的知识,希望对你有一定的参考价值。
作者:易冬
前言
没有最完美的架构,只有最合适的架构。
android 应用架构变迁:MVC
、MVP
、MVVM
、MVI
。
关于这四种架构的概念、逻辑、实现方式与优劣,技术社区内优质文章不胜枚举,此处不再赘述。
今天重点介绍如何利用 Airbnb 开源框架 Mavericks
快速实践 MVI(Model-View-Intent)
架构。
主要弄清楚下面几个问题:
- Mavericks 是什么?
- Mavericks 核心概念是什么?
- Mavericks 如何使用?
- Mavericks 实践效果如何?
Mavericks
Mavericks (formerly MvRx): Android on Autopilot
Mavericks 是 Aribnb 开源的一款功能强大且易于学习的 Android MVI 框架。Mavericks 以 Android Jetpack 和 Kotlin Coroutines 为基础搭建上层逻辑,在技术先进性和可持续方面毋庸置疑。至于框架实用性,相信接受了 Airbnb、Tonal 等大型 APP 长时间检验的 Mavericks,不会让开发者失望。
核心概念
-
MavericksState:承载界面的所有数据且只负责承载数据。
-
必须使用 Kotlin
data class
-
必须使用不可变属性
-
每个属性必须有默认值
-
MavericksViewModel:更新界面 State 并暴露单独状态以便局部更新。
-
init ...
-
setState copy(yourProp = newValue)
-
withState()
-
Async<T>
和execute(...)
处理异步事务 -
onEach()
和onAsync()
局部更新 -
MavericksView:由 State 驱动而刷新的界面。
-
invalidate()
-
通过
activityViewModel()
、fragmentViewModel()
、parentFragmentViewModel()
、existingViewModel()
和navGraphViewModel(navGraphId: Int)
等代理获取MavericksViewModel
一个简单的计数界面只需要下面几行代码,既清晰又简洁。
/** State classes contain all of the data you need to render a screen. */
data class CounterState(val count: Int = 0) : MavericksState
/** ViewModels are where all of your business logic lives. It has a simple lifecycle and is easy to test. */
class CounterViewModel(initialState: CounterState) : MavericksViewModel<CounterState>(initialState)
fun incrementCount() = setState copy(count = count + 1)
/**
* Fragments in Mavericks are simple and rarely do more than bind your state to views.
* Mavericks works well with Fragments but you can use it with whatever view architecture you use.
*/
class CounterFragment : Fragment(R.layout.counter_fragment), MavericksView
private val viewModel: CounterViewModel by fragmentViewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?)
counterText.setOnClickListener
viewModel.incrementCount()
override fun invalidate() = withState(viewModel) state ->
counterText.text = "Count: $state.count"
实践
需求:利用 WanAndroid API[1] 实现搜索热词的列表展示(支持下拉刷新)
接口:https://www.wanandroid.com/hotkey/json
1. 依赖
dependencies
implementation 'com.airbnb.android:mavericks:2.5.1'
2. 初始化
Application
内 onCreate()
函数中执行初始化。
Mavericks.initialize(this)
3. MavericksState
定义 MainState
并添加两个属性:
-
val hotKeys: List<HotKey> = emptyList()
搜索热词数据
-
val request: Async<Response<List<HotKey>>> = Uninitialized
网络请求状态(加载中、失败、成功等)
data class MainState(
val hotKeys: List<HotKey> = emptyList(),
val request: Async<Response<List<HotKey>>> = Uninitialized
) : MavericksState
4. MavericksViewModel
定义 MainViewModel
管理 MainState
,实现获取搜索热词函数。
initState
:默认状态。对应前面提到的,MavericksState
子类的每个属性都需要默认值。init……
:初始化执行。withState
:一次性获取当前状态。copy()
:拷贝对象并调整部分属性,用于更新状态。
class MainViewModel(initState: MainState) : MavericksViewModel<MainState>(initState)
init
getHotKeys()
fun getHotKeys() = withState
if (it.request is Loading) return@withState
suspend
Retrofitance.wanAndroidAPI.hotKey()
.execute(Dispatchers.IO, retainValue = MainState::request) state ->
copy(request = state, hotKeys = state()?.data ?: emptyList())
5. MavericksView
创建 MainFragment
并实现 MavericksView
接口用于展示搜索热词列表,用户可以下拉刷新请求新数据。
invalidate()
:状态更新后自动触发。withState(MavericksViewModel)
:一次性获取MavericksViewModel
管理的MavericksState
。onAsync()
:监听异步属性变化。onEach()
:监听普通属性变化。
class MainFragment : Fragment(R.layout.fragment_main), MavericksView
private val mainViewModel: MainViewModel by fragmentViewModel()
private val binding: FragmentMainBinding by viewBinding()
private val adapter by lazy
HotKeyAdapter()
override fun onCreate(savedInstanceState: Bundle?)
super.onCreate(savedInstanceState)
mainViewModel.onAsync(MainState::request,
deliveryMode = uniqueOnly(),
onFail =
viewLifecycleOwner.lifecycleScope.launchWhenStarted
Snackbar.make(
binding.root,
"HotKey request failed.",
Snackbar.LENGTH_INDEFINITE
)
.apply
setAction("DISMISS")
this.dismiss()
show()
,
onSuccess =
viewLifecycleOwner.lifecycleScope.launchWhenStarted
Snackbar.make(
binding.root,
"HotKey request successfully.",
Snackbar.LENGTH_INDEFINITE
).apply
setAction("DISMISS")
this.dismiss()
show()
)
override fun onViewCreated(view: View, savedInstanceState: Bundle?)
super.onViewCreated(view, savedInstanceState)
binding.list.adapter = adapter
binding.list.addItemDecoration(
DividerItemDecoration(
context,
DividerItemDecoration.VERTICAL
)
)
binding.refresh.setOnRefreshListener
mainViewModel.getHotKeys()
override fun invalidate()
withState(mainViewModel)
binding.refresh.isRefreshing = !it.request.complete
adapter.submitList(if (Random.nextBoolean()) it.hotKeys.reversed() else it.hotKeys)
6. 效果
源码
Talk is cheap, Show me the code。
https://github.com/onlyloveyd/AndroidSamples
划重点
1. Async
异步处理密封类,有四个子类:Uninitialized
、Loading
、Success
、Fail
,分别代表异步处理的 4 种状态。
sealed class Async<out T>(private val value: T?)
open operator fun invoke(): T? = value
object Uninitialized : Async<Nothing>(value = null)
data class Loading<out T>(private val value: T? = null) : Async<T>(value = value)
data class Success<out T>(private val value: T) : Async<T>(value = value)
override operator fun invoke(): T = value
data class Fail<out T>(val error: Throwable, private val value: T? = null) : Async<T>(value = value)
2. onAsync
异步属性状态变化监听
data class MyState(val name: Async<String>) : MavericksState
...
onAsync(MyState::name) name ->
// Called when name is Success and any time it changes.
// Or if you want to handle failures
onAsync(
MyState::name,
onFail = e -> .... ,
onSuccess = name -> ...
)
3. retainValue
加载过程中或者加载失败后显示的数据。
示例中,我们将 getHotKeys()
函数内的 retainValue
去掉,界面更新数据时会有明显的闪动
。
fun getHotKeys() = withState
if (it.request is Loading) return@withState
suspend
Retrofitance.wanAndroidAPI.hotKey()
.execute(Dispatchers.IO) state ->
copy(request = state, hotKeys = state()?.data ?: emptyList())
4. 监听模式:DeliveryMode
- RedeliverOnStart:顾名思义
- UniqueOnly:例如
SnackBar
只需要弹一次,页面重建时不应该再次显示,就适合使用UniqueOnly
的监听模式。
5. 状态监听防止崩溃
为了防止回调时界面已经被销毁而导致程序奔溃,采用 launchWhenStarted
防御策略。
mainViewModel.onAsync(MainState::request,
deliveryMode = uniqueOnly(),
onFail =
viewLifecycleOwner.lifecycleScope.launchWhenStarted
Snackbar.make(
binding.root,
"HotKey request failed.",
Snackbar.LENGTH_INDEFINITE
)
.apply
setAction("DISMISS")
this.dismiss()
show()
,
onSuccess =
viewLifecycleOwner.lifecycleScope.launchWhenStarted
Snackbar.make(
binding.root,
"HotKey request successfully.",
Snackbar.LENGTH_INDEFINITE
).apply
setAction("DISMISS")
this.dismiss()
show()
)
总结
简单好用,是选轮子的基本标准。
上手 Mavericks 后,感觉代码层次清晰,集成方便,简单易用,符合好轮子的标准。
对于鄙人这种不太爱自己折腾框架的躺平者而言,简直福音。下一步准备把之前写的 WanAndroidClient
用 Mavericks
改写下。
以上是关于听说这套框架可以解决Android MVI的主要内容,如果未能解决你的问题,请参考以下文章
Android真的推荐用MVI模式?MVI和MVVM有什么区别?