vc=67: fix subs feed scroll jank

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.
This commit is contained in:
Kayos 2026-05-26 15:32:47 -07:00
parent dd151e322d
commit 796244e065
3 changed files with 19 additions and 6 deletions

View file

@ -55,6 +55,6 @@ const val NEWPIPE_APPLICATION_ID_NEW = "net.newpipe.app"
// vc=19 / 0.1.0-AE — rust pipeline cutover. Extraction via // vc=19 / 0.1.0-AE — rust pipeline cutover. Extraction via
// strawcore-core (Sulkta-Coop/strawcore) via the UniFFI wrapper; no // strawcore-core (Sulkta-Coop/strawcore) via the UniFFI wrapper; no
// NewPipeExtractor in the runtime path. // NewPipeExtractor in the runtime path.
const val STRAW_VERSION_CODE = 66 const val STRAW_VERSION_CODE = 67
const val STRAW_VERSION_NAME = "0.1.0-BZ" const val STRAW_VERSION_NAME = "0.1.0-CA"
const val STRAW_APPLICATION_ID = "com.sulkta.straw" const val STRAW_APPLICATION_ID = "com.sulkta.straw"

View file

@ -323,7 +323,11 @@ private fun SubsPane(
visibleCount = PAGE_SIZE 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 val hasMore = filteredItems.size > visibleCount
Column { Column {
@ -442,7 +446,10 @@ private fun SubsPane(
state = listState, state = listState,
contentPadding = rememberBottomContentPadding(), contentPadding = rememberBottomContentPadding(),
) { ) {
items(displayed) { item -> items(
items = displayed,
key = { it.url },
) { item ->
FeedRow( FeedRow(
item = item, item = item,
onClick = { onOpenVideo(item.url, item.title) }, onClick = { onOpenVideo(item.url, item.title) },

View file

@ -25,13 +25,13 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
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.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import com.sulkta.straw.OverlayDimColor import com.sulkta.straw.OverlayDimColor
import com.sulkta.straw.ProgressBarFillColor import com.sulkta.straw.ProgressBarFillColor
@ -51,7 +51,13 @@ import com.sulkta.straw.util.formatDuration
@Composable @Composable
fun BoxScope.ThumbnailProgressOverlay(videoId: String?) { fun BoxScope.ThumbnailProgressOverlay(videoId: String?) {
if (videoId.isNullOrBlank()) return 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 val entry = positions[videoId] ?: return
if (entry.durationMs <= 0L) return if (entry.durationMs <= 0L) return
val fraction = (entry.positionMs.toFloat() / entry.durationMs.toFloat()) val fraction = (entry.positionMs.toFloat() / entry.durationMs.toFloat())