Skip to content

Commit

Permalink
Merge pull request element-hq#7724 from vector-im/feature/bma/launchW…
Browse files Browse the repository at this point in the history
…henResumed

Observe ViewEvents only when resumed
  • Loading branch information
bmarty authored Jan 6, 2023
2 parents f856142 + e9d1de8 commit 93021a6
Show file tree
Hide file tree
Showing 12 changed files with 158 additions and 54 deletions.
1 change: 1 addition & 0 deletions changelog.d/7724.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Observe ViewEvents only when resumed and ensure ViewEvents are not lost.
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.preference.PreferenceManager
import androidx.viewbinding.ViewBinding
import com.airbnb.mvrx.MavericksView
Expand Down Expand Up @@ -91,6 +92,7 @@ import im.vector.app.features.themes.ActivityOtherThemes
import im.vector.app.features.themes.ThemeUtils
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.failure.GlobalError
Expand Down Expand Up @@ -123,14 +125,20 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
protected val viewModelProvider
get() = ViewModelProvider(this, viewModelFactory)

fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) {
viewEvents
.stream()
.onEach {
hideWaitingView()
observer(it)
}
.launchIn(lifecycleScope)
fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(
observer: (T) -> Unit,
) {
val tag = this@VectorBaseActivity::class.simpleName.toString()
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
viewEvents
.stream(tag)
.collect {
hideWaitingView()
observer(it)
}
}
}
}

var toolbar: ToolbarConfig? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.annotation.CallSuper
import androidx.annotation.FloatRange
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.viewbinding.ViewBinding
import com.airbnb.mvrx.MavericksView
import com.google.android.material.bottomsheet.BottomSheetBehavior
Expand All @@ -43,6 +45,7 @@ import im.vector.app.features.analytics.plan.MobileScreen
import io.github.hyuwah.draggableviewlib.Utils
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import reactivecircus.flowbinding.android.view.clicks
import timber.log.Timber

Expand Down Expand Up @@ -199,12 +202,18 @@ abstract class VectorBaseBottomSheetDialogFragment<VB : ViewBinding> : BottomShe
* ViewEvents
* ========================================================================================== */

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

Expand Down Expand Up @@ -145,11 +148,15 @@ abstract class VectorBaseDialogFragment<VB : ViewBinding> : DialogFragment(), Ma
* ========================================================================================== */

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

Expand Down Expand Up @@ -272,14 +274,20 @@ abstract class VectorBaseFragment<VB : ViewBinding> : Fragment(), MavericksView
* ViewEvents
* ========================================================================================== */

protected fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) {
viewEvents
.stream()
.onEach {
dismissLoadingDialog()
observer(it)
}
.launchIn(viewLifecycleOwner.lifecycleScope)
protected fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(
observer: (T) -> Unit,
) {
val tag = this@VectorBaseFragment::class.simpleName.toString()
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
viewEvents
.stream(tag)
.collect {
dismissLoadingDialog()
observer(it)
}
}
}
}

/* ==========================================================================================
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<S : MavericksState, VA : VectorViewModelAction, VE : VectorViewEvents>(initialState: S) :
MavericksViewModel<S>(initialState) {

// Used to post transient events to the View
protected val _viewEvents = PublishDataSource<VE>()
val viewEvents: DataSource<VE> = _viewEvents
protected val _viewEvents = EventQueue<VE>(capacity = 64)
val viewEvents: SharedEvents<VE>
get() = _viewEvents

abstract fun handle(action: VA)
}
58 changes: 58 additions & 0 deletions vector/src/main/java/im/vector/app/core/utils/SharedEvent.kt
Original file line number Diff line number Diff line change
@@ -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) }
}
8 changes: 3 additions & 5 deletions vector/src/main/java/im/vector/app/features/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,6 @@ import im.vector.app.features.themes.ActivityOtherThemes
import im.vector.app.features.ui.UiStateRepository
import im.vector.lib.core.utils.compat.getParcelableExtraCompat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
Expand Down Expand Up @@ -142,9 +140,9 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
startAppViewModel.onEach {
renderState(it)
}
startAppViewModel.viewEvents.stream()
.onEach(::handleViewEvents)
.launchIn(lifecycleScope)
startAppViewModel.observeViewEvents {
handleViewEvents(it)
}

startAppViewModel.handle(StartAppAction.StartApp)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class SharedSecureStorageActivity :

views.toolbar.visibility = View.GONE

viewModel.observeViewEvents { observeViewEvents(it) }
viewModel.observeViewEvents { onViewEvents(it) }

viewModel.onEach { renderState(it) }
}
Expand All @@ -85,7 +85,7 @@ class SharedSecureStorageActivity :
showFragment(fragment)
}

private fun observeViewEvents(it: SharedSecureStorageViewEvent?) {
private fun onViewEvents(it: SharedSecureStorageViewEvent) {
when (it) {
is SharedSecureStorageViewEvent.Dismiss -> {
finish()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ import androidx.core.transition.addListener
import androidx.core.view.ViewCompat
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.transition.Transition
import com.airbnb.mvrx.viewModel
import dagger.hilt.android.AndroidEntryPoint
Expand All @@ -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.getParcelableExtraCompat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
Expand Down Expand Up @@ -239,10 +239,15 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), AttachmentInt
}

private fun observeViewEvents() {
viewModel.viewEvents
.stream()
.onEach(::handleViewEvents)
.launchIn(lifecycleScope)
val tag = this::class.simpleName.toString()
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
viewModel
.viewEvents
.stream(tag)
.collect(::handleViewEvents)
}
}
}

private fun handleViewEvents(event: VectorAttachmentViewerViewEvents) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ import android.content.Context
import android.os.Bundle
import android.view.View
import androidx.annotation.CallSuper
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.preference.PreferenceFragmentCompat
import com.airbnb.mvrx.MavericksView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
Expand All @@ -35,6 +37,7 @@ import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.plan.MobileScreen
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session
import reactivecircus.flowbinding.android.view.clicks
import timber.log.Timber
Expand Down Expand Up @@ -66,13 +69,19 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), Maverick
* ViewEvents
* ========================================================================================== */

protected fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) {
viewEvents
.stream()
.onEach {
observer(it)
}
.launchIn(viewLifecycleOwner.lifecycleScope)
protected fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(
observer: (T) -> Unit,
) {
val tag = this@VectorSettingsBaseFragment::class.simpleName.toString()
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
viewEvents
.stream(tag)
.collect {
observer(it)
}
}
}
}

/* ==========================================================================================
Expand Down
2 changes: 1 addition & 1 deletion vector/src/test/java/im/vector/app/test/Extensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
val testResultCollectingScope = CoroutineScope(Dispatchers.Unconfined)
val state = stateFlow.test(testResultCollectingScope)
val viewEvents = viewEvents.stream().test(testResultCollectingScope)
val viewEvents = viewEvents.stream("test").test(testResultCollectingScope)
return ViewModelTest(state, viewEvents)
}

Expand Down

0 comments on commit 93021a6

Please sign in to comment.