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
+28 -22

No files matched your search

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