Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added emitting ON_RESUME and ON_PAUSE for transition ... #59

Merged
merged 5 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.lifecycle.Lifecycle.Event.ON_PAUSE
import androidx.lifecycle.Lifecycle.Event.ON_RESUME
import androidx.lifecycle.Lifecycle.Event.ON_START
import androidx.lifecycle.Lifecycle.Event.ON_STOP
import com.github.terrakok.modo.android.ModoScreenAndroidAdapter
import com.github.terrakok.modo.animation.ScreenTransition
import com.github.terrakok.modo.animation.displayingScreensAfterScreenContent
import com.github.terrakok.modo.animation.displayingScreensBeforeScreenContent
import com.github.terrakok.modo.lifecycle.LifecycleDependency
Expand Down Expand Up @@ -44,11 +49,22 @@ private val LocalAfterScreenContentOnDispose = staticCompositionLocalOf<() -> Un
* 2. Adds support of Android-related features, such as ViewModel, LifeCycle and SavedStateHandle.
* 3. Handles lifecycle of [Screen] by adding [DisposableEffect] before and after content, in order to notify [ComposeRenderer]
* when [Screen.Content] is about to leave composition and when it has left composition.
* @param modifier is a modifier that will be passed into [Screen.Content]
* @param manualResumePause define whenever we are going to manually call [LifecycleDependency.onResume] and [LifecycleDependency.onPause]
* to emmit [ON_RESUME] and [ON_PAUSE]. Otherwise, [ON_RESUME] will be called straight after [ON_START] and [ON_PAUSE] will be called straight
* before [ON_STOP].
*
* F.e. it is used by [ScreenTransition]:
* + [ON_RESUME] emitted when animation of showing screen is finished
* + [ON_PAUSE] emitted when animation of hiding screen is started
*/
@Composable
fun Screen.SaveableContent(modifier: Modifier = Modifier) {
fun Screen.SaveableContent(
modifier: Modifier = Modifier,
manualResumePause: Boolean = false
) {
LocalSaveableStateHolder.currentOrThrow.SaveableStateProvider(key = screenKey) {
ModoScreenAndroidAdapter.get(this).ProvideAndroidIntegration {
ModoScreenAndroidAdapter.get(this).ProvideAndroidIntegration(manualResumePause) {
BeforeScreenContent()
Content(modifier)
AfterScreenContent()
Expand Down Expand Up @@ -143,7 +159,7 @@ internal class ComposeRenderer<State : NavigationState>(
// to let Screen.Content to handle ON_DISPOSE by using functions like DisposableEffect
val afterScreenContentOnDispose = remember {
{
afterScreenContentOnDispose()
onPreDispose()
}
}

Expand All @@ -163,6 +179,10 @@ internal class ComposeRenderer<State : NavigationState>(
* @param clearAll - forces to remove all screen states that renderer holds (removed and "displayed")
*/
private fun clearScreens(stateHolder: SaveableStateHolder, clearAll: Boolean = false) {
fun Iterable<Screen>.clearStates(stateHolder: SaveableStateHolder) = forEach { screen ->
screen.clearState(stateHolder)
}

if (clearAll) {
state?.getChildScreens()?.clearStates(stateHolder)
}
Expand All @@ -178,26 +198,21 @@ internal class ComposeRenderer<State : NavigationState>(
}

/**
* Clear states of removed screens from given [stateHolder].
* @param stateHolder - SaveableStateHolder that contains screen states
* @param clearAll - forces to remove all screen states that renderer holds (removed and "displayed")
* Called onPreDispose for removed screens
* @param clearAll - forces to call onPreDispose on all children screen states that renderer holds (removed and "displayed")
*/
private fun afterScreenContentOnDispose(clearAll: Boolean = false) {
private fun onPreDispose(clearAll: Boolean = false) {
fun Iterable<Screen>.onPreDispose() = forEach { screen ->
screen.onPreDispose()
}

if (clearAll) {
state?.getChildScreens()?.afterScreenContentOnDispose()
state?.getChildScreens()?.onPreDispose()
}
// There can be several transition of different screens on the screen,
// so it is important properly clear screens that are not visible for user.
val safeToRemove = removedScreens.filter { it !in displayingScreensAfterScreenContent }
safeToRemove.afterScreenContentOnDispose()
}

private fun Iterable<Screen>.clearStates(stateHolder: SaveableStateHolder) = forEach { screen ->
screen.clearState(stateHolder)
}

private fun Iterable<Screen>.afterScreenContentOnDispose() = forEach { screen ->
screen.afterScreenContentOnDispose()
safeToRemove.onPreDispose()
}

private fun Screen.clearState(stateHolder: SaveableStateHolder) {
Expand All @@ -217,13 +232,13 @@ internal class ComposeRenderer<State : NavigationState>(
}

// need for correct handling lifecycle
private fun Screen.afterScreenContentOnDispose() {
private fun Screen.onPreDispose() {
// Log.d("LifecycleDebug", "afterScreenContentOnDispose $screenKey")
dependenciesSortedByRemovePriority()
.filterIsInstance<LifecycleDependency>()
.forEach { it.onPreDispose() }
// send afterScreenContentOnDispose to nested screens
((this as? ContainerScreen<*, *>)?.renderer as? ComposeRenderer<*>)?.afterScreenContentOnDispose(clearAll = true)
((this as? ContainerScreen<*, *>)?.renderer as? ComposeRenderer<*>)?.onPreDispose(clearAll = true)
}

private fun calculateRemovedScreens(oldState: NavigationState, newState: NavigationState): List<Screen> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
import androidx.lifecycle.HasDefaultViewModelProviderFactory
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.Lifecycle.Event.ON_CREATE
import androidx.lifecycle.Lifecycle.Event.ON_DESTROY
import androidx.lifecycle.Lifecycle.Event.ON_PAUSE
import androidx.lifecycle.Lifecycle.Event.ON_RESUME
import androidx.lifecycle.Lifecycle.Event.ON_START
import androidx.lifecycle.Lifecycle.Event.ON_STOP
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner
Expand Down Expand Up @@ -99,34 +105,14 @@
enableSavedStateHandles()
}

private fun onCreate(savedState: Bundle?) {
check(!isCreated) { "onCreate already called" }
isCreated = true
controller.performRestore(savedState)
initEvents.forEach {
lifecycle.safeHandleLifecycleEvent(it)
}
}

private fun emitOnStartEvents() {
startEvents.forEach {
lifecycle.safeHandleLifecycleEvent(it)
}
}

private fun emitOnStopEvents() {
stopEvents.forEach {
lifecycle.safeHandleLifecycleEvent(it)
}
}

@Composable
fun ProvideAndroidIntegration(
content: @Composable () -> Unit
manualResumePause: Boolean = false,
content: @Composable () -> Unit,
) {
val context: Context = LocalContext.current
val parentLifecycleOwner = LocalLifecycleOwner.current
LifecycleDisposableEffect(context, parentLifecycleOwner) {
LifecycleDisposableEffect(context, parentLifecycleOwner, manualResumePause) {
@Suppress("SpreadOperator")
CompositionLocalProvider(*getProviders()) {
content()
Expand All @@ -141,18 +127,31 @@
*/
override fun onPreDispose() {
// Log.d("LifecycleDebug", "${screen.screenKey} ModoScreenAndroidAdapter.onPreDispose, emit ON_DESTROY event.")
disposeEvents.forEach { event ->
lifecycle.safeHandleLifecycleEvent(event)
}
safeHandleLifecycleEvent(ON_DESTROY)
}

override fun onPause() {
safeHandleLifecycleEvent(ON_PAUSE)
}

override fun onResume() {
safeHandleLifecycleEvent(ON_RESUME)
}

override fun toString(): String = "${ModoScreenAndroidAdapter::class.simpleName}, screenKey: ${screen.screenKey}"

@Suppress("UnusedParameter")
private fun onDispose() {
// Log.d("LifecycleDebug", "${screen.screenKey} ModoScreenAndroidAdapter.onDispose. Clear ViewModelStore.")
viewModelStore.clear()
}

override fun toString(): String = "${ModoScreenAndroidAdapter::class.simpleName}, screenKey: ${screen.screenKey}"
private fun onCreate(savedState: Bundle?) {
check(!isCreated) { "onCreate already called" }
isCreated = true
controller.performRestore(savedState)
safeHandleLifecycleEvent(ON_CREATE)
}

private fun performSave(outState: Bundle) {
controller.performSave(outState)
Expand Down Expand Up @@ -209,6 +208,7 @@
private fun LifecycleDisposableEffect(
context: Context,
parentLifecycleOwner: LifecycleOwner,
manualResumePause: Boolean,
content: @Composable () -> Unit
) {
val activity = remember(context) {
Expand All @@ -220,34 +220,31 @@
}

DisposableEffect(this) {
emitOnStartEvents()
safeHandleLifecycleEvent(ON_START)
if (!manualResumePause) {
safeHandleLifecycleEvent(ON_RESUME)
}
onDispose { }
}

content()

DisposableEffect(this) {
val unregisterLifecycle = registerParentLifecycleListener(parentLifecycleOwner) {
LifecycleEventObserver { owner, event ->
when {
/**
* Instance of the screen isn't recreated during config changes so skip this event
* to avoid crash while accessing to ViewModel with SavedStateHandle, because after
* ON_DESTROY, [androidx.lifecycle.SavedStateHandleController] is marked as not
* attached and next call of [registerSavedStateProvider] after recreating Activity
* on the same instance causing the crash.
*
* Also when activity is destroyed, but not finished, screen is not destroyed.
*
* In the case of Fragments, we unsubscribe before ON_DESTROY event, so there is no problem with this.
*/
event == Lifecycle.Event.ON_DESTROY && (activity?.isFinishing == false || activity?.isChangingConfigurations == true) ->
return@LifecycleEventObserver
// when the Application goes to background, perform save
event == Lifecycle.Event.ON_STOP ->
performSave(savedState)
LifecycleEventObserver { _, event ->
// when the Application goes to background, perform save
if (event == ON_STOP) {
performSave(savedState)
}
if (
needPropagateLifecycleEventFromParent(
event,
isActivityFinishing = activity?.isFinishing,
isChangingConfigurations = activity?.isChangingConfigurations
)
) {
safeHandleLifecycleEvent(event)
}
lifecycle.safeHandleLifecycleEvent(event)
}
}

Expand All @@ -257,43 +254,34 @@
// when the screen goes to stack, perform save
performSave(savedState)
// notify lifecycle screen listeners
emitOnStopEvents()
if (!manualResumePause) {
safeHandleLifecycleEvent(ON_PAUSE)
}
safeHandleLifecycleEvent(ON_STOP)
}
}
}

private fun LifecycleRegistry.safeHandleLifecycleEvent(event: Lifecycle.Event) {
val currentState = currentState
val skippEvent = !currentState.isAtLeast(Lifecycle.State.INITIALIZED) ||
// Protection from double event sending from the parent
((event in startEvents || event in initEvents) && event.targetState <= currentState) ||
(event in stopEvents && event.targetState >= currentState)

// For debugging
// Log.d("ModoScreenAndroidAdapter", "safeHandleLifecycleEvent ${screen.screenKey} $event")
private fun safeHandleLifecycleEvent(event: Lifecycle.Event) {
val skippEvent = needSkipEvent(lifecycle.currentState, event)
Fixed Show fixed Hide fixed
if (!skippEvent) {
handleLifecycleEvent(event)
// Log.d("ModoScreenAndroidAdapter", "${screen.screenKey} handleLifecycleEvent $event")
lifecycle.handleLifecycleEvent(event)
}
}

companion object {

private val initEvents = arrayOf(
Lifecycle.Event.ON_CREATE
private val moveLifecycleStateUpEvents = setOf(
ON_CREATE,
ON_START,
ON_RESUME
)

private val startEvents = arrayOf(
Lifecycle.Event.ON_START,
Lifecycle.Event.ON_RESUME
)

private val stopEvents = arrayOf(
Lifecycle.Event.ON_PAUSE,
Lifecycle.Event.ON_STOP
)

private val disposeEvents = arrayOf(
Lifecycle.Event.ON_DESTROY
private val moveLifecycleStateDownEvents = setOf(
ON_STOP,
ON_PAUSE,
ON_DESTROY
)

/**
Expand All @@ -302,8 +290,38 @@
fun get(screen: Screen): ModoScreenAndroidAdapter =
ScreenModelStore.getOrPutDependency(
screen = screen,
name = "AndroidScreenLifecycleOwner",
name = LifecycleDependency.KEY,
onDispose = { it.onDispose() },
) { ModoScreenAndroidAdapter(screen) }

fun needPropagateLifecycleEventFromParent(
event: Lifecycle.Event,
isActivityFinishing: Boolean?,
isChangingConfigurations: Boolean?
) =
/*
* Instance of the screen isn't recreated during config changes so skip this event
* to avoid crash while accessing to ViewModel with SavedStateHandle, because after
* ON_DESTROY, [androidx.lifecycle.SavedStateHandleController] is marked as not
* attached and next call of [registerSavedStateProvider] after recreating Activity
* on the same instance causing the crash.
*
* Also, when activity is destroyed, but not finished, screen is not destroyed.
*
* In the case of Fragments, we unsubscribe before ON_DESTROY event, so there is no problem with this.
*/
if (event == ON_DESTROY && (isActivityFinishing == false || isChangingConfigurations == true)) {
false
} else {
// Parent can only move lifecycle state down. Because parent cant be already resumed, but child is not, because of running animation.
event !in moveLifecycleStateUpEvents
}

private fun needSkipEvent(currentState: Lifecycle.State, event: Lifecycle.Event) =
!currentState.isAtLeast(Lifecycle.State.INITIALIZED) ||
// Skipping events that moves lifecycle state up, but this state is already reached.
(event in moveLifecycleStateUpEvents && event.targetState <= currentState) ||
// Skipping events that moves lifecycle state down, but this state is already reached.
(event in moveLifecycleStateDownEvents && event.targetState >= currentState)
}
}
Loading