Implement androidx Paging and unify TimeLineViewModel state

This commit is contained in:
Shinokuni 2023-08-23 23:03:19 +02:00
parent 16ed0ef05e
commit 36f768044a
8 changed files with 153 additions and 141 deletions

View File

@ -3,10 +3,10 @@ package com.readrops.app.compose.timelime
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@ -31,6 +31,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.paging.compose.collectAsLazyPagingItems
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import cafe.adriel.voyager.navigator.tab.Tab
@ -48,27 +49,22 @@ object TimelineTab : Tab {
override val options: TabOptions
@Composable
get() {
return TabOptions(
index = 1u,
title = "Timeline",
)
}
get() = TabOptions(
index = 1u,
title = "Timeline",
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
override fun Content() {
val viewModel = getViewModel<TimelineViewModel>()
val state by viewModel.timelineState.collectAsStateWithLifecycle()
val drawerState by viewModel.drawerState.collectAsStateWithLifecycle()
val isRefreshing by viewModel.isRefreshing.collectAsStateWithLifecycle()
val navigator = LocalNavigator.currentOrThrow
val scrollState = rememberLazyListState()
val swipeState = rememberSwipeRefreshState(isRefreshing)
val drawerUIState = rememberDrawerState(
val swipeState = rememberSwipeRefreshState(state.isRefreshing)
val drawerState = rememberDrawerState(
initialValue = DrawerValue.Closed,
confirmStateChange = {
if (it == DrawerValue.Closed) {
@ -82,23 +78,23 @@ object TimelineTab : Tab {
)
BackHandler(
enabled = drawerState.isOpen,
enabled = state.isDrawerOpen,
onBack = { viewModel.closeDrawer() }
)
LaunchedEffect(drawerState.isOpen) {
if (drawerState.isOpen) {
drawerUIState.open()
LaunchedEffect(state.isDrawerOpen) {
if (state.isDrawerOpen) {
drawerState.open()
} else {
drawerUIState.close()
drawerState.close()
}
}
ModalNavigationDrawer(
drawerState = drawerUIState,
drawerState = drawerState,
drawerContent = {
TimelineDrawer(
state = drawerState,
state = state,
onClickDefaultItem = {
viewModel.updateDrawerDefaultItem(it)
},
@ -156,8 +152,8 @@ object TimelineTab : Tab {
onRefresh = { viewModel.refreshTimeline() },
modifier = Modifier.padding(paddingValues)
) {
when (state) {
is TimelineState.Loading -> {
when (val itemState = state.items) {
is ItemState.Loading -> {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
@ -167,34 +163,28 @@ object TimelineTab : Tab {
}
}
is TimelineState.Error -> {
is ItemState.Error -> TODO()
is ItemState.Loaded -> {
val items = itemState.items.collectAsLazyPagingItems()
}
is TimelineState.Loaded -> {
val items = (state as TimelineState.Loaded).items
if (items.isNotEmpty()) {
LazyColumn(
state = scrollState,
verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.shortSpacing)
) {
items(
items = items,
key = { it.item.id },
contentType = { "item_with_feed" }
) { itemWithFeed ->
TimelineItem(
itemWithFeed = itemWithFeed,
onClick = { navigator.push(ItemScreen()) },
onFavorite = {},
onReadLater = {},
onShare = {},
)
}
LazyColumn(
state = scrollState,
contentPadding = PaddingValues(vertical = MaterialTheme.spacing.shortSpacing),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.shortSpacing)
) {
items(
count = items.itemCount,
key = { items[it]!!.item.id },
contentType = { "item_with_feed" }
) { itemCount ->
TimelineItem(
itemWithFeed = items[itemCount]!!,
onClick = { navigator.push(ItemScreen()) },
onFavorite = {},
onReadLater = {},
onShare = {},
)
}
} else {
NoItemPlaceholder()
}
}
}

View File

@ -2,23 +2,26 @@ package com.readrops.app.compose.timelime
import androidx.compose.runtime.Immutable
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.readrops.app.compose.base.TabViewModel
import com.readrops.app.compose.repositories.GetFoldersWithFeeds
import com.readrops.app.compose.timelime.drawer.DrawerDefaultItemsSelection
import com.readrops.db.Database
import com.readrops.db.entities.Feed
import com.readrops.db.entities.Folder
import com.readrops.db.filters.FilterType
import com.readrops.db.pojo.ItemWithFeed
import com.readrops.db.queries.ItemsQueryBuilder
import com.readrops.db.queries.QueryFilters
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@ -29,108 +32,128 @@ class TimelineViewModel(
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) : TabViewModel(database) {
private val _timelineState = MutableStateFlow<TimelineState>(TimelineState.Loading)
private val _timelineState = MutableStateFlow(TimelineState())
val timelineState = _timelineState.asStateFlow()
private var _isRefreshing = MutableStateFlow(false)
val isRefreshing = _isRefreshing.asStateFlow()
private val _drawerState = MutableStateFlow(DrawerState())
val drawerState = _drawerState.asStateFlow()
private val filters = MutableStateFlow(_timelineState.value.filters)
init {
viewModelScope.launch(dispatcher) {
accountEvent.consumeAsFlow().collectLatest { account ->
val query = ItemsQueryBuilder.buildItemsQuery(QueryFilters(accountId = account.id))
combine(
accountEvent.consumeAsFlow(),
filters
) { account, filters ->
Pair(account, filters)
}.collectLatest { (account, filters) ->
val query = ItemsQueryBuilder.buildItemsQuery(filters.copy(accountId = account.id))
val items = async {
database.newItemDao().selectAll(query)
.catch { _timelineState.value = TimelineState.Error(Exception(it)) }
.collect {
_timelineState.value = TimelineState.Loaded(it)
}
}
val drawer = async {
_drawerState.update {
it.copy(
foldersAndFeeds = getFoldersWithFeeds.get(account.id)
_timelineState.update {
it.copy(
foldersAndFeeds = getFoldersWithFeeds.get(account.id),
items = ItemState.Loaded(
items = Pager(
config = PagingConfig(
pageSize = 100,
prefetchDistance = 150
),
pagingSourceFactory = {
database.newItemDao().selectAll(query)
},
).flow
.cachedIn(viewModelScope)
)
}
)
}
awaitAll(items, drawer)
}
}
}
fun refreshTimeline() {
_isRefreshing.value = true
_timelineState.update { it.copy(isRefreshing = true) }
viewModelScope.launch(dispatcher) {
repository?.synchronize(null) {
}
_isRefreshing.value = false
_timelineState.update { it.copy(isRefreshing = false) }
}
}
fun openDrawer() {
_drawerState.update { it.copy(isOpen = true) }
_timelineState.update { it.copy(isDrawerOpen = true) }
}
fun closeDrawer() {
_drawerState.update { it.copy(isOpen = false) }
_timelineState.update { it.copy(isDrawerOpen = false) }
}
fun updateDrawerDefaultItem(selection: DrawerDefaultItemsSelection) {
_drawerState.update {
fun updateDrawerDefaultItem(selection: FilterType) {
_timelineState.update {
it.copy(
isOpen = false,
selection = selection,
selectedFolderId = 0,
selectedFeedId = 0,
filters = updateFilters {
it.filters.copy(
filterType = selection
)
},
isDrawerOpen = false
)
}
}
fun updateDrawerFolderSelection(folderId: Int) {
_drawerState.update {
_timelineState.update {
it.copy(
isOpen = false,
selectedFolderId = folderId,
selectedFeedId = 0
filters = updateFilters {
it.filters.copy(
filterType = FilterType.FOLDER_FILER,
filterFolderId = folderId,
filterFeedId = 0
)
},
isDrawerOpen = false
)
}
}
fun updateDrawerFeedSelection(feedId: Int) {
_drawerState.update {
_timelineState.update {
it.copy(
isOpen = false,
selectedFeedId = feedId,
selectedFolderId = 0
filters = updateFilters {
it.filters.copy(
filterType = FilterType.FEED_FILTER,
filterFeedId = feedId,
filterFolderId = 0
)
},
isDrawerOpen = false
)
}
}
}
sealed class TimelineState {
object Loading : TimelineState()
private fun updateFilters(block: () -> QueryFilters): QueryFilters {
val filter = block()
filters.update { filter }
@Immutable
data class Error(val exception: Exception) : TimelineState()
@Immutable
data class Loaded(val items: List<ItemWithFeed>) : TimelineState()
return filter
}
}
@Immutable
data class DrawerState(
val isOpen: Boolean = false,
val selection: DrawerDefaultItemsSelection = DrawerDefaultItemsSelection.ARTICLES,
val selectedFolderId: Int = 0,
val selectedFeedId: Int = 0,
val foldersAndFeeds: Map<Folder?, List<Feed>> = emptyMap()
data class TimelineState(
val isRefreshing: Boolean = false,
val isDrawerOpen: Boolean = false,
val filters: QueryFilters = QueryFilters(),
val foldersAndFeeds: Map<Folder?, List<Feed>> = emptyMap(),
val items: ItemState = ItemState.Loading
)
sealed class ItemState {
@Immutable
object Loading : ItemState()
@Immutable
data class Error(val exception: Exception) : ItemState()
@Immutable
data class Loaded(val items: Flow<PagingData<ItemWithFeed>>) : ItemState()
}

View File

@ -23,20 +23,14 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.readrops.app.compose.R
import com.readrops.app.compose.timelime.DrawerState
import com.readrops.app.compose.timelime.TimelineState
import com.readrops.app.compose.util.theme.spacing
enum class DrawerDefaultItemsSelection {
ARTICLES,
NEW,
FAVORITES,
READ_LATER
}
import com.readrops.db.filters.FilterType
@Composable
fun TimelineDrawer(
state: DrawerState,
onClickDefaultItem: (DrawerDefaultItemsSelection) -> Unit,
state: TimelineState,
onClickDefaultItem: (FilterType) -> Unit,
onFolderClick: (Int) -> Unit,
onFeedClick: (Int) -> Unit,
) {
@ -50,7 +44,7 @@ fun TimelineDrawer(
Spacer(modifier = Modifier.size(MaterialTheme.spacing.drawerSpacing))
DrawerDefaultItems(
selectedItem = state.selection,
selectedItem = state.filters.filterType,
onClick = { onClickDefaultItem(it) }
)
@ -78,10 +72,10 @@ fun TimelineDrawer(
badge = {
Text(folderEntry.value.sumOf { it.unreadCount }.toString())
},
selected = state.selectedFolderId == folder.id,
selected = state.filters.filterFolderId == folder.id,
onClick = { onFolderClick(folder.id) },
feeds = folderEntry.value,
selectedFeed = state.selectedFeedId,
selectedFeed = state.filters.filterFeedId,
onFeedClick = { onFeedClick(it) },
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
@ -106,7 +100,7 @@ fun TimelineDrawer(
)
},
badge = { Text(feed.unreadCount.toString()) },
selected = feed.id == state.selectedFeedId,
selected = feed.id == state.filters.filterFeedId,
onClick = { onFeedClick(feed.id) },
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
@ -119,8 +113,8 @@ fun TimelineDrawer(
@Composable
fun DrawerDefaultItems(
selectedItem: DrawerDefaultItemsSelection,
onClick: (DrawerDefaultItemsSelection) -> Unit,
selectedItem: FilterType,
onClick: (FilterType) -> Unit,
) {
NavigationDrawerItem(
label = { Text("Articles") },
@ -130,8 +124,8 @@ fun DrawerDefaultItems(
contentDescription = null
)
},
selected = selectedItem == DrawerDefaultItemsSelection.ARTICLES,
onClick = { onClick(DrawerDefaultItemsSelection.ARTICLES) },
selected = selectedItem == FilterType.NO_FILTER,
onClick = { onClick(FilterType.NO_FILTER) },
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
@ -143,8 +137,8 @@ fun DrawerDefaultItems(
contentDescription = null
)
},
selected = selectedItem == DrawerDefaultItemsSelection.NEW,
onClick = { onClick(DrawerDefaultItemsSelection.NEW) },
selected = selectedItem == FilterType.NEW,
onClick = { onClick(FilterType.NEW) },
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
@ -156,8 +150,8 @@ fun DrawerDefaultItems(
contentDescription = null
)
},
selected = selectedItem == DrawerDefaultItemsSelection.FAVORITES,
onClick = { onClick(DrawerDefaultItemsSelection.FAVORITES) },
selected = selectedItem == FilterType.STARS_FILTER,
onClick = { onClick(FilterType.STARS_FILTER) },
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
@ -169,8 +163,8 @@ fun DrawerDefaultItems(
contentDescription = null
)
},
selected = selectedItem == DrawerDefaultItemsSelection.READ_LATER,
onClick = { onClick(DrawerDefaultItemsSelection.READ_LATER) },
selected = selectedItem == FilterType.READ_IT_LATER_FILTER,
onClick = { onClick(FilterType.READ_IT_LATER_FILTER) },
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
}

View File

@ -82,6 +82,7 @@ dependencies {
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-rxjava2:$room_version"
androidTestImplementation "androidx.room:room-testing:$room_version"
implementation "androidx.room:room-paging:$room_version"
implementation 'com.github.MatrixDev.Roomigrant:RoomigrantLib:0.3.4'
kapt 'com.github.MatrixDev.Roomigrant:RoomigrantCompiler:0.3.4'
@ -98,4 +99,7 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'
api "androidx.paging:paging-runtime:3.1.1"
api "androidx.paging:paging-compose:1.0.0-alpha18"
}

View File

@ -18,7 +18,7 @@ import io.reactivex.Completable
interface ItemDao : BaseDao<Item> {
@RawQuery(observedEntities = [Item::class, Folder::class, Feed::class, ItemState::class])
fun selectAll(query: SupportSQLiteQuery): DataSource.Factory<Int?, ItemWithFeed>
fun selectAll(query: SupportSQLiteQuery): DataSource.Factory<Int, ItemWithFeed>
@Query("Select * From Item Where id = :itemId")
fun select(itemId: Int): Item

View File

@ -1,5 +1,6 @@
package com.readrops.db.dao.newdao
import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Query
import androidx.room.RawQuery
@ -9,13 +10,12 @@ import com.readrops.db.entities.Folder
import com.readrops.db.entities.Item
import com.readrops.db.entities.ItemState
import com.readrops.db.pojo.ItemWithFeed
import kotlinx.coroutines.flow.Flow
@Dao
abstract class NewItemDao : NewBaseDao<Item> {
@RawQuery(observedEntities = [Item::class, Feed::class, Folder::class, ItemState::class])
abstract fun selectAll(query: SupportSQLiteQuery): Flow<List<ItemWithFeed>>
abstract fun selectAll(query: SupportSQLiteQuery): PagingSource<Int, ItemWithFeed>
@Query("Select count(*) From Item Where feed_id = :feedId And read = 0")
abstract fun selectUnreadCount(feedId: Int): Int

View File

@ -5,5 +5,6 @@ enum class FilterType {
FOLDER_FILER,
READ_IT_LATER_FILTER,
STARS_FILTER,
NO_FILTER
NO_FILTER,
NEW
}

View File

@ -81,11 +81,11 @@ object ItemsQueryBuilder {
}
class QueryFilters(
var showReadItems: Boolean = true,
var filterFeedId: Int = 0,
var filterFolderId: Int = 0,
var accountId: Int = 0,
var filterType: FilterType = FilterType.NO_FILTER,
var sortType: ListSortType = ListSortType.NEWEST_TO_OLDEST,
data class QueryFilters(
var showReadItems: Boolean = true,
var filterFeedId: Int = 0,
var filterFolderId: Int = 0,
var accountId: Int = 0,
var filterType: FilterType = FilterType.NO_FILTER,
var sortType: ListSortType = ListSortType.NEWEST_TO_OLDEST,
)