fix: 第三轮优化 — @Immutable覆盖、rememberImageRequest辅助函数、LazyRow keys
- EmbyApiModels: 8个嵌套类型添加@Immutable(EmbyUserData/EmbyPerson/EmbyNamedReference/EmbyMediaStream/EmbyItemCounts/EmbyResourceStats/EmbyChapter/EmbyMediaSource) - LumaComponents: 新增rememberImageRequest()辅助函数避免ImageRequest.Builder重复分配 - HomeScreen/HomeLibraryAllScreen/FavoritesAllScreen/SearchScreen/SearchAllScreen/RecordsScreen/LibraryScreen: 17处ImageRequest.Builder替换为rememberImageRequest() - MediaDetailScreen: episodes/genres/qualities/streams LazyRow添加stable key - LibraryScreen: servers LazyVerticalGrid添加stable key
This commit is contained in:
1 parent
0ca7cf39b7
commit
f44bc036e8
10 files changed
+52
-68
No files matched your search
@@ -110,12 +110,14 @@ data class EmbyItem(
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class EmbyUserData(
|
||||
@SerializedName("PlaybackPositionTicks") val playbackPositionTicks: Long = 0L,
|
||||
@SerializedName("Played") val played: Boolean = false,
|
||||
@SerializedName("LastPlayedDate") val lastPlayedDate: String? = null,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
data class EmbyPerson(
|
||||
@SerializedName("Id") val id: String = "",
|
||||
@SerializedName("Name") val name: String = "",
|
||||
@@ -124,12 +126,14 @@ data class EmbyPerson(
|
||||
@SerializedName("PrimaryImageTag") val primaryImageTag: String? = null,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
data class EmbyNamedReference(
|
||||
@SerializedName("Name") val name: String = "",
|
||||
)
|
||||
|
||||
/* ---------- Detail ---------- */
|
||||
|
||||
@Immutable
|
||||
data class EmbyMediaStream(
|
||||
@SerializedName("DisplayTitle") val displayTitle: String? = null,
|
||||
@SerializedName("DisplayLanguage") val displayLanguage: String? = null,
|
||||
@@ -161,12 +165,14 @@ data class EmbyMediaStream(
|
||||
@SerializedName("IsForced") val isForced: Boolean = false,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
data class EmbyItemCounts(
|
||||
@SerializedName("MovieCount") val movieCount: Int = 0,
|
||||
@SerializedName("SeriesCount") val seriesCount: Int = 0,
|
||||
@SerializedName("EpisodeCount") val episodeCount: Int = 0,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
data class EmbyResourceStats(
|
||||
val withVersions: EmbyItemCounts = EmbyItemCounts(),
|
||||
val withoutVersions: EmbyItemCounts = EmbyItemCounts(),
|
||||
@@ -181,6 +187,7 @@ data class EmbyResourceStats(
|
||||
)
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class EmbyChapter(
|
||||
@SerializedName("StartPositionTicks") val startPositionTicks: Long = 0L,
|
||||
@SerializedName("Name") val name: String? = null,
|
||||
@@ -196,6 +203,7 @@ data class EmbyPlaybackInfoResponse(
|
||||
@SerializedName("PlaySessionId") val playSessionId: String? = null,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
data class EmbyMediaSource(
|
||||
@SerializedName("Id") val id: String,
|
||||
@SerializedName("PlaySessionId") val playSessionId: String? = null,
|
||||
|
||||
@@ -33,14 +33,14 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.example.myapplication.ui.design.rememberImageRequest
|
||||
import com.example.myapplication.R as Res
|
||||
import com.example.myapplication.data.EmbyServer
|
||||
import com.example.myapplication.data.FavoriteEntity
|
||||
@@ -108,10 +108,7 @@ fun FavoritesAllScreen(
|
||||
) {
|
||||
if (server != null && server.iconUrl.isNotBlank()) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(server.iconUrl)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
model = rememberImageRequest(server.iconUrl),
|
||||
contentDescription = server.name,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
@@ -227,10 +224,7 @@ private fun FavoriteGridCard(
|
||||
) {
|
||||
if (imageUrl != null) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(imageUrl)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
model = rememberImageRequest(imageUrl),
|
||||
contentDescription = title,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
|
||||
@@ -29,11 +29,11 @@ import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.layout.boundsInRoot
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.layout.positionInRoot
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.request.ImageRequest
|
||||
import com.example.myapplication.ui.design.rememberImageRequest
|
||||
import com.example.myapplication.R as Res
|
||||
import com.example.myapplication.data.EmbyItem
|
||||
import com.example.myapplication.data.EmbyRepository
|
||||
@@ -286,7 +286,7 @@ private fun HomeLibraryMediaCard(
|
||||
) {
|
||||
LumaMediaCard(
|
||||
model = EmbyRepository.buildImageUrl(serverUrl, item)?.let {
|
||||
ImageRequest.Builder(LocalContext.current).data(it).crossfade(true).build()
|
||||
rememberImageRequest(it)
|
||||
},
|
||||
title = item.name,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
|
||||
@@ -69,6 +69,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import coil.request.ImageRequest
|
||||
import com.example.myapplication.ui.design.rememberImageRequest
|
||||
import coil.imageLoader
|
||||
import com.example.myapplication.R as Res
|
||||
import com.example.myapplication.data.EmbyItem
|
||||
@@ -343,7 +344,7 @@ private fun FeaturedItemBanner(
|
||||
Box(Modifier.fillMaxWidth().height(72.dp), contentAlignment = Alignment.Center) {
|
||||
if (logoUrl != null) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current).data(logoUrl).crossfade(true).build(),
|
||||
model = rememberImageRequest(logoUrl),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
placeholder = ColorPainter(Color.Transparent),
|
||||
|
||||
@@ -110,6 +110,7 @@ import com.example.myapplication.ui.design.LumaGlassLevel
|
||||
import com.example.myapplication.ui.design.LumaShapes
|
||||
import com.example.myapplication.ui.design.lumaGlass
|
||||
import coil.request.ImageRequest
|
||||
import com.example.myapplication.ui.design.rememberImageRequest
|
||||
import com.kyant.backdrop.Backdrop
|
||||
import com.kyant.backdrop.backdrops.layerBackdrop
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -381,7 +382,7 @@ fun LibraryScreen(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
// Servers section
|
||||
items(servers.size) { index ->
|
||||
items(servers.size, key = { servers[it].id }) { index ->
|
||||
val server = servers[index]
|
||||
val tint = serverTints[index % serverTints.size]
|
||||
if (isGrid) {
|
||||
@@ -872,7 +873,7 @@ private fun IconLibraryManagerCard(library: IconLibrary, onClick: () -> Unit) {
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
LumaAsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current).data(icon.url).crossfade(true).build(),
|
||||
model = rememberImageRequest(icon.url),
|
||||
contentDescription = icon.name,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize().padding(4.dp).clip(RoundedCornerShape(6.dp)),
|
||||
@@ -1014,7 +1015,7 @@ private fun ManageIconLibraryDialog(
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
LumaAsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current).data(icon.url).crossfade(true).build(),
|
||||
model = rememberImageRequest(icon.url),
|
||||
contentDescription = icon.name,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize().padding(4.dp).clip(RoundedCornerShape(10.dp)),
|
||||
@@ -1213,7 +1214,7 @@ private fun AddIconLibraryDialog(
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
LumaAsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current).data(icon.url).crossfade(true).build(),
|
||||
model = rememberImageRequest(icon.url),
|
||||
contentDescription = icon.name,
|
||||
modifier = Modifier.fillMaxSize().padding(4.dp).clip(RoundedCornerShape(10.dp)),
|
||||
contentScale = ContentScale.Crop,
|
||||
@@ -1429,7 +1430,7 @@ private fun IconPickerDialog(
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
LumaAsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current).data(icon.url).crossfade(true).build(),
|
||||
model = rememberImageRequest(icon.url),
|
||||
contentDescription = icon.name,
|
||||
modifier = Modifier.fillMaxSize().padding(4.dp).clip(RoundedCornerShape(10.dp)),
|
||||
contentScale = ContentScale.Crop,
|
||||
|
||||
@@ -583,7 +583,7 @@ fun MediaDetailScreen(
|
||||
if (detail.genres.isNotEmpty()) {
|
||||
Spacer(Modifier.height(18.dp))
|
||||
LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
items(detail.genres.size) { index ->
|
||||
items(detail.genres.size, key = { detail.genres[it] }) { index ->
|
||||
val genre = detail.genres[index]
|
||||
Box(
|
||||
Modifier
|
||||
@@ -648,7 +648,7 @@ fun MediaDetailScreen(
|
||||
val episodeSpacing = 9.dp
|
||||
val episodeWidth = (maxWidth - episodeSpacing) / 2
|
||||
LazyRow(horizontalArrangement = Arrangement.spacedBy(episodeSpacing)) {
|
||||
items(episodes.size) { index ->
|
||||
items(episodes.size, key = { episodes[it].id }) { index ->
|
||||
val episode = episodes[index]
|
||||
Box(Modifier.width(episodeWidth)) {
|
||||
EpisodeCard(
|
||||
@@ -1186,7 +1186,7 @@ private fun ResourceSection(
|
||||
if (qualities.isNotEmpty()) {
|
||||
Spacer(Modifier.height(12.dp))
|
||||
LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
items(qualities.size) { index ->
|
||||
items(qualities.size, key = { qualities[it] }) { index ->
|
||||
Box(
|
||||
Modifier
|
||||
.height(36.dp)
|
||||
@@ -1562,7 +1562,7 @@ private fun MediaInformationSection(item: EmbyItem, loading: Boolean) {
|
||||
fontSize = 12.sp,
|
||||
)
|
||||
else -> LazyRow(horizontalArrangement = Arrangement.spacedBy(9.dp)) {
|
||||
items(streams.size) { index ->
|
||||
items(streams.size, key = { "${streams[it].type}_${streams[it].codec}_${streams[it].language}" }) { index ->
|
||||
val stream = streams[index]
|
||||
val typeIndex = streams.take(index + 1).count { it.type.equals(stream.type, ignoreCase = true) }
|
||||
val typeCount = streams.count { it.type.equals(stream.type, ignoreCase = true) }
|
||||
@@ -1774,7 +1774,7 @@ private fun EpisodeListDialog(
|
||||
modifier = Modifier.fillMaxWidth().heightIn(max = 440.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
items(episodes.size) { index ->
|
||||
items(episodes.size, key = { episodes[it].id }) { index ->
|
||||
val episode = episodes[index]
|
||||
CompactEpisodeRow(
|
||||
item = episode,
|
||||
|
||||
@@ -41,7 +41,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@@ -49,7 +49,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.example.myapplication.ui.design.rememberImageRequest
|
||||
import com.example.myapplication.R as Res
|
||||
import com.example.myapplication.data.EmbyItem
|
||||
import com.example.myapplication.data.EmbyRepository
|
||||
@@ -254,13 +254,7 @@ private fun ServerHeader(server: EmbyServer) {
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
if (server.iconUrl.isNotBlank()) {
|
||||
val ctx = LocalContext.current
|
||||
val iconRequest = remember(server.iconUrl) {
|
||||
ImageRequest.Builder(ctx)
|
||||
.data(server.iconUrl)
|
||||
.crossfade(true)
|
||||
.build()
|
||||
}
|
||||
val iconRequest = rememberImageRequest(server.iconUrl)
|
||||
AsyncImage(
|
||||
model = iconRequest,
|
||||
contentDescription = server.name,
|
||||
@@ -396,10 +390,7 @@ private fun FavoritePosterCard(
|
||||
) {
|
||||
if (imageUrl != null) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(imageUrl)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
model = rememberImageRequest(imageUrl),
|
||||
contentDescription = title,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
@@ -792,10 +783,7 @@ private fun CalendarEpisodeCard(episode: TmdbEpisodeCalendar, onClick: () -> Uni
|
||||
) {
|
||||
if (posterUrl != null) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(posterUrl)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
model = rememberImageRequest(posterUrl),
|
||||
contentDescription = episode.seriesName,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
@@ -834,10 +822,7 @@ private fun CalendarEpisodeCard(episode: TmdbEpisodeCalendar, onClick: () -> Uni
|
||||
if (episode.logoPath != null) {
|
||||
Spacer(Modifier.width(6.dp))
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(episode.logoPath)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
model = rememberImageRequest(episode.logoPath),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier
|
||||
@@ -870,10 +855,7 @@ private fun CalendarEpisodeCard(episode: TmdbEpisodeCalendar, onClick: () -> Uni
|
||||
Spacer(Modifier.width(4.dp))
|
||||
if (episode.networkLogoPath != null) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(episode.networkLogoPath)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
model = rememberImageRequest(episode.networkLogoPath),
|
||||
contentDescription = episode.networkName,
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier
|
||||
|
||||
@@ -32,14 +32,14 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.example.myapplication.ui.design.rememberImageRequest
|
||||
import com.example.myapplication.R as Res
|
||||
import com.example.myapplication.data.EmbyItem
|
||||
import com.example.myapplication.data.EmbyRepository
|
||||
@@ -119,10 +119,7 @@ fun SearchAllScreen(
|
||||
) {
|
||||
if (server != null && server.iconUrl.isNotBlank()) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(server.iconUrl)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
model = rememberImageRequest(server.iconUrl),
|
||||
contentDescription = server.name,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
@@ -252,10 +249,7 @@ private fun SearchAllMediaCard(
|
||||
) {
|
||||
if (imageUrl != null) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(imageUrl)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
model = rememberImageRequest(imageUrl),
|
||||
contentDescription = title,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
|
||||
@@ -50,7 +50,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.example.myapplication.ui.design.rememberImageRequest
|
||||
import com.example.myapplication.R as Res
|
||||
import com.example.myapplication.data.EmbyItem
|
||||
import com.example.myapplication.data.EmbyRepository
|
||||
@@ -551,10 +551,7 @@ private fun ServerSearchSection(
|
||||
) {
|
||||
if (server.iconUrl.isNotBlank()) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(server.iconUrl)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
model = rememberImageRequest(server.iconUrl),
|
||||
contentDescription = server.name,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
@@ -759,10 +756,7 @@ private fun SearchMediaPoster(
|
||||
) {
|
||||
if (imageUrl != null) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(imageUrl)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
model = rememberImageRequest(imageUrl),
|
||||
contentDescription = title,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
|
||||
@@ -52,6 +52,7 @@ import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.painter.ColorPainter
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.layout.LayoutCoordinates
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
@@ -70,6 +71,15 @@ import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import top.yukonga.miuix.kmp.basic.Text
|
||||
|
||||
// P2: Reusable helper — creates a remembered ImageRequest to avoid allocation on every recomposition
|
||||
@Composable
|
||||
fun rememberImageRequest(modelData: Any?): ImageRequest? {
|
||||
val context = LocalContext.current
|
||||
return remember(modelData) {
|
||||
modelData?.let { ImageRequest.Builder(context).data(it).crossfade(true).build() }
|
||||
}
|
||||
}
|
||||
|
||||
private enum class LumaImageState {
|
||||
Loading,
|
||||
Success,
|
||||
|
||||
Reference in new issue
Block a user