saving scroll position only while app is working & add divider with spacer on conversations screen

This commit is contained in:
2024-12-14 00:24:09 +03:00
parent 077332a27b
commit 93ba9c0285
8 changed files with 111 additions and 59 deletions
@@ -60,6 +60,9 @@ interface ConversationsViewModel {
fun onOptionClicked(conversation: UiConversation, option: ConversationOption) fun onOptionClicked(conversation: UiConversation, option: ConversationOption)
fun onErrorConsumed() fun onErrorConsumed()
fun setScrollIndex(index: Int)
fun setScrollOffset(offset: Int)
} }
class ConversationsViewModelImpl( class ConversationsViewModelImpl(
@@ -206,6 +209,14 @@ class ConversationsViewModelImpl(
baseError.setValue { null } baseError.setValue { null }
} }
override fun setScrollIndex(index: Int) {
screenState.setValue { old -> old.copy(scrollIndex = index) }
}
override fun setScrollOffset(offset: Int) {
screenState.setValue { old -> old.copy(scrollOffset = offset) }
}
private fun hideOptions(conversationId: Int) { private fun hideOptions(conversationId: Int) {
screenState.setValue { old -> screenState.setValue { old ->
old.copy( old.copy(
@@ -9,7 +9,9 @@ data class ConversationsScreenState(
val isLoading: Boolean, val isLoading: Boolean,
val isPaginating: Boolean, val isPaginating: Boolean,
val isPaginationExhausted: Boolean, val isPaginationExhausted: Boolean,
val profileImageUrl: String? val profileImageUrl: String?,
val scrollIndex: Int,
val scrollOffset: Int
) { ) {
companion object { companion object {
@@ -19,7 +21,9 @@ data class ConversationsScreenState(
isLoading = true, isLoading = true,
isPaginating = false, isPaginating = false,
isPaginationExhausted = false, isPaginationExhausted = false,
profileImageUrl = null profileImageUrl = null,
scrollIndex = 0,
scrollOffset = 0,
) )
} }
} }
@@ -45,8 +45,6 @@ fun ConversationsList(
) { ) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val conversations = screenState.conversations
val bottomPadding = LocalBottomPadding.current val bottomPadding = LocalBottomPadding.current
LazyColumn( LazyColumn(
@@ -55,9 +53,10 @@ fun ConversationsList(
) { ) {
item { item {
Spacer(modifier = Modifier.height(padding.calculateTopPadding())) Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
Spacer(modifier = Modifier.height(8.dp))
} }
items( items(
items = conversations, items = screenState.conversations,
key = UiConversation::id, key = UiConversation::id,
) { conversation -> ) { conversation ->
val isUserAccount by remember(conversation) { val isUserAccount by remember(conversation) {
@@ -1,6 +1,5 @@
package dev.meloda.fast.conversations.presentation package dev.meloda.fast.conversations.presentation
import android.content.SharedPreferences
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Animatable
@@ -29,6 +28,7 @@ import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
@@ -63,7 +63,6 @@ import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.edit
import androidx.core.view.HapticFeedbackConstantsCompat import androidx.core.view.HapticFeedbackConstantsCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.imageLoader import coil.imageLoader
@@ -88,7 +87,6 @@ import dev.meloda.fast.ui.util.isScrollingUp
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.compose.koinInject
import dev.meloda.fast.ui.R as UiR import dev.meloda.fast.ui.R as UiR
@Composable @Composable
@@ -100,8 +98,6 @@ fun ConversationsRoute(
) { ) {
val context = LocalContext.current val context = LocalContext.current
val prefs: SharedPreferences = koinInject()
val screenState by viewModel.screenState.collectAsStateWithLifecycle() val screenState by viewModel.screenState.collectAsStateWithLifecycle()
val baseError by viewModel.baseError.collectAsStateWithLifecycle() val baseError by viewModel.baseError.collectAsStateWithLifecycle()
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle() val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
@@ -121,8 +117,6 @@ fun ConversationsRoute(
screenState = screenState, screenState = screenState,
baseError = baseError, baseError = baseError,
canPaginate = canPaginate, canPaginate = canPaginate,
firstVisibleItemIndex = prefs.getInt("conversations_all_scroll_position", 0),
firstVisibleItemScrollOffset = prefs.getInt("conversations_all_scroll_offset", 0),
onSessionExpiredLogOutButtonClicked = { onError(BaseError.SessionExpired) }, onSessionExpiredLogOutButtonClicked = { onError(BaseError.SessionExpired) },
onConversationItemClicked = { id -> onConversationItemClicked = { id ->
onConversationItemClicked(id) onConversationItemClicked(id)
@@ -134,15 +128,10 @@ fun ConversationsRoute(
onRefreshDropdownItemClicked = viewModel::onRefresh, onRefreshDropdownItemClicked = viewModel::onRefresh,
onRefresh = viewModel::onRefresh, onRefresh = viewModel::onRefresh,
onConversationPhotoClicked = onConversationPhotoClicked, onConversationPhotoClicked = onConversationPhotoClicked,
onSaveScrollPosition = { index -> setScrollIndex = viewModel::setScrollIndex,
prefs.edit { putInt("conversations_all_scroll_position", index) } setScrollOffset = viewModel::setScrollOffset
},
onSaveScrollOffsetPosition = { offset ->
prefs.edit { putInt("conversations_all_scroll_offset", offset) }
}
) )
HandleDialogs( HandleDialogs(
screenState = screenState, screenState = screenState,
viewModel = viewModel viewModel = viewModel
@@ -158,8 +147,6 @@ fun ConversationsScreen(
screenState: ConversationsScreenState = ConversationsScreenState.EMPTY, screenState: ConversationsScreenState = ConversationsScreenState.EMPTY,
baseError: BaseError? = null, baseError: BaseError? = null,
canPaginate: Boolean = false, canPaginate: Boolean = false,
firstVisibleItemIndex: Int = 0,
firstVisibleItemScrollOffset: Int = 0,
onSessionExpiredLogOutButtonClicked: () -> Unit, onSessionExpiredLogOutButtonClicked: () -> Unit,
onConversationItemClicked: (conversationId: Int) -> Unit = {}, onConversationItemClicked: (conversationId: Int) -> Unit = {},
onConversationItemLongClicked: (conversation: UiConversation) -> Unit = {}, onConversationItemLongClicked: (conversation: UiConversation) -> Unit = {},
@@ -168,8 +155,8 @@ fun ConversationsScreen(
onRefreshDropdownItemClicked: () -> Unit = {}, onRefreshDropdownItemClicked: () -> Unit = {},
onRefresh: () -> Unit = {}, onRefresh: () -> Unit = {},
onConversationPhotoClicked: (url: String) -> Unit = {}, onConversationPhotoClicked: (url: String) -> Unit = {},
onSaveScrollPosition: (Int) -> Unit = {}, setScrollIndex: (Int) -> Unit = {},
onSaveScrollOffsetPosition: (Int) -> Unit = {} setScrollOffset: (Int) -> Unit = {}
) { ) {
val view = LocalView.current val view = LocalView.current
val currentTheme = LocalThemeConfig.current val currentTheme = LocalThemeConfig.current
@@ -179,20 +166,20 @@ fun ConversationsScreen(
} }
val listState = rememberLazyListState( val listState = rememberLazyListState(
initialFirstVisibleItemIndex = firstVisibleItemIndex, initialFirstVisibleItemIndex = screenState.scrollIndex,
initialFirstVisibleItemScrollOffset = firstVisibleItemScrollOffset initialFirstVisibleItemScrollOffset = screenState.scrollOffset
) )
LaunchedEffect(listState) { LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex } snapshotFlow { listState.firstVisibleItemIndex }
.debounce(500L) .debounce(500L)
.collectLatest(onSaveScrollPosition) .collectLatest(setScrollIndex)
} }
LaunchedEffect(listState) { LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemScrollOffset } snapshotFlow { listState.firstVisibleItemScrollOffset }
.debounce(500L) .debounce(500L)
.collectLatest(onSaveScrollOffsetPosition) .collectLatest(setScrollOffset)
} }
val paginationConditionMet by remember(canPaginate, listState) { val paginationConditionMet by remember(canPaginate, listState) {
@@ -306,6 +293,9 @@ fun ConversationsScreen(
AnimatedVisibility(showHorizontalProgressBar) { AnimatedVisibility(showHorizontalProgressBar) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
} }
AnimatedVisibility(!showHorizontalProgressBar) {
HorizontalDivider()
}
} }
}, },
floatingActionButton = { floatingActionButton = {
@@ -31,6 +31,11 @@ interface FriendsViewModel {
fun onRefresh() fun onRefresh()
fun onErrorConsumed() fun onErrorConsumed()
fun setScrollIndex(index: Int)
fun setScrollOffset(offset: Int)
fun setScrollIndexOnline(index: Int)
fun setScrollOffsetOnline(offset: Int)
} }
class FriendsViewModelImpl( class FriendsViewModelImpl(
@@ -66,6 +71,22 @@ class FriendsViewModelImpl(
baseError.setValue { null } baseError.setValue { null }
} }
override fun setScrollIndex(index: Int) {
screenState.setValue { old -> old.copy(scrollIndex = index) }
}
override fun setScrollOffset(offset: Int) {
screenState.setValue { old -> old.copy(scrollOffset = offset) }
}
override fun setScrollIndexOnline(index: Int) {
screenState.setValue { old -> old.copy(scrollIndexOnline = index) }
}
override fun setScrollOffsetOnline(offset: Int) {
screenState.setValue { old -> old.copy(scrollOffsetOnline = offset) }
}
private fun loadFriends(offset: Int = currentOffset.value) { private fun loadFriends(offset: Int = currentOffset.value) {
friendsUseCase.getFriends(count = LOAD_COUNT, offset = offset) friendsUseCase.getFriends(count = LOAD_COUNT, offset = offset)
.listenValue(viewModelScope) { state -> .listenValue(viewModelScope) { state ->
@@ -8,7 +8,11 @@ data class FriendsScreenState(
val friends: List<UiFriend>, val friends: List<UiFriend>,
val onlineFriends: List<UiFriend>, val onlineFriends: List<UiFriend>,
val isPaginating: Boolean, val isPaginating: Boolean,
val isPaginationExhausted: Boolean val isPaginationExhausted: Boolean,
val scrollIndex: Int,
val scrollOffset: Int,
val scrollIndexOnline: Int,
val scrollOffsetOnline: Int
) { ) {
companion object { companion object {
@@ -17,7 +21,11 @@ data class FriendsScreenState(
friends = emptyList(), friends = emptyList(),
onlineFriends = emptyList(), onlineFriends = emptyList(),
isPaginating = false, isPaginating = false,
isPaginationExhausted = false isPaginationExhausted = false,
scrollIndex = 0,
scrollOffset = 0,
scrollIndexOnline = 0,
scrollOffsetOnline = 0,
) )
} }
} }
@@ -16,7 +16,9 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.IconButtonDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -35,8 +37,14 @@ fun FriendsList(
listState: LazyListState, listState: LazyListState,
maxLines: Int, maxLines: Int,
padding: PaddingValues, padding: PaddingValues,
onPhotoClicked: (url: String) -> Unit onPhotoClicked: (url: String) -> Unit,
setCanScrollBackward: (Boolean) -> Unit
) { ) {
LaunchedEffect(listState) {
snapshotFlow { listState.canScrollBackward }
.collect(setCanScrollBackward)
}
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val friends = uiFriends.toList() val friends = uiFriends.toList()
@@ -56,7 +64,6 @@ fun FriendsList(
items = friends, items = friends,
key = UiFriend::userId, key = UiFriend::userId,
) { friend -> ) { friend ->
FriendItem( FriendItem(
friend = friend, friend = friend,
maxLines = maxLines, maxLines = maxLines,
@@ -1,6 +1,5 @@
package dev.meloda.fast.friends.presentation package dev.meloda.fast.friends.presentation
import android.content.SharedPreferences
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.FastOutLinearInEasing import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
@@ -35,6 +34,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@@ -47,7 +47,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.edit
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.imageLoader import coil.imageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
@@ -69,7 +68,6 @@ import dev.meloda.fast.ui.util.ImmutableList
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import org.koin.compose.koinInject
import dev.meloda.fast.ui.R as UiR import dev.meloda.fast.ui.R as UiR
@Composable @Composable
@@ -80,8 +78,6 @@ fun FriendsRoute(
) { ) {
val context = LocalContext.current val context = LocalContext.current
val prefs: SharedPreferences = koinInject()
val screenState by viewModel.screenState.collectAsStateWithLifecycle() val screenState by viewModel.screenState.collectAsStateWithLifecycle()
val baseError by viewModel.baseError.collectAsStateWithLifecycle() val baseError by viewModel.baseError.collectAsStateWithLifecycle()
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle() val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
@@ -101,23 +97,17 @@ fun FriendsRoute(
screenState = screenState, screenState = screenState,
baseError = baseError, baseError = baseError,
canPaginate = canPaginate, canPaginate = canPaginate,
firstVisibleItemIndex = prefs.getInt("friends_all_scroll_position", 0),
firstVisibleItemScrollOffset = prefs.getInt("friends_all_scroll_offset", 0),
onSessionExpiredLogOutButtonClicked = { onError(BaseError.SessionExpired) }, onSessionExpiredLogOutButtonClicked = { onError(BaseError.SessionExpired) },
onPaginationConditionsMet = viewModel::onPaginationConditionsMet, onPaginationConditionsMet = viewModel::onPaginationConditionsMet,
onRefresh = viewModel::onRefresh, onRefresh = viewModel::onRefresh,
onPhotoClicked = onPhotoClicked, onPhotoClicked = onPhotoClicked,
onSaveScrollPosition = { index -> setScrollIndex = viewModel::setScrollIndex,
prefs.edit { putInt("friends_all_scroll_position", index) } setScrollOffset = viewModel::setScrollOffset,
}, setScrollIndexOnline = viewModel::setScrollIndexOnline,
onSaveScrollOffsetPosition = { offset -> setScrollOffsetOnline = viewModel::setScrollOffsetOnline,
prefs.edit { putInt("friends_all_scroll_offset", offset) }
}
) )
} }
// TODO: 13/07/2024, Danil Nikolaev: support for online
@OptIn( @OptIn(
ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class,
ExperimentalHazeMaterialsApi::class ExperimentalHazeMaterialsApi::class
@@ -127,14 +117,14 @@ fun FriendsScreen(
screenState: FriendsScreenState = FriendsScreenState.EMPTY, screenState: FriendsScreenState = FriendsScreenState.EMPTY,
baseError: BaseError? = null, baseError: BaseError? = null,
canPaginate: Boolean = false, canPaginate: Boolean = false,
firstVisibleItemIndex: Int = 0,
firstVisibleItemScrollOffset: Int = 0,
onSessionExpiredLogOutButtonClicked: () -> Unit = {}, onSessionExpiredLogOutButtonClicked: () -> Unit = {},
onPaginationConditionsMet: () -> Unit = {}, onPaginationConditionsMet: () -> Unit = {},
onRefresh: () -> Unit = {}, onRefresh: () -> Unit = {},
onPhotoClicked: (url: String) -> Unit = {}, onPhotoClicked: (url: String) -> Unit = {},
onSaveScrollPosition: (Int) -> Unit = {}, setScrollIndex: (Int) -> Unit,
onSaveScrollOffsetPosition: (Int) -> Unit = {} setScrollOffset: (Int) -> Unit,
setScrollIndexOnline: (Int) -> Unit,
setScrollOffsetOnline: (Int) -> Unit,
) { ) {
val currentTheme = LocalThemeConfig.current val currentTheme = LocalThemeConfig.current
@@ -145,20 +135,36 @@ fun FriendsScreen(
} }
val listState = rememberLazyListState( val listState = rememberLazyListState(
initialFirstVisibleItemIndex = firstVisibleItemIndex, initialFirstVisibleItemIndex = screenState.scrollIndex,
initialFirstVisibleItemScrollOffset = firstVisibleItemScrollOffset initialFirstVisibleItemScrollOffset = screenState.scrollOffset
)
val listStateOnline = rememberLazyListState(
initialFirstVisibleItemIndex = screenState.scrollIndexOnline,
initialFirstVisibleItemScrollOffset = screenState.scrollOffsetOnline
) )
LaunchedEffect(listState) { LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex } snapshotFlow { listState.firstVisibleItemIndex }
.debounce(500L) .debounce(500L)
.collectLatest(onSaveScrollPosition) .collectLatest(setScrollIndex)
} }
LaunchedEffect(listState) { LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemScrollOffset } snapshotFlow { listState.firstVisibleItemScrollOffset }
.debounce(500L) .debounce(500L)
.collectLatest(onSaveScrollOffsetPosition) .collectLatest(setScrollOffset)
}
LaunchedEffect(listStateOnline) {
snapshotFlow { listStateOnline.firstVisibleItemIndex }
.debounce(500L)
.collectLatest(setScrollIndexOnline)
}
LaunchedEffect(listStateOnline) {
snapshotFlow { listStateOnline.firstVisibleItemScrollOffset }
.debounce(500L)
.collectLatest(setScrollOffsetOnline)
} }
val paginationConditionMet by remember(canPaginate, listState) { val paginationConditionMet by remember(canPaginate, listState) {
@@ -177,8 +183,12 @@ fun FriendsScreen(
val hazeState = LocalHazeState.current val hazeState = LocalHazeState.current
var canScrollBackward by remember {
mutableStateOf(false)
}
val topBarContainerColorAlpha by animateFloatAsState( val topBarContainerColorAlpha by animateFloatAsState(
targetValue = if (!currentTheme.enableBlur || !listState.canScrollBackward) 1f else 0f, targetValue = if (!currentTheme.enableBlur || !canScrollBackward) 1f else 0f,
label = "toolbarColorAlpha", label = "toolbarColorAlpha",
animationSpec = tween( animationSpec = tween(
durationMillis = 200, durationMillis = 200,
@@ -187,8 +197,7 @@ fun FriendsScreen(
) )
val topBarContainerColor by animateColorAsState( val topBarContainerColor by animateColorAsState(
targetValue = targetValue = if (currentTheme.enableBlur || !canScrollBackward)
if (currentTheme.enableBlur || !listState.canScrollBackward)
MaterialTheme.colorScheme.surface MaterialTheme.colorScheme.surface
else else
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp), MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
@@ -341,10 +350,13 @@ fun FriendsScreen(
}.fillMaxSize(), }.fillMaxSize(),
screenState = screenState, screenState = screenState,
uiFriends = ImmutableList.copyOf(friendsToDisplay), uiFriends = ImmutableList.copyOf(friendsToDisplay),
listState = listState, listState = if (index == 0) listState else listStateOnline,
maxLines = maxLines, maxLines = maxLines,
padding = padding, padding = padding,
onPhotoClicked = onPhotoClicked onPhotoClicked = onPhotoClicked,
setCanScrollBackward = { can ->
canScrollBackward = can
}
) )
if (friendsToDisplay.isEmpty()) { if (friendsToDisplay.isEmpty()) {