Skip to content

Commit ece260b

Browse files
authored
Merge pull request #59 from ikarenkov/pause-resume-sync-with-animation
Added emitting ON_RESUME and ON_PAUSE for transition ...
2 parents ae85a1d + 9b06d27 commit ece260b

File tree

13 files changed

+336
-149
lines changed

13 files changed

+336
-149
lines changed

Diff for: gradle/libs.versions.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[versions]
22
leakcanaryAndroid = "2.14"
3-
modo = "0.10.0-alpha2"
3+
modo = "0.10.0-alpha3"
44
androidGradlePlugin = "8.4.0"
55
detektComposeVersion = "0.3.20"
66
detektVersion = "1.23.6"

Diff for: modo-compose/src/main/java/com/github/terrakok/modo/ComposeRender.kt

+34-19
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ import androidx.compose.runtime.saveable.rememberSaveable
1313
import androidx.compose.runtime.setValue
1414
import androidx.compose.runtime.staticCompositionLocalOf
1515
import androidx.compose.ui.Modifier
16+
import androidx.lifecycle.Lifecycle.Event.ON_PAUSE
17+
import androidx.lifecycle.Lifecycle.Event.ON_RESUME
18+
import androidx.lifecycle.Lifecycle.Event.ON_START
19+
import androidx.lifecycle.Lifecycle.Event.ON_STOP
1620
import com.github.terrakok.modo.android.ModoScreenAndroidAdapter
21+
import com.github.terrakok.modo.animation.ScreenTransition
1722
import com.github.terrakok.modo.animation.displayingScreensAfterScreenContent
1823
import com.github.terrakok.modo.animation.displayingScreensBeforeScreenContent
1924
import com.github.terrakok.modo.lifecycle.LifecycleDependency
@@ -44,11 +49,22 @@ private val LocalAfterScreenContentOnDispose = staticCompositionLocalOf<() -> Un
4449
* 2. Adds support of Android-related features, such as ViewModel, LifeCycle and SavedStateHandle.
4550
* 3. Handles lifecycle of [Screen] by adding [DisposableEffect] before and after content, in order to notify [ComposeRenderer]
4651
* when [Screen.Content] is about to leave composition and when it has left composition.
52+
* @param modifier is a modifier that will be passed into [Screen.Content]
53+
* @param manualResumePause define whenever we are going to manually call [LifecycleDependency.onResume] and [LifecycleDependency.onPause]
54+
* to emmit [ON_RESUME] and [ON_PAUSE]. Otherwise, [ON_RESUME] will be called straight after [ON_START] and [ON_PAUSE] will be called straight
55+
* before [ON_STOP].
56+
*
57+
* F.e. it is used by [ScreenTransition]:
58+
* + [ON_RESUME] emitted when animation of showing screen is finished
59+
* + [ON_PAUSE] emitted when animation of hiding screen is started
4760
*/
4861
@Composable
49-
fun Screen.SaveableContent(modifier: Modifier = Modifier) {
62+
fun Screen.SaveableContent(
63+
modifier: Modifier = Modifier,
64+
manualResumePause: Boolean = false
65+
) {
5066
LocalSaveableStateHolder.currentOrThrow.SaveableStateProvider(key = screenKey) {
51-
ModoScreenAndroidAdapter.get(this).ProvideAndroidIntegration {
67+
ModoScreenAndroidAdapter.get(this).ProvideAndroidIntegration(manualResumePause) {
5268
BeforeScreenContent()
5369
Content(modifier)
5470
AfterScreenContent()
@@ -143,7 +159,7 @@ internal class ComposeRenderer<State : NavigationState>(
143159
// to let Screen.Content to handle ON_DISPOSE by using functions like DisposableEffect
144160
val afterScreenContentOnDispose = remember {
145161
{
146-
afterScreenContentOnDispose()
162+
onPreDispose()
147163
}
148164
}
149165

@@ -163,6 +179,10 @@ internal class ComposeRenderer<State : NavigationState>(
163179
* @param clearAll - forces to remove all screen states that renderer holds (removed and "displayed")
164180
*/
165181
private fun clearScreens(stateHolder: SaveableStateHolder, clearAll: Boolean = false) {
182+
fun Iterable<Screen>.clearStates(stateHolder: SaveableStateHolder) = forEach { screen ->
183+
screen.clearState(stateHolder)
184+
}
185+
166186
if (clearAll) {
167187
state?.getChildScreens()?.clearStates(stateHolder)
168188
}
@@ -178,26 +198,21 @@ internal class ComposeRenderer<State : NavigationState>(
178198
}
179199

180200
/**
181-
* Clear states of removed screens from given [stateHolder].
182-
* @param stateHolder - SaveableStateHolder that contains screen states
183-
* @param clearAll - forces to remove all screen states that renderer holds (removed and "displayed")
201+
* Called onPreDispose for removed screens
202+
* @param clearAll - forces to call onPreDispose on all children screen states that renderer holds (removed and "displayed")
184203
*/
185-
private fun afterScreenContentOnDispose(clearAll: Boolean = false) {
204+
private fun onPreDispose(clearAll: Boolean = false) {
205+
fun Iterable<Screen>.onPreDispose() = forEach { screen ->
206+
screen.onPreDispose()
207+
}
208+
186209
if (clearAll) {
187-
state?.getChildScreens()?.afterScreenContentOnDispose()
210+
state?.getChildScreens()?.onPreDispose()
188211
}
189212
// There can be several transition of different screens on the screen,
190213
// so it is important properly clear screens that are not visible for user.
191214
val safeToRemove = removedScreens.filter { it !in displayingScreensAfterScreenContent }
192-
safeToRemove.afterScreenContentOnDispose()
193-
}
194-
195-
private fun Iterable<Screen>.clearStates(stateHolder: SaveableStateHolder) = forEach { screen ->
196-
screen.clearState(stateHolder)
197-
}
198-
199-
private fun Iterable<Screen>.afterScreenContentOnDispose() = forEach { screen ->
200-
screen.afterScreenContentOnDispose()
215+
safeToRemove.onPreDispose()
201216
}
202217

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

219234
// need for correct handling lifecycle
220-
private fun Screen.afterScreenContentOnDispose() {
235+
private fun Screen.onPreDispose() {
221236
// Log.d("LifecycleDebug", "afterScreenContentOnDispose $screenKey")
222237
dependenciesSortedByRemovePriority()
223238
.filterIsInstance<LifecycleDependency>()
224239
.forEach { it.onPreDispose() }
225240
// send afterScreenContentOnDispose to nested screens
226-
((this as? ContainerScreen<*, *>)?.renderer as? ComposeRenderer<*>)?.afterScreenContentOnDispose(clearAll = true)
241+
((this as? ContainerScreen<*, *>)?.renderer as? ComposeRenderer<*>)?.onPreDispose(clearAll = true)
227242
}
228243

229244
private fun calculateRemovedScreens(oldState: NavigationState, newState: NavigationState): List<Screen> {

Diff for: modo-compose/src/main/java/com/github/terrakok/modo/android/ModoScreenAndroidAdapter.kt

+94-73
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
1818
import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
1919
import androidx.lifecycle.HasDefaultViewModelProviderFactory
2020
import androidx.lifecycle.Lifecycle
21+
import androidx.lifecycle.Lifecycle.Event.ON_CREATE
22+
import androidx.lifecycle.Lifecycle.Event.ON_DESTROY
23+
import androidx.lifecycle.Lifecycle.Event.ON_PAUSE
24+
import androidx.lifecycle.Lifecycle.Event.ON_RESUME
25+
import androidx.lifecycle.Lifecycle.Event.ON_START
26+
import androidx.lifecycle.Lifecycle.Event.ON_STOP
2127
import androidx.lifecycle.LifecycleEventObserver
2228
import androidx.lifecycle.LifecycleObserver
2329
import androidx.lifecycle.LifecycleOwner
@@ -99,34 +105,14 @@ class ModoScreenAndroidAdapter private constructor(
99105
enableSavedStateHandles()
100106
}
101107

102-
private fun onCreate(savedState: Bundle?) {
103-
check(!isCreated) { "onCreate already called" }
104-
isCreated = true
105-
controller.performRestore(savedState)
106-
initEvents.forEach {
107-
lifecycle.safeHandleLifecycleEvent(it)
108-
}
109-
}
110-
111-
private fun emitOnStartEvents() {
112-
startEvents.forEach {
113-
lifecycle.safeHandleLifecycleEvent(it)
114-
}
115-
}
116-
117-
private fun emitOnStopEvents() {
118-
stopEvents.forEach {
119-
lifecycle.safeHandleLifecycleEvent(it)
120-
}
121-
}
122-
123108
@Composable
124109
fun ProvideAndroidIntegration(
125-
content: @Composable () -> Unit
110+
manualResumePause: Boolean = false,
111+
content: @Composable () -> Unit,
126112
) {
127113
val context: Context = LocalContext.current
128114
val parentLifecycleOwner = LocalLifecycleOwner.current
129-
LifecycleDisposableEffect(context, parentLifecycleOwner) {
115+
LifecycleDisposableEffect(context, parentLifecycleOwner, manualResumePause) {
130116
@Suppress("SpreadOperator")
131117
CompositionLocalProvider(*getProviders()) {
132118
content()
@@ -141,18 +127,31 @@ class ModoScreenAndroidAdapter private constructor(
141127
*/
142128
override fun onPreDispose() {
143129
// Log.d("LifecycleDebug", "${screen.screenKey} ModoScreenAndroidAdapter.onPreDispose, emit ON_DESTROY event.")
144-
disposeEvents.forEach { event ->
145-
lifecycle.safeHandleLifecycleEvent(event)
146-
}
130+
safeHandleLifecycleEvent(ON_DESTROY)
131+
}
132+
133+
override fun onPause() {
134+
safeHandleLifecycleEvent(ON_PAUSE)
135+
}
136+
137+
override fun onResume() {
138+
safeHandleLifecycleEvent(ON_RESUME)
147139
}
148140

141+
override fun toString(): String = "${ModoScreenAndroidAdapter::class.simpleName}, screenKey: ${screen.screenKey}"
142+
149143
@Suppress("UnusedParameter")
150144
private fun onDispose() {
151145
// Log.d("LifecycleDebug", "${screen.screenKey} ModoScreenAndroidAdapter.onDispose. Clear ViewModelStore.")
152146
viewModelStore.clear()
153147
}
154148

155-
override fun toString(): String = "${ModoScreenAndroidAdapter::class.simpleName}, screenKey: ${screen.screenKey}"
149+
private fun onCreate(savedState: Bundle?) {
150+
check(!isCreated) { "onCreate already called" }
151+
isCreated = true
152+
controller.performRestore(savedState)
153+
safeHandleLifecycleEvent(ON_CREATE)
154+
}
156155

157156
private fun performSave(outState: Bundle) {
158157
controller.performSave(outState)
@@ -209,6 +208,7 @@ class ModoScreenAndroidAdapter private constructor(
209208
private fun LifecycleDisposableEffect(
210209
context: Context,
211210
parentLifecycleOwner: LifecycleOwner,
211+
manualResumePause: Boolean,
212212
content: @Composable () -> Unit
213213
) {
214214
val activity = remember(context) {
@@ -220,34 +220,31 @@ class ModoScreenAndroidAdapter private constructor(
220220
}
221221

222222
DisposableEffect(this) {
223-
emitOnStartEvents()
223+
safeHandleLifecycleEvent(ON_START)
224+
if (!manualResumePause) {
225+
safeHandleLifecycleEvent(ON_RESUME)
226+
}
224227
onDispose { }
225228
}
226229

227230
content()
228231

229232
DisposableEffect(this) {
230233
val unregisterLifecycle = registerParentLifecycleListener(parentLifecycleOwner) {
231-
LifecycleEventObserver { owner, event ->
232-
when {
233-
/**
234-
* Instance of the screen isn't recreated during config changes so skip this event
235-
* to avoid crash while accessing to ViewModel with SavedStateHandle, because after
236-
* ON_DESTROY, [androidx.lifecycle.SavedStateHandleController] is marked as not
237-
* attached and next call of [registerSavedStateProvider] after recreating Activity
238-
* on the same instance causing the crash.
239-
*
240-
* Also when activity is destroyed, but not finished, screen is not destroyed.
241-
*
242-
* In the case of Fragments, we unsubscribe before ON_DESTROY event, so there is no problem with this.
243-
*/
244-
event == Lifecycle.Event.ON_DESTROY && (activity?.isFinishing == false || activity?.isChangingConfigurations == true) ->
245-
return@LifecycleEventObserver
246-
// when the Application goes to background, perform save
247-
event == Lifecycle.Event.ON_STOP ->
248-
performSave(savedState)
234+
LifecycleEventObserver { _, event ->
235+
// when the Application goes to background, perform save
236+
if (event == ON_STOP) {
237+
performSave(savedState)
238+
}
239+
if (
240+
needPropagateLifecycleEventFromParent(
241+
event,
242+
isActivityFinishing = activity?.isFinishing,
243+
isChangingConfigurations = activity?.isChangingConfigurations
244+
)
245+
) {
246+
safeHandleLifecycleEvent(event)
249247
}
250-
lifecycle.safeHandleLifecycleEvent(event)
251248
}
252249
}
253250

@@ -257,53 +254,77 @@ class ModoScreenAndroidAdapter private constructor(
257254
// when the screen goes to stack, perform save
258255
performSave(savedState)
259256
// notify lifecycle screen listeners
260-
emitOnStopEvents()
257+
if (!manualResumePause) {
258+
safeHandleLifecycleEvent(ON_PAUSE)
259+
}
260+
safeHandleLifecycleEvent(ON_STOP)
261261
}
262262
}
263263
}
264264

265-
private fun LifecycleRegistry.safeHandleLifecycleEvent(event: Lifecycle.Event) {
266-
val currentState = currentState
267-
val skippEvent = !currentState.isAtLeast(Lifecycle.State.INITIALIZED) ||
268-
// Protection from double event sending from the parent
269-
((event in startEvents || event in initEvents) && event.targetState <= currentState) ||
270-
(event in stopEvents && event.targetState >= currentState)
271-
272-
// For debugging
273-
// Log.d("ModoScreenAndroidAdapter", "safeHandleLifecycleEvent ${screen.screenKey} $event")
265+
private fun safeHandleLifecycleEvent(event: Lifecycle.Event) {
266+
val skippEvent = needSkipEvent(lifecycle.currentState, event)
274267
if (!skippEvent) {
275-
handleLifecycleEvent(event)
268+
// Log.d("ModoScreenAndroidAdapter", "${screen.screenKey} handleLifecycleEvent $event")
269+
lifecycle.handleLifecycleEvent(event)
276270
}
277271
}
278272

279273
companion object {
280274

281-
private val initEvents = arrayOf(
282-
Lifecycle.Event.ON_CREATE
275+
private val moveLifecycleStateUpEvents = setOf(
276+
ON_CREATE,
277+
ON_START,
278+
ON_RESUME
283279
)
284280

285-
private val startEvents = arrayOf(
286-
Lifecycle.Event.ON_START,
287-
Lifecycle.Event.ON_RESUME
288-
)
289-
290-
private val stopEvents = arrayOf(
291-
Lifecycle.Event.ON_PAUSE,
292-
Lifecycle.Event.ON_STOP
293-
)
294-
295-
private val disposeEvents = arrayOf(
296-
Lifecycle.Event.ON_DESTROY
281+
private val moveLifecycleStateDownEvents = setOf(
282+
ON_STOP,
283+
ON_PAUSE,
284+
ON_DESTROY
297285
)
298286

299287
/**
300288
* Creates delegate for integration with android for the given [screen] or returns existed from cache.
301289
*/
290+
@JvmStatic
302291
fun get(screen: Screen): ModoScreenAndroidAdapter =
303292
ScreenModelStore.getOrPutDependency(
304293
screen = screen,
305-
name = "AndroidScreenLifecycleOwner",
294+
name = LifecycleDependency.KEY,
306295
onDispose = { it.onDispose() },
307296
) { ModoScreenAndroidAdapter(screen) }
297+
298+
@JvmStatic
299+
fun needPropagateLifecycleEventFromParent(
300+
event: Lifecycle.Event,
301+
isActivityFinishing: Boolean?,
302+
isChangingConfigurations: Boolean?
303+
) =
304+
/*
305+
* Instance of the screen isn't recreated during config changes so skip this event
306+
* to avoid crash while accessing to ViewModel with SavedStateHandle, because after
307+
* ON_DESTROY, [androidx.lifecycle.SavedStateHandleController] is marked as not
308+
* attached and next call of [registerSavedStateProvider] after recreating Activity
309+
* on the same instance causing the crash.
310+
*
311+
* Also, when activity is destroyed, but not finished, screen is not destroyed.
312+
*
313+
* In the case of Fragments, we unsubscribe before ON_DESTROY event, so there is no problem with this.
314+
*/
315+
if (event == ON_DESTROY && (isActivityFinishing == false || isChangingConfigurations == true)) {
316+
false
317+
} else {
318+
// Parent can only move lifecycle state down. Because parent cant be already resumed, but child is not, because of running animation.
319+
event !in moveLifecycleStateUpEvents
320+
}
321+
322+
@JvmStatic
323+
internal fun needSkipEvent(currentState: Lifecycle.State, event: Lifecycle.Event) =
324+
!currentState.isAtLeast(Lifecycle.State.INITIALIZED) ||
325+
// Skipping events that moves lifecycle state up, but this state is already reached.
326+
(event in moveLifecycleStateUpEvents && event.targetState <= currentState) ||
327+
// Skipping events that moves lifecycle state down, but this state is already reached.
328+
(event in moveLifecycleStateDownEvents && event.targetState >= currentState)
308329
}
309330
}

0 commit comments

Comments
 (0)