Mavericks:Airbnb 使用这套框架减少了 50% 代码量
The following article is from AndroidKt Author 易冬
前言
“没有最完美的架构,只有最合适的架构。
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<T>
异步处理密封类,有四个子类: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
改写下。
参考资料
WanAndroid: https://www.wanandroid.com/
推荐文章
加好友进交流群,技术干货聊不停