pref: 优化动态列表性能

This commit is contained in:
HuanCheng65 2023-07-11 14:49:08 +08:00
parent 2ef051f7fa
commit db88b51b14
No known key found for this signature in database
GPG Key ID: 5EC9DD60A32C7360
4 changed files with 231 additions and 166 deletions

View File

@ -2,6 +2,8 @@ package com.huanchengfly.tieba.post.arch
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@Stable @Stable
class StableHolder<T>(val item: T) { class StableHolder<T>(val item: T) {
@ -53,8 +55,8 @@ class ImmutableHolder<T>(val item: T) {
return wrapImmutable(getter(item)) return wrapImmutable(getter(item))
} }
fun <R> getImmutableList(getter: T.() -> List<R>): List<ImmutableHolder<R>> { fun <R> getImmutableList(getter: T.() -> List<R>): ImmutableList<ImmutableHolder<R>> {
return getter(item).map { wrapImmutable(it) } return getter(item).map { wrapImmutable(it) }.toImmutableList()
} }
@Stable @Stable
@ -68,4 +70,5 @@ fun <T> wrapStable(item: T): StableHolder<T> = StableHolder(item)
fun <T> wrapImmutable(item: T): ImmutableHolder<T> = ImmutableHolder(item) fun <T> wrapImmutable(item: T): ImmutableHolder<T> = ImmutableHolder(item)
fun <T> List<T>.wrapImmutable(): List<ImmutableHolder<T>> = map { wrapImmutable(it) } fun <T> List<T>.wrapImmutable(): ImmutableList<ImmutableHolder<T>> =
map { wrapImmutable(it) }.toImmutableList()

View File

@ -57,6 +57,8 @@ import com.huanchengfly.tieba.post.arch.collectPartialAsState
import com.huanchengfly.tieba.post.arch.onEvent import com.huanchengfly.tieba.post.arch.onEvent
import com.huanchengfly.tieba.post.arch.pageViewModel import com.huanchengfly.tieba.post.arch.pageViewModel
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
import com.huanchengfly.tieba.post.ui.page.LocalNavigator
import com.huanchengfly.tieba.post.ui.page.destinations.ForumPageDestination
import com.huanchengfly.tieba.post.ui.page.main.MainUiEvent import com.huanchengfly.tieba.post.ui.page.main.MainUiEvent
import com.huanchengfly.tieba.post.ui.widgets.compose.FeedCard import com.huanchengfly.tieba.post.ui.widgets.compose.FeedCard
import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad
@ -76,6 +78,7 @@ fun PersonalizedPage(
viewModel.initialized = true viewModel.initialized = true
} }
val context = LocalContext.current val context = LocalContext.current
val navigator = LocalNavigator.current
val isRefreshing by viewModel.uiState.collectPartialAsState( val isRefreshing by viewModel.uiState.collectPartialAsState(
prop1 = PersonalizedUiState::isRefreshing, prop1 = PersonalizedUiState::isRefreshing,
initial = false initial = false
@ -172,6 +175,9 @@ fun PersonalizedPage(
) )
}, },
onRefresh = { viewModel.send(PersonalizedUiIntent.Refresh) }, onRefresh = { viewModel.send(PersonalizedUiIntent.Refresh) },
onOpenForum = {
navigator.navigate(ForumPageDestination(it))
},
state = lazyStaggeredGridState state = lazyStaggeredGridState
) )
LaunchedEffect(data.firstOrNull()?.get { id }) { LaunchedEffect(data.firstOrNull()?.get { id }) {
@ -223,6 +229,7 @@ private fun FeedList(
onAgree: (ThreadInfo) -> Unit, onAgree: (ThreadInfo) -> Unit,
onDislike: (ThreadInfo, Long, List<ImmutableHolder<DislikeReason>>) -> Unit, onDislike: (ThreadInfo, Long, List<ImmutableHolder<DislikeReason>>) -> Unit,
onRefresh: () -> Unit, onRefresh: () -> Unit,
onOpenForum: (forumName: String) -> Unit = {},
state: LazyStaggeredGridState, state: LazyStaggeredGridState,
) { ) {
val data = dataProvider() val data = dataProvider()
@ -259,6 +266,9 @@ private fun FeedList(
onAgree = { onAgree = {
onAgree(item.get()) onAgree(item.get())
}, },
onClickForum = {
onOpenForum(item.get { forumInfo?.name ?: "" })
}
) { ) {
Dislike( Dislike(
personalized = threadPersonalizedData[index], personalized = threadPersonalizedData[index],

View File

@ -1,6 +1,7 @@
package com.huanchengfly.tieba.post.ui.utils package com.huanchengfly.tieba.post.ui.utils
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.huanchengfly.tieba.post.api.models.protos.Media
import com.huanchengfly.tieba.post.api.models.protos.Post import com.huanchengfly.tieba.post.api.models.protos.Post
import com.huanchengfly.tieba.post.api.models.protos.ThreadInfo import com.huanchengfly.tieba.post.api.models.protos.ThreadInfo
import com.huanchengfly.tieba.post.arch.ImmutableHolder import com.huanchengfly.tieba.post.arch.ImmutableHolder
@ -54,19 +55,35 @@ fun getPhotoViewData(
threadInfo: ThreadInfo, threadInfo: ThreadInfo,
index: Int index: Int
): PhotoViewData { ): PhotoViewData {
val media = threadInfo.media[index] return getPhotoViewData(
medias = threadInfo.media,
forumId = threadInfo.forumId,
forumName = threadInfo.forumName,
threadId = threadInfo.threadId,
index = index
)
}
fun getPhotoViewData(
medias: List<Media>,
forumId: Long,
forumName: String,
threadId: Long,
index: Int
): PhotoViewData {
val media = medias[index]
return PhotoViewData( return PhotoViewData(
data_ = LoadPicPageData( data_ = LoadPicPageData(
forumId = threadInfo.forumId, forumId = forumId,
forumName = threadInfo.forumName, forumName = forumName,
threadId = threadInfo.threadId, threadId = threadId,
postId = media.postId, postId = media.postId,
seeLz = false, seeLz = false,
objType = "index", objType = "index",
picId = ImageUtil.getPicId(media.originPic), picId = ImageUtil.getPicId(media.originPic),
picIndex = index + 1 picIndex = index + 1
), ),
picItems = threadInfo.media.mapIndexed { mediaIndex, mediaItem -> picItems = medias.mapIndexed { mediaIndex, mediaItem ->
PicItem( PicItem(
picId = ImageUtil.getPicId(mediaItem.originPic), picId = ImageUtil.getPicId(mediaItem.originPic),
picIndex = mediaIndex + 1, picIndex = mediaIndex + 1,

View File

@ -29,6 +29,7 @@ import androidx.compose.material.icons.rounded.SwapCalls
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
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
@ -62,8 +63,6 @@ import com.huanchengfly.tieba.post.api.models.protos.User
import com.huanchengfly.tieba.post.arch.ImmutableHolder import com.huanchengfly.tieba.post.arch.ImmutableHolder
import com.huanchengfly.tieba.post.arch.wrapImmutable import com.huanchengfly.tieba.post.arch.wrapImmutable
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
import com.huanchengfly.tieba.post.ui.page.LocalNavigator
import com.huanchengfly.tieba.post.ui.page.destinations.ForumPageDestination
import com.huanchengfly.tieba.post.ui.utils.getImmutablePhotoViewData import com.huanchengfly.tieba.post.ui.utils.getImmutablePhotoViewData
import com.huanchengfly.tieba.post.ui.widgets.VideoPlayerStandard import com.huanchengfly.tieba.post.ui.widgets.VideoPlayerStandard
import com.huanchengfly.tieba.post.utils.DateTimeUtils import com.huanchengfly.tieba.post.utils.DateTimeUtils
@ -306,20 +305,187 @@ private fun ForumInfoChip(
} }
} }
@Composable
private fun ThreadMedia(
item: ImmutableHolder<ThreadInfo>
) {
val isVideo = remember(item) {
item.isNotNull { videoInfo }
}
val medias = remember(item) {
item.getImmutableList { media }
}
if (isVideo) {
val videoInfo = item.getImmutable { videoInfo!! }
VideoPlayer(
videoUrl = videoInfo.get { videoUrl },
thumbnailUrl = videoInfo.get { thumbnailUrl },
modifier = Modifier
.fillMaxWidth()
.aspectRatio(
max(
videoInfo
.get { thumbnailWidth }
.toFloat() / videoInfo.get { thumbnailHeight },
16f / 9
)
)
.clip(RoundedCornerShape(8.dp))
)
} else if (medias.isNotEmpty()) {
Box {
Row(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(if (medias.size == 1) 2f else 3f)
.clip(RoundedCornerShape(8.dp)),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
medias.subList(0, min(medias.size, 3))
.forEachIndexed { index, media ->
val photoViewData = remember(item, index) {
getImmutablePhotoViewData(item.get(), index)
}
NetworkImage(
imageUri = media.url,
contentDescription = null,
modifier = Modifier.weight(1f),
photoViewData = photoViewData,
contentScale = ContentScale.Crop
)
}
}
if (medias.size > 3) {
Badge(
icon = Icons.Rounded.PhotoSizeSelectActual,
text = "${medias.size}",
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(8.dp)
)
}
}
}
}
@Composable
private fun ThreadForumInfo(
item: ImmutableHolder<ThreadInfo>,
onClick: () -> Unit
) {
val hasForumInfo = remember(item) { item.isNotNull { forumInfo } }
if (hasForumInfo) {
val forumInfo = remember(item) { item.getImmutable { forumInfo!! } }
if (forumInfo.get { name }.isNotBlank()) {
ForumInfoChip(
imageUriProvider = { StringUtil.getAvatarUrl(forumInfo.get { avatar }) },
nameProvider = { forumInfo.get { name } },
onClick = onClick
)
}
}
}
@Composable
private fun ThreadCommentBtn(
commentNum: Int,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
ActionBtn(
icon = {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_comment_new),
contentDescription = stringResource(id = R.string.desc_comment),
)
},
text = {
Text(
text = if (commentNum == 0)
stringResource(id = R.string.title_reply)
else "$commentNum"
)
},
modifier = modifier,
onClick = onClick
)
}
@Composable
private fun ThreadAgreeBtn(
hasAgree: Boolean,
agreeNum: Int,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val contentColor =
if (hasAgree) ExtendedTheme.colors.accent else ExtendedTheme.colors.textSecondary
val animatedColor by animateColorAsState(contentColor, label = "agreeBtnContentColor")
ActionBtn(
icon = {
Icon(
imageVector = if (hasAgree) Icons.Rounded.Favorite else Icons.Rounded.FavoriteBorder,
contentDescription = stringResource(id = R.string.desc_like),
)
},
text = {
Text(
text = if (agreeNum == 0)
stringResource(id = R.string.title_agree)
else "$agreeNum"
)
},
modifier = modifier,
color = animatedColor,
onClick = onClick
)
}
@Composable
private fun ThreadShareBtn(
shareNum: Long,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
ActionBtn(
icon = {
Icon(
imageVector = Icons.Rounded.SwapCalls,
contentDescription = stringResource(id = R.string.desc_share),
)
},
text = {
Text(
text = if (shareNum == 0L)
stringResource(id = R.string.title_share)
else "$shareNum"
)
},
modifier = modifier,
onClick = onClick
)
}
@Composable @Composable
fun FeedCard( fun FeedCard(
item: ImmutableHolder<ThreadInfo>, item: ImmutableHolder<ThreadInfo>,
onClick: () -> Unit, onClick: () -> Unit,
onAgree: () -> Unit, onAgree: () -> Unit,
onClickForum: () -> Unit = {},
dislikeAction: @Composable () -> Unit = {}, dislikeAction: @Composable () -> Unit = {},
) { ) {
Card( Card(
header = { header = {
if (item.isNotNull { author }) { val hasAuthor = remember(item) { item.isNotNull { author } }
val author = item.getImmutable { author!! } if (hasAuthor) {
val author = remember(item) { item.getImmutable { author!! } }
val time = remember(item) { item.get { lastTimeInt } }
DefaultUserHeader( DefaultUserHeader(
user = author, user = author,
time = item.get { lastTimeInt }) { dislikeAction() } time = time
) { dislikeAction() }
} }
}, },
content = { content = {
@ -332,130 +498,29 @@ fun FeedCard(
isGood = item.get { isGood == 1 } isGood = item.get { isGood == 1 }
) )
if (item.isNotNull { videoInfo }) { ThreadMedia(item = item)
val videoInfo = item.getImmutable { videoInfo!! }
VideoPlayer(
videoUrl = videoInfo.get { videoUrl },
thumbnailUrl = videoInfo.get { thumbnailUrl },
modifier = Modifier
.fillMaxWidth()
.aspectRatio(
max(
videoInfo
.get { thumbnailWidth }
.toFloat() / videoInfo.get { thumbnailHeight },
16f / 9
)
)
.clip(RoundedCornerShape(8.dp))
)
} else if (item.getImmutableList { media }.isNotEmpty()) {
val media = item.getImmutableList { media }
Box {
Row(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(if (media.size == 1) 2f else 3f)
.clip(RoundedCornerShape(8.dp)),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
media.subList(0, min(media.size, 3))
.forEachIndexed { index, media ->
NetworkImage(
imageUri = media.url,
contentDescription = null,
modifier = Modifier.weight(1f),
photoViewData = getImmutablePhotoViewData(item.get(), index),
contentScale = ContentScale.Crop
)
}
}
if (media.size > 3) {
Badge(
icon = Icons.Rounded.PhotoSizeSelectActual,
text = "${media.size}",
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(8.dp)
)
}
}
}
if (item.isNotNull { forumInfo }) { ThreadForumInfo(item = item, onClick = onClickForum)
val navigator = LocalNavigator.current
val forumInfo = item.getImmutable { forumInfo!! }
ForumInfoChip(
imageUriProvider = { StringUtil.getAvatarUrl(forumInfo.get { avatar }) },
nameProvider = { forumInfo.get { name } },
onClick = {
navigator.navigate(ForumPageDestination(forumInfo.get { name }))
}
)
}
}, },
action = { action = {
Row(modifier = Modifier.fillMaxWidth()) { Row(modifier = Modifier.fillMaxWidth()) {
ActionBtn( ThreadCommentBtn(
icon = { commentNum = item.get { commentNum },
Icon( onClick = onClick,
imageVector = ImageVector.vectorResource(id = R.drawable.ic_comment_new), modifier = Modifier.weight(1f)
contentDescription = stringResource(id = R.string.desc_comment)
)
},
text = {
val replyNum = item.get { replyNum }
Text(
text = if (replyNum == 0)
stringResource(id = R.string.title_reply)
else "$replyNum"
)
},
modifier = Modifier.weight(1f),
color = ExtendedTheme.colors.textSecondary,
onClick = {},
) )
val hasAgree = item.get { agree?.hasAgree == 1 } ThreadAgreeBtn(
ActionBtn( hasAgree = item.get { agree?.hasAgree == 1 },
icon = { agreeNum = item.get { agreeNum },
Icon( onClick = onAgree,
imageVector = if (hasAgree) Icons.Rounded.Favorite else Icons.Rounded.FavoriteBorder, modifier = Modifier.weight(1f)
contentDescription = stringResource(id = R.string.desc_like)
)
},
text = {
val agreeNum = item.get { agreeNum }
Text(
text = if (agreeNum == 0)
stringResource(id = R.string.title_agree)
else "$agreeNum"
)
},
modifier = Modifier.weight(1f),
color = if (hasAgree) ExtendedTheme.colors.accent else ExtendedTheme.colors.textSecondary,
onClick = onAgree
) )
ActionBtn( ThreadShareBtn(
icon = { shareNum = item.get { shareNum },
Icon(
imageVector = Icons.Rounded.SwapCalls,
contentDescription = stringResource(id = R.string.desc_share)
)
},
text = {
val shareNum = item.get { shareNum }
Text(
text = if (shareNum == 0L)
stringResource(id = R.string.title_share)
else shareNum.toString()
)
},
modifier = Modifier.weight(1f),
color = ExtendedTheme.colors.textSecondary,
onClick = {}, onClick = {},
modifier = Modifier.weight(1f)
) )
} }
}, },
@ -486,35 +551,6 @@ private fun ActionBtnPlaceholder(
} }
} }
@Composable
private fun ActionBtn(
icon: ImageVector,
contentDescription: String?,
text: String,
modifier: Modifier = Modifier,
color: Color = LocalContentColor.current,
onClick: (() -> Unit)? = null,
) {
val animatedColor by animateColorAsState(targetValue = color)
val clickableModifier = if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier
Row(
modifier = clickableModifier
.padding(vertical = 16.dp)
.then(modifier),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
Icon(
imageVector = icon,
contentDescription = contentDescription,
modifier = Modifier.size(18.dp),
tint = animatedColor,
)
Spacer(modifier = Modifier.width(8.dp))
Text(text = text, style = MaterialTheme.typography.caption, color = animatedColor)
}
}
@Composable @Composable
private fun ActionBtn( private fun ActionBtn(
icon: @Composable () -> Unit, icon: @Composable () -> Unit,
@ -523,7 +559,6 @@ private fun ActionBtn(
color: Color = LocalContentColor.current, color: Color = LocalContentColor.current,
onClick: (() -> Unit)? = null, onClick: (() -> Unit)? = null,
) { ) {
val animatedColor by animateColorAsState(targetValue = color)
val clickableModifier = if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier val clickableModifier = if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier
Row( Row(
modifier = clickableModifier modifier = clickableModifier
@ -532,12 +567,12 @@ private fun ActionBtn(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
) { ) {
Box(modifier = Modifier.size(18.dp)) { ProvideContentColor(color = color) {
icon() Box(modifier = Modifier.size(18.dp)) {
} icon()
Spacer(modifier = Modifier.width(8.dp)) }
ProvideTextStyle(value = MaterialTheme.typography.caption) { Spacer(modifier = Modifier.width(8.dp))
ProvideContentColor(color = animatedColor) { ProvideTextStyle(value = MaterialTheme.typography.caption) {
text() text()
} }
} }
@ -548,8 +583,8 @@ private fun ActionBtn(
fun VideoPlayer( fun VideoPlayer(
videoUrl: String, videoUrl: String,
thumbnailUrl: String, thumbnailUrl: String,
title: String = "", modifier: Modifier = Modifier,
modifier: Modifier = Modifier title: String = ""
) { ) {
AndroidView( AndroidView(
factory = { context -> factory = { context ->