状态管理注解:@State、@Action注解在MVVM中的应用

说实话,MVVM架构在Android开发中已经不是什么新鲜事了。但每次我看到项目里那些重复的LiveData声明、手写的状态更新逻辑,心里就犯嘀咕——这玩意儿能不能再优雅一点?

我个人习惯用注解处理器来干掉这些模板代码。今天我们就聊聊怎么用@State@Action这两个注解,把MVVM的状态管理做得更清爽。

为什么需要状态管理注解?

先想想看,一个典型的ViewModel里有多少重复劳动?

  • 声明一个LiveData或MutableStateFlow
  • 写一个更新方法,里面调用postValue()emit()
  • 在UI层观察这个状态

我接手过一个老项目,一个ViewModel里光状态字段就二十多个,每个都配一个setter方法。代码量翻倍不说,还容易漏掉通知更新。说白了,这些代码就是「体力活」,应该交给机器去干。

核心思路:用注解标记状态和动作,编译时自动生成状态管理代码。开发者只需要关注业务逻辑本身。

@State注解:标记状态字段

@State注解用来标记ViewModel中需要被观察的状态字段。它的设计其实很简单:

@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.SOURCE)
annotation class State(
    val key: String = "",
    val defaultValue: String = ""
)

嗯,这里要注意几个点:

  • key参数:给状态一个唯一标识,方便在UI层引用。不传的话默认用字段名。
  • defaultValue参数:指定初始值。我建议所有状态都显式声明默认值,避免空指针。

举个例子,一个登录页面的ViewModel可以这样写:

class LoginViewModel : BaseViewModel() {

    @State(key = "username", defaultValue = "")
    var username: String = ""

    @State(key = "password", defaultValue = "")
    var password: String = ""

    @State(key = "loading", defaultValue = "false")
    var isLoading: Boolean = false

    @State(key = "errorMsg", defaultValue = "null")
    var errorMessage: String? = null
}

你看,代码干净多了。没有LiveData,没有StateFlow,就是普通的Kotlin属性。注解处理器会在编译时帮我们生成对应的状态管理代码。

小技巧:我习惯把状态字段的可见性设为internalpublic,因为注解处理器需要访问它们。如果你用private,记得在注解处理器里开启反射访问。

@Action注解:标记状态更新方法

光有状态还不够,还得有更新状态的动作。@Action注解就是干这个的:

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.SOURCE)
annotation class Action(
    val value: String = ""
)

它的用法也很直观:

class LoginViewModel : BaseViewModel() {

    @Action("login")
    fun login(username: String, password: String) {
        // 更新状态
        this.username = username
        this.password = password
        this.isLoading = true
        this.errorMessage = null

        // 模拟网络请求
        viewModelScope.launch {
            try {
                val result = apiService.login(username, password)
                this@LoginViewModel.isLoading = false
                // 处理登录成功
            } catch (e: Exception) {
                this@LoginViewModel.isLoading = false
                this@LoginViewModel.errorMessage = e.message
            }
        }
    }
}

等等,这里有个坑。你可能会问:「我直接在方法里赋值,注解处理器怎么知道要通知UI更新?」

好问题。答案是在编译时,注解处理器会扫描所有被@Action标记的方法,然后生成一个包装类。这个包装类会在每次属性赋值后,自动调用状态通知机制。

注解处理器如何工作?

我画了一张图,帮你理解整个流程:

@State / @Action 注解处理流程 源码阶段 @State 标记字段 @Action 标记方法 编译阶段 注解处理器扫描 生成状态管理代码 生成产物 ViewModel_StateProxy 状态通知机制 运行时阶段 UI层通过生成的代理类观察状态变化 @Action 方法自动触发状态通知 关键点 • 注解处理器在编译时生成代码,不影响运行时性能 • 生成的代理类自动处理状态通知,开发者无需手动调用 postValue()

流程其实不复杂:

  1. 源码阶段:你在ViewModel里用@State@Action标记好字段和方法
  2. 编译阶段:注解处理器扫描这些注解,生成一个ViewModel_StateProxy
  3. 运行时阶段:UI层通过这个代理类来观察状态,每次@Action方法执行后,代理类自动通知所有观察者

生成的代码长什么样?

我直接给你看生成的代码片段,你就明白了:

// 由注解处理器自动生成
class LoginViewModel_StateProxy(
    private val viewModel: LoginViewModel
) {
    private val _stateMap = mutableMapOf<String, MutableStateFlow<Any?>>()

    init {
        // 为每个 @State 字段创建 StateFlow
        _stateMap["username"] = MutableStateFlow(viewModel.username)
        _stateMap["password"] = MutableStateFlow(viewModel.password)
        _stateMap["loading"] = MutableStateFlow(viewModel.isLoading)
        _stateMap["errorMsg"] = MutableStateFlow(viewModel.errorMessage)
    }

    fun observe(key: String): StateFlow<Any?> = _stateMap[key] ?: error("State $key not found")

    fun dispatch(action: String, vararg args: Any?) {
        when (action) {
            "login" -> {
                viewModel.login(args[0] as String, args[1] as String)
                // 自动通知所有状态变化
                syncStates()
            }
        }
    }

    private fun syncStates() {
        _stateMap["username"]?.value = viewModel.username
        _stateMap["password"]?.value = viewModel.password
        _stateMap["loading"]?.value = viewModel.isLoading
        _stateMap["errorMsg"]?.value = viewModel.errorMessage
    }
}

你看,核心就是syncStates()方法。每次@Action方法执行完后,它会把所有@State字段的最新值同步到对应的StateFlow中。UI层只需要观察这些StateFlow就行了。

避坑指南:我曾经在一个项目里遇到性能问题——状态字段太多,每次syncStates()都全量同步。后来我改成了增量同步,只更新那些在@Action方法中实际被修改的字段。具体做法是在注解处理器里分析方法的字节码,找出哪些字段被赋值了。

在UI层如何使用?

UI层的使用也很简单。以Compose为例:

@Composable
fun LoginScreen(viewModel: LoginViewModel) {
    val proxy = remember { LoginViewModel_StateProxy(viewModel) }

    val username by proxy.observe("username").collectAsState()
    val password by proxy.observe("password").collectAsState()
    val isLoading by proxy.observe("loading").collectAsState()
    val errorMsg by proxy.observe("errorMsg").collectAsState()

    // 触发动作
    Button(onClick = { proxy.dispatch("login", username, password) }) {
        Text("登录")
    }
}

嗯,这里有个设计取舍。我见过有些方案把dispatch封装成扩展函数,让调用更自然:

inline fun LoginViewModel_StateProxy.login(username: String, password: String) {
    dispatch("login", username, password)
}

这样UI层就可以直接调用proxy.login(username, password),不用手动传字符串。我个人比较推荐这种方式,类型安全更好。

总结一下

@State@Action注解的核心价值,说白了就是把MVVM状态管理中的「通知」这个环节自动化了。你只需要关注业务逻辑——哪些是状态、哪些是动作,剩下的交给注解处理器。

我在实际项目中用这套方案重构过一个模块,代码量减少了40%左右,而且再也没出现过「忘记调用postValue()」这种低级bug。如果你也在做MVVM相关的开发,不妨试试这个思路。

一句话总结:用注解标记状态和动作,编译时自动生成通知代码,运行时零反射开销。这就是注解处理器在MVVM状态管理中的正确打开方式。


公众号:蓝海资料掘金营,微信deep3321