Hi everyone! I have some experience with coroutine...
# android
g
Hi everyone! I have some experience with coroutine but none with Flow and I'm trying to use them in one of my view. I have my ViewModel `HelpViewmodel`and my view
HelpView
. The view is a custom view that extends
FrameLayout
and
LifecycleOwner
. The view is sometimes being hidden, sometimes being shown. So it goes through the states
onAttachedToWindow
and
onDetachedFromWindow
where I set my life cycle state:
Copy code
override fun onAttachedToWindow() {
    super.onAttachedToWindow()
    lifecycleRegistry.currentState = Lifecycle.State.RESUMED
  }

  override fun onDetachedFromWindow() {
    super.onDetachedFromWindow()
    lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
  }
From my
init
in the view I am collection the
uiState
, a StateFlow emited by my ViewModel.
Copy code
viewModel.uiState.collectIn(this) { uiState ->
  loaderManager.dismiss()
  errorNetwork.isVisible = uiState is HelpViewModel.UiState.Error
  when (uiState) {
    is HelpViewModel.UiState.Loaded -> showContent(<http://uiState.help|uiState.help>)
    HelpViewModel.UiState.Loading -> loaderManager.show(R.string.help_loading)
    else -> return@collectIn
  }
}
The
collectIn
function is an extension function:
Copy code
inline fun <T> Flow<T>.collectIn(
  owner: LifecycleOwner,
  crossinline onCollect: suspend (T) -> Unit
) = owner.lifecycleScope.launch { owner.repeatOnLifecycle(Lifecycle.State.STARTED) { collect { onCollect(it) } } }
My view is receiving correctly the new states at first but once it's hidden it stops collection the new uiState. My viewModel still emits some new state but the view seems to have stop subscribing to it or is not receiving it for some reason. Any idea why?
a
Documentation of `repeatOnLifecycle`:
Runs the given block in a new coroutine when this
Lifecycle
is at least at state and suspends the execution until this
Lifecycle
is
Lifecycle.State.DESTROYED
.
Setting state to
DESTROYED
in
onDetachedFromWindow
may be the culprit.
👍 1
g
Not setting the state to
Lifecycle.State.DESTROYED
solve the problem but it also means it never stop observing this sateflow. In that case i might not have a choice but I wonder if there is a better way to handle StateFlow from a custom view? In this case also this view should be a fragment but it's an old project I don't really have the time to change for now but I could solve the problem...
r
@alex.krupa but then it restarts that block whenever it is back to at least STARTED state
a
@Radoslaw Juszczyk Well, yeah. But isn't that the point here when using
repeatOnLifecycle(STARTED)
? Here's another part of the method's documentation:
Runs the block of code in a coroutine when the lifecycle is at least
STARTED
.
The coroutine will be cancelled when the
ON_STOP
event happens and will
restart executing if the lifecycle receives the
ON_START
event again.
@Galou Minisini I haven't used it myself, but you can look into ViewTreeLifecycleOwner. `ViewTreeLifecycleOwner.get`:
Retrieve the
LifecycleOwner
responsible for managing the given
View
. This may be used to scope work or heavyweight resources associated with the view that may span cycles of the view becoming detached and reattached from a window.
Alternatively, maybe you don't need to rely on
Lifecycle
at all? Instead have a
CoroutineScope
backed by a
SupervisorJob
,
collect
the flow in
onAttachedFromWindow
and cancel the scope in
onDetachedFromWindow
. This is just a rough idea which I haven't tested. However, I remember something similar being done back when RxJava was more popular and I don't see why this wouldn't be doable with coroutines.
r
@alex.krupa 'Setting state to
DESTROYED
in
onDetachedFromWindow
may be the culprit.' - I was relating to this, as per the docs the block inside repeatOnLifecycle should be reexecuted when it is back to at least STARTED. Imo the problem is somewhere else. Maybe lifecycleRegistry is instantiated again at some point and State.RESUMED is passed to the new instance (not the one which was used to start the collection).
a
Are you sure it will be re-executed after it's been destroyed? The repeating job should be completed then, which is confirmed by this test and this implementation line. I may be wrong here, but I always considered
onDestroy
as a terminal event in Android-world. Once something is destroyed it's not reusable, you can only create a new something.
r
@alex.krupatrue, it looks like when DESTROYED state is passed the repeatOnLifecycle do not expect any updates anymore and therefore passing RESUMED has no effect. btw I think he should use State.STARTED and State.STOPPED, instead RESUMED / DESTROYED, first of all it is strange when there is no symetry, second of all as you suggested DESTROYED seems to be finishing it without waiting for any other states.
g
There is no
State.STOPPED
Copy code
public enum State {
        /**
         * Destroyed state for a LifecycleOwner. After this event, this Lifecycle will not dispatch
         * any more events. For instance, for an {@link android.app.Activity}, this state is reached
         * <b>right before</b> Activity's {@link android.app.Activity#onDestroy() onDestroy} call.
         */
        DESTROYED,

        /**
         * Initialized state for a LifecycleOwner. For an {@link android.app.Activity}, this is
         * the state when it is constructed but has not received
         * {@link android.app.Activity#onCreate(android.os.Bundle) onCreate} yet.
         */
        INITIALIZED,

        /**
         * Created state for a LifecycleOwner. For an {@link android.app.Activity}, this state
         * is reached in two cases:
         * <ul>
         *     <li>after {@link android.app.Activity#onCreate(android.os.Bundle) onCreate} call;
         *     <li><b>right before</b> {@link android.app.Activity#onStop() onStop} call.
         * </ul>
         */
        CREATED,

        /**
         * Started state for a LifecycleOwner. For an {@link android.app.Activity}, this state
         * is reached in two cases:
         * <ul>
         *     <li>after {@link android.app.Activity#onStart() onStart} call;
         *     <li><b>right before</b> {@link android.app.Activity#onPause() onPause} call.
         * </ul>
         */
        STARTED,

        /**
         * Resumed state for a LifecycleOwner. For an {@link android.app.Activity}, this state
         * is reached after {@link android.app.Activity#onResume() onResume} is called.
         */
        RESUMED;

        /**
         * Compares if this State is greater or equal to the given {@code state}.
         *
         * @param state State to compare with
         * @return true if this State is greater or equal to the given {@code state}
         */
        public boolean isAtLeast(@NonNull State state) {
            return compareTo(state) >= 0;
        }
    }
r
It says Destroyed State for a lifecycle owner. After this event , this lifecycle will not dispatch any more events. So maybe you should replace it with State.CREATED as this is the on called before onStop
You are right about Stopped - I just assumed it existed