From f692e0105ed14392058c06c8ba55f9ede3b2eeea Mon Sep 17 00:00:00 2001 From: Sulkta Date: Tue, 26 May 2026 15:32:47 -0700 Subject: [PATCH] vc=67: fix subs feed scroll jank MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LazyColumn items() now keyed by url so pagination doesn't re-key every row from scratch when visibleCount jumps. The displayed page slice is remembered so SubsPane doesn't reallocate the take() ArrayList on every recomposition. ThumbnailProgressOverlay switched from collectAsStateWithLifecycle to plain collectAsState — the lifecycle wrapper added a DisposableEffect per call site, which adds up across the ~30 visible rows and was contributing to scroll hitch. --- buildSrc/src/main/kotlin/ProjectConfig.kt | 4 ++-- .../src/main/kotlin/com/sulkta/straw/StrawHome.kt | 11 +++++++++-- .../sulkta/straw/feature/player/ThumbnailProgress.kt | 10 ++++++++-- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/buildSrc/src/main/kotlin/ProjectConfig.kt b/buildSrc/src/main/kotlin/ProjectConfig.kt index 8a49a4aa5..654a70da6 100644 --- a/buildSrc/src/main/kotlin/ProjectConfig.kt +++ b/buildSrc/src/main/kotlin/ProjectConfig.kt @@ -55,6 +55,6 @@ const val NEWPIPE_APPLICATION_ID_NEW = "net.newpipe.app" // vc=19 / 0.1.0-AE — rust pipeline cutover. Extraction via // strawcore-core (Sulkta-OSS/strawcore) via the UniFFI wrapper; no // NewPipeExtractor in the runtime path. -const val STRAW_VERSION_CODE = 66 -const val STRAW_VERSION_NAME = "0.1.0-BZ" +const val STRAW_VERSION_CODE = 67 +const val STRAW_VERSION_NAME = "0.1.0-CA" const val STRAW_APPLICATION_ID = "com.sulkta.straw" diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/StrawHome.kt b/strawApp/src/main/kotlin/com/sulkta/straw/StrawHome.kt index e45bb5b48..257442147 100644 --- a/strawApp/src/main/kotlin/com/sulkta/straw/StrawHome.kt +++ b/strawApp/src/main/kotlin/com/sulkta/straw/StrawHome.kt @@ -323,7 +323,11 @@ private fun SubsPane( visibleCount = PAGE_SIZE } } - val displayed = filteredItems.take(visibleCount) + // remember the page-slice so we don't allocate a new ArrayList on + // every recomposition (scroll hitch vc=67). + val displayed = remember(filteredItems, visibleCount) { + filteredItems.take(visibleCount) + } val hasMore = filteredItems.size > visibleCount Column { @@ -442,7 +446,10 @@ private fun SubsPane( state = listState, contentPadding = rememberBottomContentPadding(), ) { - items(displayed) { item -> + items( + items = displayed, + key = { it.url }, + ) { item -> FeedRow( item = item, onClick = { onOpenVideo(item.url, item.title) }, diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/feature/player/ThumbnailProgress.kt b/strawApp/src/main/kotlin/com/sulkta/straw/feature/player/ThumbnailProgress.kt index e9c40b0bb..74b3aa9d3 100644 --- a/strawApp/src/main/kotlin/com/sulkta/straw/feature/player/ThumbnailProgress.kt +++ b/strawApp/src/main/kotlin/com/sulkta/straw/feature/player/ThumbnailProgress.kt @@ -25,13 +25,13 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import com.sulkta.straw.OverlayDimColor import com.sulkta.straw.ProgressBarFillColor @@ -51,7 +51,13 @@ import com.sulkta.straw.util.formatDuration @Composable fun BoxScope.ThumbnailProgressOverlay(videoId: String?) { if (videoId.isNullOrBlank()) return - val positions by Resume.get().positions.collectAsStateWithLifecycle() + // Plain collectAsState — collectAsStateWithLifecycle adds a + // DisposableEffect for lifecycle observation per call site, which + // adds up across 30 visible LazyColumn rows and contributes to + // scroll jank (vc=67). The Lifecycle pause optimization doesn't + // matter for a foreground feed that's only collected while the + // composable is on screen anyway. + val positions by Resume.get().positions.collectAsState() val entry = positions[videoId] ?: return if (entry.durationMs <= 0L) return val fraction = (entry.positionMs.toFloat() / entry.durationMs.toFloat())