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.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)
}

View File

@ -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? {

View File

@ -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,12 +71,14 @@ 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)
.forEach {
it.map { feed -> async { syncFeed(feed) } }
.awaitAll() .awaitAll()
.forEach { .forEach {
if (it.isNotify) { if (it.isNotify) {
@ -90,6 +92,8 @@ class LocalRssRepository @Inject constructor(
articleDao.insertListIfNotExist(it.feedWithArticle.articles) 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(

View File

@ -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
val syndFeed = SyndFeedInput().build(XmlReader(inputStream(okHttpClient, feed.url))) return inputStream(okHttpClient, feed.url).use {
syndFeed.entries.asSequence() SyndFeedInput().apply { isPreserveWireFeed = true }
.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()

View File

@ -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 {

View File

@ -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(

View File

@ -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(
when {
isStarred -> R.plurals.starred_desc
isUnread -> R.plurals.unread_desc
else -> R.plurals.all_desc
},
this,
this
) )
} }
} }.flowOn(dispatcherDefault),
groupWithFeedList = combine(
private suspend fun pullFeeds(isStarred: Boolean, isUnread: Boolean) {
combine(
rssRepository.get().pullFeeds(),
rssRepository.get().pullImportant(isStarred, isUnread), rssRepository.get().pullImportant(isStarred, isUnread),
) { groupWithFeedList, importantMap -> rssRepository.get().pullFeeds()
groupWithFeedList.fastForEach { ) { importantMap, groupWithFeedList ->
var groupImportant = 0 val groupIterator = groupWithFeedList.iterator()
it.feeds.fastForEach { while (groupIterator.hasNext()) {
it.important = importantMap[it.id] val groupWithFeed = groupIterator.next()
groupImportant += it.important ?: 0 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
} }
it.group.important = groupImportant
} }
groupWithFeedList groupWithFeedList
}.mapLatest { groupWithFeedList -> }.mapLatest { groupWithFeedList ->
_feedsUiState.update { groupWithFeedList.map {
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
)
}
},
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,
) )