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是一样的。
您可以通过不同的方式解决这个问题。以下是其中一些:
将data class
替换为class
:现在事件将通过指针而不是字段进行比较。
给数据添加一个随机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
}
现在我在 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是一样的。
您可以通过不同的方式解决这个问题。以下是其中一些:
将
data class
替换为class
:现在事件将通过指针而不是字段进行比较。给数据添加一个随机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
}