简体   繁体   English

Android 与单个事件组合

[英]Android Compose with single event

Right now I have an Event class in the ViewModel that is exposed as a Flow this way:现在我在 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()
    }
}

And this is the composable managing it:这是管理它的可组合项:

@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)
        )
    }
}

I followed the pattern for single events with Flow from here .我从这里开始使用 Flow 遵循单个事件的模式。

My problem is, the event is handled correctly only the first time (SnackBar is shown correctly).我的问题是,事件仅在第一次被正确处理(SnackBar 显示正确)。 But after that, seems like the events are not collected anymore.但在那之后,似乎不再收集这些事件了。 At least until I leave the screen and come back.至少直到我离开屏幕再回来。 And in that case, all events are triggered consecutively.在这种情况下,所有事件都会连续触发。

Can't see what I'm doing wrong.看不出我做错了什么。 When debugged, events are sent to the Channel correctly, but seems like the state value is not updated in the composable.调试时,事件会正确发送到通道,但似乎 state 值未在可组合项中更新。

Rather than placing your logic right inside composable place them inside而不是将您的逻辑放在可组合中,而是将它们放在里面

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

And also rather than using it as state just collect value from flow in LaunchedEffect block.而且,与其将其用作状态,还不如从LaunchedEffect块中的流中收集值。 This is how I implemented single event in my application这就是我在应用程序中实现单个事件的方式

Here's a modified version of Jack's answer, as an extension function following new guidelines for safer flow collection .这是 Jack 答案的修改版本,作为遵循更安全流收集的新准则的扩展功能。

@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)
        }
    }
}

Usage:用法:

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

Here's a modified version of Soroush Lotfi answer making sure we also stop flow collection whenever the composable is not visible anymore: just replace the LaunchedEffect with a DisposableEffect这是 Soroush Lotfi 答案的修改版本,确保当组合不再可见时我们也会停止流收集:只需将LaunchedEffect替换为DisposableEffect

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

        onDispose {
            job.cancel()
        }
    }
}

https://github.com/Kotlin-Android-Open-Source/Jetpack-Compose-MVI-Coroutines-Flow/blob/master/core-ui/src/main/java/com/hoc/flowmvi/core_ui/rememberFlowWithLifecycle.kt https://github.com/Kotlin-Android-Open-Source/Jetpack-Compose-MVI-Coroutines-Flow/blob/master/core-ui/src/main/java/com/hoc/flowmvi/core_ui/rememberFlowWithLifecycle。克拉

@Suppress("ComposableNaming")
@Composable
fun <T> Flow<T>.collectInLaunchedEffectWithLifecycle(
  vararg keys: Any?,
  lifecycle: Lifecycle = LocalLifecycleOwner.current.lifecycle,
  minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
  collector: suspend CoroutineScope.(T) -> Unit
) {
  val flow = this
  val currentCollector by rememberUpdatedState(collector)

  LaunchedEffect(flow, lifecycle, minActiveState, *keys) {
    withContext(Dispatchers.Main.immediate) {
      lifecycle.repeatOnLifecycle(minActiveState) {
        flow.collect { currentCollector(it) }
      }
    }
  }
}

class ViewModel {
  val singleEvent: Flow<E> = eventChannel.receiveAsFlow()
}

@Composable fun Demo() {
  val snackbarHostState by rememberUpdatedState(LocalSnackbarHostState.current)
  val scope = rememberCoroutineScope()
  viewModel.singleEvent.collectInLaunchedEffectWithLifecycle { event ->
    when (event) {
      SingleEvent.Refresh.Success -> {
        scope.launch {
          snackbarHostState.showSnackbar("Refresh successfully")
        }
      }
      is SingleEvent.Refresh.Failure -> {
        scope.launch {
          snackbarHostState.showSnackbar("Failed to refresh")
        }
      }
      is SingleEvent.GetUsersError -> {
        scope.launch {
          snackbarHostState.showSnackbar("Failed to get users")
        }
      }
      is SingleEvent.RemoveUser.Success -> {
        scope.launch {
          snackbarHostState.showSnackbar("Removed '${event.user.fullName}'")
        }
      }
      is SingleEvent.RemoveUser.Failure -> {
        scope.launch {
          snackbarHostState.showSnackbar("Failed to remove '${event.user.fullName}'")
        }
      }
    }
  }
}

I'm not sure how you manage to compile the code, because I get an error on launch .我不确定您是如何编译代码的,因为我在launch遇到错误。

Calls to launch should happen inside a LaunchedEffect and not composition调用 launch 应该发生在 LaunchedEffect 而不是组合中

Usually you can use LaunchedEffect which is already running in the coroutine scope, so you don't need coroutineScope.launch .通常你可以使用已经在协程范围内运行的LaunchedEffect ,所以你不需要coroutineScope.launch Read more about side effects in documentation .文档中阅读更多关于副作用的信息

A little kotlin advice: when using when in types, you don't need to manually cast the variable to a type with as . kotlin 的一点建议:在类型中使用when ,您不需要手动将变量强制转换为带有as的类型。 In cases like this, you can declare val along with your variable to prevent Smart cast to ... is impossible, because ... is a property that has open or custom getter error:在这种情况下,您可以将val与变量一起声明,以防止Smart cast to ... is impossible, because ... is a property that has open or custom getter错误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
            )
        }
    }
}

This code has one problem: if you send the same event many times, it will not be shown because LaunchedEffect will not be restarted: event as key is the same.此代码有一个问题:如果多次发送相同的事件,则不会显示,因为不会重新启动LaunchedEffectevent与键相同。

You can solve this problem in different ways.您可以通过不同的方式解决此问题。 Here are some of them:这里是其中的一些:

  1. Replace data class with class : now events will be compared by pointer, not by fields.替换data classclass :现在事件将通过指针,而不是由字段进行比较。

  2. Add a random id to the data class, so that each new element is not equal to another:向数据类添加一个随机 id,以便每个新元素不等于另一个:

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

Note that the coroutine LaunchedEffect will be canceled when a new event occurs.注意协程LaunchedEffect会在新事件发生时取消。 And since showSnackbar is a suspend function, the previous snackbar will be hidden to display the new one.而且由于showSnackbar是一个暂停功能,以前的小吃店将被隐藏以显示新的小吃店。 If you run showSnackbar on coroutineScope.launch (still doing it inside LaunchedEffect ), the new snackbar will wait until the previous snackbar disappears before it appears.如果您在coroutineScope.launch上运行showSnackbar (仍在LaunchedEffect ),新的小吃店将等到前一个小吃店消失后才会出现。

Another option, which seems cleaner to me, is to reset the state of the event because you have already reacted to it.另一种对我来说似乎更清晰的选择是重置事件的状态,因为您已经对其做出了反应。 You can add another event to do this:您可以添加另一个事件来执行此操作:

object Clean : Event()

And send it after the snackbar disappears:并在小吃店消失后发送:

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)
}

But in this case, if you send the same event while the previous one has not yet disappeared, it will be ignored as before.但是在这种情况下,如果您在前一个尚未消失的情况下发送相同的事件,它将像以前一样被忽略。 This can be perfectly normal, depending on the structure of your application, but to prevent this you can show it on coroutineScope as before.这可能是完全正常的,具体取决于您的应用程序的结构,但为了防止这种情况,您可以像以前一样在coroutineScope上显示它。

Also, check out the more general solution implemented in the JetNews compose app example.此外,请查看JetNews compose 应用程序示例中实现的更通用的解决方案。 I suggest you download the project and inspect it starting from location where the snackbar is displayed.我建议您下载该项目并从显示小吃店的位置开始检查它。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 VIewModel observeAsState as Single event in Android Compose - VIewModel observeAsState as Single event in Android Compose Android 在 Jetpack Compose 屏幕上处理生命周期事件 - Android handle lifecycle event on Jetpack Compose Screen state 更改的触发事件,在 android 喷气背包中组成 - Trigger event on state change, in android jetpack compose Android Compose:可组合物可以存储多个 state 吗? - Android Compose: can composables store more than a single state? 如何在android compose ui中禁用开关对触摸事件的响应? - How to disable switch's response to touch event in android compose ui? Android Compose - 如何处理 JetPackCompose 中的 ViewModel 清除焦点事件? - Android Compose - How to handle ViewModel clear focus event in JetPackCompose? 如何使用 jetpack compose android 以编程方式为 TextField 调用按键事件? - How to call keypress event programmatically for TextField using jetpack compose, android? 在Android中对单个图像执行多次点击事件 - Perform multi click event on a single image in Android Jetpack Compose 从视图中展示小吃店 model - 单场直播活动 - Jetpack Compose show snack bar from view model - Single Live Event 活动启动器(文件选择器)在单个事件中多次加载 - Jetpack compose - Activity Launcher(File picker) is loading multiple times in single event - Jetpack compose
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM