Optimize feed loading and chunk sync
This commit is contained in:
parent
1384012c44
commit
9a933ce486
|
@ -7,7 +7,10 @@ import androidx.compose.material.icons.rounded.Star
|
||||||
import androidx.compose.material.icons.rounded.StarOutline
|
import androidx.compose.material.icons.rounded.StarOutline
|
||||||
import androidx.compose.material.icons.rounded.Subject
|
import androidx.compose.material.icons.rounded.Subject
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
|
|
||||||
|
@ -40,9 +43,19 @@ class Filter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Stable
|
||||||
@Composable
|
@Composable
|
||||||
fun Filter.getName(): String = when (this) {
|
fun Filter.getName(): String = when (this) {
|
||||||
Filter.Unread -> stringResource(R.string.unread)
|
Filter.Unread -> stringResource(R.string.unread)
|
||||||
Filter.Starred -> stringResource(R.string.starred)
|
Filter.Starred -> stringResource(R.string.starred)
|
||||||
else -> stringResource(R.string.all)
|
else -> stringResource(R.string.all)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
|
@Stable
|
||||||
|
@Composable
|
||||||
|
fun Filter.getDesc(important: Int): String = when (this) {
|
||||||
|
Filter.Starred -> pluralStringResource(R.plurals.starred_desc, important, important)
|
||||||
|
Filter.Unread -> pluralStringResource(R.plurals.unread_desc, important, important)
|
||||||
|
else -> pluralStringResource(R.plurals.all_desc, important, important)
|
||||||
|
}
|
|
@ -27,6 +27,7 @@ abstract class AbstractRssRepository constructor(
|
||||||
private val feedDao: FeedDao,
|
private val feedDao: FeedDao,
|
||||||
private val workManager: WorkManager,
|
private val workManager: WorkManager,
|
||||||
private val dispatcherIO: CoroutineDispatcher,
|
private val dispatcherIO: CoroutineDispatcher,
|
||||||
|
private val dispatcherDefault: CoroutineDispatcher,
|
||||||
) {
|
) {
|
||||||
abstract suspend fun updateArticleInfo(article: Article)
|
abstract suspend fun updateArticleInfo(article: Article)
|
||||||
|
|
||||||
|
@ -113,11 +114,18 @@ abstract class AbstractRssRepository constructor(
|
||||||
else -> articleDao.queryImportantCountWhenIsAll(accountId)
|
else -> articleDao.queryImportantCountWhenIsAll(accountId)
|
||||||
}.mapLatest {
|
}.mapLatest {
|
||||||
mapOf(
|
mapOf(
|
||||||
|
// Groups
|
||||||
|
*(it.groupBy { it.groupId }.map {
|
||||||
|
it.key to it.value.sumOf { it.important }
|
||||||
|
}.toTypedArray()),
|
||||||
|
// Feeds
|
||||||
*(it.map {
|
*(it.map {
|
||||||
it.feedId to it.important
|
it.feedId to it.important
|
||||||
}.toTypedArray())
|
}.toTypedArray()),
|
||||||
|
// All summary
|
||||||
|
"sum" to it.sumOf { it.important }
|
||||||
)
|
)
|
||||||
}.flowOn(dispatcherIO)
|
}.flowOn(dispatcherDefault)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun findFeedById(id: String): Feed? {
|
suspend fun findFeedById(id: String): Feed? {
|
||||||
|
|
|
@ -9,7 +9,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.supervisorScope
|
||||||
import me.ash.reader.data.dao.AccountDao
|
import me.ash.reader.data.dao.AccountDao
|
||||||
import me.ash.reader.data.dao.ArticleDao
|
import me.ash.reader.data.dao.ArticleDao
|
||||||
import me.ash.reader.data.dao.FeedDao
|
import me.ash.reader.data.dao.FeedDao
|
||||||
|
@ -35,14 +35,14 @@ class LocalRssRepository @Inject constructor(
|
||||||
private val notificationHelper: NotificationHelper,
|
private val notificationHelper: NotificationHelper,
|
||||||
private val accountDao: AccountDao,
|
private val accountDao: AccountDao,
|
||||||
private val groupDao: GroupDao,
|
private val groupDao: GroupDao,
|
||||||
@DispatcherDefault
|
|
||||||
private val dispatcherDefault: CoroutineDispatcher,
|
|
||||||
@DispatcherIO
|
@DispatcherIO
|
||||||
private val dispatcherIO: CoroutineDispatcher,
|
private val dispatcherIO: CoroutineDispatcher,
|
||||||
|
@DispatcherDefault
|
||||||
|
private val dispatcherDefault: CoroutineDispatcher,
|
||||||
workManager: WorkManager,
|
workManager: WorkManager,
|
||||||
) : AbstractRssRepository(
|
) : AbstractRssRepository(
|
||||||
context, accountDao, articleDao, groupDao,
|
context, accountDao, articleDao, groupDao,
|
||||||
feedDao, workManager, dispatcherIO
|
feedDao, workManager, dispatcherIO, dispatcherDefault
|
||||||
) {
|
) {
|
||||||
|
|
||||||
override suspend fun updateArticleInfo(article: Article) {
|
override suspend fun updateArticleInfo(article: Article) {
|
||||||
|
@ -71,25 +71,29 @@ class LocalRssRepository @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun sync(coroutineWorker: CoroutineWorker): ListenableWorker.Result {
|
override suspend fun sync(coroutineWorker: CoroutineWorker): ListenableWorker.Result {
|
||||||
return withContext(dispatcherDefault) {
|
return supervisorScope {
|
||||||
val preTime = System.currentTimeMillis()
|
val preTime = System.currentTimeMillis()
|
||||||
val accountId = context.currentAccountId
|
val accountId = context.currentAccountId
|
||||||
feedDao.queryAll(accountId)
|
feedDao.queryAll(accountId)
|
||||||
.also { coroutineWorker.setProgress(setIsSyncing(true)) }
|
.also { coroutineWorker.setProgress(setIsSyncing(true)) }
|
||||||
.map { feed -> async { syncFeed(feed) } }
|
.chunked(16)
|
||||||
.awaitAll()
|
|
||||||
.forEach {
|
.forEach {
|
||||||
if (it.isNotify) {
|
it.map { feed -> async { syncFeed(feed) } }
|
||||||
notificationHelper.notify(
|
.awaitAll()
|
||||||
FeedWithArticle(
|
.forEach {
|
||||||
it.feedWithArticle.feed,
|
if (it.isNotify) {
|
||||||
|
notificationHelper.notify(
|
||||||
|
FeedWithArticle(
|
||||||
|
it.feedWithArticle.feed,
|
||||||
|
articleDao.insertListIfNotExist(it.feedWithArticle.articles)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
articleDao.insertListIfNotExist(it.feedWithArticle.articles)
|
articleDao.insertListIfNotExist(it.feedWithArticle.articles)
|
||||||
)
|
}
|
||||||
)
|
}
|
||||||
} else {
|
|
||||||
articleDao.insertListIfNotExist(it.feedWithArticle.articles)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.i("RlOG", "onCompletion: ${System.currentTimeMillis() - preTime}")
|
Log.i("RlOG", "onCompletion: ${System.currentTimeMillis() - preTime}")
|
||||||
accountDao.queryById(accountId)?.let { account ->
|
accountDao.queryById(accountId)?.let { account ->
|
||||||
accountDao.update(
|
accountDao.update(
|
||||||
|
|
|
@ -80,10 +80,12 @@ class RssHelper @Inject constructor(
|
||||||
feed: Feed,
|
feed: Feed,
|
||||||
latestLink: String? = null,
|
latestLink: String? = null,
|
||||||
): List<Article> {
|
): List<Article> {
|
||||||
return withContext(dispatcherIO) {
|
val accountId = context.currentAccountId
|
||||||
val accountId = context.currentAccountId
|
return inputStream(okHttpClient, feed.url).use {
|
||||||
val syndFeed = SyndFeedInput().build(XmlReader(inputStream(okHttpClient, feed.url)))
|
SyndFeedInput().apply { isPreserveWireFeed = true }
|
||||||
syndFeed.entries.asSequence()
|
.build(XmlReader(it))
|
||||||
|
.entries
|
||||||
|
.asSequence()
|
||||||
.takeWhile { latestLink == null || latestLink != it.link }
|
.takeWhile { latestLink == null || latestLink != it.link }
|
||||||
.map { article(feed, accountId, it) }
|
.map { article(feed, accountId, it) }
|
||||||
.toList()
|
.toList()
|
||||||
|
|
|
@ -6,6 +6,8 @@ import androidx.hilt.work.HiltWorker
|
||||||
import androidx.work.*
|
import androidx.work.*
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
@ -18,7 +20,9 @@ class SyncWorker @AssistedInject constructor(
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
Log.i("RLog", "doWork: ")
|
Log.i("RLog", "doWork: ")
|
||||||
return rssRepository.get().sync(this)
|
return withContext(Dispatchers.Default) {
|
||||||
|
rssRepository.get().sync(this@SyncWorker)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -65,6 +65,10 @@ fun FeedsPage(
|
||||||
|
|
||||||
val feedsUiState = feedsViewModel.feedsUiState.collectAsStateValue()
|
val feedsUiState = feedsViewModel.feedsUiState.collectAsStateValue()
|
||||||
val filterUiState = homeViewModel.filterUiState.collectAsStateValue()
|
val filterUiState = homeViewModel.filterUiState.collectAsStateValue()
|
||||||
|
val importantSum =
|
||||||
|
feedsUiState.importantSum.collectAsStateValue(initial = stringResource(R.string.loading))
|
||||||
|
val groupWithFeedList =
|
||||||
|
feedsUiState.groupWithFeedList.collectAsStateValue(initial = emptyList())
|
||||||
|
|
||||||
val newVersion = LocalNewVersionNumber.current
|
val newVersion = LocalNewVersionNumber.current
|
||||||
val skipVersion = LocalSkipVersionNumber.current
|
val skipVersion = LocalSkipVersionNumber.current
|
||||||
|
@ -107,9 +111,9 @@ fun FeedsPage(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val groupsVisible = remember(feedsUiState.groupWithFeedList) {
|
val groupsVisible = remember(groupWithFeedList) {
|
||||||
mutableStateMapOf(
|
mutableStateMapOf(
|
||||||
*(feedsUiState.groupWithFeedList.filterIsInstance<GroupFeedsView.Group>().map {
|
*(groupWithFeedList.filterIsInstance<GroupFeedsView.Group>().map {
|
||||||
it.group.id to groupListExpand.value
|
it.group.id to groupListExpand.value
|
||||||
}.toTypedArray())
|
}.toTypedArray())
|
||||||
)
|
)
|
||||||
|
@ -121,7 +125,7 @@ fun FeedsPage(
|
||||||
|
|
||||||
LaunchedEffect(filterUiState) {
|
LaunchedEffect(filterUiState) {
|
||||||
snapshotFlow { filterUiState }.collect {
|
snapshotFlow { filterUiState }.collect {
|
||||||
feedsViewModel.fetchData(it)
|
feedsViewModel.pullFeeds(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,7 +184,7 @@ fun FeedsPage(
|
||||||
item {
|
item {
|
||||||
Banner(
|
Banner(
|
||||||
title = filterUiState.filter.getName(),
|
title = filterUiState.filter.getName(),
|
||||||
desc = feedsUiState.importantSum.ifEmpty { stringResource(R.string.loading) },
|
desc = importantSum,
|
||||||
icon = filterUiState.filter.iconOutline,
|
icon = filterUiState.filter.iconOutline,
|
||||||
action = {
|
action = {
|
||||||
Icon(
|
Icon(
|
||||||
|
@ -207,7 +211,7 @@ fun FeedsPage(
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
itemsIndexed(feedsUiState.groupWithFeedList) { index, groupWithFeed ->
|
itemsIndexed(groupWithFeedList) { index, groupWithFeed ->
|
||||||
when (groupWithFeed) {
|
when (groupWithFeed) {
|
||||||
is GroupFeedsView.Group -> {
|
is GroupFeedsView.Group -> {
|
||||||
if (index != 0) {
|
if (index != 0) {
|
||||||
|
@ -218,7 +222,7 @@ fun FeedsPage(
|
||||||
group = groupWithFeed.group,
|
group = groupWithFeed.group,
|
||||||
alpha = groupAlpha,
|
alpha = groupAlpha,
|
||||||
indicatorAlpha = groupIndicatorAlpha,
|
indicatorAlpha = groupIndicatorAlpha,
|
||||||
isEnded = { index == feedsUiState.groupWithFeedList.lastIndex },
|
isEnded = { index == groupWithFeedList.lastIndex },
|
||||||
onExpanded = {
|
onExpanded = {
|
||||||
groupsVisible[groupWithFeed.group.id] =
|
groupsVisible[groupWithFeed.group.id] =
|
||||||
!(groupsVisible[groupWithFeed.group.id] ?: false)
|
!(groupsVisible[groupWithFeed.group.id] ?: false)
|
||||||
|
@ -239,7 +243,7 @@ fun FeedsPage(
|
||||||
feed = groupWithFeed.feed,
|
feed = groupWithFeed.feed,
|
||||||
alpha = groupAlpha,
|
alpha = groupAlpha,
|
||||||
badgeAlpha = feedBadgeAlpha,
|
badgeAlpha = feedBadgeAlpha,
|
||||||
isEnded = { index == feedsUiState.groupWithFeedList.lastIndex || feedsUiState.groupWithFeedList[index + 1] is GroupFeedsView.Group },
|
isEnded = { index == groupWithFeedList.lastIndex || groupWithFeedList[index + 1] is GroupFeedsView.Group },
|
||||||
isExpanded = { groupsVisible[groupWithFeed.feed.groupId] ?: false },
|
isExpanded = { groupsVisible[groupWithFeed.feed.groupId] ?: false },
|
||||||
) {
|
) {
|
||||||
filterChange(
|
filterChange(
|
||||||
|
|
|
@ -2,7 +2,6 @@ package me.ash.reader.ui.page.home.feeds
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.ui.util.fastForEach
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
@ -54,52 +53,52 @@ class FeedsViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fetchData(filterState: FilterState) {
|
fun pullFeeds(filterState: FilterState) {
|
||||||
viewModelScope.launch(dispatcherIO) {
|
val isStarred = filterState.filter.isStarred()
|
||||||
pullFeeds(
|
val isUnread = filterState.filter.isUnread()
|
||||||
isStarred = filterState.filter.isStarred(),
|
_feedsUiState.update {
|
||||||
isUnread = filterState.filter.isUnread(),
|
it.copy(
|
||||||
)
|
importantSum = rssRepository.get().pullImportant(isStarred, isUnread)
|
||||||
}
|
.mapLatest {
|
||||||
}
|
(it["sum"] ?: 0).run {
|
||||||
|
stringsRepository.getQuantityString(
|
||||||
private suspend fun pullFeeds(isStarred: Boolean, isUnread: Boolean) {
|
when {
|
||||||
combine(
|
isStarred -> R.plurals.starred_desc
|
||||||
rssRepository.get().pullFeeds(),
|
isUnread -> R.plurals.unread_desc
|
||||||
rssRepository.get().pullImportant(isStarred, isUnread),
|
else -> R.plurals.all_desc
|
||||||
) { groupWithFeedList, importantMap ->
|
},
|
||||||
groupWithFeedList.fastForEach {
|
|
||||||
var groupImportant = 0
|
|
||||||
it.feeds.fastForEach {
|
|
||||||
it.important = importantMap[it.id]
|
|
||||||
groupImportant += it.important ?: 0
|
|
||||||
}
|
|
||||||
it.group.important = groupImportant
|
|
||||||
}
|
|
||||||
groupWithFeedList
|
|
||||||
}.mapLatest { groupWithFeedList ->
|
|
||||||
_feedsUiState.update {
|
|
||||||
it.copy(
|
|
||||||
importantSum = groupWithFeedList.sumOf { it.group.important ?: 0 }.run {
|
|
||||||
when {
|
|
||||||
isStarred -> stringsRepository.getQuantityString(
|
|
||||||
R.plurals.starred_desc,
|
|
||||||
this,
|
|
||||||
this
|
|
||||||
)
|
|
||||||
isUnread -> stringsRepository.getQuantityString(
|
|
||||||
R.plurals.unread_desc,
|
|
||||||
this,
|
|
||||||
this
|
|
||||||
)
|
|
||||||
else -> stringsRepository.getQuantityString(
|
|
||||||
R.plurals.all_desc,
|
|
||||||
this,
|
this,
|
||||||
this
|
this
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
}.flowOn(dispatcherDefault),
|
||||||
groupWithFeedList = groupWithFeedList.map {
|
groupWithFeedList = combine(
|
||||||
|
rssRepository.get().pullImportant(isStarred, isUnread),
|
||||||
|
rssRepository.get().pullFeeds()
|
||||||
|
) { importantMap, groupWithFeedList ->
|
||||||
|
val groupIterator = groupWithFeedList.iterator()
|
||||||
|
while (groupIterator.hasNext()) {
|
||||||
|
val groupWithFeed = groupIterator.next()
|
||||||
|
val groupImportant = importantMap[groupWithFeed.group.id] ?: 0
|
||||||
|
if ((isStarred || isUnread) && groupImportant == 0) {
|
||||||
|
groupIterator.remove()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
groupWithFeed.group.important = groupImportant
|
||||||
|
val feedIterator = groupWithFeed.feeds.iterator()
|
||||||
|
while (feedIterator.hasNext()) {
|
||||||
|
val feed = feedIterator.next()
|
||||||
|
val feedImportant = importantMap[feed.id] ?: 0
|
||||||
|
if ((isStarred || isUnread) && feedImportant == 0) {
|
||||||
|
feedIterator.remove()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
feed.important = feedImportant
|
||||||
|
}
|
||||||
|
}
|
||||||
|
groupWithFeedList
|
||||||
|
}.mapLatest { groupWithFeedList ->
|
||||||
|
groupWithFeedList.map {
|
||||||
mutableListOf<GroupFeedsView>(GroupFeedsView.Group(it.group)).apply {
|
mutableListOf<GroupFeedsView>(GroupFeedsView.Group(it.group)).apply {
|
||||||
addAll(
|
addAll(
|
||||||
it.feeds.map {
|
it.feeds.map {
|
||||||
|
@ -107,19 +106,17 @@ class FeedsViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}.flatten(),
|
}.flatten()
|
||||||
)
|
}.flowOn(dispatcherDefault),
|
||||||
}
|
)
|
||||||
}.catch {
|
}
|
||||||
Log.e("RLog", "catch in articleRepository.pullFeeds(): ${it.message}")
|
|
||||||
}.flowOn(dispatcherDefault).collect()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class FeedsUiState(
|
data class FeedsUiState(
|
||||||
val account: Account? = null,
|
val account: Account? = null,
|
||||||
val importantSum: String = "",
|
val importantSum: Flow<String> = emptyFlow(),
|
||||||
val groupWithFeedList: List<GroupFeedsView> = emptyList(),
|
val groupWithFeedList: Flow<List<GroupFeedsView>> = emptyFlow(),
|
||||||
val listState: LazyListState = LazyListState(),
|
val listState: LazyListState = LazyListState(),
|
||||||
val groupsVisible: Boolean = true,
|
val groupsVisible: Boolean = true,
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user