-
Notifications
You must be signed in to change notification settings - Fork 17
/
ScreenEffects.kt
141 lines (131 loc) · 4.8 KB
/
ScreenEffects.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
package com.github.terrakok.modo.lifecycle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisallowComposableCalls
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.DisposableEffectResult
import androidx.compose.runtime.DisposableEffectScope
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.LifecycleObserver
import com.github.terrakok.modo.ExperimentalModoApi
import com.github.terrakok.modo.Screen
import com.github.terrakok.modo.model.DependencyKey
import com.github.terrakok.modo.model.ON_SCREEN_REMOVED_CALLBACK_NAME
import com.github.terrakok.modo.model.ScreenModelStore
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import java.util.UUID
/**
* A side effect of screen creation that must be reversed or cleaned up if the Screen leaves the hierarchy.
* You can make any suspend invocation inside [block],
* but the [CoroutineScope] in witch this block is running will be canceled as soon as screen leaves hierarchy.
*/
@ExperimentalModoApi
@Composable
fun Screen.LaunchedScreenEffect(
tag: String = rememberSaveable { UUID.randomUUID().toString() },
block: suspend CoroutineScope.() -> Unit
) {
val latestBlock by rememberUpdatedState(newValue = block)
LaunchedEffect(this) {
ScreenModelStore.getOrPutDependency<CoroutineScope>(
screen = this@LaunchedScreenEffect,
name = "LaunchedScreenEffect",
tag = tag,
onDispose = { scope ->
scope.cancel()
},
factory = { key ->
screenCoroutineMainScope(key).also { scope ->
scope.launch {
latestBlock()
}
}
}
)
}
}
/**
* A side effect of screen creation that must be reversed or cleaned up if the Screen leaves the hierarchy.
* Similar to the [DisposableEffect], but
* 1. [effect] lambda called once per screen
* 2. `onDispose` is called when the screen lives hierarchy
*/
@Suppress("LambdaParameterInRestartableEffect")
@ExperimentalModoApi
@Composable
fun Screen.DisposableScreenEffect(
tag: String = rememberSaveable { UUID.randomUUID().toString() },
effect: DisposableEffectScope.() -> DisposableEffectResult
) {
LaunchedEffect(this) {
ScreenModelStore.getOrPutDependency<DisposableScreenEffectImpl>(
screen = this@DisposableScreenEffect,
name = "DisposableScreenEffect",
tag = tag,
onDispose = { disposableScreenEffect -> disposableScreenEffect.onDisposed() },
factory = { _ ->
DisposableScreenEffectImpl(effect)
}
)
}
}
/**
* A shortcut to subscribe to screen lifecycle. Uses [DisposableScreenEffect] under the hood.
* Automatically creates observer, adds it to screen lifecycle and removes it, when screen disposed.
*/
@ExperimentalModoApi
@Composable
fun Screen.LifecycleScreenEffect(
lifecycleObserverFactory: () -> LifecycleObserver
) {
val lifecycleOwner = LocalLifecycleOwner.current
DisposableScreenEffect(
tag = rememberSaveable { "LifecycleScreenEffect" + UUID.randomUUID().toString() }
) {
val observer = lifecycleObserverFactory()
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
@Suppress("LambdaParameterInRestartableEffect")
@ExperimentalModoApi
@Composable
inline fun Screen.OnScreenRemoved(
tag: String = rememberSaveable { UUID.randomUUID().toString() },
crossinline onScreenRemoved: @DisallowComposableCalls () -> Unit
) {
LaunchedEffect(tag) {
ScreenModelStore.getOrPutDependency(
screen = this@OnScreenRemoved,
name = ON_SCREEN_REMOVED_CALLBACK_NAME,
tag = tag,
onDispose = { onScreenRemoved() },
factory = { Any() }
)
}
}
private val InternalDisposableScreenEffectScope = DisposableEffectScope()
private class DisposableScreenEffectImpl(
effect: DisposableEffectScope.() -> DisposableEffectResult
) {
private var onDispose: DisposableEffectResult? = null
init {
onDispose = InternalDisposableScreenEffectScope.effect()
}
fun onDisposed() {
onDispose?.dispose()
onDispose = null
}
}
private fun screenCoroutineMainScope(key: DependencyKey) =
CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate + CoroutineName(key))