fix: lazy list state issues when changing feed or sort type (#831)

This commit is contained in:
Diego Beraldin 2024-05-11 11:47:14 +02:00 committed by GitHub
parent b3584665a0
commit 3801c7e2d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 212 additions and 125 deletions

View File

@ -98,10 +98,12 @@ class SettingsScreen : Screen {
LaunchedEffect(Unit) {
navigationCoordinator.onDoubleTabSelection.onEach { section ->
if (section == TabNavigationSection.Settings) {
scrollState.scrollTo(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
runCatching {
if (section == TabNavigationSection.Settings) {
scrollState.scrollTo(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
}
}
}.launchIn(this)
}

View File

@ -118,7 +118,9 @@ class InboxChatScreen(
}
InboxChatMviModel.Effect.ScrollToBottom -> {
lazyListState.scrollToItem(0)
runCatching {
lazyListState.scrollToItem(0)
}
}
}
}.launchIn(this)

View File

@ -221,17 +221,21 @@ class CommunityDetailScreen(
}
CommunityDetailMviModel.Effect.BackToTop -> {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
runCatching {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
}
}
is CommunityDetailMviModel.Effect.ZombieModeTick -> {
if (effect.index >= 0) {
lazyListState.animateScrollBy(
value = settings.zombieModeScrollAmount,
animationSpec = tween(350),
)
runCatching {
if (effect.index >= 0) {
lazyListState.animateScrollBy(
value = settings.zombieModeScrollAmount,
animationSpec = tween(350),
)
}
}
}
@ -577,9 +581,11 @@ class CommunityDetailScreen(
text = LocalXmlStrings.current.actionBackToTop,
onSelected = rememberCallback {
scope.launch {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
runCatching {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
}
}
},
)
@ -590,9 +596,11 @@ class CommunityDetailScreen(
onSelected = rememberCallback {
model.reduce(CommunityDetailMviModel.Intent.ClearRead)
scope.launch {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
runCatching {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
}
}
},
)

View File

@ -36,6 +36,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.IO
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@ -308,6 +309,7 @@ class CommunityDetailViewModel(
private suspend fun refresh() {
hideReadPosts = false
val currentState = uiState.value
zombieModeHelper.pause()
postPaginationManager.reset(
PostPaginationSpecification.Community(
id = currentState.community.id,
@ -338,6 +340,7 @@ class CommunityDetailViewModel(
community = refreshedCommunity,
moderators = moderators,
loading = false,
zombieModeActive = false,
)
}
}
@ -352,6 +355,7 @@ class CommunityDetailViewModel(
updateState { it.copy(sortType = value) }
screenModelScope.launch(Dispatchers.IO) {
emitEffect(CommunityDetailMviModel.Effect.BackToTop)
delay(50)
refresh()
}
}

View File

@ -146,10 +146,12 @@ class ExploreScreen(
LaunchedEffect(navigationCoordinator) {
navigationCoordinator.onDoubleTabSelection.onEach { section ->
if (section == TabNavigationSection.Explore) {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
runCatching {
if (section == TabNavigationSection.Explore) {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
}
}
}.launchIn(this)
}
@ -157,9 +159,11 @@ class ExploreScreen(
model.effects.onEach {
when (it) {
ExploreMviModel.Effect.BackToTop -> {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
runCatching {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
}
}
ExploreMviModel.Effect.OperationFailure -> {

View File

@ -130,9 +130,11 @@ class FilteredContentsScreen(
model.effects.onEach { effect ->
when (effect) {
FilteredContentsMviModel.Effect.BackToTop -> {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
runCatching {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
}
}
}
}.launchIn(this)
@ -200,9 +202,11 @@ class FilteredContentsScreen(
text = LocalXmlStrings.current.actionBackToTop,
onSelected = rememberCallback {
scope.launch {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
runCatching {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
}
}
},
)

View File

@ -81,7 +81,9 @@ class InstanceInfoScreen(
model.effects.onEach { effect ->
when (effect) {
InstanceInfoMviModel.Effect.BackToTop -> {
listState.scrollToItem(0)
runCatching {
listState.scrollToItem(0)
}
}
}
}.launchIn(this)

View File

@ -117,9 +117,11 @@ class ManageSubscriptionsScreen : Screen {
model.effects.onEach { event ->
when (event) {
ManageSubscriptionsMviModel.Effect.BackToTop -> {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
runCatching {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
}
}
ManageSubscriptionsMviModel.Effect.Success -> {
@ -171,9 +173,11 @@ class ManageSubscriptionsScreen : Screen {
text = LocalXmlStrings.current.actionBackToTop,
onSelected = rememberCallback {
scope.launch {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
runCatching {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
}
}
},
)

View File

@ -84,8 +84,10 @@ class InboxMentionsScreen : Tab {
LaunchedEffect(navigationCoordinator) {
navigationCoordinator.onDoubleTabSelection.onEach { section ->
if (section == TabNavigationSection.Inbox) {
lazyListState.scrollToItem(0)
runCatching {
if (section == TabNavigationSection.Inbox) {
lazyListState.scrollToItem(0)
}
}
}.launchIn(this)
}
@ -97,7 +99,9 @@ class InboxMentionsScreen : Tab {
}
InboxMentionsMviModel.Effect.BackToTop -> {
lazyListState.scrollToItem(0)
runCatching {
lazyListState.scrollToItem(0)
}
}
}
}.launchIn(this)

View File

@ -61,8 +61,10 @@ class InboxMessagesScreen : Tab {
LaunchedEffect(navigationCoordinator) {
navigationCoordinator.onDoubleTabSelection.onEach { section ->
if (section == TabNavigationSection.Inbox) {
lazyListState.scrollToItem(0)
runCatching {
if (section == TabNavigationSection.Inbox) {
lazyListState.scrollToItem(0)
}
}
}.launchIn(this)
}
@ -74,7 +76,9 @@ class InboxMessagesScreen : Tab {
}
InboxMessagesMviModel.Effect.BackToTop -> {
lazyListState.scrollToItem(0)
runCatching {
lazyListState.scrollToItem(0)
}
}
}
}.launchIn(this)

View File

@ -30,7 +30,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.nestedscroll.nestedScroll
@ -65,7 +64,6 @@ import com.github.diegoberaldin.raccoonforlemmy.unit.modlog.components.ModlogIte
import com.github.diegoberaldin.raccoonforlemmy.unit.modlog.components.RemoveCommunityItem
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.koin.core.parameter.parametersOf
class ModlogScreen(
@ -80,7 +78,6 @@ class ModlogScreen(
val topAppBarState = rememberTopAppBarState()
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(topAppBarState)
val navigationCoordinator = remember { getNavigationCoordinator() }
val scope = rememberCoroutineScope()
val settingsRepository = remember { getSettingsRepository() }
val settings by settingsRepository.currentSettings.collectAsState()
val lazyListState = rememberLazyListState()
@ -96,7 +93,7 @@ class ModlogScreen(
model.effects.onEach { effect ->
when (effect) {
ModlogMviModel.Effect.BackToTop -> {
scope.launch {
runCatching {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f

View File

@ -137,9 +137,11 @@ class MultiCommunityScreen(
}
MultiCommunityMviModel.Effect.BackToTop -> {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
runCatching {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
}
}
}
}.launchIn(this)
@ -231,9 +233,11 @@ class MultiCommunityScreen(
text = LocalXmlStrings.current.actionBackToTop,
onSelected = rememberCallback {
scope.launch {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
runCatching {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
}
}
},
)
@ -243,9 +247,11 @@ class MultiCommunityScreen(
onSelected = rememberCallback {
model.reduce(MultiCommunityMviModel.Intent.ClearRead)
scope.launch {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
runCatching {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
}
}
},
)

View File

@ -24,6 +24,7 @@ import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PostRepo
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.SiteRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@ -243,6 +244,7 @@ class MultiCommunityViewModel(
updateState { it.copy(sortType = value) }
screenModelScope.launch {
emitEffect(MultiCommunityMviModel.Effect.BackToTop)
delay(50)
refresh()
}
}

View File

@ -93,8 +93,10 @@ object ProfileLoggedScreen : Tab {
LaunchedEffect(navigationCoordinator) {
navigationCoordinator.onDoubleTabSelection.onEach { section ->
if (section == TabNavigationSection.Profile) {
lazyListState.scrollToItem(0)
runCatching {
if (section == TabNavigationSection.Profile) {
lazyListState.scrollToItem(0)
}
}
}.launchIn(this)
}

View File

@ -252,11 +252,13 @@ class PostDetailScreen(
}
is PostDetailMviModel.Effect.ScrollToComment -> {
lazyListState.scrollToItem(effect.index)
runCatching {
lazyListState.scrollToItem(effect.index)
}
}
PostDetailMviModel.Effect.BackToTop -> {
scope.launch {
runCatching {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
@ -595,9 +597,11 @@ class PostDetailScreen(
text = LocalXmlStrings.current.actionBackToTop,
onSelected = rememberCallback {
scope.launch {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
runCatching {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
}
}
},
)

View File

@ -29,6 +29,7 @@ import com.github.diegoberaldin.raccoonforlemmy.unit.postdetail.utils.sortToNest
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@ -466,6 +467,7 @@ class PostDetailViewModel(
updateState { it.copy(sortType = value) }
screenModelScope.launch {
emitEffect(PostDetailMviModel.Effect.BackToTop)
delay(50)
refresh()
}
}

View File

@ -144,9 +144,11 @@ class PostListScreen : Screen {
LaunchedEffect(navigationCoordinator) {
navigationCoordinator.onDoubleTabSelection.onEach { section ->
if (section == TabNavigationSection.Home) {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
runCatching {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
}
}
}.launchIn(this)
}
@ -154,17 +156,21 @@ class PostListScreen : Screen {
model.effects.onEach { effect ->
when (effect) {
PostListMviModel.Effect.BackToTop -> {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
runCatching {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
}
}
is PostListMviModel.Effect.ZombieModeTick -> {
if (effect.index >= 0) {
lazyListState.animateScrollBy(
value = settings.zombieModeScrollAmount,
animationSpec = tween(350),
)
runCatching {
if (effect.index >= 0) {
lazyListState.animateScrollBy(
value = settings.zombieModeScrollAmount,
animationSpec = tween(350),
)
}
}
}
@ -256,9 +262,11 @@ class PostListScreen : Screen {
text = LocalXmlStrings.current.actionBackToTop,
onSelected = rememberCallback {
scope.launch {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
runCatching {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
}
}
},
)
@ -269,9 +277,11 @@ class PostListScreen : Screen {
onSelected = rememberCallback {
model.reduce(PostListMviModel.Intent.ClearRead)
scope.launch {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
runCatching {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
}
}
},
)

View File

@ -271,6 +271,7 @@ class PostListViewModel(
hideReadPosts = false
val listingType = uiState.value.listingType ?: return
val sortType = uiState.value.sortType ?: return
zombieModeHelper.pause()
postPaginationManager.reset(
PostPaginationSpecification.Listing(
listingType = listingType,
@ -284,6 +285,7 @@ class PostListViewModel(
canFetchMore = true,
refreshing = true,
loading = false,
zombieModeActive = false,
)
}
loadNextPage()
@ -331,6 +333,7 @@ class PostListViewModel(
updateState { it.copy(sortType = value) }
screenModelScope.launch {
emitEffect(PostListMviModel.Effect.BackToTop)
delay(50)
refresh()
}
}
@ -342,6 +345,7 @@ class PostListViewModel(
updateState { it.copy(listingType = value) }
screenModelScope.launch(Dispatchers.IO) {
emitEffect(PostListMviModel.Effect.BackToTop)
delay(50)
refresh()
}
}

View File

@ -84,8 +84,10 @@ class InboxRepliesScreen : Tab {
LaunchedEffect(navigationCoordinator) {
navigationCoordinator.onDoubleTabSelection.onEach { section ->
if (section == TabNavigationSection.Inbox) {
lazyListState.scrollToItem(0)
runCatching {
if (section == TabNavigationSection.Inbox) {
lazyListState.scrollToItem(0)
}
}
}.launchIn(this)
}
@ -97,7 +99,9 @@ class InboxRepliesScreen : Tab {
}
InboxRepliesMviModel.Effect.BackToTop -> {
lazyListState.scrollToItem(0)
runCatching {
lazyListState.scrollToItem(0)
}
}
}
}.launchIn(this)

View File

@ -50,5 +50,7 @@ interface SavedItemsMviModel :
val availableSortTypes: List<SortType> = emptyList(),
)
sealed interface Effect
sealed interface Effect {
data object BackToTop : Effect
}
}

View File

@ -158,9 +158,11 @@ class SavedItemsScreen : Screen {
text = LocalXmlStrings.current.actionBackToTop,
onSelected = rememberCallback {
scope.launch {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
runCatching {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
}
}
},
)

View File

@ -21,9 +21,9 @@ import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.CommentRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.GetSortTypesUseCase
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PostRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.SiteRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@ -31,7 +31,6 @@ import kotlinx.coroutines.launch
class SavedItemsViewModel(
private val identityRepository: IdentityRepository,
private val apiConfigurationRepository: ApiConfigurationRepository,
private val siteRepository: SiteRepository,
private val postPaginationManager: PostPaginationManager,
private val commentPaginationManager: CommentPaginationManager,
private val postRepository: PostRepository,
@ -92,8 +91,14 @@ class SavedItemsViewModel(
override fun reduce(intent: SavedItemsMviModel.Intent) {
when (intent) {
SavedItemsMviModel.Intent.LoadNextPage -> loadNextPage()
SavedItemsMviModel.Intent.Refresh -> refresh()
SavedItemsMviModel.Intent.LoadNextPage -> screenModelScope.launch {
loadNextPage()
}
SavedItemsMviModel.Intent.Refresh -> screenModelScope.launch {
refresh()
}
is SavedItemsMviModel.Intent.ChangeSection -> changeSection(intent.section)
is SavedItemsMviModel.Intent.DownVoteComment -> {
if (intent.feedback) {
@ -160,7 +165,7 @@ class SavedItemsViewModel(
}
}
private fun refresh() {
private suspend fun refresh() {
postPaginationManager.reset(
PostPaginationSpecification.Saved(sortType = uiState.value.sortType)
)
@ -177,36 +182,34 @@ class SavedItemsViewModel(
loadNextPage()
}
private fun loadNextPage() {
private suspend fun loadNextPage() {
val currentState = uiState.value
if (!currentState.canFetchMore || currentState.loading) {
updateState { it.copy(refreshing = false) }
return
}
screenModelScope.launch {
updateState { it.copy(loading = true) }
val section = currentState.section
if (section == SavedItemsSection.Posts) {
val posts = postPaginationManager.loadNextPage()
updateState {
it.copy(
posts = posts,
loading = false,
canFetchMore = postPaginationManager.canFetchMore,
refreshing = false,
)
}
} else {
val comments = commentPaginationManager.loadNextPage()
updateState {
it.copy(
comments = comments,
loading = false,
canFetchMore = commentPaginationManager.canFetchMore,
refreshing = false,
)
}
updateState { it.copy(loading = true) }
val section = currentState.section
if (section == SavedItemsSection.Posts) {
val posts = postPaginationManager.loadNextPage()
updateState {
it.copy(
posts = posts,
loading = false,
canFetchMore = postPaginationManager.canFetchMore,
refreshing = false,
)
}
} else {
val comments = commentPaginationManager.loadNextPage()
updateState {
it.copy(
comments = comments,
loading = false,
canFetchMore = commentPaginationManager.canFetchMore,
refreshing = false,
)
}
}
}
@ -216,7 +219,11 @@ class SavedItemsViewModel(
return
}
updateState { it.copy(sortType = value) }
refresh()
screenModelScope.launch {
emitEffect(SavedItemsMviModel.Effect.BackToTop)
delay(50)
refresh()
}
}
private fun handlePostUpdate(post: PostModel) {

View File

@ -182,9 +182,11 @@ class UserDetailScreen(
UserDetailMviModel.Effect.BackToTop -> {
scope.launch {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
runCatching {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
}
}
}
@ -374,9 +376,11 @@ class UserDetailScreen(
text = LocalXmlStrings.current.actionBackToTop,
onSelected = rememberCallback {
scope.launch {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
runCatching {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
}
}
},
)

View File

@ -34,6 +34,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@ -236,6 +237,8 @@ class UserDetailViewModel(
updateState { it.copy(sortType = value) }
screenModelScope.launch(Dispatchers.Main) {
emitEffect(UserDetailMviModel.Effect.BackToTop)
delay(50)
refresh()
}
}