From 796244e0655115907ed6c9d056c0a67bd4b6bd2b Mon Sep 17 00:00:00 2001 From: Kayos 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 f7ebda3b7..5e3d33135 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-Coop/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 06acdf2cb..94ac9fe69 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 bc4bd4e4f..26daa53f0 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())