Android 架构 - MVI

一、概念

Android 架构 - MVI_第1张图片

概念 基于单向数据流,数据永远在一个环形结构中单向流动,便于追踪测试。
通信

View→ViewModel:将用户操作以Intent形式通知给ViewModel,监听ViewModel中State(MutableState、Flow)的变化会自动更新到UI。

ViewModel→Model:对Intent类型分类判断做对应的逻辑处理,调用Model获取数据。

ViewModel→View:会获取的数据更新到State,State的变化会自动更新View。

  • 模型 Model:负责处理数据的状态和逻辑。
  • 视图 View:负责展示界面和数据的状态。
  • 意图 Intent:代表用户的操作(点击、输入、获取列表数据等)。
  • 状态 State:反应数据当前值。

1.1 唯一可信数据源

为了解决 MVVM 中 UI 订阅多个分散的状态(ViewModel中的LiveData/Flow)导致各种数据并行更新或数据相互依赖时,无法清晰掌握整个页面的状态。MVI使用 UiState 将所有状态整合在一处,UI刷新只依赖这一个数据源。

1.2 数据单向流动

DataBinding  数据模型和视图一方发生变化就会同步到另一方,数据的流动是双向的,这样不便于追踪测试。MVI强调数据的源头只有一个,目的地也只有一个。数据从 Data Layer 流向 UI Layer 的 ViewModel 中,ViewModel 将数据转换成 State 给 UI Element 更新。

1.3 事件驱动

MVVM没有约束 View 和 ViewModel 的交互方式,View 可以随意调用 ViewModel 中的方法。MVI使用 Intent 实现了屏蔽,View发送事件,ViewModel通过对事件类型的识别做出相应的业务处理并更新状态。

二、单 Activity 架构的问题

Compose 通过 AndroidComposeView 来与 Activity 交互,使用 单Activity 页面跳转都能在Compose内部完成。Navigation不仅支持 View 的 单Activity+多Fragment 架构,也支持 Compose 的 单Activity+多Composable 架构。

2.1 ViewModel的销毁

       Compose 中的 viewModel() 函数可以从任何组合项中获取 ViewModel,考虑到函数的生命周期和作用域,应在屏幕级组合函数中获取 ViewModel 实例,也就是被 Activity、Navigation目的地调用的根级组合项。不要直接将 ViewModel 实例传递给子组合项用,而是传递子组合项所需要的数据或函数(即状态提升)。

  • 如果根组合项托管在 Activity 中,不同组合项中调用 viewModel() 获取相同类型的 ViewModel 将会是同一个实例。因为是 单Activity 架构,绑定的作用域是同一个ViewModelStoreOwner,也因此 ViewModel 的生命周期不会随组合项的销毁而回收。
  • 如果根组合项托管在 Navigstion 目的地中,不同组合项中调用 viewModel() 获取相同类型的 ViewModel 是不同的实例。因为作用域被限定在了目的地,ViewModel的生命周期会跟随目的地从返回站中弹出而清除。

2.2 获取生命周期

2.2.1 可组合项的生命周期处理

对于组合函数的生命周期:onActive 首次挂载到组件树、onCommit 重组刷新、onDispose 从组件树上移除,可以通过附带效应来监听。

LaunchedEffect 第一次调用Compose函数时执行(首次进入页面)。
DisposableEffect 需要重写 onDispose() 函数当页面退出时调用(退出页面时释放资源)。
SideEffect Compose函数每次执行都会调用该方法(每次重组时)。
@Composable
fun Demo() {
    //在进入页面时更新状态,从而让界面根据状态显示数据。
    LaunchedEffect {...}
}

2.2.2 Activity 的生命周期获取

@Composable
fun Demo() {
    val lifecycleOwner = LocalLifecycleOwner.current
    DisposableEffect(Unit) {
        val observer = LifecycleEventObserver { source, event ->
            when(event) {
                Lifecycle.Event.ON_CREATE -> TODO()
                Lifecycle.Event.ON_START -> TODO()
                Lifecycle.Event.ON_RESUME -> TODO()
                Lifecycle.Event.ON_PAUSE -> TODO()
                Lifecycle.Event.ON_STOP -> TODO()
                Lifecycle.Event.ON_DESTROY -> TODO()
                Lifecycle.Event.ON_ANY -> TODO()
            }
        }
        lifecycleOwner.lifecycle.addObserver(observer)
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }
}

三、搭建项目(Compose版)

3.1 定义状态 State

  • 命名采用:功能+UiState。
  • 采用 data class 因为自带 copy() 功能,非常方便更新部分属性。界面刷新用到的状态全都定义成属性,集中在一起实现唯一可信数据源。
  • 一般同 ViewModel 写在同一个 .kt 文件中,也可以单独写在一个 .kt 文件中。
  • 根据多个状态派生出来的状态,定义在 data class 的类体中。
  • UI 所需要的某些状态若是相互独立,不要定义在同一个数据类中,刷新频率高的那个会造成低的频繁更新。
data class DemoUiState(
    val isLoading: Boolean = false,
    val success: List = emptyList(),
    val isLogin: Boolean = false,    //是否登录
    val isPremium: Boolean= false    //是不是会员
) {
    val canDownload: Boolean = isLogin && isPremium    //派生状态
}

3.2 定义事件 Event

  • 命名采用:场景+Event。
  • 采用 sealed interface 除了保证类型受控优化 when 判断,子类不带参数就定义成 object 方便复用(因为创建的实例无状态区别)。将可能的用户行为全部定义成子类。
  • 一般同 ViewModel 写在同一个 .kt 文件中,也可以单独写在一个 .kt 文件中。
sealed interface DemoEvent {
    //条目点击
    data class DemoItemClick(val url: String) : DemoEvent
    //初始化数据
    object InitData: DemoEvent
}

3.3 处理事件+更新状态(ViewModel)

选择 Channel 的原因:

  • Compose 向外部(ViewModel)发送 Intent 属于副作用,涉及并发安全问题,因此考虑协程间通信。
  • 事件只在 ViewModel 中被消费(只有一个订阅者)。
  • 事件必须执行(不能丢弃元素)。
  • 事件只能被接收(消费)一次,不能回放造成粘性事件。
class DemoViewModel(
    private val repository: DemoRepository
) : ViewModel() {
    //暴露给UI订阅State
    var demoUiState by mutableStateOf(DemoUiState())
        private set

    //定义发送Event的Channel
    private val eventChannel = Channel()

    //初始化时就启动Event处理
    init { handleEvent() }

    //处理Event
    private fun handleEvent() {
        viewModelScope.launch {
            eventChannel.consumeEach { event ->
                when(event) {
                    is DemoEvent.DemoItemClick -> getData()
                    DemoEvent.InitData -> getData()
                }
            }
        }
    }

    //暴露给UI发送Event(比直接在UI中获取Channel发送方便)
    fun dispatchEvent(event: DemoEvent) {
        viewModelScope.launch {
            eventChannel.send(event)
        }
    }

    //(在业务代码里)更新State
    private suspend fun getData() {
        demoUiState = demoUiState.copy(isLoading = true) //状态设为加载中
        runCatching {
            repository.getData()
        }.onSuccess { response ->
            response.getData().onSuccess {
                demoUiState = demoUiState.copy(success = it)  //状态设为成功
            }.onFailure {//网络的错误
                demoUiState = demoUiState.copy(fail = it.message.toString())  //状态设为失败
            }
        }.onFailure { //协程的错误
            demoUiState = demoUiState.copy(fail = it.message.toString())  //状态设为失败
        }
    }

    override fun onCleared() {
        super.onCleared()
        eventChannel.close()  //释放资源
    }
}

3.4 处理状态+发送事件(UI)

  • 在屏幕级组合项获取ViewModel
@Compose
fun MainScreen(
    viewModel: DemoViewModel = viewModel()    //普通获取
//  viewModel: DemoViewModel = viewModel(factory = DemoViewModelFactory(DemoRepository(DemoDataSource())))    //普通获取(带参)
) {
    //发送意图
    viewModel.dispatchEvent(DemoEvent.initData)
    //读取状态并处理
    Content(
        data = viewModel.demoUiState.success
    )
}

@Compose
private fun Content(
    data: DataBean
) {
    //将数据设置给子组件
}

四、搭建项目(View版)

参考文章

你可能感兴趣的:(架构,android)