Optimize feed loading and chunk sync

This commit is contained in:
Ash 2022-06-05 21:05:13 +08:00
parent 1384012c44
commit 9a933ce486
7 changed files with 113 additions and 81 deletions

View File

@ -7,7 +7,10 @@ import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.StarOutline
import androidx.compose.material.icons.rounded.Subject
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.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import me.ash.reader.R
@ -40,9 +43,19 @@ class Filter(
}
}
@Stable
@Composable
fun Filter.getName(): String = when (this) {
Filter.Unread -> stringResource(R.string.unread)
Filter.Starred -> stringResource(R.string.starred)
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)
}

View File

@ -27,6 +27,7 @@ abstract class AbstractRssRepository constructor(
private val feedDao: FeedDao,
private val workManager: WorkManager,
private val dispatcherIO: CoroutineDispatcher,
private val dispatcherDefault: CoroutineDispatcher,
) {
abstract suspend fun updateArticleInfo(article: Article)
@ -113,11 +114,18 @@ abstract class AbstractRssRepository constructor(
else -> articleDao.queryImportantCountWhenIsAll(accountId)
}.mapLatest {
mapOf(
// Groups
*(it.groupBy { it.groupId }.map {
it.key to it.value.sumOf { it.important }
}.toTypedArray()),
// Feeds
*(it.map {
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? {

View File

@ -9,7 +9,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.async
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.ArticleDao
import me.ash.reader.data.dao.FeedDao
@ -35,14 +35,14 @@ class LocalRssRepository @Inject constructor(
private val notificationHelper: NotificationHelper,
private val accountDao: AccountDao,
private val groupDao: GroupDao,
@DispatcherDefault
private val dispatcherDefault: CoroutineDispatcher,
@DispatcherIO
private val dispatcherIO: CoroutineDispatcher,
@DispatcherDefault
private val dispatcherDefault: CoroutineDispatcher,
workManager: WorkManager,
) : AbstractRssRepository(
context, accountDao, articleDao, groupDao,
feedDao, workManager, dispatcherIO
feedDao, workManager, dispatcherIO, dispatcherDefault
) {
override suspend fun updateArticleInfo(article: Article) {
@ -71,25 +71,29 @@ class LocalRssRepository @Inject constructor(
}
override suspend fun sync(coroutineWorker: CoroutineWorker): ListenableWorker.Result {
return withContext(dispatcherDefault) {
return supervisorScope {
val preTime = System.currentTimeMillis()
val accountId = context.currentAccountId
feedDao.queryAll(accountId)
.also { coroutineWorker.setProgress(setIsSyncing(true)) }
.map { feed -> async { syncFeed(feed) } }
.awaitAll()
.chunked(16)
.forEach {
if (it.isNotify) {
notificationHelper.notify(
FeedWithArticle(
it.feedWithArticle.feed,
it.map { feed -> async { syncFeed(feed) } }
.awaitAll()
.forEach {
if (it.isNotify) {
notificationHelper.notify(
FeedWithArticle(
it.feedWithArticle.feed,
articleDao.insertListIfNotExist(it.feedWithArticle.articles)
)
)
} else {
articleDao.insertListIfNotExist(it.feedWithArticle.articles)
)
)
} else {
articleDao.insertListIfNotExist(it.feedWithArticle.articles)
}
}
}
}
Log.i("RlOG", "onCompletion: ${System.currentTimeMillis() - preTime}")
accountDao.queryById(accountId)?.let { account ->
accountDao.update(

View File

@ -80,10 +80,12 @@ class RssHelper @Inject constructor(
feed: Feed,
latestLink: String? = null,
): List<Article> {
return withContext(dispatcherIO) {
val accountId = context.currentAccountId
val syndFeed = SyndFeedInput().build(XmlReader(inputStream(okHttpClient, feed.url)))
syndFeed.entries.asSequence()
val accountId = context.currentAccountId
return inputStream(okHttpClient, feed.url).use {
SyndFeedInput().apply { isPreserveWireFeed = true }
.build(XmlReader(it))
.entries
.asSequence()
.takeWhile { latestLink == null || latestLink != it.link }
.map { article(feed, accountId, it) }
.toList()

View File

@ -6,6 +6,8 @@ import androidx.hilt.work.HiltWorker
import androidx.work.*
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.*
import java.util.concurrent.TimeUnit
@ -18,7 +20,9 @@ class SyncWorker @AssistedInject constructor(
override suspend fun doWork(): Result {
Log.i("RLog", "doWork: ")
return rssRepository.get().sync(this)
return withContext(Dispatchers.Default) {
rssRepository.get().sync(this@SyncWorker)
}
}
companion object {

View File

@ -65,6 +65,10 @@ fun FeedsPage(
val feedsUiState = feedsViewModel.feedsUiState.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 skipVersion = LocalSkipVersionNumber.current
@ -107,9 +111,9 @@ fun FeedsPage(
}
}
val groupsVisible = remember(feedsUiState.groupWithFeedList) {
val groupsVisible = remember(groupWithFeedList) {
mutableStateMapOf(
*(feedsUiState.groupWithFeedList.filterIsInstance<GroupFeedsView.Group>().map {
*(groupWithFeedList.filterIsInstance<GroupFeedsView.Group>().map {
it.group.id to groupListExpand.value
}.toTypedArray())
)
@ -121,7 +125,7 @@ fun FeedsPage(
LaunchedEffect(filterUiState) {
snapshotFlow { filterUiState }.collect {
feedsViewModel.fetchData(it)
feedsViewModel.pullFeeds(it)
}
}
@ -180,7 +184,7 @@ fun FeedsPage(
item {
Banner(
title = filterUiState.filter.getName(),
desc = feedsUiState.importantSum.ifEmpty { stringResource(R.string.loading) },
desc = importantSum,
icon = filterUiState.filter.iconOutline,
action = {
Icon(
@ -207,7 +211,7 @@ fun FeedsPage(
)
Spacer(modifier = Modifier.height(8.dp))
}
itemsIndexed(feedsUiState.groupWithFeedList) { index, groupWithFeed ->
itemsIndexed(groupWithFeedList) { index, groupWithFeed ->
when (groupWithFeed) {
is GroupFeedsView.Group -> {
if (index != 0) {
@ -218,7 +222,7 @@ fun FeedsPage(
group = groupWithFeed.group,
alpha = groupAlpha,
indicatorAlpha = groupIndicatorAlpha,
isEnded = { index == feedsUiState.groupWithFeedList.lastIndex },
isEnded = { index == groupWithFeedList.lastIndex },
onExpanded = {
groupsVisible[groupWithFeed.group.id] =
!(groupsVisible[groupWithFeed.group.id] ?: false)
@ -239,7 +243,7 @@ fun FeedsPage(
feed = groupWithFeed.feed,
alpha = groupAlpha,
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 },
) {
filterChange(

View File

@ -2,7 +2,6 @@ package me.ash.reader.ui.page.home.feeds
import android.util.Log
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.ui.util.fastForEach
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
@ -54,52 +53,52 @@ class FeedsViewModel @Inject constructor(
}
}
fun fetchData(filterState: FilterState) {
viewModelScope.launch(dispatcherIO) {
pullFeeds(
isStarred = filterState.filter.isStarred(),
isUnread = filterState.filter.isUnread(),
)
}
}
private suspend fun pullFeeds(isStarred: Boolean, isUnread: Boolean) {
combine(
rssRepository.get().pullFeeds(),
rssRepository.get().pullImportant(isStarred, isUnread),
) { 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,
fun pullFeeds(filterState: FilterState) {
val isStarred = filterState.filter.isStarred()
val isUnread = filterState.filter.isUnread()
_feedsUiState.update {
it.copy(
importantSum = rssRepository.get().pullImportant(isStarred, isUnread)
.mapLatest {
(it["sum"] ?: 0).run {
stringsRepository.getQuantityString(
when {
isStarred -> R.plurals.starred_desc
isUnread -> R.plurals.unread_desc
else -> R.plurals.all_desc
},
this,
this
)
}
},
groupWithFeedList = groupWithFeedList.map {
}.flowOn(dispatcherDefault),
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 {
addAll(
it.feeds.map {
@ -107,19 +106,17 @@ class FeedsViewModel @Inject constructor(
}
)
}
}.flatten(),
)
}
}.catch {
Log.e("RLog", "catch in articleRepository.pullFeeds(): ${it.message}")
}.flowOn(dispatcherDefault).collect()
}.flatten()
}.flowOn(dispatcherDefault),
)
}
}
}
data class FeedsUiState(
val account: Account? = null,
val importantSum: String = "",
val groupWithFeedList: List<GroupFeedsView> = emptyList(),
val importantSum: Flow<String> = emptyFlow(),
val groupWithFeedList: Flow<List<GroupFeedsView>> = emptyFlow(),
val listState: LazyListState = LazyListState(),
val groupsVisible: Boolean = true,
)