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:
+16
-8
@@ -28,6 +28,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
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.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clipToBounds
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
@@ -83,7 +84,7 @@ fun DictionaryListItem(
|
|||||||
onToggle: () -> Unit,
|
onToggle: () -> Unit,
|
||||||
onDelete: () -> Unit,
|
onDelete: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
canDelete: Boolean = true
|
isDeleteBlocked: () -> Boolean = { false }
|
||||||
) {
|
) {
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
@@ -95,8 +96,9 @@ fun DictionaryListItem(
|
|||||||
val offsetAnim = remember { Animatable(0f) }
|
val offsetAnim = remember { Animatable(0f) }
|
||||||
|
|
||||||
var rawOffset by remember { mutableFloatStateOf(0f) }
|
var rawOffset by remember { mutableFloatStateOf(0f) }
|
||||||
val isDeleteRevealed by remember(canDelete) {
|
val isDeleteBlockedCurrent by rememberUpdatedState(isDeleteBlocked)
|
||||||
derivedStateOf { canDelete && rawOffset <= -swipeThresholdPx }
|
val isDeleteRevealed by remember {
|
||||||
|
derivedStateOf { !isDeleteBlockedCurrent() && rawOffset <= -swipeThresholdPx }
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(isDeleteRevealed) {
|
LaunchedEffect(isDeleteRevealed) {
|
||||||
@@ -105,10 +107,13 @@ fun DictionaryListItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(canDelete) {
|
LaunchedEffect(Unit) {
|
||||||
if (!canDelete) {
|
snapshotFlow { isDeleteBlockedCurrent() }
|
||||||
|
.collect { blocked ->
|
||||||
|
if (blocked) {
|
||||||
rawOffset = 0f
|
rawOffset = 0f
|
||||||
offsetAnim.snapTo(0f)
|
scope.launch { offsetAnim.snapTo(0f) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,13 +147,14 @@ fun DictionaryListItem(
|
|||||||
.height(DictionaryItemHeight)
|
.height(DictionaryItemHeight)
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||||
.graphicsLayer { translationX = offsetAnim.value }
|
.graphicsLayer { translationX = offsetAnim.value }
|
||||||
.pointerInput(canDelete, maxSwipePx, swipeThresholdPx) {
|
.pointerInput(maxSwipePx, swipeThresholdPx) {
|
||||||
if (!canDelete) return@pointerInput
|
|
||||||
detectHorizontalDragGestures(
|
detectHorizontalDragGestures(
|
||||||
onDragStart = {
|
onDragStart = {
|
||||||
|
if (isDeleteBlockedCurrent()) return@detectHorizontalDragGestures
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||||
},
|
},
|
||||||
onDragEnd = {
|
onDragEnd = {
|
||||||
|
if (isDeleteBlockedCurrent()) return@detectHorizontalDragGestures
|
||||||
val target = if (rawOffset <= -swipeThresholdPx) -maxSwipePx else 0f
|
val target = if (rawOffset <= -swipeThresholdPx) -maxSwipePx else 0f
|
||||||
rawOffset = target
|
rawOffset = target
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -159,6 +165,7 @@ fun DictionaryListItem(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDragCancel = {
|
onDragCancel = {
|
||||||
|
if (isDeleteBlockedCurrent()) return@detectHorizontalDragGestures
|
||||||
val target = if (rawOffset <= -swipeThresholdPx) -maxSwipePx else 0f
|
val target = if (rawOffset <= -swipeThresholdPx) -maxSwipePx else 0f
|
||||||
rawOffset = target
|
rawOffset = target
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -169,6 +176,7 @@ fun DictionaryListItem(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onHorizontalDrag = { _, dragAmount ->
|
onHorizontalDrag = { _, dragAmount ->
|
||||||
|
if (isDeleteBlockedCurrent()) return@detectHorizontalDragGestures
|
||||||
val newOffset = (rawOffset + dragAmount).coerceIn(-maxSwipePx, 0f)
|
val newOffset = (rawOffset + dragAmount).coerceIn(-maxSwipePx, 0f)
|
||||||
rawOffset = newOffset
|
rawOffset = newOffset
|
||||||
scope.launch { offsetAnim.snapTo(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.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.rememberUpdatedState
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
@@ -75,6 +76,13 @@ fun DictionaryManagement(
|
|||||||
derivedStateOf { importState is ImportState.Importing || importState is ImportState.Extracting }
|
derivedStateOf { importState is ImportState.Importing || importState is ImportState.Extracting }
|
||||||
}
|
}
|
||||||
val hasEnabledSources by remember(sources) { derivedStateOf { sources.any { it.isEnabled } } }
|
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(
|
SectionCard(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@@ -125,13 +133,13 @@ fun DictionaryManagement(
|
|||||||
|
|
||||||
// Disable accessibility on background content during import to improve performance.
|
// Disable accessibility on background content during import to improve performance.
|
||||||
// The progress dialog remains accessible for cancellation.
|
// The progress dialog remains accessible for cancellation.
|
||||||
val backgroundModifier = if (isInProgress) {
|
val backgroundModifier = if (isBlockingMutations) {
|
||||||
Modifier.semantics(mergeDescendants = true) { }
|
Modifier.semantics(mergeDescendants = true) { }
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dictionaries.isEmpty() && !isInProgress) {
|
if (dictionaries.isEmpty() && !isBlockingMutations) {
|
||||||
Column(
|
Column(
|
||||||
modifier = backgroundModifier
|
modifier = backgroundModifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -153,7 +161,7 @@ fun DictionaryManagement(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (dictionaryStatus is DictionaryStatus.NeedsUpdate && !isInProgress && hasEnabledSources) {
|
if (dictionaryStatus is DictionaryStatus.NeedsUpdate && !isBlockingMutations && hasEnabledSources) {
|
||||||
DictionaryUpdateCard(
|
DictionaryUpdateCard(
|
||||||
onStartDownload = onStartDownload,
|
onStartDownload = onStartDownload,
|
||||||
modifier = backgroundModifier
|
modifier = backgroundModifier
|
||||||
@@ -167,10 +175,10 @@ fun DictionaryManagement(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.then(backgroundModifier),
|
.then(backgroundModifier),
|
||||||
canDelete = !isInProgress
|
isDeleteBlocked = isDeleteBlocked
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!isInProgress) {
|
if (!isBlockingMutations) {
|
||||||
DictionaryActionButtons(
|
DictionaryActionButtons(
|
||||||
onAddSource = {
|
onAddSource = {
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||||
@@ -248,7 +256,7 @@ private fun DictionaryListSection(
|
|||||||
onToggleDictionary: (String) -> Unit,
|
onToggleDictionary: (String) -> Unit,
|
||||||
onDeleteDictionary: (Dictionary) -> Unit,
|
onDeleteDictionary: (Dictionary) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
canDelete: Boolean = true
|
isDeleteBlocked: () -> Boolean = { false }
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
@@ -259,7 +267,7 @@ private fun DictionaryListSection(
|
|||||||
dictionary = dictionary,
|
dictionary = dictionary,
|
||||||
onToggle = { onToggleDictionary(dictionary.path) },
|
onToggle = { onToggleDictionary(dictionary.path) },
|
||||||
onDelete = { onDeleteDictionary(dictionary) },
|
onDelete = { onDeleteDictionary(dictionary) },
|
||||||
canDelete = canDelete
|
isDeleteBlocked = isDeleteBlocked
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user