Without paging library.
Jan Rabe - TTL Android Profis
*due to invalidation race conditions & loops
@Dao internal interface MessageItemDao { @Query( """ SELECT * FROM native_messenger_messages WHERE conversation_id = :conversationId AND hidden_at = '' ORDER BY created_at_in_millis ASC """ ) fun getAllMessages(conversationId: String): Flow<List<MessageLocal>?> } fun collectAllMessages() { viewLifecycleOwner .lifecycleScope .launch { viewModel ?.getAllMessages(id) ?.map { it.toDomain() } ?.collectLatest { conversationAdapter?.submitList(it) } } }
@Dao internal interface MessageItemDao { @Query( """ SELECT * FROM native_messenger_messages WHERE conversation_id = :conversationId AND hidden_at = '' ORDER BY created_at_in_millis ASC """ ) fun getAllMessages(conversationId: String): Flow<List<MessageLocal>?> } fun collectAllMessages() { viewLifecycleOwner .lifecycleScope .launch { viewModel ?.getAllMessages(id) ?.map { it.toDomain() } ?.collectLatest { conversationAdapter?.submitList(it) } } }
# PRESENTING CODE
# PRESENTING CODE
fun RecyclerView.subscribeToPageOnScroll() { viewLifecycleOwner.lifecycleScope.launch { pagingOnScroll() .distinctUntilChanged() .collectLatest { viewModel.loadMessages(it) } } } fun RecyclerView.subscribeToRefreshOnScroll() { viewLifecycleOwner.lifecycleScope.launch { refreshOnScroll() .distinctUntilChanged() .collectLatest { viewModel.loadMessages(it) } } } suspend fun loadMessages(instruction: PageLoadInstruction): Unit = when (instruction) { is PageLoadInstruction.Prepend -> prependMessagesFor(instruction.cursor) is PageLoadInstruction.Append -> appendMessagesFor(instruction.cursor) } suspend fun prependMessagesFor(cursor: String): Unit = withContext(Dispatchers.IO) { val response = getMessagesBeforeFromRemote(cursor, NativeMessengerViewModel.MESSAGE_LOAD_COUNT) updateDatabase(response) } suspend fun appendMessagesFor(cursor: String): Unit = withContext(Dispatchers.IO) { val response = getMessagesAfterFromRemote(cursor, NativeMessengerViewModel.MESSAGE_LOAD_COUNT) updateDatabase(response) }
fun RecyclerView.subscribeToPageOnScroll() { viewLifecycleOwner.lifecycleScope.launch { pagingOnScroll() .distinctUntilChanged() .collectLatest { viewModel.loadMessages(it) } } } fun RecyclerView.subscribeToRefreshOnScroll() { viewLifecycleOwner.lifecycleScope.launch { refreshOnScroll() .distinctUntilChanged() .collectLatest { viewModel.loadMessages(it) } } } suspend fun loadMessages(instruction: PageLoadInstruction): Unit = when (instruction) { is PageLoadInstruction.Prepend -> prependMessagesFor(instruction.cursor) is PageLoadInstruction.Append -> appendMessagesFor(instruction.cursor) } suspend fun prependMessagesFor(cursor: String): Unit = withContext(Dispatchers.IO) { val response = getMessagesBeforeFromRemote(cursor, NativeMessengerViewModel.MESSAGE_LOAD_COUNT) updateDatabase(response) } suspend fun appendMessagesFor(cursor: String): Unit = withContext(Dispatchers.IO) { val response = getMessagesAfterFromRemote(cursor, NativeMessengerViewModel.MESSAGE_LOAD_COUNT) updateDatabase(response) }
fun RecyclerView.subscribeToPageOnScroll() { viewLifecycleOwner.lifecycleScope.launch { pagingOnScroll() .distinctUntilChanged() .collectLatest { viewModel.loadMessages(it) } } } fun RecyclerView.subscribeToRefreshOnScroll() { viewLifecycleOwner.lifecycleScope.launch { refreshOnScroll() .distinctUntilChanged() .collectLatest { viewModel.loadMessages(it) } } } suspend fun loadMessages(instruction: PageLoadInstruction): Unit = when (instruction) { is PageLoadInstruction.Prepend -> prependMessagesFor(instruction.cursor) is PageLoadInstruction.Append -> appendMessagesFor(instruction.cursor) } suspend fun prependMessagesFor(cursor: String): Unit = withContext(Dispatchers.IO) { val response = getMessagesBeforeFromRemote(cursor, NativeMessengerViewModel.MESSAGE_LOAD_COUNT) updateDatabase(response) } suspend fun appendMessagesFor(cursor: String): Unit = withContext(Dispatchers.IO) { val response = getMessagesAfterFromRemote(cursor, NativeMessengerViewModel.MESSAGE_LOAD_COUNT) updateDatabase(response) }
# PRESENTING CODE
private class PagingScrollListener( @IntRange(from = 1L, to = Long.MAX_VALUE) private val pageSize: Int, private val onSend: (PageLoadInstruction) -> Unit ) : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { val isScrollingUp = 0 > dy val isScrollingDown = 0 < dy val adapter = recyclerView.adapter as? BindingListAdapter ?: return if (recyclerView.firstVisibleItemPosition <= pageSize && isScrollingUp) { val cursor = adapter.getCursorOrAfter(0) if (cursor != null) onSend(Prepend(cursor)) } if (recyclerView.lastVisibleItemPosition >= (adapter.itemCount - pageSize).coerceAtLeast(0) && isScrollingDown) { val cursor = adapter.getCursorOrBefore(adapter.itemCount - 1) if (cursor != null) onSend(Append(cursor)) } } } internal fun RecyclerView.pagingOnScroll( pageSize: Int = NativeMessengerViewModel.MESSAGE_LOAD_COUNT ): Flow<PageLoadInstruction> = callbackFlow { val listener = PagingScrollListener(pageSize) { trySend(it) } addOnScrollListener(listener) awaitClose { removeOnScrollListener(listener) } }
private class PagingScrollListener( @IntRange(from = 1L, to = Long.MAX_VALUE) private val pageSize: Int, private val onSend: (PageLoadInstruction) -> Unit ) : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { val isScrollingUp = 0 > dy val isScrollingDown = 0 < dy val adapter = recyclerView.adapter as? BindingListAdapter ?: return if (recyclerView.firstVisibleItemPosition <= pageSize && isScrollingUp) { val cursor = adapter.getCursorOrAfter(0) if (cursor != null) onSend(Prepend(cursor)) } if (recyclerView.lastVisibleItemPosition >= (adapter.itemCount - pageSize).coerceAtLeast(0) && isScrollingDown) { val cursor = adapter.getCursorOrBefore(adapter.itemCount - 1) if (cursor != null) onSend(Append(cursor)) } } } internal fun RecyclerView.pagingOnScroll( pageSize: Int = NativeMessengerViewModel.MESSAGE_LOAD_COUNT ): Flow<PageLoadInstruction> = callbackFlow { val listener = PagingScrollListener(pageSize) { trySend(it) } addOnScrollListener(listener) awaitClose { removeOnScrollListener(listener) } }
private class PagingScrollListener( @IntRange(from = 1L, to = Long.MAX_VALUE) private val pageSize: Int, private val onSend: (PageLoadInstruction) -> Unit ) : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { val isScrollingUp = 0 > dy val isScrollingDown = 0 < dy val adapter = recyclerView.adapter as? BindingListAdapter ?: return if (recyclerView.firstVisibleItemPosition <= pageSize && isScrollingUp) { val cursor = adapter.getCursorOrAfter(0) if (cursor != null) onSend(Prepend(cursor)) } if (recyclerView.lastVisibleItemPosition >= (adapter.itemCount - pageSize).coerceAtLeast(0) && isScrollingDown) { val cursor = adapter.getCursorOrBefore(adapter.itemCount - 1) if (cursor != null) onSend(Append(cursor)) } } } internal fun RecyclerView.pagingOnScroll( pageSize: Int = NativeMessengerViewModel.MESSAGE_LOAD_COUNT ): Flow<PageLoadInstruction> = callbackFlow { val listener = PagingScrollListener(pageSize) { trySend(it) } addOnScrollListener(listener) awaitClose { removeOnScrollListener(listener) } }
private class PagingScrollListener( @IntRange(from = 1L, to = Long.MAX_VALUE) private val pageSize: Int, private val onSend: (PageLoadInstruction) -> Unit ) : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { val isScrollingUp = 0 > dy val isScrollingDown = 0 < dy val adapter = recyclerView.adapter as? BindingListAdapter ?: return if (recyclerView.firstVisibleItemPosition <= pageSize && isScrollingUp) { val cursor = adapter.getCursorOrAfter(0) if (cursor != null) onSend(Prepend(cursor)) } if (recyclerView.lastVisibleItemPosition >= (adapter.itemCount - pageSize).coerceAtLeast(0) && isScrollingDown) { val cursor = adapter.getCursorOrBefore(adapter.itemCount - 1) if (cursor != null) onSend(Append(cursor)) } } } internal fun RecyclerView.pagingOnScroll( pageSize: Int = NativeMessengerViewModel.MESSAGE_LOAD_COUNT ): Flow<PageLoadInstruction> = callbackFlow { val listener = PagingScrollListener(pageSize) { trySend(it) } addOnScrollListener(listener) awaitClose { removeOnScrollListener(listener) } }
private class PagingScrollListener( @IntRange(from = 1L, to = Long.MAX_VALUE) private val pageSize: Int, private val onSend: (PageLoadInstruction) -> Unit ) : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { val isScrollingUp = 0 > dy val isScrollingDown = 0 < dy val adapter = recyclerView.adapter as? BindingListAdapter ?: return if (recyclerView.firstVisibleItemPosition <= pageSize && isScrollingUp) { val cursor = adapter.getCursorOrAfter(0) if (cursor != null) onSend(Prepend(cursor)) } if (recyclerView.lastVisibleItemPosition >= (adapter.itemCount - pageSize).coerceAtLeast(0) && isScrollingDown) { val cursor = adapter.getCursorOrBefore(adapter.itemCount - 1) if (cursor != null) onSend(Append(cursor)) } } } internal fun RecyclerView.pagingOnScroll( pageSize: Int = NativeMessengerViewModel.MESSAGE_LOAD_COUNT ): Flow<PageLoadInstruction> = callbackFlow { val listener = PagingScrollListener(pageSize) { trySend(it) } addOnScrollListener(listener) awaitClose { removeOnScrollListener(listener) } }
# PRESENTING CODE
# PRESENTING CODE
private class PagingRefreshScrollListener( @IntRange(from = 1L, to = Long.MAX_VALUE) private val pageSize: Int, private val onRefresh: (PageLoadInstruction) -> Unit ) : RecyclerView.OnScrollListener() { private var currentPage = -1 override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { val isScrollingUp = 0 > dy val isScrollingDown = 0 < dy if (isScrollingUp) recyclerView.onScrollingUp() if (isScrollingDown) recyclerView.onScrollingDown() } private fun RecyclerView.onScrollingUp() { val adapter = (adapter as? BindingListAdapter) ?: return val pageNumber = firstVisibleItemPosition / pageSize val pageBottomItemPosition = pageNumber * pageSize + pageSize - 1 if (pageNumber == currentPage) return currentPage = pageNumber val cursor = adapter.getCursorOrBefore(pageBottomItemPosition) ?: return onRefresh(Prepend(cursor)) } private fun RecyclerView.onScrollingDown() { val adapter = (adapter as? BindingListAdapter) ?: return val pageNumber = lastVisibleItemPosition / pageSize val pageTopItemPosition = pageNumber * pageSize if (pageNumber == currentPage) return val cursor = adapter.getCursorOrAfter(pageTopItemPosition) ?: return onRefresh(Append(cursor)) } }
private class PagingRefreshScrollListener( @IntRange(from = 1L, to = Long.MAX_VALUE) private val pageSize: Int, private val onRefresh: (PageLoadInstruction) -> Unit ) : RecyclerView.OnScrollListener() { private var currentPage = -1 override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { val isScrollingUp = 0 > dy val isScrollingDown = 0 < dy if (isScrollingUp) recyclerView.onScrollingUp() if (isScrollingDown) recyclerView.onScrollingDown() } private fun RecyclerView.onScrollingUp() { val adapter = (adapter as? BindingListAdapter) ?: return val pageNumber = firstVisibleItemPosition / pageSize val pageBottomItemPosition = pageNumber * pageSize + pageSize - 1 if (pageNumber == currentPage) return currentPage = pageNumber val cursor = adapter.getCursorOrBefore(pageBottomItemPosition) ?: return onRefresh(Prepend(cursor)) } private fun RecyclerView.onScrollingDown() { val adapter = (adapter as? BindingListAdapter) ?: return val pageNumber = lastVisibleItemPosition / pageSize val pageTopItemPosition = pageNumber * pageSize if (pageNumber == currentPage) return val cursor = adapter.getCursorOrAfter(pageTopItemPosition) ?: return onRefresh(Append(cursor)) } }
private class PagingRefreshScrollListener( @IntRange(from = 1L, to = Long.MAX_VALUE) private val pageSize: Int, private val onRefresh: (PageLoadInstruction) -> Unit ) : RecyclerView.OnScrollListener() { private var currentPage = -1 override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { val isScrollingUp = 0 > dy val isScrollingDown = 0 < dy if (isScrollingUp) recyclerView.onScrollingUp() if (isScrollingDown) recyclerView.onScrollingDown() } private fun RecyclerView.onScrollingUp() { val adapter = (adapter as? BindingListAdapter) ?: return val pageNumber = firstVisibleItemPosition / pageSize val pageBottomItemPosition = pageNumber * pageSize + pageSize - 1 if (pageNumber == currentPage) return currentPage = pageNumber val cursor = adapter.getCursorOrBefore(pageBottomItemPosition) ?: return onRefresh(Prepend(cursor)) } private fun RecyclerView.onScrollingDown() { val adapter = (adapter as? BindingListAdapter) ?: return val pageNumber = lastVisibleItemPosition / pageSize val pageTopItemPosition = pageNumber * pageSize if (pageNumber == currentPage) return val cursor = adapter.getCursorOrAfter(pageTopItemPosition) ?: return onRefresh(Append(cursor)) } }
private class PagingRefreshScrollListener( @IntRange(from = 1L, to = Long.MAX_VALUE) private val pageSize: Int, private val onRefresh: (PageLoadInstruction) -> Unit ) : RecyclerView.OnScrollListener() { private var currentPage = -1 override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { val isScrollingUp = 0 > dy val isScrollingDown = 0 < dy if (isScrollingUp) recyclerView.onScrollingUp() if (isScrollingDown) recyclerView.onScrollingDown() } private fun RecyclerView.onScrollingUp() { val adapter = (adapter as? BindingListAdapter) ?: return val pageNumber = firstVisibleItemPosition / pageSize val pageBottomItemPosition = pageNumber * pageSize + pageSize - 1 if (pageNumber == currentPage) return currentPage = pageNumber val cursor = adapter.getCursorOrBefore(pageBottomItemPosition) ?: return onRefresh(Prepend(cursor)) } private fun RecyclerView.onScrollingDown() { val adapter = (adapter as? BindingListAdapter) ?: return val pageNumber = lastVisibleItemPosition / pageSize val pageTopItemPosition = pageNumber * pageSize if (pageNumber == currentPage) return val cursor = adapter.getCursorOrAfter(pageTopItemPosition) ?: return onRefresh(Append(cursor)) } }
private class PagingRefreshScrollListener( @IntRange(from = 1L, to = Long.MAX_VALUE) private val pageSize: Int, private val onRefresh: (PageLoadInstruction) -> Unit ) : RecyclerView.OnScrollListener() { private var currentPage = -1 override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { val isScrollingUp = 0 > dy val isScrollingDown = 0 < dy if (isScrollingUp) recyclerView.onScrollingUp() if (isScrollingDown) recyclerView.onScrollingDown() } private fun RecyclerView.onScrollingUp() { val adapter = (adapter as? BindingListAdapter) ?: return val pageNumber = firstVisibleItemPosition / pageSize val pageBottomItemPosition = pageNumber * pageSize + pageSize - 1 if (pageNumber == currentPage) return currentPage = pageNumber val cursor = adapter.getCursorOrBefore(pageBottomItemPosition) ?: return onRefresh(Prepend(cursor)) } private fun RecyclerView.onScrollingDown() { val adapter = (adapter as? BindingListAdapter) ?: return val pageNumber = lastVisibleItemPosition / pageSize val pageTopItemPosition = pageNumber * pageSize if (pageNumber == currentPage) return val cursor = adapter.getCursorOrAfter(pageTopItemPosition) ?: return onRefresh(Append(cursor)) } }
private class PagingRefreshScrollListener( @IntRange(from = 1L, to = Long.MAX_VALUE) private val pageSize: Int, private val onRefresh: (PageLoadInstruction) -> Unit ) : RecyclerView.OnScrollListener() { private var currentPage = -1 override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { val isScrollingUp = 0 > dy val isScrollingDown = 0 < dy if (isScrollingUp) recyclerView.onScrollingUp() if (isScrollingDown) recyclerView.onScrollingDown() } private fun RecyclerView.onScrollingUp() { val adapter = (adapter as? BindingListAdapter) ?: return val pageNumber = firstVisibleItemPosition / pageSize val pageBottomItemPosition = pageNumber * pageSize + pageSize - 1 if (pageNumber == currentPage) return currentPage = pageNumber val cursor = adapter.getCursorOrBefore(pageBottomItemPosition) ?: return onRefresh(Prepend(cursor)) } private fun RecyclerView.onScrollingDown() { val adapter = (adapter as? BindingListAdapter) ?: return val pageNumber = lastVisibleItemPosition / pageSize val pageTopItemPosition = pageNumber * pageSize if (pageNumber == currentPage) return val cursor = adapter.getCursorOrAfter(pageTopItemPosition) ?: return onRefresh(Append(cursor)) } }
# PRESENTING CODE
internal fun RecyclerView.refreshOnScroll( pageSize: Int = NativeMessengerViewModel.MESSAGE_LOAD_COUNT ): Flow<PageLoadInstruction> = callbackFlow { val listener = PagingRefreshScrollListener(pageSize) { trySend(it) } addOnScrollListener(listener) awaitClose { removeOnScrollListener(listener) } }
# PRESENTING CODE
# PRESENTING CODE
internal suspend fun RecyclerView.autoScrollToBottom( isEnabled: () -> Boolean ) = suspendCancellableCoroutine<Unit> { continuation -> val listener = BottomScrollListener() val dataObserver = OnChangeAdapterObserver { if (!listener.isScrolledToBottom) return@OnChangeAdapterObserver if (!isEnabled()) return@OnChangeAdapterObserver scrollToPosition(lastIndex) } adapter?.registerAdapterDataObserver(dataObserver) addOnScrollListener(listener) continuation.invokeOnCancellation { adapter?.unregisterAdapterDataObserver(dataObserver) removeOnScrollListener(listener) } }
internal suspend fun RecyclerView.autoScrollToBottom( isEnabled: () -> Boolean ) = suspendCancellableCoroutine<Unit> { continuation -> val listener = BottomScrollListener() val dataObserver = OnChangeAdapterObserver { if (!listener.isScrolledToBottom) return@OnChangeAdapterObserver if (!isEnabled()) return@OnChangeAdapterObserver scrollToPosition(lastIndex) } adapter?.registerAdapterDataObserver(dataObserver) addOnScrollListener(listener) continuation.invokeOnCancellation { adapter?.unregisterAdapterDataObserver(dataObserver) removeOnScrollListener(listener) } }
internal suspend fun RecyclerView.autoScrollToBottom( isEnabled: () -> Boolean ) = suspendCancellableCoroutine<Unit> { continuation -> val listener = BottomScrollListener() val dataObserver = OnChangeAdapterObserver { if (!listener.isScrolledToBottom) return@OnChangeAdapterObserver if (!isEnabled()) return@OnChangeAdapterObserver scrollToPosition(lastIndex) } adapter?.registerAdapterDataObserver(dataObserver) addOnScrollListener(listener) continuation.invokeOnCancellation { adapter?.unregisterAdapterDataObserver(dataObserver) removeOnScrollListener(listener) } }
internal suspend fun RecyclerView.autoScrollToBottom( isEnabled: () -> Boolean ) = suspendCancellableCoroutine<Unit> { continuation -> val listener = BottomScrollListener() val dataObserver = OnChangeAdapterObserver { if (!listener.isScrolledToBottom) return@OnChangeAdapterObserver if (!isEnabled()) return@OnChangeAdapterObserver scrollToPosition(lastIndex) } adapter?.registerAdapterDataObserver(dataObserver) addOnScrollListener(listener) continuation.invokeOnCancellation { adapter?.unregisterAdapterDataObserver(dataObserver) removeOnScrollListener(listener) } }
# PRESENTING CODE
# PRESENTING CODE
internal suspend fun RecyclerView.scrollToUnread( hasNewInitialPage: (() -> Boolean), consumeInitialPage: () -> Unit ) = suspendCancellableCoroutine<Unit> { continuation -> val dataObserver = OnChangeAdapterObserver { val unreadMessageBannerPosition = (adapter as? BindingListAdapter) ?.currentList ?.indexOfLast { it.layout == R.layout.profis_native_messenger_item_unread_banner } ?: RecyclerView.NO_POSITION val onInitialCompleted = hasNewInitialPage() val hasUnreadMessageBanner = unreadMessageBannerPosition > 0 when { onInitialCompleted && hasUnreadMessageBanner -> { // we have initial page scrollToPositionWithOffset(unreadMessageBannerPosition, center) findViewTreeLifecycleOwner() ?.lifecycleScope ?.launch { awaitScrollEnd() consumeInitialPage() } } !hasUnreadMessageBanner -> consumeInitialPage() } } adapter?.registerAdapterDataObserver(dataObserver) continuation.invokeOnCancellation { adapter?.unregisterAdapterDataObserver(dataObserver) } }
# PRESENTING CODE
# PRESENTING CODE
internal suspend fun RecyclerView.awaitAdapterUpdate() = suspendCancellableCoroutine { continuation -> var dataObserver: OnChangeAdapterObserver? = null dataObserver = OnChangeAdapterObserver { dataObserver?.let { adapter?.unregisterAdapterDataObserver(it) dataObserver = null } continuation.resume(Unit) } dataObserver?.let { adapter?.registerAdapterDataObserver(it) } continuation.invokeOnCancellation { dataObserver?.let { adapter?.unregisterAdapterDataObserver(it) } } }
# PRESENTING CODE
internal suspend fun RecyclerView.awaitScrollEnd() { // If a smooth scroll has just been started, it won't actually start until the next // animation frame, so we'll await that first awaitAnimationFrame() // Now we can check if we're actually idle. If so, return now if (scrollState == RecyclerView.SCROLL_STATE_IDLE) return suspendCancellableCoroutine { continuation -> val onScrollListener = object : RecyclerView.OnScrollListener() { override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { if (newState == RecyclerView.SCROLL_STATE_IDLE) { // Make sure we remove the listener so we don't leak the coroutine continuation recyclerView.removeOnScrollListener(this) // Finally, resume the coroutine continuation.resume(Unit) } } } continuation.invokeOnCancellation { // If the coroutine is cancelled, remove the scroll listener removeOnScrollListener(onScrollListener) } addOnScrollListener(onScrollListener) } } internal suspend fun View.awaitAnimationFrame(): Unit = suspendCancellableCoroutine { continuation -> val runnable = Runnable { continuation.resume(Unit) } // If the coroutine is cancelled, remove the callback continuation.invokeOnCancellation { removeCallbacks(runnable) } // And finally post the runnable postOnAnimation(runnable) }
# PRESENTING CODE
internal class BottomScrollListener( private val onBottom: ((Boolean) -> Unit)? = null ) : RecyclerView.OnScrollListener() { var isScrolledToBottom: Boolean by Delegates.observable(false) { _, oldValue, newValue -> if (oldValue != newValue) onBottom?.invoke(isScrolledToBottom) } override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { isScrolledToBottom = recyclerView.isScrolledToBottom } } internal fun RecyclerView.isBottom(): Flow<Boolean> = callbackFlow { if (adapter?.itemCount != 0) { val isScrolledToBottom = isScrolledToBottom trySend(isScrolledToBottom) } val listener = BottomScrollListener { trySend(it) } addOnScrollListener(listener) awaitClose { removeOnScrollListener(listener) } }
# PRESENTING CODE
internal class OnChangeAdapterObserver(private val onDataChanged: () -> Unit) : RecyclerView.AdapterDataObserver() { override fun onChanged() = onDataChanged() override fun onItemRangeChanged(positionStart: Int, itemCount: Int) = onDataChanged() override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) = onDataChanged() override fun onItemRangeInserted(positionStart: Int, itemCount: Int) = onDataChanged() override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) = onDataChanged() override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) = onDataChanged() }
# PRESENTING CODE
Arif Akdogan, Jannis König, Fabian Jung, Sergej Istomin, Yasar Naci Gündüz​, Fabian Szymanczyk
Without Paging Library
Jan Rabe - TTL Android Profis
// viewmodel val state = MyStateHolder(items: MutableState<List<Items>> = mutableStateOf(emptyList()) repository .getMyItems() .collectLatest { items -> withContext(Dispatchers.Main) { state.items.value = items } } // composable LazyColumn( modifier = Modifier .fillMaxSize() .nestedScroll(nestedScrollConnection), state = scrollState, ) { items( items = state.items.value, key = { it.id }, contentType = { 0 } ) { item -> // my composable item } }
// viewmodel val state = MyStateHolder(items: MutableState<List<Items>> = mutableStateOf(emptyList()) repository .getMyItems() .collectLatest { items -> withContext(Dispatchers.Main) { state.items.value = items } } // composable LazyColumn( modifier = Modifier .fillMaxSize() .nestedScroll(nestedScrollConnection), state = scrollState, ) { items( items = state.items.value, key = { it.id }, contentType = { 0 } ) { item -> // my composable item } }
# PRESENTING CODE
val scrollState: LazyListState = state.lazyListState val nestedScrollConnection = remember { PageRefreshNestedScrollConnection( scrollState = scrollState, cursor = state.cursorForPosition, onRefresh = { state.load(it) } ) } LazyColumn( modifier = Modifier .fillMaxSize() .nestedScroll(nestedScrollConnection), state = scrollState, ) { items( items = state.items.value, key = { it.id }, contentType = { 0 } ) { item -> // my composable item } }
val scrollState: LazyListState = state.lazyListState val nestedScrollConnection = remember { PageRefreshNestedScrollConnection( scrollState = scrollState, cursor = state.cursorForPosition, onRefresh = { state.load(it) } ) } LazyColumn( modifier = Modifier .fillMaxSize() .nestedScroll(nestedScrollConnection), state = scrollState, ) { items( items = state.items.value, key = { it.id }, contentType = { 0 } ) { item -> // my composable item } }
# PRESENTING CODE
val nestedScrollConnection = remember { PageRefreshNestedScrollConnection( scrollState = scrollState, cursor = state.cursorForPosition, onRefresh = { state.load(it) } ) } val cursorForPosition: (index: Int) -> String? = { if (it in 0..items.value.lastIndex) items.value[it].cursorId // could be updatedAt else null }
val nestedScrollConnection = remember { PageRefreshNestedScrollConnection( scrollState = scrollState, cursor = state.cursorForPosition, onRefresh = { state.load(it) } ) } val cursorForPosition: (index: Int) -> String? = { if (it in 0..items.value.lastIndex) items.value[it].cursorId // could be updatedAt else null }
# PRESENTING CODE
class PageRefreshNestedScrollConnection( @IntRange(from = 1L, to = Long.MAX_VALUE) private val pageSize: Int = DEFAULT_PAGE_SIZE, private val scrollState: LazyListState, private val cursor: (index: Int) -> String?, private val onRefresh: (LoadParams) -> Unit ) : NestedScrollConnection { private var currentPage = -1 override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { if (available.getDistance() <= 4.dp) return Offset.Zero val dy = available.y val isScrollingUp = 0 < dy val isScrollingDown = 0 > dy if (isScrollingUp) onScrollingUp() if (isScrollingDown) onScrollingDown() return Offset.Zero } private fun onScrollingUp() { val firstVisibleItemPosition = scrollState.firstVisibleItemIndex val pageNumber = firstVisibleItemPosition / pageSize val pageBottomItemPosition = pageNumber * pageSize + pageSize - 1 if (pageNumber == currentPage) return currentPage = pageNumber val cursor = cursor(pageBottomItemPosition) if (cursor != null) { onRefresh(LoadParams.Prepend(cursor)) } } private fun onScrollingDown() { val lastVisibleItemPosition = scrollState.lastVisibleItemIndex val pageNumber = lastVisibleItemPosition / pageSize val pageTopItemPosition = pageNumber * pageSize if (pageNumber == currentPage) return currentPage = pageNumber val cursor = cursor(pageTopItemPosition) if (cursor != null) { onRefresh(LoadParams.Append(cursor)) } } }
class PageRefreshNestedScrollConnection( @IntRange(from = 1L, to = Long.MAX_VALUE) private val pageSize: Int = DEFAULT_PAGE_SIZE, private val scrollState: LazyListState, private val cursor: (index: Int) -> String?, private val onRefresh: (LoadParams) -> Unit ) : NestedScrollConnection { private var currentPage = -1 override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { if (available.getDistance() <= 4.dp) return Offset.Zero val dy = available.y val isScrollingUp = 0 < dy val isScrollingDown = 0 > dy if (isScrollingUp) onScrollingUp() if (isScrollingDown) onScrollingDown() return Offset.Zero } private fun onScrollingUp() { val firstVisibleItemPosition = scrollState.firstVisibleItemIndex val pageNumber = firstVisibleItemPosition / pageSize val pageBottomItemPosition = pageNumber * pageSize + pageSize - 1 if (pageNumber == currentPage) return currentPage = pageNumber val cursor = cursor(pageBottomItemPosition) if (cursor != null) { onRefresh(LoadParams.Prepend(cursor)) } } private fun onScrollingDown() { val lastVisibleItemPosition = scrollState.lastVisibleItemIndex val pageNumber = lastVisibleItemPosition / pageSize val pageTopItemPosition = pageNumber * pageSize if (pageNumber == currentPage) return currentPage = pageNumber val cursor = cursor(pageTopItemPosition) if (cursor != null) { onRefresh(LoadParams.Append(cursor)) } } }
# PRESENTING CODE
private fun onScrollingUp() { val firstVisibleItemPosition = scrollState.firstVisibleItemIndex val pageNumber = firstVisibleItemPosition / pageSize val pageBottomItemPosition = pageNumber * pageSize + pageSize - 1 if (pageNumber == currentPage) return currentPage = pageNumber val cursor = cursor(pageBottomItemPosition) if (cursor != null) { onRefresh(LoadParams.Prepend(cursor)) } } private fun onScrollingDown() { val lastVisibleItemPosition = scrollState.lastVisibleItemIndex val pageNumber = lastVisibleItemPosition / pageSize val pageTopItemPosition = pageNumber * pageSize if (pageNumber == currentPage) return currentPage = pageNumber val cursor = cursor(pageTopItemPosition) if (cursor != null) { onRefresh(LoadParams.Append(cursor)) } } }
private fun onScrollingUp() { val firstVisibleItemPosition = scrollState.firstVisibleItemIndex val pageNumber = firstVisibleItemPosition / pageSize val pageBottomItemPosition = pageNumber * pageSize + pageSize - 1 if (pageNumber == currentPage) return currentPage = pageNumber val cursor = cursor(pageBottomItemPosition) if (cursor != null) { onRefresh(LoadParams.Prepend(cursor)) } } private fun onScrollingDown() { val lastVisibleItemPosition = scrollState.lastVisibleItemIndex val pageNumber = lastVisibleItemPosition / pageSize val pageTopItemPosition = pageNumber * pageSize if (pageNumber == currentPage) return currentPage = pageNumber val cursor = cursor(pageTopItemPosition) if (cursor != null) { onRefresh(LoadParams.Append(cursor)) } } }
# PRESENTING CODE
# PRESENTING CODE
class VerticalNestedScrollConnection( private val onScrollingUp: (() -> Unit)? = null, private val onScrollingDown: (() -> Unit)? = null, ) : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { val dy = available.y val isScrollingUp = 0 < dy val isScrollingDown = 0 > dy if (isScrollingUp) onScrollingUp?.invoke() if (isScrollingDown) onScrollingDown?.invoke() return super.onPreScroll(available, source) } } val verticalNestedScrollConnection = remember { VerticalNestedScrollConnection( onScrollingDown = { scope.launch { state.hideKeyboard() } } ) } LazyColumn( modifier = Modifier .fillMaxSize() .nestedScroll(nestedScrollConnection) .nestedScroll(verticalNestedScrollConnection), state = scrollState, ) { items( items = state.items.value, key = { it.id }, contentType = { 0 } ) { item -> // my composable item } }
class VerticalNestedScrollConnection( private val onScrollingUp: (() -> Unit)? = null, private val onScrollingDown: (() -> Unit)? = null, ) : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { val dy = available.y val isScrollingUp = 0 < dy val isScrollingDown = 0 > dy if (isScrollingUp) onScrollingUp?.invoke() if (isScrollingDown) onScrollingDown?.invoke() return super.onPreScroll(available, source) } } val verticalNestedScrollConnection = remember { VerticalNestedScrollConnection( onScrollingDown = { scope.launch { state.hideKeyboard() } } ) } LazyColumn( modifier = Modifier .fillMaxSize() .nestedScroll(nestedScrollConnection) .nestedScroll(verticalNestedScrollConnection), state = scrollState, ) { items( items = state.items.value, key = { it.id }, contentType = { 0 } ) { item -> // my composable item } }
# PRESENTING CODE
val scope = rememberCoroutineScope() val keyboardController = LocalSoftwareKeyboardController.current val focusManager = LocalFocusManager.current LaunchedEffect(Unit) { state.showKeyboard.collectLatest { show -> if (show) { keyboardController?.show() } else { keyboardController?.hide() focusManager.clearFocus() } } }
# PRESENTING CODE
# PRESENTING CODE
val LazyListState.lastVisibleItemIndex: Int get() = layoutInfo.visibleItemsInfo.lastIndex + firstVisibleItemIndex val LazyListState.isScrolledToEnd: Boolean get() = layoutInfo.visibleItemsInfo.lastOrNull()?.index == layoutInfo.totalItemsCount - 1 val LazyListState.isScrolledToEndCompletely: Boolean get() { val last = layoutInfo.visibleItemsInfo.lastOrNull() return last?.index == layoutInfo.totalItemsCount - 1 && last.offset == 0 } val LazyListState.isScrolledToStart: Boolean get() = layoutInfo.visibleItemsInfo.firstOrNull()?.index == 0 val LazyListState.isScrolledToStartCompletely: Boolean get() { val first = layoutInfo.visibleItemsInfo.firstOrNull() return first?.index == 0 && first.offset == 0 }
# PRESENTING CODE