Android Compose 具有单一事件

Duq*_*uqe 14 events android state snackbar android-jetpack-compose

现在我在 ViewModel 中有一个 Event 类,它以 Flow 的方式公开:

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()
    }
}
Run Code Online (Sandbox Code Playgroud)

这是管理它的可组合项:

@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)
        )
    }
}
Run Code Online (Sandbox Code Playgroud)

我从这里开始使用 Flow 遵循单个事件的模式。

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

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

小智 21

不要将逻辑放在可组合项中,而是将它们放在可组合项中

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

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


Sor*_*tfi 13

这是杰克答案的修改版本,作为遵循新准则的扩展函数,以实现更安全的流量收集

@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)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

viewModel.flow.observeWithLifecycle { value ->
    //Use the collected value
}
Run Code Online (Sandbox Code Playgroud)


Phi*_*hov 8

我不确定你如何编译代码,因为我在launch.

对 launch 的调用应该发生在 LaunchedEffect 内部,而不是合成内部

通常你可以使用LaunchedEffect已经在协程作用域中运行的 which ,所以你不需要coroutineScope.launch. 阅读文档中有关副作用的更多信息。

一些 kotlin 建议: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
            )
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这段代码有一个问题:如果多次发送同一事件,则不会显示该事件,因为LaunchedEffect不会重新启动:event因为密钥相同。

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

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

  2. 向数据类添加一个随机 id,以便每个新元素不等于另一个:

    data class ShowSnackbarResource(val resource: Int, val id: UUID = UUID.randomUUID()) : Event()
    
    Run Code Online (Sandbox Code Playgroud)

LaunchedEffect请注意,当新事件发生时,协程将被取消。由于它showSnackbar是一个暂停功能,因此前一个小吃栏将被隐藏以显示新的小吃栏。如果你继续运行showSnackbarcoroutineScope.launch仍在内部执行LaunchedEffect),新的小吃栏将等到前一个小吃栏消失后再出现。

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

object Clean : Event()
Run Code Online (Sandbox Code Playgroud)

并在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)
}
Run Code Online (Sandbox Code Playgroud)

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

另外,请查看JetNews撰写应用程序示例中实现的更通用的解决方案。我建议您下载该项目并从显示小吃栏的位置开始检查它。