Apply Compose performance audit fixes

- FolderTreePickerSheet: add contentType to lazy folder items for slot reuse
- GalleryScreen: use plain remember instead of derivedStateOf over non-state
  inputs; defer photo fade alpha to Draw phase via graphicsLayer; react to
  zoom threshold with snapshotFlow instead of per-frame effect relaunch
- GalleryViewModel: drop @Immutable from MovePhotoSheetState so Strong
  Skipping compares the lazy feed by O(1) reference equality
- SystemCameraCapture: move temp-file disk I/O off the main thread
This commit is contained in:
OneWay committed 2026-10-14 16:06:12 +08:00
1 parent 9910ba6917
commit c2ddfdf89d
4 files changed
+48 -42

No files matched your search

@@ -37,7 +37,9 @@ import androidx.lifecycle.compose.LifecycleResumeEffect
import com.example.onecamera.R
import com.example.onecamera.ui.components.OneCameraEmptyState
import com.example.onecamera.ui.components.OneCameraScreenPadding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.launch
import java.io.File
@@ -111,23 +113,26 @@ fun SystemCameraCapture(
LaunchedEffect(hasLaunchedCamera) {
if (!hasLaunchedCamera) {
viewModel.clearLastSaveResult()
val tempDir = File(context.cacheDir, "camera_captures").apply { mkdirs() }
val (preparedPath, preparedUri) = withContext(Dispatchers.IO) {
val tempDir = File(context.cacheDir, "camera_captures").apply { mkdirs() }
tempDir.listFiles()?.forEach { runCatching { it.delete() } }
val tempFile = File(tempDir, "capture_${System.currentTimeMillis()}.jpg").apply {
createNewFile()
tempDir.listFiles()?.forEach { runCatching { it.delete() } }
val tempFile = File(tempDir, "capture_${System.currentTimeMillis()}.jpg").apply {
createNewFile()
}
val uri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
tempFile,
)
tempFile.absolutePath to uri.toString()
}
val uri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
tempFile,
)
tempPhotoUriString = uri.toString()
tempPhotoPath = tempFile.absolutePath
tempPhotoUriString = preparedUri
tempPhotoPath = preparedPath
hasLaunchedCamera = true
hasReceivedCameraResult = false
launchUptimeMillis = SystemClock.elapsedRealtime()
takePictureLauncher.launch(uri)
takePictureLauncher.launch(Uri.parse(preparedUri))
}
}
@@ -143,7 +148,7 @@ fun SystemCameraCapture(
tempPhotoUriString != null
) {
val tempLength = tempPhotoPath?.let { path ->
runCatching { File(path).length() }.getOrNull()
runCatching { withContext(Dispatchers.IO) { File(path).length() } }.getOrNull()
} ?: 0L
val tempPhotoUri = tempPhotoUriString?.let(Uri::parse)
if (tempLength > 0L && tempPhotoUri != null) {
@@ -105,7 +105,11 @@ fun FolderTreePickerSheet(
.fillMaxWidth()
.weight(1f, fill = false),
) {
items(state.folders, key = { it.uri }) { folder ->
items(
items = state.folders,
key = { it.uri },
contentType = { "folder" },
) { folder ->
val isCurrent = currentFolderUri == folder.uri.toString()
val isSelected = selectedFolderUri == folder.uri.toString()
ListItem(
@@ -41,16 +41,16 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
@@ -86,6 +86,7 @@ import com.example.onecamera.ui.folders.RenameFolderDialog
import com.github.panpf.zoomimage.CoilZoomAsyncImage
import com.github.panpf.zoomimage.rememberCoilZoomState
import com.github.panpf.zoomimage.zoom.GestureType
import kotlinx.coroutines.flow.distinctUntilChanged
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
@@ -198,10 +199,8 @@ fun GalleryScreen(
uiState.subfolders.sortedByDescending { it.name.lowercase() }
}
}
val sortedGalleryPhotos by remember(sortedPhotos) {
derivedStateOf {
sortedPhotos.filter { it.kind == GalleryFileKind.Photo }
}
val sortedGalleryPhotos = remember(sortedPhotos) {
sortedPhotos.filter { it.kind == GalleryFileKind.Photo }
}
when (val dialog = uiState.dialog) {
@@ -537,10 +536,8 @@ internal fun FullscreenPhotoPagerContent(
onDismiss: () -> Unit,
onDelete: (Photo) -> Unit,
) {
val initialPage by remember(photos, initialPhoto.uri) {
derivedStateOf {
photos.indexOfFirst { it.uri == initialPhoto.uri }.coerceAtLeast(0)
}
val initialPage = remember(photos, initialPhoto.uri) {
photos.indexOfFirst { it.uri == initialPhoto.uri }.coerceAtLeast(0)
}
val pagerState = rememberPagerState(initialPage = initialPage) { photos.size.coerceAtLeast(1) }
@@ -646,7 +643,6 @@ private fun ZoomablePhoto(
animationSpec = tween(durationMillis = fadeMs),
label = "fullAlpha",
)
val scale by remember { derivedStateOf { zoomState.zoomable.transform.scaleX } }
LaunchedEffect(photo.uri) {
@@ -663,25 +659,27 @@ private fun ZoomablePhoto(
}
}
LaunchedEffect(scale) {
val zoomed = scale > 1.01f
onZoomChange(zoomed)
zoomState.subsampling.setDisabled(!zoomed)
LaunchedEffect(zoomState) {
snapshotFlow { zoomState.zoomable.transform.scaleX > 1.01f }
.distinctUntilChanged()
.collect { zoomed ->
onZoomChange(zoomed)
zoomState.subsampling.setDisabled(!zoomed)
}
}
Box(modifier = modifier.fillMaxSize()) {
if (fullAlpha < 1f) {
AsyncImage(
model = remember(photo.uri, context, fadeMs) {
thumbnailDraftRequest(context, photo.uri, crossfadeMillis = fadeMs)
},
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier.fillMaxSize(),
)
}
AsyncImage(
model = remember(photo.uri, context, fadeMs) {
thumbnailDraftRequest(context, photo.uri, crossfadeMillis = fadeMs)
},
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier
.fillMaxSize()
.graphicsLayer { alpha = 1f - fullAlpha },
)
CoilZoomAsyncImage(
model = fullImageRequest,
contentDescription = photo.name,
@@ -690,7 +688,7 @@ private fun ZoomablePhoto(
onError = { isFullReady = false },
modifier = Modifier
.fillMaxSize()
.alpha(fullAlpha),
.graphicsLayer { alpha = fullAlpha },
zoomState = zoomState,
)
}
@@ -45,7 +45,6 @@ sealed interface GalleryDialog {
data class DeleteSubfolder(val folder: FolderNode) : GalleryDialog
}
@Immutable
data class MovePhotoSheetState(
val file: Photo? = null,
val files: List<Photo> = emptyList(),