@@ -46,7 +46,9 @@ import androidx.compose.ui.text.style.TextOverflow
46import androidx.compose.ui.unit.dp
46import androidx.compose.ui.unit.dp
47import androidx.compose.ui.window.Dialog
47import androidx.compose.ui.window.Dialog
48import androidx.compose.ui.window.DialogProperties
48import androidx.compose.ui.window.DialogProperties
49−import coil3.compose.SubcomposeAsyncImage
49+import coil3.compose.AsyncImagePainter
50+import coil3.compose.rememberAsyncImagePainter
51+import androidx.compose.foundation.Image
50import coil3.network.NetworkHeaders
52import coil3.network.NetworkHeaders
51import coil3.network.httpHeaders
53import coil3.network.httpHeaders
52import coil3.request.ImageRequest
54import coil3.request.ImageRequest
@@ -210,18 +212,20 @@ fun RemoteImage(part: FilePart, ctx: ImageLoadContext, modifier: Modifier = Modi
210 .crossfade(true)
212 .crossfade(true)
213− SubcomposeAsyncImage(
215+ // Use rememberAsyncImagePainter + Image instead of SubcomposeAsyncImage to avoid
216+ // SubcomposeLayout, which triggers draw-phase remeasures that NPE inside the LazyColumn
217+ // when the image loads and the row's size changes post-mount — the same crash class
218+ // previously fixed for animateItem(), AnimatedContent, and animateContentSize.
214− model = request,
219+ val painter = rememberAsyncImagePainter(model = request)
215− contentDescription = part.filename ?: stringResource(R.string.image),
220+ val painterState = painter.state.value
216− contentScale = ContentScale.FillWidth,
221+ Box(
217 modifier = modifier
222 modifier = modifier
218 .heightIn(max = NetworkConfig.inlineImageMaxHeightDp.dp)
223 .heightIn(max = NetworkConfig.inlineImageMaxHeightDp.dp)
219 .clip(MaterialTheme.shapes.small)
224 .clip(MaterialTheme.shapes.small)
220− // Tap to open a fullscreen zoomable viewer so the user can inspect details
221− // without the inline 320dp height cap.
222 .clickable(role = Role.Button) { showFullscreen = true },
225 .clickable(role = Role.Button) { showFullscreen = true },
224− Box(
227+ when (painterState) {
228+ is AsyncImagePainter.State.Loading -> Box(
225 modifier = Modifier.fillMaxWidth().heightIn(min = NetworkConfig.inlineImageMinHeightDp.dp),
229 modifier = Modifier.fillMaxWidth().heightIn(min = NetworkConfig.inlineImageMinHeightDp.dp),
226 contentAlignment = Alignment.Center,
230 contentAlignment = Alignment.Center,
@@ -230,9 +234,15 @@ fun RemoteImage(part: FilePart, ctx: ImageLoadContext, modifier: Modifier = Modi
230 modifier = Modifier.semantics { contentDescription = loadingLabel },
234 modifier = Modifier.semantics { contentDescription = loadingLabel },
234− error = { ImageRetry(onRetry = { retryKey++ }) },
237+ is AsyncImagePainter.State.Error -> ImageRetry(onRetry = { retryKey++ })
235− )
238+ else -> Image(
240+ contentDescription = part.filename ?: stringResource(R.string.image),
241+ contentScale = ContentScale.FillWidth,
242+ modifier = Modifier.fillMaxWidth(),
236 if (showFullscreen) {
246 if (showFullscreen) {
237 FullscreenImageViewer(request = request, contentDescription = part.filename ?: stringResource(R.string.image)) {
247 FullscreenImageViewer(request = request, contentDescription = part.filename ?: stringResource(R.string.image)) {
238 showFullscreen = false
248 showFullscreen = false
@@ -342,8 +352,9 @@ internal fun FullscreenImageViewer(
343 contentAlignment = Alignment.Center,
353 contentAlignment = Alignment.Center,
345− SubcomposeAsyncImage(
355+ val fullscreenPainter = rememberAsyncImagePainter(model = request)
346− model = request,
356+ Image(
357+ painter = fullscreenPainter,
347 contentDescription = contentDescription,
358 contentDescription = contentDescription,
348 contentScale = ContentScale.Fit,
359 contentScale = ContentScale.Fit,
349 modifier = Modifier
360 modifier = Modifier