Android的MVI架构最佳实践(三):Compose封装
- Android的MVI架构最佳实践(一):Model和Intent
- Android的MVI架构最佳实践(二):View和repeatOnLifecycle
- Android的MVI架构最佳实践(四):单元测试
- Android的MVI架构最佳实践(四):UI测试
前言
声明式UI的最佳伙伴肯定是MVI了,例如前端的Flux或Redux。Compose作为Android官方的声明式UI框架已经十分成熟了,虽然现在从功能上无法碾压旧版本可是也在不断提高了,未来可期。此篇中咱们就实现一个简略的脚手架消除样板代码,满足咱们快速的构建一个MVI的Composable。
Compose的State
首先咱们处理UIState,在Compose中为了在重组中订阅坚持的数据,会运用State<out T>
,咱们在composable中运用函数val result = remember { }
来坚持数据,防止重组中的功能消耗,可是ViewModel的特性便是生命周期内坚持数据,因此咱们只需要界说androidx.compose.runtime.State
,对于纯Compose的项目咱们修正BaseViewModel是再好不过:
abstract class BaseViewModel<S : State> : ViewModel() {
abstract fun initialState(): S
private val _viewState: MutableState<S> by lazy { mutableStateOf(initialState()) }
val viewState: androidx.compose.runtime.State<S> = _viewState
}
Composable中运用示例
sealed class TestState : State {
object Loading : TestState()
object Content : TestState()
}
class SampleViewModel : BaseViewModel<TestState>() {
override fun initialState(): TestState = TestState.Loading
}
@Composable
fun StateEffectScaffold() {
val viewModel = viewModel<SampleViewModel>()
when (viewModel.viewState) {
TestState.Loading -> CircularProgressIndicator()
TestState.Content -> Text("Content")
}
}
兼容Fragment
大多时分咱们还不是纯Compose开发,咱们还需要一个兼容的BaseViewModel
该如何做呢?官方也供给了许多扩展函数来实现这些需求:
-
LiveData
转化Compose State
@Composable
fun <T> LiveData<T>.observeAsState(): State<T?> = ...
-
flow
转化Compose State
@Composable
fun <T> Flow<T>.collectAsStateWithLifecycle(
initialValue: T,
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
context: CoroutineContext = EmptyCoroutineContext
): State<T> = ...
这样咱们之前篇幅中封装的BaseViewModel
就能够经过这些api兼容到composable中了
经过ViewModel取得ComposeState
- 若
BaseViewModel
中的state运用的是StateFlow
。StateFlow要求有必要有默认值,因此Compose的uiState
始终有值。
// viewModel.state is StateFlow
val uiState = viewModel.state.collectAsStateWithLifecycle()
- 若
BaseViewModel
中的state运用的是SharedFlow(replay = 1)
。LiveData和SharedFlow
相同没有默认值,因此Compose的uiState
可空,官方供给的函数就要求初始化一个默认值,不然state
就可能为null。
val initialState: S = ..
// viewModel.state is LiveDate
val uiState = viewModel.state.observeAsState(initial = initialState)
// viewModel.state is SharedFlow
val uiState = viewModel.state.collectAsStateWithLifecycle(
initialValue = initialState
)
- 优化
option2
中必传默认值参数问题。LiveData和SharedFlow
不传递默认值会让state
可空,咱们过滤null不处理,那么默认值就变成可选参数了。
val initialState: S? = null
val BaseViewModel<*,*,*>.replayState = state.replayCache.firstOrNull()
val uiState = viewModel.state.collectAsStateWithLifecycle(
initialValue = replayState
)
(uiState.value ?: initialState)?.let { ... }
State样板代码封装
Compose关注回调的state,而@Composable
函数能够嵌套,那么咱们能够简略封装取得一个脚手架让MVI运用更简略。乃至还能够套娃并同享同一个ViewModel来同享获取数据。
@Composable
fun <S : State, VM : BaseViewModel<*, S, *>> StateEffectScaffold(
viewModel: VM,
initialState: S? = null,
lifecycle: Lifecycle = LocalLifecycleOwner.current.lifecycle,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
context: CoroutineContext = EmptyCoroutineContext,
content: @Composable (VM, S) -> Unit
) {
val uiState = viewModel.state.collectAsStateWithLifecycle(
initialValue = viewModel.replayState,
lifecycle = lifecycle,
minActiveState = minActiveState,
context = context
)
(uiState.value ?: initialState)?.let { content(viewModel, it) }
}
//实战效果:
StateEffectScaffold(
viewModel = hiltViewModel<ABaseViewModel>()
) { viewModel, state ->
when (state) {
TestState.Loading -> CircularProgressIndicator()
TestState.Content -> Text("Content")
}
}
Compose的Effect
MVI中UIState分为state
和effect
,effect不需要state相同在重组中坚持数据,在Compose中effect有专门的处理方式。再配合repeatOnLifecycle
即可实现和Fragment中相同的效果。有时分咱们不需要effect那么把这个函数参数作为可空传递。
@Composable
fun <E : Effect, VM : BaseViewModel<*, *, E>> StateEffectScaffold(
viewModel: VM,
lifecycle: Lifecycle = LocalLifecycleOwner.current.lifecycle,
sideEffect: (suspend (VM, E) -> Unit)? = null,
) {
sideEffect?.let {
val lambdaEffect by rememberUpdatedState(sideEffect)
LaunchedEffect(viewModel.effect, lifecycle) {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.effect.collect { lambdaEffect(viewModel, it) }
}
}
}
}
Composable脚手架
上面咱们分别处理了State和Effect
,只需要稍加组合而且把一些参数悉数露出出来,就得到一个快速开发的脚手架了。这里用state是SharedFlow
来演示代码
@Composable
fun <S : State, E : Effect, V : BaseViewModel<*, S, E>> StateEffectScaffold(
viewModel: V,
initialState: S? = null,
lifecycle: Lifecycle = LocalLifecycleOwner.current.lifecycle,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
context: CoroutineContext = EmptyCoroutineContext,
sideEffect: (suspend (V, E) -> Unit)? = null,
content: @Composable (V, S) -> Unit
) {
sideEffect?.let {
val lambdaEffect by rememberUpdatedState(sideEffect)
LaunchedEffect(viewModel.effect, lifecycle, minActiveState) {
lifecycle.repeatOnLifecycle(minActiveState) {
if (context == EmptyCoroutineContext) {
viewModel.effect.collect { lambdaEffect(viewModel, it) }
} else withContext(context) {
viewModel.effect.collect { lambdaEffect(viewModel, it) }
}
}
}
}
// collectAsStateWithLifecycle 在反正屏变化时会先回调initialState 所以有必要把replay latest state传递曩昔
val uiState = viewModel.state.collectAsStateWithLifecycle(
initialValue = viewModel.replayState,
lifecycle = lifecycle,
minActiveState = minActiveState,
context = context
)
(uiState.value ?: initialState)?.let { content(viewModel, it) }
}
脚手架运用演示
@Composable
fun TemplateScreen() {
StateEffectScaffold(
viewModel = hiltViewModel<TemplateViewModel>(),
sideEffect = { viewModel, sideEffect ->
when (sideEffect) {
is TemplateEffect.ShowToast -> {
TODO("ShowToast ${sideEffect.content}")
}
}
}
) { viewModel, state ->
when (state) {
TemplateState.Loading -> Loading()
TemplateState.Empty -> Empty()
}
}
}
AndroidStudio Live Template提高开发功率
IDEA 能够经过 NEW -> Activity/Fragment
来选择一个模版快速生成一些代码,可是新版的AndroidStudio假如要自界说模版需要自己开发一个IDEA Plugin才能够做到。怎么既简略又能快速满足这个要求呢,那Live Template
就能够发挥一定作用了,可是生成的代码在一个文件中需要自己手动分包。复制下面供给的模版到剪贴板,按图顺序操作。
你输入
mvi
后立马自动取得下面模版中的代码,而且会等待你进一步输入操作。$NAME$
是要输入主命名的占位符,你输入Home按下回车后,所有占位符位置会自动以Home替换,演示实战请看下节。
import androidx.compose.runtime.Composable
import androidx.hilt.navigation.compose.hiltViewModel
import com.arch.mvi.intent.Action
import com.arch.mvi.model.Effect
import com.arch.mvi.model.State
import com.arch.mvi.view.StateEffectScaffold
import com.arch.mvi.viewmodel.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
/**
* - new build package and dirs
* - contract
* - viewmodel
* - view
*/
/** - contract */
sealed class $NAME$Action : Action {
data object LoadData : $NAME$Action()
data class OnButtonClicked(val id: Int) : $NAME$Action()
}
sealed class $NAME$State : State {
data object Loading : $NAME$State()
data object Empty : $NAME$State()
}
sealed class $NAME$Effect : Effect {
data class ShowToast(val content: String) : $NAME$Effect()
}
/** - viewmodel */
@HiltViewModel
class $NAME$ViewModel @Inject constructor(
// private val reducer: $NAME$Reducer,
// private val repository: $NAME$Repository,
// private val dispatcherProvider: CoroutineDispatcherProvider
) : BaseViewModel<$NAME$Action, $NAME$State, $NAME$Effect>() {
init {
sendAction($NAME$Action.LoadData)
}
override fun onAction(action: $NAME$Action, currentState: $NAME$State?) {
when (action) {
$NAME$Action.LoadData -> {
/*viewModelScope.launch {
withContext(dispatcherProvider.io()) {
runCatching { repository.fetchRemoteOrLocalData() }
}.onSuccess {
emitState(reducer.reduceRemoteOrLocalData())
}.onFailure {
emitState($NAME$State.Empty)
}
}*/$END$
}
is $NAME$Action.OnButtonClicked -> {
emitEffect { $NAME$Effect.ShowToast("Clicked ${action.id}") }
}
}
}
}
/** - view */
@Composable
fun $NAME$Screen() {
StateEffectScaffold(
viewModel = hiltViewModel<$NAME$ViewModel>(),
sideEffect = { viewModel, sideEffect ->
when (sideEffect) {
is $NAME$Effect.ShowToast -> {
TODO("ShowToast ${sideEffect.content}")
}
}
}
) { viewModel, state ->
when (state) {
$NAME$State.Loading -> {
TODO("Loading")
}
$NAME$State.Empty -> {
TODO("Empty")
}
}
}
}
用Live Template快速开发
需求和最佳实践第一篇中共同: 登录页面点击登录按钮,恳求网络回来登录结果,登录成功跳转,登录失利展现过错页面。因此MVI中ViewModel和Model的界说彻底相同,仅有不同便是View运用Composable。
-
在AS新建一个空的kotlin文件,输入
mvi
取得模版,输入Logon -
修正
Action
的模版代码为需求中的action,修正State和Effect
和需求中UIState匹配sealed class LogonAction : Action { data object OnButtonClicked : LogonAction() } sealed class LogonState : State { data object LogonHub : LogonState() data object Loading : LogonState() data object Error : LogonState() } sealed class LogonEffect : Effect { data object Navigate : LogonEffect() }
-
修正ViewModel代码
class LogonViewModel : BaseViewModel<LogonAction, LogonState, LogonEffect>() { override fun onAction(action: LogonAction, currentState: LogonState?) { when (action) { LogonAction.OnButtonClicked -> { flow { kotlinx.coroutines.delay(2000) emit(Unit) }.onStart { emitState(LogonState.Loading) }.onEach { emitEffect(LogonEffect.Navigate) }.catch { emitState(LogonState.Error) }.launchIn(viewModelScope) } } } }
-
构建的不同State下的Composable,实现sideEffect处理导航事件
@Preview @Composable fun LogonScreen() { val scaffoldState = rememberScaffoldState() StateEffectScaffold(viewModel = hiltViewModel<LogonViewModel>(), initialState = LogonState.LogonHub, sideEffect = { viewModel, sideEffect -> when (sideEffect) { LogonEffect.Navigate -> { scaffoldState.snackbarHostState.showSnackbar("navigate") } } } ) { viewModel, state -> Scaffold( scaffoldState = scaffoldState ) { Box(modifier = Modifier.fillMaxSize().padding(it), contentAlignment = Alignment.Center) { when (state) { LogonState.Loading -> CircularProgressIndicator() LogonState.Error -> Text(text = "error") LogonState.LogonHub -> Button(onClick = { viewModel.sendAction(LogonAction.OnButtonClicked) }) { Text(text = "logon") } } } } } }
-
展现
总结
此篇中咱们对Compose中运用MVI架构模式做了具体解说,而且考虑非纯Compose的项目的兼容。最终对Composable简略的封装一个脚手架用于快速构建Compose的MVI结构,考虑到每次书写的模版代码太多又运用了Live Template
来再次提高构建速度。最终用实战项目来检验他们。由于大部分开发者并不会再实际开发环节书写UT,导致大家对MVI的优点易于测试感知不强,下一章开端主要讲代码质量管理环节中的MVI的Unit test(单元测试)
。