Android Airbnb Mavericks 架构教程

2,973 阅读8分钟

引言

在现代Android开发中,选择合适的架构对提高应用的可维护性和扩展性至关重要。近年来,越来越多的开发者开始关注如何更好地管理应用的状态,确保应用在各种情况下都能正确地响应用户的操作和系统的变化。Mavericks是由Airbnb开发的一个轻量级但功能强大的状态管理框架,它旨在简化这一过程。

Mavericks是一个基于Kotlin的Android状态管理库,提供了一种简洁、高效的方式来管理应用的状态。它采用了MVI(Model-View-Intent)架构模式,这是现代Android开发中一种非常流行的架构模式。

MVI架构简介

MVI(Model-View-Intent)是一种单向数据流模式,它通过明确的状态管理和事件处理,使应用的逻辑更加清晰和可预测。MVI架构由以下三个核心部分组成:

  • Model: 代表应用的状态。它是一个不可变的数据类,定义了界面当前所处的状态。
  • View: 负责渲染UI,并将用户的输入转化为Intent。
  • Intent: 用户的操作或系统的事件,这些事件会触发状态的改变。

Mavericks中的MVI

在Mavericks中,MVI架构被具体化为以下组件:

  • State: 表示UI的状态,通常是一个数据类,实现MavericksState接口。
  • ViewModel: 负责管理状态和处理业务逻辑,通过调用setState方法来更新状态。
  • Fragment/Activity: 作为View,订阅ViewModel中的状态变化并更新UI。

为什么选择Mavericks?

在传统的Android开发中,管理复杂的UI状态和处理状态变化是一项具有挑战性的任务。特别是在涉及到多个视图和复杂的用户交互时,保持状态的一致性和正确性变得尤为困难。Mavericks通过提供结构化的状态管理方案,帮助开发者更好地应对这些挑战。

  • 单一来源的真理:所有的UI状态都存储在单一的ViewModel中,确保状态的一致性和可追溯性。
  • 简洁的状态定义:状态通常是一个数据类,易于定义和理解。
  • 自动订阅和更新UI:通过观察ViewModel中的状态变化,UI能够自动更新,无需手动处理复杂的订阅逻辑。
  • Kotlin的强大功能:利用Kotlin的协程和扩展函数,Mavericks能够提供简洁而强大的API,使开发过程更加顺畅。

总的来说,Mavericks是一个功能强大且易于使用的MVI框架,适用于各种规模的Android应用开发。不论是初学者还是有经验的开发者,都能从中受益。

目录

  1. 什么是Mavericks?
  2. 环境配置
  3. 创建项目
  4. 架构概述
  5. 编写第一个Mavericks视图模型
  6. 高级功能
    1. 状态恢复和持久化
    2. 与协程的集成
    3. 状态管理
    4. 网络请求和数据加载
  1. 总结

1. 什么是Mavericks?

Mavericks是由Airbnb开发的一个Android状态管理库。它以其简洁的API和强大的功能被广泛应用于现代Android开发中。Mavericks结合了Kotlin协程,使状态管理和UI更新变得更加直观和高效。

1. 环境配置

dependencies {
    implementation 'com.airbnb.android:mavericks:x.y.z'
}

3. 创建项目

在Android Studio中创建一个新的项目,选择空活动模板。项目创建完成后,按照上面的步骤配置好Gradle文件。

4. 架构概述

Mavericks架构主要由三个部分组成:State、ViewModel 和 Fragment/Activity。

  • State: 定义界面的状态,通常是一个数据类。
  • ViewModel: 负责业务逻辑和状态管理。
  • Fragment/Activity: 用于显示UI并订阅ViewModel的状态。

5. 编写第一个Mavericks视图模型

在本节中,我们将展示如何使用Mavericks编写一个视图模型,并与传统的写法进行对比。通过这种对比,你可以更好地理解Mavericks的优势。

需求:设置一个 count 通过加和减后显示在 TextView 上。

传统写法

直接声明变量

在传统的Android开发中,我们可能会直接在Fragment中声明一个变量,并通过按钮点击事件直接修改这个变量:

class CounterFragment : Fragment(R.layout.fragment_counter) {

    private var count = 0

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val tvCount = view.findViewById<TextView>(R.id.tvCount)
        val btnIncrement = view.findViewById<Button>(R.id.btnIncrement)
        val btnDecrement = view.findViewById<Button>(R.id.btnDecrement)

        tvCount.text = "Count: $count"

        btnIncrement.setOnClickListener {
            count++
            tvCount.text = "Count: $count"
        }

        btnDecrement.setOnClickListener {
            count--
            tvCount.text = "Count: $count"
        }
    }
}

这种方法虽然简单,但缺乏结构,容易导致状态管理混乱,特别是在处理复杂逻辑时。

使用LiveData和ViewModel

另一种常见的做法是使用Android Architecture Components中的LiveData和ViewModel:

class CounterViewModel : ViewModel() {
    private val _count = MutableLiveData(0)
    val count: LiveData<Int> get() = _count

    fun increment() {
        _count.value = (_count.value ?: 0) + 1
    }

    fun decrement() {
        _count.value = (_count.value ?: 0) - 1
    }
}

class CounterFragment : Fragment(R.layout.fragment_counter) {

    private lateinit var viewModel: CounterViewModel

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewModel = ViewModelProvider(this).get(CounterViewModel::class.java)

        val tvCount = view.findViewById<TextView>(R.id.tvCount)
        val btnIncrement = view.findViewById<Button>(R.id.btnIncrement)
        val btnDecrement = view.findViewById<Button>(R.id.btnDecrement)

        viewModel.count.observe(viewLifecycleOwner, Observer { count ->
            tvCount.text = "Count: $count"
        })

        btnIncrement.setOnClickListener {
            viewModel.increment()
        }

        btnDecrement.setOnClickListener {
            viewModel.decrement()
        }
    }
}

这种方法比直接声明变量更好,因为它将状态和业务逻辑分离到了ViewModel中,并且使用LiveData来观察状态变化,确保UI能够自动更新。然而,LiveData的使用仍然需要一定的样板代码,并且在处理复杂状态和异步操作时,代码可能会变得繁琐。在接下来的部分中,我们将继续展示如何使用Mavericks创建UI并绑定视图模型。

使用Mavericks编写视图模型

首先,我们创建一个数据类来表示界面的状态:

// 注意:MavericksState
data class CounterState(val count: Int = 0) : MavericksState

接下来,创建一个MavericksViewModel来管理这个状态:

// 注意 MavericksViewModel
class CounterViewModel(initialState: CounterState) : MavericksViewModel<CounterState>(initialState) {
    // 设置新的 state
    fun increment() = setState { copy(count = count + 1) }
    fun decrement() = setState { copy(count = count - 1) }
}

在Mavericks中,状态是不可变的,每次状态变化都会创建一个新的状态对象。这使得状态管理更加安全和可预测。

使用Mavericks的Fragment

然后,在Fragment中绑定ViewModel并更新UI:

// 注意 MavericksView
class CounterFragment : Fragment(R.layout.fragment_counter), MavericksView {

    private val viewModel: CounterViewModel by fragmentViewModel()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val btnIncrement = view.findViewById<Button>(R.id.btnIncrement)
        val btnDecrement = view.findViewById<Button>(R.id.btnDecrement)

        btnIncrement.setOnClickListener {
            viewModel.increment()
        }

        btnDecrement.setOnClickListener {
            viewModel.decrement()
        }
    }

    // 当 state 发生改变时该回调会触发。
    override fun invalidate() {
        withState(viewModel) { state ->
            view?.findViewById<TextView>(R.id.tvCount)?.text = "Count: ${state.count}"
        }
    }
}

Mavericks的优势

与传统的写法相比,Mavericks在以下方面具有明显优势:

  1. 简洁的状态管理:状态管理在Mavericks中通过不可变的数据类和简洁的setState方法实现,减少了样板代码。
  2. 自动化的UI更新:Mavericks自动处理状态变化和UI更新,使代码更加简洁和易于维护。
  3. 更好的测试性:Mavericks的状态和业务逻辑集中在ViewModel中,便于进行单元测试和功能测试。
  4. 协程支持:Mavericks原生支持Kotlin协程,简化了异步操作的处理。

通过上面的对比,可以看出Mavericks在简化代码和提高开发效率方面具有显著优势。

6. 高级功能

状态恢复和持久化

在开发复杂的Android应用时,处理状态恢复和持久化是一个常见的需求。Mavericks提供了简洁而强大的工具来处理这些任务。

为什么需要状态恢复和持久化?

在Android应用中,状态恢复和持久化可以在以下场景中发挥重要作用:

  1. 配置更改:当设备旋转或语言改变时,Activity和Fragment会被销毁并重建。需要恢复先前的状态以保持用户体验一致。
  2. 应用进程重启:在内存不足时,系统可能会杀掉后台进程。当用户返回应用时,需要恢复之前的状态。
  3. 用户导航:当用户在多个页面之间导航时,保持页面状态可以提高用户体验。

使用Mavericks进行状态恢复

Mavericks利用ViewModel来管理状态,天然支持配置更改。由于ViewModel的生命周期与Activity或Fragment不同步,配置更改时,ViewModel不会被销毁,状态可以自动恢复。

使用Mavericks进行持久化

先看传统写法进行状态持久化

在传统的Android开发中,状态持久化通常通过 savedInstanceState 或 SharedPreferences 来实现。

使用 savedInstanceState:

class CounterFragment : Fragment(R.layout.fragment_counter) {

    private var count = 0

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val tvCount = view.findViewById<TextView>(R.id.tvCount)
        val btnIncrement = view.findViewById<Button>(R.id.btnIncrement)
        val btnDecrement = view.findViewById<Button>(R.id.btnDecrement)

        savedInstanceState?.let {
            count = it.getInt("COUNT", 0)
        }

        tvCount.text = "Count: $count"

        btnIncrement.setOnClickListener {
            count++
            tvCount.text = "Count: $count"
        }

        btnDecrement.setOnClickListener {
            count--
            tvCount.text = "Count: $count"
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putInt("COUNT", count)
    }
}

使用 SharedPreferences:

class CounterFragment : Fragment(R.layout.fragment_counter) {

    private var count = 0
    private lateinit var sharedPreferences: SharedPreferences

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        sharedPreferences = requireActivity().getPreferences(Context.MODE_PRIVATE)
        count = sharedPreferences.getInt("COUNT", 0)

        val tvCount = view.findViewById<TextView>(R.id.tvCount)
        val btnIncrement = view.findViewById<Button>(R.id.btnIncrement)
        val btnDecrement = view.findViewById<Button>(R.id.btnDecrement)

        tvCount.text = "Count: $count"

        btnIncrement.setOnClickListener {
            count++
            tvCount.text = "Count: $count"
            sharedPreferences.edit().putInt("COUNT", count).apply()
        }

        btnDecrement.setOnClickListener {
            count--
            tvCount.text = "Count: $count"
            sharedPreferences.edit().putInt("COUNT", count).apply()
        }
    }
}

这种方法虽然有效,但需要手动处理状态的存储和恢复,增加了样板代码的复杂性。

使用Mavericks进行状态持久化

Mavericks提供了@PersistState注解,可以更简洁地实现状态的持久化。以下是使用Mavericks进行状态持久化的示例:

在ViewModel中使用 @PersistState 注解:

data class CounterState(@PersistState val count: Int = 0) : MavericksState

class CounterViewModel(initialState: CounterState) : MavericksViewModel<CounterState>(initialState) {
    fun increment() = setState { copy(count = count + 1) }
    fun decrement() = setState { copy(count = count - 1) }
}

Mavericks会自动将标注了@PersistState的状态字段保存到内部持久化存储中,并在应用重启时自动恢复。这大大简化了状态管理的复杂性。

通过Mavericks的持久化特性,可以大幅减少手动管理状态存储和恢复的样板代码,使应用的状态管理更加简洁和高效。

与协程的集成

Mavericks的内部源码使用协程,无需集成。
PS:看似「没啥亮点」,实际却是重中之重。

状态管理

在开发复杂的Android应用时,管理应用的状态可能变得非常复杂。Mavericks提供了一套强大的工具来帮助开发者简化复杂状态的管理,使代码更加简洁和可维护。以下将介绍如何使用Mavericks处理复杂状态,包括多状态组合、依赖状态的管理以及网络请求和数据加载。

多状态组合

在实际应用中,一个界面可能涉及多个状态。例如,一个购物车界面需要显示商品列表、总价格和用户信息。在Mavericks中,可以通过定义多个状态类并在ViewModel中组合使用这些状态。

示例:购物车界面

data class Product(val id: Int, val name: String, val price: Double)
data class User(val id: Int, val name: String)

data class CartState(
    val products: List<Product> = emptyList(),
    val totalPrice: Double = 0.0,
    val user: User? = null
) : MavericksState

class CartViewModel(initialState: CartState) : MavericksViewModel<CartState>(initialState) {

    fun addProduct(product: Product) {
        setState {
            val newProducts = products + product
            copy(products = newProducts, totalPrice = newProducts.sumOf { it.price })
        }
    }

    fun removeProduct(product: Product) {
        setState {
            val newProducts = products - product
            copy(products = newProducts, totalPrice = newProducts.sumOf { it.price })
        }
    }

    fun setUser(user: User) {
        setState {
            copy(user = user)
        }
    }
}

依赖状态的管理

有时,一个状态的变化可能依赖于另一个状态。在Mavericks中,可以使用SharedViewModel模式来订阅其他状态的变化,并在状态变化时触发相应的操作。

示例:使用SharedViewModel管理用户和购物车的依赖关系

首先,创建一个UserViewModel来管理用户状态:

data class UserState(val user: User? = null) : MavericksState

class UserViewModel(initialState: UserState) : MavericksViewModel<UserState>(initialState) {

    fun setUser(user: User) {
        setState {
            copy(user = user)
        }
    }
}

然后,在Fragment中共享这个UserViewModel并将用户状态传递给CartViewModel:

class CartFragment : Fragment(R.layout.fragment_cart), MavericksView {

    private val userViewModel: UserViewModel by activityViewModel()
    private val cartViewModel: CartViewModel by fragmentViewModel()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // 对于非Async对象的State监听可以使用 onEach
        userViewModel.onEach(UserState::user) { user ->
            cartViewModel.setUser(user)
        }

        // 继续设置购物车的UI逻辑
    }

    override fun invalidate() {
        // 更新UI逻辑
    }
}

在这个示例中,我们使用activityViewModel来共享UserViewModel,并使用onEach来监听用户状态的变化。然后,将用户状态传递给CartViewModel。

使用onEach监听状态变化

onEach方法用于监听状态的变化,并在变化时触发相应的操作。可以用于处理UI更新或触发其他业务逻辑。

示例:监听购物车中的商品数量变化

data class CartState(
    val products: List<Product> = emptyList(),
    val totalPrice: Double = 0.0,
    val user: User? = null
) : MavericksState

class CartViewModel(initialState: CartState) : MavericksViewModel<CartState>(initialState) {

    init {
        onEach(CartState::products) { products ->
            println("Number of products in cart: ${products.size}")
        }
    }

    fun addProduct(product: Product) {
        setState {
            val newProducts = products + product
            copy(products = newProducts, totalPrice = newProducts.sumOf { it.price })
        }
    }

    fun removeProduct(product: Product) {
        setState {
            val newProducts = products - product
            copy(products = newProducts, totalPrice = newProducts.sumOf { it.price })
        }
    }
}

在这个示例中,CartViewModel使用onEach监听products列表的变化,并在每次变化时输出当前购物车中商品的数量。

使用派生属性

派生属性用于从现有状态派生出新的值。派生属性通常是基于当前状态计算得出的,并可以通过onEach方法监听其变化。

示例:计算购物车中商品的总数量

data class CartState(
    val products: List<Product> = emptyList(),
    val totalPrice: Double = 0.0,
    val user: User? = null
) : MavericksState {
    val totalItems: Int get() = products.size
}

class CartViewModel(initialState: CartState) : MavericksViewModel<CartState>(initialState) {

    init {
        onEach(CartState::totalItems) { totalItems ->
            println("Total items in cart: $totalItems")
        }
    }

    fun addProduct(product: Product) {
        setState {
            val newProducts = products + product
            copy(products = newProducts, totalPrice = newProducts.sumOf { it.price })
        }
    }

    fun removeProduct(product: Product) {
        setState {
            val newProducts = products - product
            copy(products = newProducts, totalPrice = newProducts.sumOf { it.price })
        }
    }
}

在这个示例中,CartState定义了一个派生属性totalItems,用于计算购物车中的商品总数量。CartViewModel使用onEach监听totalItems的变化,并在每次变化时输出当前购物车中的总商品数量。

通过onEach和派生属性,Mavericks能够更高效地管理复杂的状态变化,使得代码更加简洁和可维护。

网络请求和数据加载

处理网络请求和数据加载是现代应用开发的常见需求。Mavericks通过与Kotlin协程的集成,使得异步操作的管理变得非常简洁。可以使用execute()扩展函数来管理网络请求,并确保在状态变化时更新UI。

Mavericks提供了一个特殊的Async对象来处理异步状态。Async对象可以表示一个加载中的状态、一个成功的状态或一个失败的状态。通过使用Async对象,可以更简洁地管理异步操作的结果。

示例:异步加载用户数据并管理其状态

data class UserState(val user: Async<User> = Uninitialized) : MavericksState

class UserViewModel(initialState: UserState) : MavericksViewModel<UserState>(initialState) {

    private val apiService: ApiService = // 初始化你的ApiService

    fun fetchUser(id: Int) {
        suspend {
            apiService.getUser(id)
        }.execute { result ->
            copy(user = result)
        }
    }
}

在上述示例中,execute()扩展函数会自动处理协程的执行,并将结果包装成Async对象。这样可以轻松管理加载、成功和失败的状态。

在传统的View中,可以通过以下方式显示加载中、成功和失败的状态:

class UserFragment : Fragment(R.layout.fragment_user), MavericksView {

    private val viewModel: UserViewModel by fragmentViewModel()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewModel.fetchUser(1)

        // 观察状态并更新UI
        viewModel.onAsync(UserState::user) { asyncUser ->
            when (asyncUser) {
                is Uninitialized -> {
                    // 初始状态
                    view.findViewById<TextView>(R.id.userNameTextView).text = "No user"
                }
                is Loading -> {
                    // 显示加载中
                    view.findViewById<ProgressBar>(R.id.progressBar).isVisible = true
                }
                is Success -> {
                    // 显示成功状态
                    view.findViewById<ProgressBar>(R.id.progressBar).isVisible = false
                    view.findViewById<TextView>(R.id.userNameTextView).text = asyncUser()?.name
                }
                is Fail -> {
                    // 显示失败状态
                    view.findViewById<ProgressBar>(R.id.progressBar).isVisible = false
                    view.findViewById<TextView>(R.id.errorTextView).apply {
                        isVisible = true
                        text = asyncUser.error.message
                    }
                }
            }
        }
    }

    override fun invalidate() {
        // 在这里可以放置需要更新UI的其他逻辑
    }
}

题外话:发挥下想象,如果使用 Jetpack Compose 会是什么效果?

通过以上这些方法,Mavericks可以帮助开发者更高效地管理复杂的应用状态,使代码更加简洁和可维护。

在一个请求完成之前设置加载数据

看源码:

当一个请求发起但「还未完成」的时候可以设置一段数据;让用户在应用上看到的内容并不是「空白」的,这是一个友好的交互。例如:

  • 首页加载的时候先显示缓存的数据
  • 个人中心先加载缓存的数据

等新的数据请求成功回调之后就会覆盖缓存数据。使用代码:

7. 总结

通过本教程,你已经了解了如何使用Mavericks构建一个简单的Android应用。Mavericks的强大之处在于其简洁的API和强大的状态管理能力,使得构建复杂应用变得更加轻松。希望你能通过本教程掌握Mavericks,并将其应用到你的项目中。

参考:Maverick 官网GithubWiki