refactor: isolate dictionary rows from mutation-block recomposition
Delete-block flag becomes a () -> Boolean provider read at state/gesture time, so flipping it no longer recomposes the whole Dictionaries block.
This commit is contained in:
+18
-10
@@ -28,6 +28,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
@@ -83,7 +84,7 @@ fun DictionaryListItem(
|
||||
onToggle: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
canDelete: Boolean = true
|
||||
isDeleteBlocked: () -> Boolean = { false }
|
||||
) {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val density = LocalDensity.current
|
||||
@@ -95,8 +96,9 @@ fun DictionaryListItem(
|
||||
val offsetAnim = remember { Animatable(0f) }
|
||||
|
||||
var rawOffset by remember { mutableFloatStateOf(0f) }
|
||||
val isDeleteRevealed by remember(canDelete) {
|
||||
derivedStateOf { canDelete && rawOffset <= -swipeThresholdPx }
|
||||
val isDeleteBlockedCurrent by rememberUpdatedState(isDeleteBlocked)
|
||||
val isDeleteRevealed by remember {
|
||||
derivedStateOf { !isDeleteBlockedCurrent() && rawOffset <= -swipeThresholdPx }
|
||||
}
|
||||
|
||||
LaunchedEffect(isDeleteRevealed) {
|
||||
@@ -105,11 +107,14 @@ fun DictionaryListItem(
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(canDelete) {
|
||||
if (!canDelete) {
|
||||
rawOffset = 0f
|
||||
offsetAnim.snapTo(0f)
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { isDeleteBlockedCurrent() }
|
||||
.collect { blocked ->
|
||||
if (blocked) {
|
||||
rawOffset = 0f
|
||||
scope.launch { offsetAnim.snapTo(0f) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val currentOnDelete by rememberUpdatedState(onDelete)
|
||||
@@ -142,13 +147,14 @@ fun DictionaryListItem(
|
||||
.height(DictionaryItemHeight)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
.graphicsLayer { translationX = offsetAnim.value }
|
||||
.pointerInput(canDelete, maxSwipePx, swipeThresholdPx) {
|
||||
if (!canDelete) return@pointerInput
|
||||
.pointerInput(maxSwipePx, swipeThresholdPx) {
|
||||
detectHorizontalDragGestures(
|
||||
onDragStart = {
|
||||
if (isDeleteBlockedCurrent()) return@detectHorizontalDragGestures
|
||||
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||
},
|
||||
onDragEnd = {
|
||||
if (isDeleteBlockedCurrent()) return@detectHorizontalDragGestures
|
||||
val target = if (rawOffset <= -swipeThresholdPx) -maxSwipePx else 0f
|
||||
rawOffset = target
|
||||
scope.launch {
|
||||
@@ -159,6 +165,7 @@ fun DictionaryListItem(
|
||||
}
|
||||
},
|
||||
onDragCancel = {
|
||||
if (isDeleteBlockedCurrent()) return@detectHorizontalDragGestures
|
||||
val target = if (rawOffset <= -swipeThresholdPx) -maxSwipePx else 0f
|
||||
rawOffset = target
|
||||
scope.launch {
|
||||
@@ -169,6 +176,7 @@ fun DictionaryListItem(
|
||||
}
|
||||
},
|
||||
onHorizontalDrag = { _, dragAmount ->
|
||||
if (isDeleteBlockedCurrent()) return@detectHorizontalDragGestures
|
||||
val newOffset = (rawOffset + dragAmount).coerceIn(-maxSwipePx, 0f)
|
||||
rawOffset = newOffset
|
||||
scope.launch { offsetAnim.snapTo(newOffset) }
|
||||
|
||||
+15
-7
@@ -13,6 +13,7 @@ import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
@@ -75,6 +76,13 @@ fun DictionaryManagement(
|
||||
derivedStateOf { importState is ImportState.Importing || importState is ImportState.Extracting }
|
||||
}
|
||||
val hasEnabledSources by remember(sources) { derivedStateOf { sources.any { it.isEnabled } } }
|
||||
val isBlockingMutations = isInProgress
|
||||
|
||||
// Invariant: the mutation-block flag is read only at state-read/gesture time (derivedStateOf,
|
||||
// snapshotFlow, pointerInput callbacks), never at row composition time. A composition-time read
|
||||
// (e.g. Modifier.alpha(if (isDeleteBlocked()) ...)) would invalidate every row on each flip.
|
||||
val isMutationsBlockedState = rememberUpdatedState(isBlockingMutations)
|
||||
val isDeleteBlocked = remember { { isMutationsBlockedState.value } }
|
||||
|
||||
SectionCard(
|
||||
modifier = modifier
|
||||
@@ -125,13 +133,13 @@ fun DictionaryManagement(
|
||||
|
||||
// Disable accessibility on background content during import to improve performance.
|
||||
// The progress dialog remains accessible for cancellation.
|
||||
val backgroundModifier = if (isInProgress) {
|
||||
val backgroundModifier = if (isBlockingMutations) {
|
||||
Modifier.semantics(mergeDescendants = true) { }
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
if (dictionaries.isEmpty() && !isInProgress) {
|
||||
if (dictionaries.isEmpty() && !isBlockingMutations) {
|
||||
Column(
|
||||
modifier = backgroundModifier
|
||||
.fillMaxWidth()
|
||||
@@ -153,7 +161,7 @@ fun DictionaryManagement(
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (dictionaryStatus is DictionaryStatus.NeedsUpdate && !isInProgress && hasEnabledSources) {
|
||||
if (dictionaryStatus is DictionaryStatus.NeedsUpdate && !isBlockingMutations && hasEnabledSources) {
|
||||
DictionaryUpdateCard(
|
||||
onStartDownload = onStartDownload,
|
||||
modifier = backgroundModifier
|
||||
@@ -167,10 +175,10 @@ fun DictionaryManagement(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(backgroundModifier),
|
||||
canDelete = !isInProgress
|
||||
isDeleteBlocked = isDeleteBlocked
|
||||
)
|
||||
|
||||
if (!isInProgress) {
|
||||
if (!isBlockingMutations) {
|
||||
DictionaryActionButtons(
|
||||
onAddSource = {
|
||||
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||
@@ -248,7 +256,7 @@ private fun DictionaryListSection(
|
||||
onToggleDictionary: (String) -> Unit,
|
||||
onDeleteDictionary: (Dictionary) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
canDelete: Boolean = true
|
||||
isDeleteBlocked: () -> Boolean = { false }
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
@@ -259,7 +267,7 @@ private fun DictionaryListSection(
|
||||
dictionary = dictionary,
|
||||
onToggle = { onToggleDictionary(dictionary.path) },
|
||||
onDelete = { onDeleteDictionary(dictionary) },
|
||||
canDelete = canDelete
|
||||
isDeleteBlocked = isDeleteBlocked
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user