Skip to content

Commit 0d3da1e

Browse files
MatkovIvanigordmn
andauthored
Provide ability to save/restore saveable state on Desktop (#2225)
Fixes [CMP-7992](https://youtrack.jetbrains.com/issue/CMP-7992) ## Release Notes ### Features - Desktop - Added experimental support for save and restore compose state. `ComposePanel`, `ComposeWindow` and `ComposeDialog` now has `savedState` constructor parameter to restore previous state and `saveState` function to save the current state for later use. --------- Co-authored-by: Igor Demin <igordmn@users.noreply.github.com>
1 parent 0732244 commit 0d3da1e

File tree

12 files changed

+384
-100
lines changed

12 files changed

+384
-100
lines changed

compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/swingexample/Main.jvm.kt

Lines changed: 18 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414
* limitations under the License.
1515
*/
1616

17+
@file:OptIn(ExperimentalComposeUiApi::class)
18+
1719
package androidx.compose.desktop.examples.swingexample
1820

19-
import java.awt.Color as awtColor
2021
import androidx.compose.foundation.ContextMenuDataProvider
2122
import androidx.compose.foundation.ContextMenuItem
2223
import androidx.compose.foundation.ContextMenuState
@@ -44,16 +45,14 @@ import androidx.compose.material.Text
4445
import androidx.compose.material.TextField
4546
import androidx.compose.runtime.Composable
4647
import androidx.compose.runtime.CompositionLocalProvider
47-
import androidx.compose.runtime.DisposableEffect
4848
import androidx.compose.runtime.MutableState
4949
import androidx.compose.runtime.getValue
5050
import androidx.compose.runtime.mutableStateOf
5151
import androidx.compose.runtime.remember
52-
import androidx.compose.runtime.saveable.LocalSaveableStateRegistry
53-
import androidx.compose.runtime.saveable.SaveableStateRegistry
5452
import androidx.compose.runtime.saveable.rememberSaveable
5553
import androidx.compose.runtime.setValue
5654
import androidx.compose.ui.Alignment
55+
import androidx.compose.ui.ExperimentalComposeUiApi
5756
import androidx.compose.ui.Modifier
5857
import androidx.compose.ui.awt.ComposePanel
5958
import androidx.compose.ui.awt.SwingPanel
@@ -68,7 +67,9 @@ import androidx.compose.ui.window.ApplicationScope
6867
import androidx.compose.ui.window.Window
6968
import androidx.compose.ui.window.launchApplication
7069
import androidx.compose.ui.window.rememberWindowState
70+
import androidx.savedstate.SavedState
7171
import java.awt.BorderLayout
72+
import java.awt.Color as awtColor
7273
import java.awt.Component
7374
import java.awt.Dimension
7475
import java.awt.Graphics
@@ -100,56 +101,26 @@ fun main() = SwingUtilities.invokeLater {
100101
SwingComposeWindow()
101102
}
102103

103-
private typealias SaveableStateData = Map<String, List<Any?>>
104+
private val globalSavedState = mutableMapOf<String, SavedState?>()
104105

105-
private class GlobalSaveableStateRegistry(
106-
val saveableId: String,
107-
) : SaveableStateRegistry by SaveableStateRegistry(
108-
restoredValues = map[saveableId],
109-
canBeSaved = { true }
110-
) {
111-
fun save() { map[saveableId] = performSave() }
112-
companion object {
113-
private val map = mutableMapOf<String, SaveableStateData>()
114-
}
115-
}
116-
117-
fun createGreenComposePanel() = ComposePanel().also {
118-
val saveableStateRegistry = GlobalSaveableStateRegistry("GREEN")
106+
fun createGreenComposePanel(
107+
savedState: SavedState? = null,
108+
) = ComposePanel(savedState = savedState).also {
119109
it.background = awtColor(55, 155, 55)
120110
it.setContent {
121111
JPopupTextMenuProvider(it) {
122-
CompositionLocalProvider(
123-
LocalSaveableStateRegistry provides saveableStateRegistry,
124-
) {
125-
ComposeContent(background = Color(55, 155, 55))
126-
}
127-
}
128-
DisposableEffect(Unit) {
129-
onDispose {
130-
saveableStateRegistry.save()
131-
println("Dispose composition")
132-
}
112+
ComposeContent(background = Color(55, 155, 55))
133113
}
134114
}
135115
}
136116

137-
fun createBlueComposePanel() = ComposePanel().also {
138-
val saveableStateRegistry = GlobalSaveableStateRegistry("BLUE")
117+
fun createBlueComposePanel(
118+
savedState: SavedState? = null,
119+
) = ComposePanel(savedState = savedState).also {
139120
it.background = awtColor(55, 55, 155)
140121
it.setContent {
141122
CustomTextMenuProvider {
142-
CompositionLocalProvider(
143-
LocalSaveableStateRegistry provides saveableStateRegistry,
144-
) {
145-
ComposeContent(background = Color(55, 55, 155))
146-
}
147-
}
148-
DisposableEffect(Unit) {
149-
onDispose {
150-
saveableStateRegistry.save()
151-
println("Dispose composition")
152-
}
123+
ComposeContent(background = Color(55, 55, 155))
153124
}
154125
}
155126
}
@@ -173,10 +144,11 @@ fun SwingComposeWindow() {
173144
size = IntSize(40, 40),
174145
action = {
175146
if (composePanel1 != null) {
147+
globalSavedState["GREEN"] = composePanel1!!.saveState()
176148
panel.remove(composePanel1)
177149
composePanel1 = null
178150
} else {
179-
composePanel1 = createGreenComposePanel()
151+
composePanel1 = createGreenComposePanel(globalSavedState["GREEN"])
180152
panel.add(composePanel1, 0)
181153
}
182154
panel.revalidate()
@@ -191,10 +163,11 @@ fun SwingComposeWindow() {
191163
size = IntSize(40, 40),
192164
action = {
193165
if (composePanel2 != null) {
166+
globalSavedState["BLUE"] = composePanel2!!.saveState()
194167
panel.remove(composePanel2)
195168
composePanel2 = null
196169
} else {
197-
composePanel2 = createBlueComposePanel()
170+
composePanel2 = createBlueComposePanel(globalSavedState["BLUE"])
198171
panel.add(composePanel2)
199172
}
200173
panel.revalidate()

compose/ui/ui/api/desktop/ui.api

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -435,8 +435,6 @@ public final class androidx/compose/ui/awt/AwtWindow_desktopKt {
435435
public final class androidx/compose/ui/awt/ComposeDialog : javax/swing/JDialog {
436436
public static final field $stable I
437437
public fun <init> ()V
438-
public fun <init> (Ljava/awt/Dialog$ModalityType;)V
439-
public synthetic fun <init> (Ljava/awt/Dialog$ModalityType;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
440438
public fun <init> (Ljava/awt/GraphicsConfiguration;)V
441439
public synthetic fun <init> (Ljava/awt/GraphicsConfiguration;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
442440
public fun <init> (Ljava/awt/Window;Ljava/awt/Dialog$ModalityType;Ljava/awt/GraphicsConfiguration;)V

compose/ui/ui/api/ui.klib.api

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3928,6 +3928,7 @@ final val androidx.compose.ui.platform/androidx_compose_ui_platform_DefaultUiApp
39283928
final val androidx.compose.ui.platform/androidx_compose_ui_platform_DefaultViewConfiguration$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_DefaultViewConfiguration$stableprop|#static{}androidx_compose_ui_platform_DefaultViewConfiguration$stableprop[0]
39293929
final val androidx.compose.ui.platform/androidx_compose_ui_platform_DelegateRootForTestListener$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_DelegateRootForTestListener$stableprop|#static{}androidx_compose_ui_platform_DelegateRootForTestListener$stableprop[0]
39303930
final val androidx.compose.ui.platform/androidx_compose_ui_platform_DelegatingSoftwareKeyboardController$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_DelegatingSoftwareKeyboardController$stableprop|#static{}androidx_compose_ui_platform_DelegatingSoftwareKeyboardController$stableprop[0]
3931+
final val androidx.compose.ui.platform/androidx_compose_ui_platform_DisposableSaveableStateRegistry$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_DisposableSaveableStateRegistry$stableprop|#static{}androidx_compose_ui_platform_DisposableSaveableStateRegistry$stableprop[0]
39313932
final val androidx.compose.ui.platform/androidx_compose_ui_platform_EmptyViewConfiguration$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_EmptyViewConfiguration$stableprop|#static{}androidx_compose_ui_platform_EmptyViewConfiguration$stableprop[0]
39323933
final val androidx.compose.ui.platform/androidx_compose_ui_platform_FlushCoroutineDispatcher$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_FlushCoroutineDispatcher$stableprop|#static{}androidx_compose_ui_platform_FlushCoroutineDispatcher$stableprop[0]
39333934
final val androidx.compose.ui.platform/androidx_compose_ui_platform_GlobalSnapshotManager$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_GlobalSnapshotManager$stableprop|#static{}androidx_compose_ui_platform_GlobalSnapshotManager$stableprop[0]
@@ -4513,6 +4514,7 @@ final fun androidx.compose.ui.platform/androidx_compose_ui_platform_DefaultUiApp
45134514
final fun androidx.compose.ui.platform/androidx_compose_ui_platform_DefaultViewConfiguration$stableprop_getter(): kotlin/Int // androidx.compose.ui.platform/androidx_compose_ui_platform_DefaultViewConfiguration$stableprop_getter|androidx_compose_ui_platform_DefaultViewConfiguration$stableprop_getter(){}[0]
45144515
final fun androidx.compose.ui.platform/androidx_compose_ui_platform_DelegateRootForTestListener$stableprop_getter(): kotlin/Int // androidx.compose.ui.platform/androidx_compose_ui_platform_DelegateRootForTestListener$stableprop_getter|androidx_compose_ui_platform_DelegateRootForTestListener$stableprop_getter(){}[0]
45154516
final fun androidx.compose.ui.platform/androidx_compose_ui_platform_DelegatingSoftwareKeyboardController$stableprop_getter(): kotlin/Int // androidx.compose.ui.platform/androidx_compose_ui_platform_DelegatingSoftwareKeyboardController$stableprop_getter|androidx_compose_ui_platform_DelegatingSoftwareKeyboardController$stableprop_getter(){}[0]
4517+
final fun androidx.compose.ui.platform/androidx_compose_ui_platform_DisposableSaveableStateRegistry$stableprop_getter(): kotlin/Int // androidx.compose.ui.platform/androidx_compose_ui_platform_DisposableSaveableStateRegistry$stableprop_getter|androidx_compose_ui_platform_DisposableSaveableStateRegistry$stableprop_getter(){}[0]
45164518
final fun androidx.compose.ui.platform/androidx_compose_ui_platform_EmptyViewConfiguration$stableprop_getter(): kotlin/Int // androidx.compose.ui.platform/androidx_compose_ui_platform_EmptyViewConfiguration$stableprop_getter|androidx_compose_ui_platform_EmptyViewConfiguration$stableprop_getter(){}[0]
45174519
final fun androidx.compose.ui.platform/androidx_compose_ui_platform_FlushCoroutineDispatcher$stableprop_getter(): kotlin/Int // androidx.compose.ui.platform/androidx_compose_ui_platform_FlushCoroutineDispatcher$stableprop_getter|androidx_compose_ui_platform_FlushCoroutineDispatcher$stableprop_getter(){}[0]
45184520
final fun androidx.compose.ui.platform/androidx_compose_ui_platform_GlobalSnapshotManager$stableprop_getter(): kotlin/Int // androidx.compose.ui.platform/androidx_compose_ui_platform_GlobalSnapshotManager$stableprop_getter|androidx_compose_ui_platform_GlobalSnapshotManager$stableprop_getter(){}[0]

compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeDialog.desktop.kt

Lines changed: 49 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import androidx.compose.ui.unit.Dp
2727
import androidx.compose.ui.window.DialogWindowScope
2828
import androidx.compose.ui.window.UndecoratedWindowResizer
2929
import androidx.compose.ui.window.WindowExceptionHandler
30+
import androidx.savedstate.SavedState
3031
import java.awt.Component
3132
import java.awt.ComponentOrientation
3233
import java.awt.Frame
@@ -45,46 +46,36 @@ import org.jetbrains.skiko.SkiaLayerAnalytics
4546
* ComposeDialog inherits javax.swing.JDialog.
4647
*/
4748
class ComposeDialog : JDialog {
48-
private val skiaLayerAnalytics: SkiaLayerAnalytics
4949
private val composePanel: ComposeWindowPanel
5050

51-
internal var rootForTestListener
52-
get() = composePanel.rootForTestListener
53-
set(value) { composePanel.rootForTestListener = value }
54-
55-
private fun createComposePanel() = ComposeWindowPanel(
51+
private fun createComposePanel(
52+
skiaLayerAnalytics: SkiaLayerAnalytics = SkiaLayerAnalytics.Empty,
53+
savedState: SavedState? = null,
54+
) = ComposeWindowPanel(
5655
window = this,
5756
isUndecorated = ::isUndecorated,
5857
skiaLayerAnalytics = skiaLayerAnalytics,
58+
savedState = savedState,
5959
)
6060

61-
constructor(
62-
owner: Window?,
63-
modalityType: ModalityType = ModalityType.MODELESS,
64-
graphicsConfiguration: GraphicsConfiguration? = null
65-
) : super(owner, "", modalityType, graphicsConfiguration) {
66-
skiaLayerAnalytics = SkiaLayerAnalytics.Empty
67-
composePanel = createComposePanel()
68-
contentPane.add(composePanel)
69-
}
70-
7161
/**
7262
* ComposeDialog is a dialog for building UI using Compose for Desktop.
7363
* ComposeDialog inherits javax.swing.JDialog.
7464
*
7565
* @param skiaLayerAnalytics Analytics that helps to know more about SkiaLayer behaviour.
7666
* SkiaLayer is underlying class used internally to draw Compose content.
7767
* Implementation usually uses third-party solution to send info to some centralized analytics gatherer.
68+
* @param savedState The saved state to restore the UI state from a previous instance.
7869
*/
7970
@ExperimentalComposeUiApi
8071
constructor(
8172
owner: Window?,
8273
modalityType: ModalityType = ModalityType.MODELESS,
8374
graphicsConfiguration: GraphicsConfiguration? = null,
84-
skiaLayerAnalytics: SkiaLayerAnalytics = SkiaLayerAnalytics.Empty
75+
skiaLayerAnalytics: SkiaLayerAnalytics = SkiaLayerAnalytics.Empty,
76+
savedState: SavedState? = null,
8577
) : super(owner, "", modalityType, graphicsConfiguration) {
86-
this.skiaLayerAnalytics = skiaLayerAnalytics
87-
composePanel = createComposePanel()
78+
composePanel = createComposePanel(skiaLayerAnalytics, savedState)
8879
contentPane.add(composePanel)
8980
}
9081

@@ -95,41 +86,51 @@ class ComposeDialog : JDialog {
9586
* @param skiaLayerAnalytics Analytics that helps to know more about SkiaLayer behaviour.
9687
* SkiaLayer is underlying class used internally to draw Compose content.
9788
* Implementation usually uses third-party solution to send info to some centralized analytics gatherer.
89+
* @param savedState The saved state to restore the UI state from a previous instance.
9890
*/
9991
@ExperimentalComposeUiApi
10092
constructor(
101-
skiaLayerAnalytics: SkiaLayerAnalytics = SkiaLayerAnalytics.Empty
102-
) : super() {
103-
this.skiaLayerAnalytics = skiaLayerAnalytics
104-
composePanel = createComposePanel()
93+
skiaLayerAnalytics: SkiaLayerAnalytics = SkiaLayerAnalytics.Empty,
94+
savedState: SavedState? = null,
95+
): super() {
96+
composePanel = createComposePanel(skiaLayerAnalytics, savedState)
10597
contentPane.add(composePanel)
10698
}
10799

108-
@Deprecated("Use the constructor with setting owner explicitly. Will be removed in 1.3")
109100
constructor(
110-
modalityType: ModalityType = ModalityType.MODELESS
111-
) : super(null, modalityType) {
112-
skiaLayerAnalytics = SkiaLayerAnalytics.Empty
113-
composePanel = createComposePanel()
114-
contentPane.add(composePanel)
115-
}
101+
owner: Window?,
102+
modalityType: ModalityType = ModalityType.MODELESS,
103+
graphicsConfiguration: GraphicsConfiguration? = null,
104+
) : this(
105+
owner = owner,
106+
modalityType = modalityType,
107+
graphicsConfiguration = graphicsConfiguration,
108+
skiaLayerAnalytics = SkiaLayerAnalytics.Empty,
109+
savedState = null
110+
)
116111

117-
constructor(graphicsConfiguration: GraphicsConfiguration? = null) :
118-
super(null as Frame?, "", false, graphicsConfiguration) {
119-
skiaLayerAnalytics = SkiaLayerAnalytics.Empty
120-
composePanel = createComposePanel()
121-
contentPane.add(composePanel)
122-
}
112+
constructor(
113+
graphicsConfiguration: GraphicsConfiguration? = null,
114+
) : this(
115+
owner = null,
116+
modalityType = ModalityType.MODELESS,
117+
graphicsConfiguration = graphicsConfiguration,
118+
skiaLayerAnalytics = SkiaLayerAnalytics.Empty,
119+
savedState = null
120+
)
123121

124122
// don't replace super() by super(null, ModalityType.MODELESS), because
125123
// this constructor creates an icon in the taskbar.
126124
// Dialog's shouldn't be appeared in the taskbar.
127125
constructor() : super() {
128-
skiaLayerAnalytics = SkiaLayerAnalytics.Empty
129126
composePanel = createComposePanel()
130127
contentPane.add(composePanel)
131128
}
132129

130+
internal var rootForTestListener
131+
get() = composePanel.rootForTestListener
132+
set(value) { composePanel.rootForTestListener = value }
133+
133134
private val undecoratedWindowResizer = UndecoratedWindowResizer(this)
134135

135136
override fun add(component: Component) = composePanel.add(component)
@@ -226,6 +227,17 @@ class ComposeDialog : JDialog {
226227
*/
227228
var undecoratedResizerThickness: Dp by undecoratedWindowResizer::resizerThickness
228229

230+
/**
231+
* Saves the current UI state into a [SavedState] object. The returned state can be used
232+
* to restore the UI state later by passing it to the constructor's `savedState` parameter.
233+
*
234+
* @return A [SavedState] object containing the current UI state.
235+
*/
236+
@ExperimentalComposeUiApi
237+
fun saveState(): SavedState? {
238+
return composePanel.saveState()
239+
}
240+
229241
override fun dispose() {
230242
composePanel.dispose()
231243
super.dispose()

compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import androidx.compose.ui.awt.RenderSettings.SwingGraphics
2525
import androidx.compose.ui.focus.FocusDirection
2626
import androidx.compose.ui.scene.ComposeContainer
2727
import androidx.compose.ui.window.WindowExceptionHandler
28+
import androidx.savedstate.SavedState
2829
import java.awt.Color
2930
import java.awt.Component
3031
import java.awt.ComponentOrientation
@@ -45,13 +46,16 @@ import org.jetbrains.skiko.SkiaLayerAnalytics
4546
* @param skiaLayerAnalytics Analytics that helps to know more about SkiaLayer behaviour.
4647
* SkiaLayer is underlying class used internally to draw Compose content.
4748
* Implementation usually uses third-party solution to send info to some centralized analytics gatherer.
49+
* @param savedState The saved state to restore the UI state from a previous instance.
4850
* @param renderSettings Configuration class for rendering settings.
4951
*/
5052
class ComposePanel @ExperimentalComposeUiApi constructor(
51-
private val skiaLayerAnalytics: SkiaLayerAnalytics,
53+
private val skiaLayerAnalytics: SkiaLayerAnalytics = SkiaLayerAnalytics.Empty,
54+
private var savedState: SavedState? = null,
5255
private val renderSettings: RenderSettings = DefaultRenderSettings
5356
) : JLayeredPane() {
5457
constructor() : this(
58+
savedState = null,
5559
skiaLayerAnalytics = SkiaLayerAnalytics.Empty,
5660
renderSettings = DefaultRenderSettings
5761
)
@@ -129,6 +133,17 @@ class ComposePanel @ExperimentalComposeUiApi constructor(
129133
@ExperimentalComposeUiApi
130134
var isDisposeOnRemove: Boolean = true
131135

136+
/**
137+
* Saves the current UI state into a [SavedState] object. The returned state can be used
138+
* to restore the UI state later by passing it to the constructor's [savedState] parameter.
139+
*
140+
* @return A [SavedState] object containing the current UI state.
141+
*/
142+
@ExperimentalComposeUiApi
143+
fun saveState(): SavedState? {
144+
return _composeContainer?.saveState()
145+
}
146+
132147
/**
133148
* Disposes Compose state and rendering resources.
134149
*
@@ -139,7 +154,9 @@ class ComposePanel @ExperimentalComposeUiApi constructor(
139154
*/
140155
@ExperimentalComposeUiApi
141156
fun dispose() {
142-
_composeContainer?.dispose()
157+
val composeContainer = _composeContainer ?: return
158+
savedState = composeContainer.saveState()
159+
composeContainer.dispose()
143160
_composeContainer = null
144161
}
145162

@@ -212,6 +229,7 @@ class ComposePanel @ExperimentalComposeUiApi constructor(
212229
if (composeContent != null) {
213230
it.setContent(composeContent)
214231
}
232+
savedState = null
215233
}
216234
composeContainer.addNotify()
217235
}
@@ -220,6 +238,7 @@ class ComposePanel @ExperimentalComposeUiApi constructor(
220238
return ComposeContainer(
221239
container = this,
222240
skiaLayerAnalytics = skiaLayerAnalytics,
241+
savedState = savedState,
223242
windowContainer = windowContainer,
224243
renderSettings = renderSettings,
225244
).apply {

0 commit comments

Comments
 (0)