查看原文
其他

Mavericks:Airbnb 使用这套框架减少了 50% 代码量

AndroidPub 2022-07-13

The following article is from AndroidKt Author 易冬

前言

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

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

持续 Coding

划重点

1. Async<T>

异步处理密封类,有四个子类: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())
    }
}
no retainValue

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 改写下。

参考资料

[1]

WanAndroid: https://www.wanandroid.com/


END

推荐文章 


加好友进交流群,技术干货聊不停


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存