From 0fe43f54158584835b8d976bedb573ea8c3afa4b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 7 Dec 2022 17:48:25 +0100 Subject: [PATCH] Ensure posted events from the ViewModel are consumed (once) by the UI Inspired from https://github.com/Kotlin/kotlinx.coroutines/issues/3002 --- .../app/core/platform/VectorBaseActivity.kt | 4 +- .../VectorBaseBottomSheetDialogFragment.kt | 4 +- .../app/core/platform/VectorBaseFragment.kt | 4 +- .../app/core/platform/VectorViewModel.kt | 9 +-- .../im/vector/app/core/utils/SharedEvent.kt | 58 +++++++++++++++++++ .../media/VectorAttachmentViewerActivity.kt | 3 +- .../settings/VectorSettingsBaseFragment.kt | 5 +- 7 files changed, 77 insertions(+), 10 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/core/utils/SharedEvent.kt diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt index a87eb92b133..1e29dfff5ed 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt @@ -128,9 +128,11 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver fun VectorViewModel<*, *, T>.observeViewEvents( observer: (T) -> Unit, ) { + val tag = this@VectorBaseActivity::class.simpleName.toString() lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.RESUMED) { - viewEvents.stream() + viewEvents + .stream(tag) .collect { hideWaitingView() observer(it) diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt index ad7a86c8991..a44fb1c9acb 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt @@ -205,9 +205,11 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomShe protected fun VectorViewModel<*, *, T>.observeViewEvents( observer: (T) -> Unit, ) { + val tag = this@VectorBaseBottomSheetDialogFragment::class.simpleName.toString() lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.RESUMED) { - viewEvents.stream() + viewEvents + .stream(tag) .collect { observer(it) } diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt index 9f79db9c66d..a82cef54e56 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt @@ -277,9 +277,11 @@ abstract class VectorBaseFragment : Fragment(), MavericksView protected fun VectorViewModel<*, *, T>.observeViewEvents( observer: (T) -> Unit, ) { + val tag = this@VectorBaseFragment::class.simpleName.toString() lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.RESUMED) { - viewEvents.stream() + viewEvents + .stream(tag) .collect { dismissLoadingDialog() observer(it) diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt b/vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt index c9d58f9545e..3dd38c455fb 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt @@ -18,15 +18,16 @@ package im.vector.app.core.platform import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.MavericksViewModel -import im.vector.app.core.utils.DataSource -import im.vector.app.core.utils.PublishDataSource +import im.vector.app.core.utils.EventQueue +import im.vector.app.core.utils.SharedEvents abstract class VectorViewModel(initialState: S) : MavericksViewModel(initialState) { // Used to post transient events to the View - protected val _viewEvents = PublishDataSource() - val viewEvents: DataSource = _viewEvents + protected val _viewEvents = EventQueue(capacity = 64) + val viewEvents: SharedEvents + get() = _viewEvents abstract fun handle(action: VA) } diff --git a/vector/src/main/java/im/vector/app/core/utils/SharedEvent.kt b/vector/src/main/java/im/vector/app/core/utils/SharedEvent.kt new file mode 100644 index 00000000000..e712769c485 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/utils/SharedEvent.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.utils + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.transform +import java.util.concurrent.CopyOnWriteArraySet + +interface SharedEvents { + fun stream(consumerId: String): Flow +} + +class EventQueue(capacity: Int) : SharedEvents { + + private val innerQueue = MutableSharedFlow>(replay = capacity) + + fun post(event: T) { + innerQueue.tryEmit(OneTimeEvent(event)) + } + + override fun stream(consumerId: String): Flow = innerQueue.filterNotHandledBy(consumerId) +} + +/** + * Event designed to be delivered only once to a concrete entity, + * but it can also be delivered to multiple different entities. + * + * Keeps track of who has already handled its content. + */ +private class OneTimeEvent(private val content: T) { + + private val handlers = CopyOnWriteArraySet() + + /** + * @param asker Used to identify, whether this "asker" has already handled this Event. + * @return Event content or null if it has been already handled by asker + */ + fun getIfNotHandled(asker: String): T? = if (handlers.add(asker)) content else null +} + +private fun Flow>.filterNotHandledBy(consumerId: String): Flow = transform { event -> + event.getIfNotHandled(consumerId)?.let { emit(it) } +} diff --git a/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt index 9f9488e35db..0d240b376be 100644 --- a/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt +++ b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt @@ -239,11 +239,12 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), AttachmentInt } private fun observeViewEvents() { + val tag = this::class.simpleName.toString() lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.RESUMED) { viewModel .viewEvents - .stream() + .stream(tag) .collect(::handleViewEvents) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt index 6299d8962d0..724807a81ec 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt @@ -72,10 +72,11 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), Maverick protected fun VectorViewModel<*, *, T>.observeViewEvents( observer: (T) -> Unit, ) { + val tag = this@VectorSettingsBaseFragment::class.simpleName.toString() lifecycleScope.launch { - repeatOnLifecycle(state) { repeatOnLifecycle(Lifecycle.State.RESUMED) { - viewEvents.stream() + viewEvents + .stream(tag) .collect { observer(it) }