Skip to content

Commit e2ae5e5

Browse files
Fix crashes caused by exceptions in PlotPanelRaw for Android
1 parent 430b355 commit e2ae5e5

File tree

3 files changed

+39
-23
lines changed

3 files changed

+39
-23
lines changed

demo/plot/compose-desktop/src/main/kotlin/demo/plot/minimal/IllegalArgumentCompose.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import androidx.compose.ui.unit.dp
1414
import androidx.compose.ui.window.Window
1515
import androidx.compose.ui.window.application
1616
import org.jetbrains.letsPlot.skia.compose.PlotPanel
17-
import plotSpec.DensitySpec
1817
import plotSpec.IllegalArgumentSpec
1918

2019
fun main() = application {

lets-plot-compose/src/androidMain/kotlin/org/jetbrains/letsPlot/skia/compose/PlotPanelRaw.kt

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ private val LOG = PortableLogging.logger(name = "[PlotPanelRaw]")
2727

2828
// This flag is mentioned in the ComposeMinDemoActivity.kt
2929
// In a case of changes update the comment there too.
30-
private const val logRecompositions = false
30+
private const val logRecompositions = true
3131

3232
@Suppress("FunctionName")
3333
@Composable
@@ -39,7 +39,12 @@ actual fun PlotPanelRaw(
3939
errorModifier: Modifier,
4040
computationMessagesHandler: (List<String>) -> Unit
4141
) {
42-
var plotCanvasFigure by remember { mutableStateOf(PlotCanvasFigure()) }
42+
if (logRecompositions) {
43+
println("PlotPanel: recomposition")
44+
}
45+
46+
var plotCanvasFigure: PlotCanvasFigure? by remember { mutableStateOf(null) }
47+
val sizingPolicy = SizingPolicy.fitContainerSize(preserveAspectRatio)
4348

4449
// Cache processed plot spec to avoid reprocessing the same raw spec on every recomposition.
4550

@@ -48,15 +53,14 @@ actual fun PlotPanelRaw(
4853
val processedPlotSpec = remember(rawSpec.hashCode()) {
4954
processRawSpecs(rawSpec, frontendOnly = false)
5055
}
56+
var errorMessage: String? by remember { mutableStateOf(null) }
5157

52-
val showErrorMessage = PlotConfig.isFailure(processedPlotSpec)
53-
54-
if (logRecompositions) {
55-
println("PlotPanel: recomposition")
58+
if (PlotConfig.isFailure(processedPlotSpec)) {
59+
errorMessage = PlotConfig.getErrorMessage(processedPlotSpec)
5660
}
5761

5862
// Background
59-
val finalModifier = if (showErrorMessage) {
63+
val finalModifier = if (errorMessage != null) {
6064
modifier.background(Color.LightGray)
6165
} else {
6266
if (containsBackground(modifier)) {
@@ -70,37 +74,46 @@ actual fun PlotPanelRaw(
7074
}
7175
}
7276

73-
// LOG.info { "Recompose PlotPanel()" }
74-
if (showErrorMessage) {
77+
LaunchedEffect(processedPlotSpec, sizingPolicy, computationMessagesHandler) {
78+
runCatching {
79+
plotCanvasFigure?.update(processedPlotSpec, sizingPolicy, computationMessagesHandler)
80+
?: LOG.info { "Error updating plot figure - plotCanvasFigure is null" }
81+
}.onFailure { e ->
82+
errorMessage = e.message ?: "Unknown error: ${e::class.simpleName}"
83+
LOG.error(e) { "Error updating plot figure" }
84+
}
85+
86+
}
87+
88+
errorMessage?.let { errMsg ->
7589
// Reset the figure to resolve the 'Registration already removed' error.
7690
// On error, the CanvasView is removed and the plotCanvasFigure changes state to 'detached',
7791
// meaning it cannot be reused.
78-
@Suppress("AssignedValueIsNeverRead") // false positive? The variable is used in AndroidView below.
79-
plotCanvasFigure = PlotCanvasFigure()
92+
plotCanvasFigure = null
8093

8194
// Show error message
8295
BasicTextField(
83-
value = PlotConfig.getErrorMessage(processedPlotSpec),
96+
value = errMsg,
8497
onValueChange = { },
8598
readOnly = true,
8699
textStyle = errorTextStyle,
87100
modifier = errorModifier
88101
)
89-
} else {
102+
} ?: run {
90103
@Suppress("COMPOSE_APPLIER_CALL_MISMATCH") // Gemini says that this is a false positive
91104
AndroidView(
92105
modifier = finalModifier,
93106
factory = { ctx ->
107+
plotCanvasFigure = plotCanvasFigure ?: PlotCanvasFigure()
108+
94109
CanvasView(ctx).apply {
95110
figure = plotCanvasFigure
111+
onError = { e ->
112+
@Suppress("AssignedValueIsNeverRead")
113+
errorMessage = e.message ?: "Unknown error: ${e::class.simpleName}"
114+
LOG.error(e) { "Error in CanvasView" }
115+
}
96116
}
97-
},
98-
update = { _ ->
99-
plotCanvasFigure.update(
100-
processedPlotSpec,
101-
SizingPolicy.fitContainerSize(preserveAspectRatio),
102-
computationMessagesHandler
103-
)
104117
}
105118
)
106119
}

platf-skia/src/androidMain/kotlin/org/jetbrains/letsPlot/android/canvas/CanvasView.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ val CanvasFigure.width get() = bounds().get().dimension.x
3232
val CanvasFigure.height get() = bounds().get().dimension.y
3333

3434
@SuppressLint("ViewConstructor")
35-
class CanvasView(context: Context) : View(context) {
35+
class CanvasView(
36+
context: Context,
37+
) : View(context) {
38+
var onError: (Throwable) -> Unit = { _ -> }
3639
var figure: CanvasFigure? = null
3740
set(fig) {
3841
if (field == fig) {
@@ -90,7 +93,8 @@ class CanvasView(context: Context) : View(context) {
9093
val density = resources.displayMetrics.density
9194
val newSize = Vector(ceil(w / density).toInt(), ceil(h / density).toInt())
9295

93-
sizeListeners.forEach { it(newSize) }
96+
runCatching { sizeListeners.forEach { it(newSize) } }
97+
.onFailure() { onError(it) }
9498
}
9599

96100
override fun onDraw(canvas: android.graphics.Canvas) {

0 commit comments

Comments
 (0)