Skip to content

Commit

Permalink
Paginate feeds list (#118)
Browse files Browse the repository at this point in the history
* Add query to load paginated feeds

* Use paginated feeds list in feeds bottom sheet

* Remove `RssRepository.allFeeds`

* Rename `allFeedsPaginated` to `allFeeds`
  • Loading branch information
msasikanth authored Oct 26, 2023
1 parent 21534b6 commit cb52503
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 97 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,34 @@
*/
package dev.sasikanth.rss.reader.feeds

import app.cash.paging.PagingData
import app.cash.paging.cachedIn
import app.cash.paging.createPager
import app.cash.paging.createPagingConfig
import app.cash.paging.insertSeparators
import app.cash.paging.map
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.essenty.instancekeeper.InstanceKeeper
import com.arkivanov.essenty.instancekeeper.getOrCreate
import com.arkivanov.essenty.lifecycle.doOnCreate
import dev.sasikanth.rss.reader.feeds.ui.FeedsListItemType
import dev.sasikanth.rss.reader.models.local.Feed
import dev.sasikanth.rss.reader.repository.ObservableSelectedFeed
import dev.sasikanth.rss.reader.repository.RssRepository
import dev.sasikanth.rss.reader.utils.DispatchersProvider
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
Expand Down Expand Up @@ -121,32 +131,47 @@ class FeedsPresenter(
}

private fun init() {
rssRepository
.allFeeds()
.onEach { feeds ->
val feedsGroup = feeds.groupBy { it.pinnedAt != null }.entries
val pinnedFeeds = feedsGroup.firstOrNull()?.value.orEmpty().sortedBy { it.pinnedAt }
val unPinnedFeeds = feedsGroup.elementAtOrNull(1)?.value.orEmpty()

observableSelectedFeed.selectedFeed
.flatMapLatest { selectedFeed ->
val feedListItemTypes: Flow<PagingData<FeedsListItemType>> =
createPager(config = createPagingConfig(pageSize = 20)) { rssRepository.allFeeds() }
.flow
.cachedIn(coroutineScope)
.map { feeds ->
feeds
.map { feed -> FeedsListItemType.FeedListItem(feed = feed) }
.insertSeparators { before, after ->
when {
before?.feed?.pinnedAt != null &&
after != null &&
after.feed.pinnedAt == null -> {
FeedsListItemType.SectionSeparator
}
before?.feed != null && after?.feed != null -> {
FeedsListItemType.FeedSeparator
}
else -> {
null
}
}
}
}

rssRepository.numberOfPinnedFeeds().map { numberOfPinnedFeeds ->
Triple(selectedFeed, feedListItemTypes, numberOfPinnedFeeds)
}
}
.distinctUntilChanged()
.onEach { (selectedFeed, feedListItemTypes, numberOfPinnedFeeds) ->
_state.update {
it.copy(
pinnedFeeds = pinnedFeeds.toImmutableList(),
feeds = unPinnedFeeds.toImmutableList()
feedsListItemTypes = feedListItemTypes,
numberOfPinnedFeeds = numberOfPinnedFeeds,
selectedFeed = selectedFeed
)
}
}
.launchIn(coroutineScope)

rssRepository
.numberOfPinnedFeeds()
.onEach { numberOfPinnedFeeds ->
_state.update { it.copy(numberOfPinnedFeeds = numberOfPinnedFeeds) }
}
.launchIn(coroutineScope)

observableSelectedFeed.selectedFeed
.onEach { selectedFeed -> _state.update { it.copy(selectedFeed = selectedFeed) } }
.launchIn(coroutineScope)
}

override fun onDestroy() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,33 +16,35 @@
package dev.sasikanth.rss.reader.feeds

import androidx.compose.runtime.Immutable
import androidx.paging.PagingData
import androidx.paging.filter
import dev.sasikanth.rss.reader.feeds.ui.FeedsListItemType
import dev.sasikanth.rss.reader.models.local.Feed
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.map

@Immutable
internal data class FeedsState(
val pinnedFeeds: ImmutableList<Feed>,
val feeds: ImmutableList<Feed>,
val feedsListItemTypes: Flow<PagingData<FeedsListItemType>>,
val selectedFeed: Feed?,
val numberOfPinnedFeeds: Long
) {

val allFeeds: ImmutableList<Feed>
get() = (pinnedFeeds + feeds).toImmutableList()
val feedsOnly: Flow<PagingData<FeedsListItemType.FeedListItem>> =
feedsListItemTypes
.map { feeds ->
feeds.filter { feedListItemType -> feedListItemType is FeedsListItemType.FeedListItem }
}
.filterIsInstance()

val canPinFeeds: Boolean
get() = numberOfPinnedFeeds <= 10L
get() = numberOfPinnedFeeds < 10L

companion object {

val DEFAULT =
FeedsState(
pinnedFeeds = persistentListOf(),
feeds = persistentListOf(),
selectedFeed = null,
numberOfPinnedFeeds = 0
)
FeedsState(feedsListItemTypes = emptyFlow(), selectedFeed = null, numberOfPinnedFeeds = 0)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Divider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
Expand Down Expand Up @@ -71,7 +70,6 @@ internal fun FeedListItem(
modifier: Modifier = Modifier,
feed: Feed,
selected: Boolean,
canShowDivider: Boolean,
canPinFeeds: Boolean,
feedsSheetMode: FeedsSheetMode,
onDeleteFeed: (Feed) -> Unit,
Expand Down Expand Up @@ -130,13 +128,6 @@ internal fun FeedListItem(
)
}
}

if (canShowDivider) {
Divider(
modifier = Modifier.requiredHeight(1.dp).align(Alignment.BottomStart).padding(end = 12.dp),
color = AppTheme.colorScheme.tintedSurface
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,9 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Close
Expand All @@ -78,6 +75,8 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.dp
import app.cash.paging.compose.LazyPagingItems
import app.cash.paging.compose.collectAsLazyPagingItems
import dev.sasikanth.rss.reader.feeds.FeedsEffect
import dev.sasikanth.rss.reader.feeds.FeedsEvent
import dev.sasikanth.rss.reader.feeds.FeedsPresenter
Expand All @@ -88,7 +87,6 @@ import dev.sasikanth.rss.reader.ui.AppTheme
import dev.sasikanth.rss.reader.utils.KeyboardState
import dev.sasikanth.rss.reader.utils.inverseProgress
import dev.sasikanth.rss.reader.utils.keyboardVisibilityAsState
import kotlinx.collections.immutable.ImmutableList

@Composable
internal fun FeedsBottomSheet(
Expand Down Expand Up @@ -124,7 +122,7 @@ internal fun FeedsBottomSheet(
if (hasBottomSheetExpandedThreshold) {
BottomSheetCollapsedContent(
modifier = Modifier.graphicsLayer { alpha = bottomSheetExpandingProgress },
feeds = state.allFeeds,
feeds = state.feedsOnly.collectAsLazyPagingItems(),
selectedFeed = selectedFeed,
onFeedSelected = { feed -> feedsPresenter.dispatch(FeedsEvent.OnFeedSelected(feed)) }
)
Expand All @@ -143,8 +141,7 @@ internal fun FeedsBottomSheet(
.toFloat()
alpha = targetAlpha
},
pinnedFeeds = state.pinnedFeeds,
feeds = state.feeds,
feedsListItemTypes = state.feedsListItemTypes.collectAsLazyPagingItems(),
selectedFeed = state.selectedFeed,
feedsSheetMode = feedsSheetMode,
canPinFeeds = state.canPinFeeds,
Expand All @@ -167,8 +164,7 @@ internal fun FeedsBottomSheet(

@Composable
private fun BottomSheetExpandedContent(
pinnedFeeds: ImmutableList<Feed>,
feeds: ImmutableList<Feed>,
feedsListItemTypes: LazyPagingItems<FeedsListItemType>,
selectedFeed: Feed?,
feedsSheetMode: FeedsSheetMode,
canPinFeeds: Boolean,
Expand Down Expand Up @@ -245,42 +241,42 @@ private fun BottomSheetExpandedContent(
bottom = padding.calculateBottomPadding() + 64.dp
)
) {
itemsIndexed(pinnedFeeds) { index, feed ->
FeedListItem(
feed = feed,
selected = selectedFeed == feed,
canShowDivider = index != pinnedFeeds.lastIndex,
canPinFeeds = true,
feedsSheetMode = feedsSheetMode,
onDeleteFeed = onDeleteFeed,
onFeedSelected = onFeedSelected,
onFeedNameChanged = onFeedNameChanged,
onFeedPinClick = onFeedPinClick
)
}

if (pinnedFeeds.isNotEmpty() && feeds.isNotEmpty()) {
item {
Divider(
modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp),
color = AppTheme.colorScheme.tintedSurface
)
items(feedsListItemTypes.itemCount) { index ->
when (val feedListItemType = feedsListItemTypes[index]) {
is FeedsListItemType.FeedListItem -> {
val feed = feedListItemType.feed
FeedListItem(
feed = feed,
selected = selectedFeed == feed,
canPinFeeds = (feed.pinnedAt != null || canPinFeeds),
feedsSheetMode = feedsSheetMode,
onDeleteFeed = onDeleteFeed,
onFeedSelected = onFeedSelected,
onFeedNameChanged = onFeedNameChanged,
onFeedPinClick = onFeedPinClick
)
}
FeedsListItemType.FeedSeparator -> {
Divider(
modifier =
Modifier.requiredHeight(1.dp)
.align(Alignment.BottomStart)
.padding(start = 24.dp, end = 12.dp)
.graphicsLayer { translationY = -1f },
color = AppTheme.colorScheme.tintedSurface
)
}
FeedsListItemType.SectionSeparator -> {
Divider(
modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp),
color = AppTheme.colorScheme.tintedSurface
)
}
null -> {
// no-op
}
}
}

itemsIndexed(feeds) { index, feed ->
FeedListItem(
feed = feed,
selected = selectedFeed == feed,
canShowDivider = index != feeds.lastIndex,
canPinFeeds = canPinFeeds,
feedsSheetMode = feedsSheetMode,
onDeleteFeed = onDeleteFeed,
onFeedSelected = onFeedSelected,
onFeedNameChanged = onFeedNameChanged,
onFeedPinClick = onFeedPinClick
)
}
}

if (keyboardState == KeyboardState.Opened && feedsSheetMode == LinkEntry) {
Expand Down Expand Up @@ -362,7 +358,7 @@ private fun BoxScope.EditFeeds(onClick: () -> Unit) {

@Composable
private fun BottomSheetCollapsedContent(
feeds: ImmutableList<Feed>,
feeds: LazyPagingItems<FeedsListItemType.FeedListItem>,
selectedFeed: Feed?,
onFeedSelected: (Feed) -> Unit,
modifier: Modifier = Modifier
Expand All @@ -373,13 +369,16 @@ private fun BottomSheetCollapsedContent(
horizontalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(start = 100.dp, end = 24.dp)
) {
items(feeds) { feed ->
BottomSheetItem(
text = feed.name.uppercase(),
iconUrl = feed.icon,
selected = selectedFeed == feed,
onClick = { onFeedSelected(feed) }
)
items(feeds.itemCount) { index ->
val feed = feeds[index]?.feed
if (feed != null) {
BottomSheetItem(
text = feed.name.uppercase(),
iconUrl = feed.icon,
selected = selectedFeed == feed,
onClick = { onFeedSelected(feed) }
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2023 Sasikanth Miriyampalli
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package dev.sasikanth.rss.reader.feeds.ui

import dev.sasikanth.rss.reader.models.local.Feed

internal sealed interface FeedsListItemType {
data class FeedListItem(val feed: Feed) : FeedsListItemType

object FeedSeparator : FeedsListItemType

object SectionSeparator : FeedsListItemType
}
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,13 @@ class RssRepository(
withContext(ioDispatcher) { bookmarkQueries.deleteBookmark(link) }
}

fun allFeeds(): Flow<List<Feed>> {
return feedQueries.feeds(mapper = ::mapToFeed).asFlow().mapToList(ioDispatcher)
fun allFeeds(): PagingSource<Int, Feed> {
return QueryPagingSource(
countQuery = feedQueries.count(),
transacter = feedQueries,
context = ioDispatcher,
queryProvider = { limit, offset -> feedQueries.feedsPaginated(limit, offset, ::mapToFeed) }
)
}

fun feed(feedLink: String): Feed {
Expand Down
Loading

0 comments on commit cb52503

Please sign in to comment.