From 243e08b635f2ec7f97e95005790b68b454744d64 Mon Sep 17 00:00:00 2001 From: Ray <154766448+damontecres@users.noreply.github.com> Date: Mon, 29 Dec 2025 15:36:05 -0500 Subject: [PATCH] Some minor optimizations to improve UI speed (#593) ## Description Many tiny changes that improve UI rendering speed and reduces recompositions Still plenty of room for improvement though. --- .../wholphin/data/model/BaseItem.kt | 2 + .../damontecres/wholphin/ui/cards/ItemRow.kt | 60 +------------------ .../wholphin/ui/components/MovieComponents.kt | 3 +- .../ui/components/SeriesComponents.kt | 3 +- .../wholphin/ui/components/TimeDisplay.kt | 4 +- .../wholphin/ui/detail/PlaylistDetails.kt | 2 +- .../damontecres/wholphin/ui/main/HomePage.kt | 58 +++++++----------- .../damontecres/wholphin/ui/nav/NavDrawer.kt | 9 +-- .../wholphin/ui/util/LocalClock.kt | 21 ++++--- .../wholphin/test/TestStreamChoiceService.kt | 10 ++++ 10 files changed, 62 insertions(+), 110 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt index e5cd53bc..599b7638 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt @@ -1,5 +1,6 @@ package com.github.damontecres.wholphin.data.model +import androidx.compose.runtime.Stable import com.github.damontecres.wholphin.ui.detail.CardGridItem import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds import com.github.damontecres.wholphin.ui.formatDateTime @@ -17,6 +18,7 @@ import org.jellyfin.sdk.model.extensions.ticks import kotlin.time.Duration @Serializable +@Stable data class BaseItem( val data: BaseItemDto, val useSeriesForPrimary: Boolean, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ItemRow.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ItemRow.kt index 618b9003..c03a373a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ItemRow.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/ItemRow.kt @@ -8,22 +8,15 @@ import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRestorer -import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text -import com.github.damontecres.wholphin.data.model.BaseItem -import com.github.damontecres.wholphin.ui.AspectRatios -import com.github.damontecres.wholphin.util.FocusPair @Composable fun ItemRow( @@ -39,13 +32,10 @@ fun ItemRow( onLongClick: () -> Unit, ) -> Unit, modifier: Modifier = Modifier, - focusPair: FocusPair? = null, - cardOnFocus: ((isFocused: Boolean, index: Int) -> Unit)? = null, horizontalPadding: Dp = 16.dp, ) { val state = rememberLazyListState() val firstFocus = remember { FocusRequester() } - var focusedIndex by remember { mutableIntStateOf(focusPair?.column ?: 0) } Column( verticalArrangement = Arrangement.spacedBy(8.dp), modifier = modifier, @@ -62,23 +52,14 @@ fun ItemRow( modifier = Modifier .fillMaxWidth() - .focusRestorer(focusPair?.focusRequester ?: firstFocus), + .focusRestorer(firstFocus), ) { itemsIndexed(items) { index, item -> val cardModifier = - if (index == 0 && focusPair == null) { + if (index == 0) { Modifier.focusRequester(firstFocus) } else { - if (focusPair != null && focusPair.column == index) { - Modifier.focusRequester(focusPair.focusRequester) - } else { - Modifier - } - }.onFocusChanged { - if (it.isFocused) { - focusedIndex = index - } - cardOnFocus?.invoke(it.isFocused, index) + Modifier } cardContent.invoke( index, @@ -91,38 +72,3 @@ fun ItemRow( } } } - -@Composable -fun BannerItemRow( - title: String, - items: List, - onClickItem: (Int, BaseItem) -> Unit, - onLongClickItem: (Int, BaseItem) -> Unit, - modifier: Modifier = Modifier, - focusPair: FocusPair? = null, - cardOnFocus: ((isFocused: Boolean, index: Int) -> Unit)? = null, - aspectRatioOverride: Float? = null, -) = ItemRow( - title = title, - items = items, - onClickItem = onClickItem, - onLongClickItem = onLongClickItem, - modifier = modifier, - cardContent = { index, item, modifier, onClick, onLongClick -> - BannerCard( - name = title, - item = item, - aspectRatio = - aspectRatioOverride ?: item?.aspectRatio ?: AspectRatios.WIDE, - cornerText = item?.data?.indexNumber?.let { "E$it" }, - played = item?.data?.userData?.played ?: false, - playPercent = item?.data?.userData?.playedPercentage ?: 0.0, - onClick = onClick, - onLongClick = onLongClick, - modifier = modifier, - interactionSource = null, - ) - }, - focusPair = focusPair, - cardOnFocus = cardOnFocus, -) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/MovieComponents.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/MovieComponents.kt index e25f50be..6d9984dc 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/MovieComponents.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/MovieComponents.kt @@ -2,6 +2,7 @@ package com.github.damontecres.wholphin.ui.components import android.content.Context import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -21,7 +22,7 @@ fun MovieQuickDetails( modifier: Modifier = Modifier, ) { val context = LocalContext.current - val now = LocalClock.current.now + val now by LocalClock.current.now val details = remember(dto, now) { buildList { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/SeriesComponents.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/SeriesComponents.kt index de79bd5a..3939df91 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/SeriesComponents.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/SeriesComponents.kt @@ -1,6 +1,7 @@ package com.github.damontecres.wholphin.ui.components import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -59,7 +60,7 @@ fun EpisodeQuickDetails( modifier: Modifier = Modifier, ) { val context = LocalContext.current - val now = LocalClock.current.now + val now by LocalClock.current.now val details = remember(dto, now) { buildList { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/TimeDisplay.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/TimeDisplay.kt index 8fa6f60f..f5791e04 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/TimeDisplay.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/TimeDisplay.kt @@ -3,6 +3,7 @@ package com.github.damontecres.wholphin.ui.components import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -13,8 +14,9 @@ import com.github.damontecres.wholphin.ui.util.LocalClock @Composable fun BoxScope.TimeDisplay(modifier: Modifier = Modifier) { + val timeString by LocalClock.current.timeString Text( - text = LocalClock.current.timeString, + text = timeString, fontSize = 18.sp, color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.bodyLarge, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt index 150228a5..4907d71e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt @@ -410,7 +410,7 @@ fun PlaylistItem( }, trailingContent = { item?.data?.runTimeTicks?.ticks?.roundMinutes?.let { duration -> - val now = LocalClock.current.now + val now by LocalClock.current.now val endTimeStr = remember(item, now) { val endTime = now.toLocalTime().plusSeconds(duration.inWholeSeconds) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt index 05519d8c..c838733a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt @@ -66,7 +66,6 @@ import com.github.damontecres.wholphin.ui.detail.MoreDialogActions import com.github.damontecres.wholphin.ui.detail.PlaylistDialog import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome -import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.playback.isPlayKeyUp @@ -226,14 +225,15 @@ fun HomePageContent( var position by rememberSaveable(stateSaver = RowColumnSaver) { mutableStateOf(RowColumn(firstRow, 0)) } - var focusedItem = - position.let { - (homeRows.getOrNull(it.row) as? HomeRowLoadingState.Success)?.items?.getOrNull(it.column) + val focusedItem = + remember(position) { + position.let { + (homeRows.getOrNull(it.row) as? HomeRowLoadingState.Success)?.items?.getOrNull(it.column) + } } val listState = rememberLazyListState() - val focusRequester = remember { FocusRequester() } - val positionFocusRequester = remember { FocusRequester() } + val rowFocusRequesters = remember(homeRows.size) { List(homeRows.size) { FocusRequester() } } var focused by remember { mutableStateOf(false) } LaunchedEffect(homeRows) { if (!focused) { @@ -241,7 +241,7 @@ fun HomePageContent( .indexOfFirst { it is HomeRowLoadingState.Success && it.items.isNotEmpty() } .takeIf { it >= 0 } ?.let { - positionFocusRequester.tryRequestFocus() + rowFocusRequesters[it].tryRequestFocus() delay(50) listState.animateScrollToItem(position.row) focused = true @@ -251,9 +251,6 @@ fun HomePageContent( LaunchedEffect(position) { listState.animateScrollToItem(position.row) } - LaunchedEffect(focusedItem) { - focusedItem?.let(onUpdateBackdrop) - } Box(modifier = modifier) { Column(modifier = Modifier.fillMaxSize()) { HomePageHeader( @@ -275,16 +272,7 @@ fun HomePageContent( ), modifier = Modifier - .focusRestorer() - .onKeyEvent { - val item = focusedItem - if (isPlayKeyUp(it) && item?.type?.playable == true) { - Timber.v("Clicked play on ${item.id}") - onClickPlay.invoke(position, item) - return@onKeyEvent true - } - return@onKeyEvent false - }, + .focusRestorer(), ) { itemsIndexed(homeRows) { rowIndex, row -> when (val r = row) { @@ -347,18 +335,13 @@ fun HomePageContent( onClickItem = { index, item -> onClickItem.invoke(RowColumn(rowIndex, index), item) }, - cardOnFocus = { isFocused, index -> - if (isFocused) { - focusedItem = row.items.getOrNull(index) - position = RowColumn(rowIndex, index) - } - }, onLongClickItem = { index, item -> onLongClickItem.invoke(RowColumn(rowIndex, index), item) }, modifier = Modifier .fillMaxWidth() + .focusRequester(rowFocusRequesters[rowIndex]) .animateItem(), cardContent = { index, item, cardModifier, onClick, onLongClick -> val cornerText = @@ -385,29 +368,32 @@ fun HomePageContent( onLongClick = onLongClick, modifier = cardModifier - .ifElse( - focusedItem == item, - Modifier.focusRequester(focusRequester), - ).ifElse( - RowColumn(rowIndex, index) == position, - Modifier.focusRequester( - positionFocusRequester, - ), - ).onFocusChanged { + .onFocusChanged { if (it.isFocused) { + position = RowColumn(rowIndex, index) + item?.let(onUpdateBackdrop) + } + if (it.isFocused && onFocusPosition != null) { val nonEmptyRowBefore = homeRows .subList(0, rowIndex) .count { it is HomeRowLoadingState.Success && it.items.isEmpty() } - onFocusPosition?.invoke( + onFocusPosition.invoke( RowColumn( rowIndex - nonEmptyRowBefore, index, ), ) } + }.onKeyEvent { + if (isPlayKeyUp(it) && item?.type?.playable == true) { + Timber.v("Clicked play on ${item.id}") + onClickPlay.invoke(position, item) + return@onKeyEvent true + } + return@onKeyEvent false }, interactionSource = null, cardHeight = Cards.height2x3, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt index ef858eb7..ff3a64b3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt @@ -4,7 +4,6 @@ import android.content.Context import androidx.activity.compose.BackHandler import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.animateDpAsState -import androidx.compose.foundation.background import androidx.compose.foundation.focusGroup import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState @@ -37,11 +36,11 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalConfiguration @@ -346,10 +345,8 @@ fun NavDrawer( Modifier .fillMaxHeight() .width(drawerWidth) - .background(drawerBackground) - .onFocusChanged { - if (!it.hasFocus) { - } + .drawBehind { + drawRect(drawerBackground) }, ) { // Even though some must be clicked, focusing on it should clear other focused items diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/util/LocalClock.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/util/LocalClock.kt index 53cd4d9b..5c5d2a60 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/util/LocalClock.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/util/LocalClock.kt @@ -3,11 +3,10 @@ package com.github.damontecres.wholphin.ui.util import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState import androidx.compose.runtime.compositionLocalOf -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import com.github.damontecres.wholphin.ui.TimeFormatter import kotlinx.coroutines.delay import kotlinx.coroutines.isActive @@ -22,20 +21,28 @@ data class Clock( /** * The current [LocalDateTime] */ - val now: LocalDateTime, + val now: MutableState, /** * The current time formatted as a string with [TimeFormatter] */ - val timeString: String, + val timeString: MutableState, ) @Composable fun ProvideLocalClock(content: @Composable () -> Unit) { - var clock by remember { mutableStateOf(LocalDateTime.now().let { Clock(it, TimeFormatter.format(it)) }) } + val clock = + remember { + LocalDateTime + .now() + .let { Clock(mutableStateOf(it), mutableStateOf(TimeFormatter.format(it))) } + } LaunchedEffect(Unit) { while (isActive) { - clock = LocalDateTime.now().let { Clock(it, TimeFormatter.format(it)) } - delay(1_000) + val now = LocalDateTime.now() + val time = TimeFormatter.format(now) + clock.now.value = now + clock.timeString.value = time + delay(2_000) } } CompositionLocalProvider(LocalClock provides clock, content) diff --git a/app/src/test/java/com/github/damontecres/wholphin/test/TestStreamChoiceService.kt b/app/src/test/java/com/github/damontecres/wholphin/test/TestStreamChoiceService.kt index fa16c5ab..7d551049 100644 --- a/app/src/test/java/com/github/damontecres/wholphin/test/TestStreamChoiceService.kt +++ b/app/src/test/java/com/github/damontecres/wholphin/test/TestStreamChoiceService.kt @@ -104,6 +104,16 @@ class TestStreamChoiceServiceBasic( ), itemPlayback = itemPlayback(subtitleIndex = TrackIndex.UNSPECIFIED), ), + TestInput( + 1, + SubtitlePlaybackMode.ALWAYS, + subtitles = + listOf( + subtitle(0, "eng", forced = true, default = true), + subtitle(1, "eng", false), + subtitle(2, "eng", false), + ), + ), ) } }