Merge pull request #7724 from vector-im/feature/bma/launchWhenResumed

Observe ViewEvents only when resumed
This commit is contained in:
Benoit Marty 2023-01-06 18:43:53 +01:00 committed by GitHub
commit 93021a6028
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 158 additions and 54 deletions

1
changelog.d/7724.bugfix Normal file
View File

@ -0,0 +1 @@
Observe ViewEvents only when resumed and ensure ViewEvents are not lost.

View File

@ -41,6 +41,7 @@ import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.airbnb.mvrx.MavericksView import com.airbnb.mvrx.MavericksView
@ -91,6 +92,7 @@ import im.vector.app.features.themes.ActivityOtherThemes
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.failure.GlobalError import org.matrix.android.sdk.api.failure.GlobalError
@ -123,14 +125,20 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
protected val viewModelProvider protected val viewModelProvider
get() = ViewModelProvider(this, viewModelFactory) get() = ViewModelProvider(this, viewModelFactory)
fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) { fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(
viewEvents observer: (T) -> Unit,
.stream() ) {
.onEach { val tag = this@VectorBaseActivity::class.simpleName.toString()
hideWaitingView() lifecycleScope.launch {
observer(it) repeatOnLifecycle(Lifecycle.State.RESUMED) {
} viewEvents
.launchIn(lifecycleScope) .stream(tag)
.collect {
hideWaitingView()
observer(it)
}
}
}
} }
var toolbar: ToolbarConfig? = null var toolbar: ToolbarConfig? = null

View File

@ -26,8 +26,10 @@ import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.annotation.FloatRange import androidx.annotation.FloatRange
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.airbnb.mvrx.MavericksView import com.airbnb.mvrx.MavericksView
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
@ -43,6 +45,7 @@ import im.vector.app.features.analytics.plan.MobileScreen
import io.github.hyuwah.draggableviewlib.Utils import io.github.hyuwah.draggableviewlib.Utils
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import reactivecircus.flowbinding.android.view.clicks import reactivecircus.flowbinding.android.view.clicks
import timber.log.Timber import timber.log.Timber
@ -199,12 +202,18 @@ abstract class VectorBaseBottomSheetDialogFragment<VB : ViewBinding> : BottomShe
* ViewEvents * ViewEvents
* ========================================================================================== */ * ========================================================================================== */
protected fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) { protected fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(
viewEvents observer: (T) -> Unit,
.stream() ) {
.onEach { val tag = this@VectorBaseBottomSheetDialogFragment::class.simpleName.toString()
observer(it) lifecycleScope.launch {
} repeatOnLifecycle(Lifecycle.State.RESUMED) {
.launchIn(viewLifecycleOwner.lifecycleScope) viewEvents
.stream(tag)
.collect {
observer(it)
}
}
}
} }
} }

View File

@ -23,8 +23,10 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.airbnb.mvrx.MavericksView import com.airbnb.mvrx.MavericksView
import dagger.hilt.android.EntryPointAccessors import dagger.hilt.android.EntryPointAccessors
@ -37,6 +39,7 @@ import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import reactivecircus.flowbinding.android.view.clicks import reactivecircus.flowbinding.android.view.clicks
import timber.log.Timber import timber.log.Timber
@ -145,11 +148,15 @@ abstract class VectorBaseDialogFragment<VB : ViewBinding> : DialogFragment(), Ma
* ========================================================================================== */ * ========================================================================================== */
protected fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) { protected fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) {
viewEvents val tag = this@VectorBaseDialogFragment::class.simpleName.toString()
.stream() lifecycleScope.launch {
.onEach { repeatOnLifecycle(Lifecycle.State.RESUMED) {
observer(it) viewEvents
} .stream(tag)
.launchIn(viewLifecycleOwner.lifecycleScope) .collect {
observer(it)
}
}
}
} }
} }

View File

@ -34,6 +34,7 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.airbnb.mvrx.MavericksView import com.airbnb.mvrx.MavericksView
import com.bumptech.glide.util.Util.assertMainThread import com.bumptech.glide.util.Util.assertMainThread
@ -53,6 +54,7 @@ import im.vector.app.features.navigation.Navigator
import im.vector.lib.ui.styles.dialogs.MaterialProgressDialog import im.vector.lib.ui.styles.dialogs.MaterialProgressDialog
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import reactivecircus.flowbinding.android.view.clicks import reactivecircus.flowbinding.android.view.clicks
import timber.log.Timber import timber.log.Timber
@ -272,14 +274,20 @@ abstract class VectorBaseFragment<VB : ViewBinding> : Fragment(), MavericksView
* ViewEvents * ViewEvents
* ========================================================================================== */ * ========================================================================================== */
protected fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) { protected fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(
viewEvents observer: (T) -> Unit,
.stream() ) {
.onEach { val tag = this@VectorBaseFragment::class.simpleName.toString()
dismissLoadingDialog() lifecycleScope.launch {
observer(it) repeatOnLifecycle(Lifecycle.State.RESUMED) {
} viewEvents
.launchIn(viewLifecycleOwner.lifecycleScope) .stream(tag)
.collect {
dismissLoadingDialog()
observer(it)
}
}
}
} }
/* ========================================================================================== /* ==========================================================================================

View File

@ -18,15 +18,16 @@ package im.vector.app.core.platform
import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.MavericksViewModel import com.airbnb.mvrx.MavericksViewModel
import im.vector.app.core.utils.DataSource import im.vector.app.core.utils.EventQueue
import im.vector.app.core.utils.PublishDataSource import im.vector.app.core.utils.SharedEvents
abstract class VectorViewModel<S : MavericksState, VA : VectorViewModelAction, VE : VectorViewEvents>(initialState: S) : abstract class VectorViewModel<S : MavericksState, VA : VectorViewModelAction, VE : VectorViewEvents>(initialState: S) :
MavericksViewModel<S>(initialState) { MavericksViewModel<S>(initialState) {
// Used to post transient events to the View // Used to post transient events to the View
protected val _viewEvents = PublishDataSource<VE>() protected val _viewEvents = EventQueue<VE>(capacity = 64)
val viewEvents: DataSource<VE> = _viewEvents val viewEvents: SharedEvents<VE>
get() = _viewEvents
abstract fun handle(action: VA) abstract fun handle(action: VA)
} }

View File

@ -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<out T> {
fun stream(consumerId: String): Flow<T>
}
class EventQueue<T>(capacity: Int) : SharedEvents<T> {
private val innerQueue = MutableSharedFlow<OneTimeEvent<T>>(replay = capacity)
fun post(event: T) {
innerQueue.tryEmit(OneTimeEvent(event))
}
override fun stream(consumerId: String): Flow<T> = 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<out T>(private val content: T) {
private val handlers = CopyOnWriteArraySet<String>()
/**
* @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 <T> Flow<OneTimeEvent<T>>.filterNotHandledBy(consumerId: String): Flow<T> = transform { event ->
event.getIfNotHandled(consumerId)?.let { emit(it) }
}

View File

@ -55,8 +55,6 @@ import im.vector.app.features.themes.ActivityOtherThemes
import im.vector.app.features.ui.UiStateRepository import im.vector.app.features.ui.UiStateRepository
import im.vector.lib.core.utils.compat.getParcelableExtraCompat import im.vector.lib.core.utils.compat.getParcelableExtraCompat
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@ -142,9 +140,9 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
startAppViewModel.onEach { startAppViewModel.onEach {
renderState(it) renderState(it)
} }
startAppViewModel.viewEvents.stream() startAppViewModel.observeViewEvents {
.onEach(::handleViewEvents) handleViewEvents(it)
.launchIn(lifecycleScope) }
startAppViewModel.handle(StartAppAction.StartApp) startAppViewModel.handle(StartAppAction.StartApp)
} }

View File

@ -59,7 +59,7 @@ class SharedSecureStorageActivity :
views.toolbar.visibility = View.GONE views.toolbar.visibility = View.GONE
viewModel.observeViewEvents { observeViewEvents(it) } viewModel.observeViewEvents { onViewEvents(it) }
viewModel.onEach { renderState(it) } viewModel.onEach { renderState(it) }
} }
@ -85,7 +85,7 @@ class SharedSecureStorageActivity :
showFragment(fragment) showFragment(fragment)
} }
private fun observeViewEvents(it: SharedSecureStorageViewEvent?) { private fun onViewEvents(it: SharedSecureStorageViewEvent) {
when (it) { when (it) {
is SharedSecureStorageViewEvent.Dismiss -> { is SharedSecureStorageViewEvent.Dismiss -> {
finish() finish()

View File

@ -29,7 +29,9 @@ import androidx.core.transition.addListener
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.transition.Transition import androidx.transition.Transition
import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.viewModel
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -50,8 +52,6 @@ import im.vector.lib.attachmentviewer.AttachmentViewerActivity
import im.vector.lib.core.utils.compat.getParcelableArrayListExtraCompat import im.vector.lib.core.utils.compat.getParcelableArrayListExtraCompat
import im.vector.lib.core.utils.compat.getParcelableExtraCompat import im.vector.lib.core.utils.compat.getParcelableExtraCompat
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@ -239,10 +239,15 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), AttachmentInt
} }
private fun observeViewEvents() { private fun observeViewEvents() {
viewModel.viewEvents val tag = this::class.simpleName.toString()
.stream() lifecycleScope.launch {
.onEach(::handleViewEvents) repeatOnLifecycle(Lifecycle.State.RESUMED) {
.launchIn(lifecycleScope) viewModel
.viewEvents
.stream(tag)
.collect(::handleViewEvents)
}
}
} }
private fun handleViewEvents(event: VectorAttachmentViewerViewEvents) { private fun handleViewEvents(event: VectorAttachmentViewerViewEvents) {

View File

@ -20,7 +20,9 @@ import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import com.airbnb.mvrx.MavericksView import com.airbnb.mvrx.MavericksView
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -35,6 +37,7 @@ import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.analytics.plan.MobileScreen
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import reactivecircus.flowbinding.android.view.clicks import reactivecircus.flowbinding.android.view.clicks
import timber.log.Timber import timber.log.Timber
@ -66,13 +69,19 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), Maverick
* ViewEvents * ViewEvents
* ========================================================================================== */ * ========================================================================================== */
protected fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) { protected fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(
viewEvents observer: (T) -> Unit,
.stream() ) {
.onEach { val tag = this@VectorSettingsBaseFragment::class.simpleName.toString()
observer(it) lifecycleScope.launch {
} repeatOnLifecycle(Lifecycle.State.RESUMED) {
.launchIn(viewLifecycleOwner.lifecycleScope) viewEvents
.stream(tag)
.collect {
observer(it)
}
}
}
} }
/* ========================================================================================== /* ==========================================================================================

View File

@ -28,7 +28,7 @@ fun String.trimIndentOneLine() = trimIndent().replace("\n", "")
fun <S : MavericksState, VA : VectorViewModelAction, VE : VectorViewEvents> VectorViewModel<S, VA, VE>.test(): ViewModelTest<S, VE> { fun <S : MavericksState, VA : VectorViewModelAction, VE : VectorViewEvents> VectorViewModel<S, VA, VE>.test(): ViewModelTest<S, VE> {
val testResultCollectingScope = CoroutineScope(Dispatchers.Unconfined) val testResultCollectingScope = CoroutineScope(Dispatchers.Unconfined)
val state = stateFlow.test(testResultCollectingScope) val state = stateFlow.test(testResultCollectingScope)
val viewEvents = viewEvents.stream().test(testResultCollectingScope) val viewEvents = viewEvents.stream("test").test(testResultCollectingScope)
return ViewModelTest(state, viewEvents) return ViewModelTest(state, viewEvents)
} }