听说这套框架可以解决Android MVI

Posted 涂程

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了听说这套框架可以解决Android MVI相关的知识,希望对你有一定的参考价值。

作者:易冬

前言

没有最完美的架构,只有最合适的架构。

android 应用架构变迁:MVCMVPMVVMMVI

关于这四种架构的概念、逻辑、实现方式与优劣,技术社区内优质文章不胜枚举,此处不再赘述。

今天重点介绍如何利用 Airbnb 开源框架 Mavericks 快速实践 MVI(Model-View-Intent) 架构。

主要弄清楚下面几个问题:

  1. Mavericks 是什么?
  2. Mavericks 核心概念是什么?
  3. Mavericks 如何使用?
  4. 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. 初始化

ApplicationonCreate() 函数中执行初始化。

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

异步处理密封类,有四个子类:UninitializedLoadingSuccessFail,分别代表异步处理的 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 后,感觉代码层次清晰,集成方便,简单易用,符合好轮子的标准。

对于鄙人这种不太爱自己折腾框架的躺平者而言,简直福音。下一步准备把之前写的 WanAndroidClientMavericks 改写下。

以上是关于听说这套框架可以解决Android MVI的主要内容,如果未能解决你的问题,请参考以下文章

Android MVI框架搭建与使用

Android真的推荐用MVI模式?MVI和MVVM有什么区别?

Android:解决 MVI 架构实战痛点

关于Android架构:MVI + LiveData + ViewModel | ProAndroidDev

Android MVI 架构简介

Android MVI 架构简介