Android 与单个事件组合

Android Compose with single event

现在我在 ViewModel 中有一个事件 class 以这种方式公开为流:

abstract class BaseViewModel() : ViewModel() {

    ...

    private val eventChannel = Channel<Event>(Channel.BUFFERED)
    val eventsFlow = eventChannel.receiveAsFlow()

    fun sendEvent(event: Event) {
        viewModelScope.launch {
            eventChannel.send(event)
        }
    }

    sealed class Event {
        data class NavigateTo(val destination: Int): Event()
        data class ShowSnackbarResource(val resource: Int): Event()
        data class ShowSnackbarString(val message: String): Event()
    }
}

这是管理它的可组合项:

@Composable
fun SearchScreen(
    viewModel: SearchViewModel
) {
    val events = viewModel.eventsFlow.collectAsState(initial = null)
    val snackbarHostState = remember { SnackbarHostState() }
    val coroutineScope = rememberCoroutineScope()
    Box(
        modifier = Modifier
            .fillMaxHeight()
            .fillMaxWidth()
    ) {
        Column(
            modifier = Modifier
                .padding(all = 24.dp)
        ) {
            SearchHeader(viewModel = viewModel)
            SearchContent(
                viewModel = viewModel,
                modifier = Modifier.padding(top = 24.dp)
            )
            when(events.value) {
                is NavigateTo -> TODO()
                is ShowSnackbarResource -> {
                    val resources = LocalContext.current.resources
                    val message = (events.value as ShowSnackbarResource).resource
                    coroutineScope.launch {
                        snackbarHostState.showSnackbar(
                            message = resources.getString(message)
                        )
                    }
                }
                is ShowSnackbarString -> {
                    coroutineScope.launch {
                        snackbarHostState.showSnackbar(
                            message = (events.value as ShowSnackbarString).message
                        )
                    }
                }
            }
        }
        SnackbarHost(
            hostState = snackbarHostState,
            modifier = Modifier.align(Alignment.BottomCenter)
        )
    }
}

我遵循了 here.

中 Flow 的单个事件模式

我的问题是,事件仅在第一次被正确处理(SnackBar 显示正确)。但在那之后,似乎不再收集这些事件了。至少直到我离开屏幕再回来。在这种情况下,所有事件都会连续触发。

看不出我做错了什么。调试时,事件会正确发送到通道,但状态值似乎未在可组合项中更新。

与其将您的逻辑放在可组合项中,不如将它们放在里面

// Runs only on initial composition 
LaunchedEffect(key1 = Unit) {
  viewModel.eventsFlow.collectLatest { value -> 
    when(value) {
       // Handle events
    }
 }
}

而且,与其将其用作状态,不如从 LaunchedEffect 块中的流中收集值。这就是我在我的应用程序中实现单个事件的方式

我不确定你是如何设法编译代码的,因为我在 launch 上遇到错误。

Calls to launch should happen inside a LaunchedEffect and not composition

通常你可以使用LaunchedEffect,它已经在协程范围内运行,所以你不需要coroutineScope.launch。在 documentation.

中阅读有关副作用的更多信息

一点科特林建议:在类型中使用 when 时,您不需要手动将变量转换为具有 as 的类型。在这种情况下,您可以声明 val 连同您的变量以防止 Smart cast to ... is impossible, because ... is a property that has open or custom getter 错误:

val resources = LocalContext.current.resources
val event = events.value // allow Smart cast
LaunchedEffect(event) {
    when (event) {
        is BaseViewModel.Event.NavigateTo -> TODO()
        is BaseViewModel.Event.ShowSnackbarResource -> {
            val message = event.resource
            snackbarHostState.showSnackbar(
                message = resources.getString(message)
            )
        }
        is BaseViewModel.Event.ShowSnackbarString -> {
            snackbarHostState.showSnackbar(
                message = event.message
            )
        }
    }
}

这段代码有一个问题:如果多次发送同一个事件,将不会显示,因为LaunchedEffect不会重新启动:event因为key是一样的。

您可以通过不同的方式解决这个问题。以下是其中一些:

  1. data class替换为class:现在事件将通过指针而不是字段进行比较。

  2. 给数据添加一个随机idclass,这样每个新元素都不等于另一个:

    data class ShowSnackbarResource(val resource: Int, val id: UUID = UUID.randomUUID()) : Event()
    

注意协程LaunchedEffect会在新事件发生时被取消。并且由于 showSnackbar 是一个挂起函数,之前的 snackbar 将被隐藏以显示新的。如果你在coroutineScope.launch上运行showSnackbar(还在LaunchedEffect里面做),新的snackbar会等到之前的snackbar消失才会出现。

另一个对我来说似乎更清晰的选项是重置事件的状态,因为您已经对它做出了反应。您可以添加另一个事件来执行此操作:

object Clean : Event()

并在 snackbar 消失后发送:

LaunchedEffect(event) {
    when (event) {
        is BaseViewModel.Event.NavigateTo -> TODO()
        is BaseViewModel.Event.ShowSnackbarResource -> {
            val message = event.resource
            snackbarHostState.showSnackbar(
                message = resources.getString(message)
            )
        }
        is BaseViewModel.Event.ShowSnackbarString -> {
            snackbarHostState.showSnackbar(
                message = event.message
            )
        }
        null, BaseViewModel.Event.Clean -> return@LaunchedEffect
    }
    viewModel.sendEvent(BaseViewModel.Event.Clean)
}

但在这种情况下,如果您在前一个事件尚未消失时发送相同的事件,它将像以前一样被忽略。这可能是完全正常的,具体取决于您的应用程序的结构,但是为了防止这种情况,您可以像以前一样在 coroutineScope 上显示它。

另外,查看在显示快餐栏的 JetNews compose app example. I suggest you download the project and inspect it starting from location 中实施的更通用的解决方案。

这是 Jack 答案的修改版本,作为遵循 safer flow collection 新指南的扩展功能。

@Composable
inline fun <reified T> Flow<T>.observeWithLifecycle(
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
    minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
    noinline action: suspend (T) -> Unit
) {
    LaunchedEffect(key1 = Unit) {
        lifecycleOwner.lifecycleScope.launch {
            flowWithLifecycle(lifecycleOwner.lifecycle, minActiveState).collect(action)
        }
    }
}

用法:

viewModel.flow.observeWithLifecycle { value ->
    //Use the collected value
}