pref: 性能优化

This commit is contained in:
HuanCheng65 2023-02-05 21:08:08 +08:00
parent f4c64bbd2f
commit ac5839eb9d
No known key found for this signature in database
GPG Key ID: E9031EF91A805148
3 changed files with 209 additions and 119 deletions

View File

@ -1,16 +1,25 @@
package com.huanchengfly.tieba.post.arch package com.huanchengfly.tieba.post.arch
import android.util.Log import android.util.Log
import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.scan
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
interface PartialChangeProducer<Intent : UiIntent, PC : PartialChange<State>, State : UiState> { interface PartialChangeProducer<Intent : UiIntent, PC : PartialChange<State>, State : UiState> {
fun toPartialChangeFlow(intentFlow: Flow<Intent>): Flow<PC> fun toPartialChangeFlow(intentFlow: Flow<Intent>): Flow<PC>
} }
@Stable
abstract class BaseViewModel< abstract class BaseViewModel<
Intent : UiIntent, Intent : UiIntent,
PC : PartialChange<State>, PC : PartialChange<State>,
@ -58,4 +67,15 @@ abstract class BaseViewModel<
_intentFlow.emit(intent) _intentFlow.emit(intent)
} }
} }
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as BaseViewModel<*, *, *, *>
if (initialized != other.initialized) return false
return true
}
} }

View File

@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
import androidx.compose.foundation.lazy.staggeredgrid.itemsIndexed import androidx.compose.foundation.lazy.staggeredgrid.itemsIndexed
@ -33,6 +34,7 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -44,8 +46,12 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.github.panpf.sketch.request.PauseLoadWhenScrollingDrawableDecodeInterceptor
import com.huanchengfly.tieba.post.R import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.activities.ThreadActivity import com.huanchengfly.tieba.post.activities.ThreadActivity
import com.huanchengfly.tieba.post.api.models.protos.ThreadInfo
import com.huanchengfly.tieba.post.api.models.protos.personalized.DislikeReason
import com.huanchengfly.tieba.post.api.models.protos.personalized.ThreadPersonalized
import com.huanchengfly.tieba.post.arch.collectPartialAsState import com.huanchengfly.tieba.post.arch.collectPartialAsState
import com.huanchengfly.tieba.post.arch.pageViewModel import com.huanchengfly.tieba.post.arch.pageViewModel
import com.huanchengfly.tieba.post.arch.wrapImmutable import com.huanchengfly.tieba.post.arch.wrapImmutable
@ -123,96 +129,50 @@ fun PersonalizedPage(
showRefreshTip = false showRefreshTip = false
} }
} }
if (lazyStaggeredGridState.isScrollInProgress) {
DisposableEffect(Unit) {
PauseLoadWhenScrollingDrawableDecodeInterceptor.scrolling = true
onDispose {
PauseLoadWhenScrollingDrawableDecodeInterceptor.scrolling = false
}
}
}
Box(modifier = Modifier.pullRefresh(pullRefreshState)) { Box(modifier = Modifier.pullRefresh(pullRefreshState)) {
LoadMoreLayout( LoadMoreLayout(
isLoading = isLoadingMore, isLoading = isLoadingMore,
loadEnd = false, loadEnd = false,
onLoadMore = { viewModel.send(PersonalizedUiIntent.LoadMore(currentPage + 1)) }, onLoadMore = { viewModel.send(PersonalizedUiIntent.LoadMore(currentPage + 1)) },
) { ) {
LazyVerticalStaggeredGrid( FeedList(
columns = StaggeredGridCells.Adaptive(240.dp), dataProvider = { data },
personalizedDataProvider = { threadPersonalizedData },
refreshPositionProvider = { refreshPosition },
hiddenThreadIdsProvider = { hiddenThreadIds },
onItemClick = { threadInfo ->
ThreadActivity.launch(context, threadInfo.id.toString())
},
onAgree = { item ->
viewModel.send(
PersonalizedUiIntent.Agree(
item.threadId,
item.firstPostId,
item.agree?.hasAgree ?: 0
)
)
},
onDislike = { item, clickTime, reasons ->
viewModel.send(
PersonalizedUiIntent.Dislike(
item.forumInfo?.id ?: 0,
item.threadId,
reasons,
clickTime
)
)
},
onRefresh = { viewModel.send(PersonalizedUiIntent.Refresh) },
state = lazyStaggeredGridState state = lazyStaggeredGridState
) { )
itemsIndexed(
items = data,
key = { _, item -> "${item.id}" },
contentType = { _, item ->
when {
item.videoInfo != null -> "Video"
item.media.size == 1 -> "SingleMedia"
item.media.size > 1 -> "MultiMedia"
else -> "PlainText"
}
}
) { index, item ->
Column {
AnimatedVisibility(
visible = !hiddenThreadIds.contains(item.threadId),
enter = EnterTransition.None,
exit = shrinkVertically() + fadeOut()
) {
FeedCard(
info = wrapImmutable(item),
onClick = {
ThreadActivity.launch(context, item.threadId.toString())
},
onAgree = {
viewModel.send(
PersonalizedUiIntent.Agree(
item.threadId,
item.firstPostId,
item.agree?.hasAgree ?: 0
)
)
},
) {
Dislike(
personalized = threadPersonalizedData[index],
onDislike = { clickTime, reasons ->
viewModel.send(
PersonalizedUiIntent.Dislike(
item.forumInfo?.id ?: 0,
item.threadId,
reasons,
clickTime
)
)
}
)
}
}
if (!hiddenThreadIds.contains(item.threadId)) {
if ((refreshPosition == 0 || index + 1 != refreshPosition) && index < data.size - 1) {
VerticalDivider(
modifier = Modifier.padding(horizontal = 16.dp),
thickness = 2.dp
)
}
}
if (refreshPosition != 0 && index + 1 == refreshPosition) {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable { viewModel.send(PersonalizedUiIntent.Refresh) }
.padding(8.dp),
) {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = stringResource(id = R.string.tip_refresh),
style = MaterialTheme.typography.subtitle1
)
}
}
}
}
}
LaunchedEffect(data.firstOrNull()?.id) { LaunchedEffect(data.firstOrNull()?.id) {
//delay(50) //delay(50)
lazyStaggeredGridState.scrollToItem(0, 0) lazyStaggeredGridState.scrollToItem(0, 0)
@ -242,7 +202,99 @@ fun PersonalizedPage(
.padding(horizontal = 16.dp, vertical = 8.dp) .padding(horizontal = 16.dp, vertical = 8.dp)
.align(Alignment.TopCenter) .align(Alignment.TopCenter)
) { ) {
Text(text = stringResource(id = R.string.toast_feed_refresh, refreshCount), color = ExtendedTheme.colors.onAccent) Text(
text = stringResource(id = R.string.toast_feed_refresh, refreshCount),
color = ExtendedTheme.colors.onAccent
)
}
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun FeedList(
dataProvider: () -> List<ThreadInfo>,
personalizedDataProvider: () -> List<ThreadPersonalized>,
refreshPositionProvider: () -> Int,
hiddenThreadIdsProvider: () -> List<Long>,
onItemClick: (ThreadInfo) -> Unit,
onAgree: (ThreadInfo) -> Unit,
onDislike: (ThreadInfo, Long, List<DislikeReason>) -> Unit,
onRefresh: () -> Unit,
state: LazyStaggeredGridState,
) {
val data = dataProvider()
val threadPersonalizedData = personalizedDataProvider()
val refreshPosition = refreshPositionProvider()
val hiddenThreadIds = hiddenThreadIdsProvider()
LazyVerticalStaggeredGrid(
columns = StaggeredGridCells.Adaptive(240.dp),
state = state
) {
itemsIndexed(
items = data,
key = { _, item -> "${item.id}" },
contentType = { _, item ->
when {
item.videoInfo != null -> "Video"
item.media.size == 1 -> "SingleMedia"
item.media.size > 1 -> "MultiMedia"
else -> "PlainText"
}
}
) { index, item ->
Column {
AnimatedVisibility(
visible = !hiddenThreadIds.contains(item.threadId),
enter = EnterTransition.None,
exit = shrinkVertically() + fadeOut()
) {
FeedCard(
info = wrapImmutable(item),
onClick = {
onItemClick(item)
},
onAgree = {
onAgree(item)
},
) {
Dislike(
personalized = threadPersonalizedData[index],
onDislike = { clickTime, reasons ->
onDislike(item, clickTime, reasons)
}
)
}
}
if (!hiddenThreadIds.contains(item.threadId)) {
if ((refreshPosition == 0 || index + 1 != refreshPosition) && index < data.size - 1) {
VerticalDivider(
modifier = Modifier.padding(horizontal = 16.dp),
thickness = 2.dp
)
}
}
if (refreshPosition != 0 && index + 1 == refreshPosition) {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onRefresh)
.padding(8.dp),
) {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = stringResource(id = R.string.tip_refresh),
style = MaterialTheme.typography.subtitle1
)
}
}
} }
} }
} }

View File

@ -57,6 +57,7 @@ import com.huanchengfly.tieba.post.activities.UserActivity
import com.huanchengfly.tieba.post.api.abstractText import com.huanchengfly.tieba.post.api.abstractText
import com.huanchengfly.tieba.post.api.hasAbstract import com.huanchengfly.tieba.post.api.hasAbstract
import com.huanchengfly.tieba.post.api.models.protos.Media import com.huanchengfly.tieba.post.api.models.protos.Media
import com.huanchengfly.tieba.post.api.models.protos.SimpleForum
import com.huanchengfly.tieba.post.api.models.protos.ThreadInfo import com.huanchengfly.tieba.post.api.models.protos.ThreadInfo
import com.huanchengfly.tieba.post.api.models.protos.User import com.huanchengfly.tieba.post.api.models.protos.User
import com.huanchengfly.tieba.post.arch.ImmutableHolder import com.huanchengfly.tieba.post.arch.ImmutableHolder
@ -178,30 +179,35 @@ private fun Badge(
} }
} }
private val ThreadContent: @Composable ColumnScope.(ThreadInfo) -> Unit = { @Composable
val showTitle = it.isNoTitle != 1 && it.title.isNotBlank() fun ThreadContent(
val showAbstract = it.hasAbstract info: ImmutableHolder<ThreadInfo>,
) {
val (item) = info
val showTitle = item.isNoTitle != 1 && item.title.isNotBlank()
val showAbstract = item.hasAbstract
val content = buildAnnotatedString { val content = buildAnnotatedString {
if (showTitle) { if (showTitle) {
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
if (it.isGood == 1) { if (item.isGood == 1) {
withStyle(style = SpanStyle(color = ExtendedTheme.colors.primary)) { withStyle(style = SpanStyle(color = ExtendedTheme.colors.primary)) {
append(stringResource(id = R.string.tip_good)) append(stringResource(id = R.string.tip_good))
} }
append(" ") append(" ")
} }
if (it.tabName.isNotBlank()) { if (item.tabName.isNotBlank()) {
append(it.tabName) append(item.tabName)
append(" | ") append(" | ")
} }
append(it.title) append(item.title)
} }
} }
if (showTitle && showAbstract) append('\n') if (showTitle && showAbstract) append('\n')
if (showAbstract) { if (showAbstract) {
append(it.abstractText.emoticonString) append(item.abstractText.emoticonString)
} }
} }
@ -261,6 +267,37 @@ fun FeedCardPlaceholder() {
) )
} }
@Composable
private fun ForumInfoChip(
info: ImmutableHolder<SimpleForum>,
onClick: () -> Unit
) {
val (forumInfo) = info
Row(
modifier = Modifier
.clip(RoundedCornerShape(4.dp))
.background(color = ExtendedTheme.colors.chip)
.clickable(onClick = onClick)
.padding(4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
NetworkImage(
imageUri = StringUtil.getAvatarUrl(forumInfo.avatar),
contentDescription = null,
modifier = Modifier
.size(12.dp)
.clip(RoundedCornerShape(4.dp))
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(id = R.string.title_forum_name, forumInfo.name),
style = MaterialTheme.typography.body2,
color = ExtendedTheme.colors.onChip,
fontSize = 12.sp,
)
}
}
@Composable @Composable
fun FeedCard( fun FeedCard(
info: ImmutableHolder<ThreadInfo>, info: ImmutableHolder<ThreadInfo>,
@ -276,7 +313,7 @@ fun FeedCard(
} }
}, },
content = { content = {
ThreadContent(item) ThreadContent(info)
if (item.videoInfo != null) { if (item.videoInfo != null) {
VideoPlayer( VideoPlayer(
@ -326,31 +363,12 @@ fun FeedCard(
if (item.forumInfo != null) { if (item.forumInfo != null) {
val navigator = LocalNavigator.current val navigator = LocalNavigator.current
Row( ForumInfoChip(
modifier = Modifier info = wrapImmutable(item.forumInfo),
.clip(RoundedCornerShape(4.dp)) onClick = {
.background(color = ExtendedTheme.colors.chip) navigator.navigate(ForumPageDestination(item.forumInfo.name))
.clickable { }
navigator.navigate(ForumPageDestination(item.forumInfo.name)) )
}
.padding(4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
NetworkImage(
imageUri = StringUtil.getAvatarUrl(item.forumInfo.avatar),
contentDescription = null,
modifier = Modifier
.size(12.dp)
.clip(RoundedCornerShape(4.dp))
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(id = R.string.title_forum_name, item.forumInfo.name),
style = MaterialTheme.typography.body2,
color = ExtendedTheme.colors.onChip,
fontSize = 12.sp,
)
}
} }
}, },
action = { action = {