Implement paging
This is currently breaking swipe to dismiss so it can't stay like this but it's probably better performance-wise
This commit is contained in:
parent
d95b63aa49
commit
bbbbbeae0d
14 changed files with 178 additions and 62 deletions
|
@ -19,5 +19,5 @@ suspend fun syncAll(
|
||||||
iconRepository.getFeedIcon(it.feed.id)
|
iconRepository.getFeedIcon(it.feed.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entryRepository.getAll(true)
|
entryRepository.fetch()
|
||||||
}
|
}
|
|
@ -2,12 +2,14 @@ package com.wbrawner.nanoflux.data.viewmodel
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.paging.Pager
|
||||||
|
import androidx.paging.PagingConfig
|
||||||
|
import androidx.paging.cachedIn
|
||||||
|
import com.wbrawner.nanoflux.network.EntryAndFeedPagingSource
|
||||||
import com.wbrawner.nanoflux.network.repository.*
|
import com.wbrawner.nanoflux.network.repository.*
|
||||||
import com.wbrawner.nanoflux.storage.model.Entry
|
import com.wbrawner.nanoflux.storage.model.Entry
|
||||||
import com.wbrawner.nanoflux.storage.model.EntryAndFeed
|
|
||||||
import com.wbrawner.nanoflux.syncAll
|
import com.wbrawner.nanoflux.syncAll
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -24,7 +26,12 @@ abstract class EntryListViewModel(
|
||||||
val loading = _loading.asStateFlow()
|
val loading = _loading.asStateFlow()
|
||||||
private val _errorMessage = MutableStateFlow<String?>(null)
|
private val _errorMessage = MutableStateFlow<String?>(null)
|
||||||
val errorMessage = _errorMessage.asStateFlow()
|
val errorMessage = _errorMessage.asStateFlow()
|
||||||
abstract val entries: Flow<List<EntryAndFeed>>
|
protected open val entryStatus: EntryStatus? = null
|
||||||
|
private val pagingSource: EntryAndFeedPagingSource
|
||||||
|
get() = EntryAndFeedPagingSource(entryRepository, entryStatus)
|
||||||
|
val entries = Pager(PagingConfig(pageSize = 15)) {
|
||||||
|
pagingSource
|
||||||
|
}.flow.cachedIn(viewModelScope)
|
||||||
|
|
||||||
fun dismissError() {
|
fun dismissError() {
|
||||||
_errorMessage.value = null
|
_errorMessage.value = null
|
||||||
|
@ -39,9 +46,10 @@ abstract class EntryListViewModel(
|
||||||
return entry.url
|
return entry.url
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refresh(status: EntryStatus? = null) = viewModelScope.launch(Dispatchers.IO) {
|
fun refresh() = viewModelScope.launch(Dispatchers.IO) {
|
||||||
_loading.emit(true)
|
_loading.emit(true)
|
||||||
syncAll(categoryRepository, feedRepository, iconRepository, entryRepository)
|
syncAll(categoryRepository, feedRepository, iconRepository, entryRepository)
|
||||||
|
pagingSource.invalidate()
|
||||||
_loading.emit(false)
|
_loading.emit(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,9 +59,11 @@ abstract class EntryListViewModel(
|
||||||
} else if (entry.status == Entry.Status.UNREAD) {
|
} else if (entry.status == Entry.Status.UNREAD) {
|
||||||
entryRepository.markEntryRead(entry)
|
entryRepository.markEntryRead(entry)
|
||||||
}
|
}
|
||||||
|
pagingSource.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleStar(entry: Entry) = viewModelScope.launch(Dispatchers.IO) {
|
fun toggleStar(entry: Entry) = viewModelScope.launch(Dispatchers.IO) {
|
||||||
entryRepository.toggleStar(entry)
|
entryRepository.toggleStar(entry)
|
||||||
|
pagingSource.invalidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -36,7 +36,7 @@ class EntryViewModel @Inject constructor(
|
||||||
_errorMessage.value = null
|
_errorMessage.value = null
|
||||||
try {
|
try {
|
||||||
var markedRead = false
|
var markedRead = false
|
||||||
entryRepository.getEntry(id).collect {
|
entryRepository.observeEntry(id).collect {
|
||||||
_entry.value = it
|
_entry.value = it
|
||||||
_loading.value = false
|
_loading.value = false
|
||||||
if (!markedRead) {
|
if (!markedRead) {
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
package com.wbrawner.nanoflux.data.viewmodel
|
package com.wbrawner.nanoflux.data.viewmodel
|
||||||
|
|
||||||
import com.wbrawner.nanoflux.network.repository.CategoryRepository
|
import com.wbrawner.nanoflux.network.repository.*
|
||||||
import com.wbrawner.nanoflux.network.repository.EntryRepository
|
|
||||||
import com.wbrawner.nanoflux.network.repository.FeedRepository
|
|
||||||
import com.wbrawner.nanoflux.network.repository.IconRepository
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -22,5 +19,5 @@ class HistoryViewModel @Inject constructor(
|
||||||
iconRepository,
|
iconRepository,
|
||||||
logger
|
logger
|
||||||
) {
|
) {
|
||||||
override val entries = entryRepository.observeRead()
|
override val entryStatus: EntryStatus = EntryStatus.HISTORY
|
||||||
}
|
}
|
|
@ -24,7 +24,7 @@ class MainViewModel @Inject constructor(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
if (entryRepository.getCount() == 0L) {
|
if (entryRepository.count() == 0L) {
|
||||||
syncAll(categoryRepository, feedRepository, iconRepository, entryRepository)
|
syncAll(categoryRepository, feedRepository, iconRepository, entryRepository)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
package com.wbrawner.nanoflux.data.viewmodel
|
package com.wbrawner.nanoflux.data.viewmodel
|
||||||
|
|
||||||
import com.wbrawner.nanoflux.network.repository.CategoryRepository
|
import com.wbrawner.nanoflux.network.repository.*
|
||||||
import com.wbrawner.nanoflux.network.repository.EntryRepository
|
|
||||||
import com.wbrawner.nanoflux.network.repository.FeedRepository
|
|
||||||
import com.wbrawner.nanoflux.network.repository.IconRepository
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -22,5 +19,5 @@ class StarredViewModel @Inject constructor(
|
||||||
iconRepository,
|
iconRepository,
|
||||||
logger
|
logger
|
||||||
) {
|
) {
|
||||||
override val entries = entryRepository.observeStarred()
|
override val entryStatus: EntryStatus = EntryStatus.STARRED
|
||||||
}
|
}
|
|
@ -1,9 +1,6 @@
|
||||||
package com.wbrawner.nanoflux.data.viewmodel
|
package com.wbrawner.nanoflux.data.viewmodel
|
||||||
|
|
||||||
import com.wbrawner.nanoflux.network.repository.CategoryRepository
|
import com.wbrawner.nanoflux.network.repository.*
|
||||||
import com.wbrawner.nanoflux.network.repository.EntryRepository
|
|
||||||
import com.wbrawner.nanoflux.network.repository.FeedRepository
|
|
||||||
import com.wbrawner.nanoflux.network.repository.IconRepository
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -22,5 +19,5 @@ class UnreadViewModel @Inject constructor(
|
||||||
iconRepository,
|
iconRepository,
|
||||||
logger
|
logger
|
||||||
) {
|
) {
|
||||||
override val entries = entryRepository.observeUnread()
|
override val entryStatus: EntryStatus = EntryStatus.UNREAD
|
||||||
}
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package com.wbrawner.nanoflux.ui
|
package com.wbrawner.nanoflux.ui
|
||||||
|
|
||||||
|
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
@ -7,8 +8,9 @@ import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.layout.Arrangement.spacedBy
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Email
|
import androidx.compose.material.icons.filled.Email
|
||||||
|
@ -27,6 +29,8 @@ import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.paging.compose.LazyPagingItems
|
||||||
|
import androidx.paging.compose.itemsIndexed
|
||||||
import com.google.accompanist.flowlayout.FlowCrossAxisAlignment
|
import com.google.accompanist.flowlayout.FlowCrossAxisAlignment
|
||||||
import com.google.accompanist.flowlayout.FlowRow
|
import com.google.accompanist.flowlayout.FlowRow
|
||||||
import com.google.accompanist.swiperefresh.SwipeRefresh
|
import com.google.accompanist.swiperefresh.SwipeRefresh
|
||||||
|
@ -44,7 +48,7 @@ import kotlin.math.roundToInt
|
||||||
)
|
)
|
||||||
@Composable
|
@Composable
|
||||||
fun EntryList(
|
fun EntryList(
|
||||||
entries: List<EntryAndFeed>,
|
entries: LazyPagingItems<EntryAndFeed>,
|
||||||
onEntryItemClicked: (entry: Entry) -> Unit,
|
onEntryItemClicked: (entry: Entry) -> Unit,
|
||||||
onFeedClicked: (feed: Feed) -> Unit,
|
onFeedClicked: (feed: Feed) -> Unit,
|
||||||
onCategoryClicked: (category: Category) -> Unit,
|
onCategoryClicked: (category: Category) -> Unit,
|
||||||
|
@ -61,10 +65,16 @@ fun EntryList(
|
||||||
) {
|
) {
|
||||||
LazyColumn {
|
LazyColumn {
|
||||||
itemsIndexed(entries, { _, entry -> entry.entry.id }) { index, entry ->
|
itemsIndexed(entries, { _, entry -> entry.entry.id }) { index, entry ->
|
||||||
|
if (entry == null) {
|
||||||
|
EntryListPlaceholder()
|
||||||
|
return@itemsIndexed
|
||||||
|
}
|
||||||
val dismissState = rememberDismissState()
|
val dismissState = rememberDismissState()
|
||||||
LaunchedEffect(key1 = dismissState.currentValue) {
|
LaunchedEffect(key1 = dismissState.currentValue, key2 = entry.entry.id) {
|
||||||
when (dismissState.currentValue) {
|
when (dismissState.currentValue) {
|
||||||
DismissValue.DismissedToStart -> onToggleReadClicked(entry.entry)
|
DismissValue.DismissedToStart -> {
|
||||||
|
onToggleReadClicked(entry.entry)
|
||||||
|
}
|
||||||
DismissValue.DismissedToEnd -> {
|
DismissValue.DismissedToEnd -> {
|
||||||
onStarClicked(entry.entry)
|
onStarClicked(entry.entry)
|
||||||
dismissState.reset()
|
dismissState.reset()
|
||||||
|
@ -113,7 +123,7 @@ fun EntryList(
|
||||||
onShareClicked = onShareClicked,
|
onShareClicked = onShareClicked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (index < entries.lastIndex) {
|
if (index < entries.itemCount - 1) {
|
||||||
Divider()
|
Divider()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -121,6 +131,40 @@ fun EntryList(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EntryListPlaceholder() {
|
||||||
|
Surface(color = MaterialTheme.colors.surface) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp),
|
||||||
|
verticalArrangement = spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(30.dp),
|
||||||
|
shape = RoundedCornerShape(4.dp),
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.2f)
|
||||||
|
) {}
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(30.dp),
|
||||||
|
shape = RoundedCornerShape(4.dp),
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.2f)
|
||||||
|
) {}
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(30.dp),
|
||||||
|
shape = RoundedCornerShape(4.dp),
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.2f)
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun EntryListItem(
|
fun EntryListItem(
|
||||||
entry: Entry,
|
entry: Entry,
|
||||||
|
@ -245,6 +289,7 @@ fun CategoryButton(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@Preview
|
@Preview
|
||||||
|
@Preview(uiMode = UI_MODE_NIGHT_YES)
|
||||||
fun EntryListItem_Preview() {
|
fun EntryListItem_Preview() {
|
||||||
NanofluxApp {
|
NanofluxApp {
|
||||||
EntryListItem(
|
EntryListItem(
|
||||||
|
@ -310,6 +355,15 @@ fun EntryListItem_Preview() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
@Preview(uiMode = UI_MODE_NIGHT_YES)
|
||||||
|
fun EntryListPlaceholder_Preview() {
|
||||||
|
NanofluxApp {
|
||||||
|
EntryListPlaceholder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private const val MILLIS_IN_SECOND = 1000.0
|
private const val MILLIS_IN_SECOND = 1000.0
|
||||||
private const val MILLIS_IN_MINUTE = 60000.0
|
private const val MILLIS_IN_MINUTE = 60000.0
|
||||||
private const val MILLIS_IN_HOUR = 3_600_000.0
|
private const val MILLIS_IN_HOUR = 3_600_000.0
|
||||||
|
|
|
@ -10,6 +10,7 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
import com.wbrawner.nanoflux.data.viewmodel.EntryListViewModel
|
import com.wbrawner.nanoflux.data.viewmodel.EntryListViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@ -21,7 +22,7 @@ fun EntryListScreen(
|
||||||
) {
|
) {
|
||||||
val loading by entryListViewModel.loading.collectAsState()
|
val loading by entryListViewModel.loading.collectAsState()
|
||||||
val errorMessage by entryListViewModel.errorMessage.collectAsState()
|
val errorMessage by entryListViewModel.errorMessage.collectAsState()
|
||||||
val entries by entryListViewModel.entries.collectAsState(emptyList())
|
val entries = entryListViewModel.entries.collectAsLazyPagingItems()
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
errorMessage?.let {
|
errorMessage?.let {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
|
|
|
@ -13,7 +13,9 @@ ktor = "2.1.0"
|
||||||
material = "1.3.0"
|
material = "1.3.0"
|
||||||
maxSdk = "33"
|
maxSdk = "33"
|
||||||
minSdk = "21"
|
minSdk = "21"
|
||||||
|
paging-compose = "1.0.0-alpha17"
|
||||||
room = "2.4.3"
|
room = "2.4.3"
|
||||||
|
room-paging = "2.5.0-beta02"
|
||||||
versionCode = "1"
|
versionCode = "1"
|
||||||
versionName = "1.0"
|
versionName = "1.0"
|
||||||
work = "2.7.1"
|
work = "2.7.1"
|
||||||
|
@ -24,6 +26,7 @@ accompanist-swiperefresh = { module = "com.google.accompanist:accompanist-swiper
|
||||||
accompanist-webview = { module = "com.google.accompanist:accompanist-webview", version.ref = "accompanist" }
|
accompanist-webview = { module = "com.google.accompanist:accompanist-webview", version.ref = "accompanist" }
|
||||||
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
|
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
|
||||||
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
|
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
|
||||||
|
androidx-paging = { module = "androidx.paging:paging-compose", version.ref = "paging-compose" }
|
||||||
material = { module = "com.google.android.material:material", version.ref = "material" }
|
material = { module = "com.google.android.material:material", version.ref = "material" }
|
||||||
compose-compiler = { module = "androidx.compose.compiler:compiler", version.ref = "compose-compiler" }
|
compose-compiler = { module = "androidx.compose.compiler:compiler", version.ref = "compose-compiler" }
|
||||||
compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose-ui" }
|
compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose-ui" }
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
package com.wbrawner.nanoflux.network
|
||||||
|
|
||||||
|
import androidx.paging.PagingSource
|
||||||
|
import androidx.paging.PagingState
|
||||||
|
import com.wbrawner.nanoflux.network.repository.EntryRepository
|
||||||
|
import com.wbrawner.nanoflux.network.repository.EntryStatus
|
||||||
|
import com.wbrawner.nanoflux.storage.model.EntryAndFeed
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class EntryAndFeedPagingSource(
|
||||||
|
private val entryRepository: EntryRepository,
|
||||||
|
private val entryStatus: EntryStatus? = null
|
||||||
|
) : PagingSource<Int, EntryAndFeed>() {
|
||||||
|
init {
|
||||||
|
Timber.tag("Nanoflux").d("EntryAndFeedPagingSource created")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRefreshKey(state: PagingState<Int, EntryAndFeed>): Int? = state.anchorPosition
|
||||||
|
?.let { anchorPosition ->
|
||||||
|
val anchorPage = state.closestPageToPosition(anchorPosition)
|
||||||
|
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, EntryAndFeed> {
|
||||||
|
return try {
|
||||||
|
val nextPageNumber = params.key ?: 1
|
||||||
|
val offset = params.loadSize * (nextPageNumber - 1)
|
||||||
|
val nextKey = if (offset + params.loadSize < entryRepository.count()) {
|
||||||
|
nextPageNumber + 1
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
Timber.tag("Nanoflux").d("Loading data at page $nextPageNumber")
|
||||||
|
val loadFunction = when (entryStatus) {
|
||||||
|
EntryStatus.UNREAD -> entryRepository::loadUnread
|
||||||
|
EntryStatus.HISTORY -> entryRepository::loadRead
|
||||||
|
EntryStatus.STARRED -> entryRepository::loadStarred
|
||||||
|
else -> entryRepository::load
|
||||||
|
}
|
||||||
|
LoadResult.Page(
|
||||||
|
data = loadFunction(params.loadSize, offset),
|
||||||
|
prevKey = null,
|
||||||
|
nextKey = nextKey
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e("Failed to load page", e)
|
||||||
|
LoadResult.Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,10 @@ import com.wbrawner.nanoflux.storage.model.Entry
|
||||||
import com.wbrawner.nanoflux.storage.model.EntryAndFeed
|
import com.wbrawner.nanoflux.storage.model.EntryAndFeed
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import io.ktor.util.network.*
|
import io.ktor.util.network.*
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -20,41 +23,36 @@ class EntryRepository @Inject constructor(
|
||||||
private val entryDao: EntryDao,
|
private val entryDao: EntryDao,
|
||||||
private val logger: Timber.Tree
|
private val logger: Timber.Tree
|
||||||
) {
|
) {
|
||||||
fun observeRead(): Flow<List<EntryAndFeed>> = entryDao.observeRead()
|
fun observeRead() = entryDao.observeRead()
|
||||||
fun observeStarred(): Flow<List<EntryAndFeed>> = entryDao.observeStarred()
|
fun observeStarred() = entryDao.observeStarred()
|
||||||
fun observeUnread(): Flow<List<EntryAndFeed>> = entryDao.observeUnread()
|
fun observeUnread() = entryDao.observeUnread()
|
||||||
|
|
||||||
fun getCount(): Long = entryDao.getCount()
|
suspend fun count(): Long = entryDao.count()
|
||||||
|
|
||||||
suspend fun getAll(fetch: Boolean = false, afterId: Long? = null): List<EntryAndFeed> {
|
suspend fun fetch(afterId: Long? = null) {
|
||||||
if (fetch) {
|
getEntries { page ->
|
||||||
getEntries { page ->
|
apiService.getEntries(
|
||||||
apiService.getEntries(
|
order = Entry.Order.PUBLISHED_AT,
|
||||||
order = Entry.Order.PUBLISHED_AT,
|
direction = Entry.SortDirection.DESC,
|
||||||
direction = Entry.SortDirection.DESC,
|
offset = 100L * page,
|
||||||
offset = 100L * page,
|
limit = 100,
|
||||||
limit = 100,
|
afterEntryId = afterId
|
||||||
afterEntryId = afterId
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return entryDao.getAll()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getAllUnread(fetch: Boolean = false): List<EntryAndFeed> {
|
suspend fun load(limit: Int, offset: Int) = entryDao.load(limit, offset)
|
||||||
if (fetch) {
|
suspend fun loadRead(limit: Int, offset: Int) = entryDao.loadRead(limit, offset)
|
||||||
getEntries { page ->
|
suspend fun loadUnread(limit: Int, offset: Int) = entryDao.loadUnread(limit, offset)
|
||||||
apiService.getEntries(
|
suspend fun loadStarred(limit: Int, offset: Int) = entryDao.loadStarred(limit, offset)
|
||||||
offset = 100L * page,
|
|
||||||
limit = 100,
|
|
||||||
status = listOf(Entry.Status.UNREAD)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return entryDao.getAllUnread()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getEntry(id: Long): Flow<EntryAndFeed> {
|
suspend fun getEntry(id: Long): EntryAndFeed? = entryDao.get(id)
|
||||||
|
?: run {
|
||||||
|
entryDao.insertAll(apiService.getEntry(id))
|
||||||
|
entryDao.get(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun observeEntry(id: Long): Flow<EntryAndFeed> {
|
||||||
val entry = entryDao.observe(id)
|
val entry = entryDao.observe(id)
|
||||||
if (entryDao.get(id) == null) {
|
if (entryDao.get(id) == null) {
|
||||||
entryDao.insertAll(apiService.getEntry(id))
|
entryDao.insertAll(apiService.getEntry(id))
|
||||||
|
|
|
@ -51,6 +51,7 @@ dependencies {
|
||||||
implementation(libs.preference)
|
implementation(libs.preference)
|
||||||
implementation(libs.room.ktx)
|
implementation(libs.room.ktx)
|
||||||
kapt(libs.room.kapt)
|
kapt(libs.room.kapt)
|
||||||
|
api(libs.androidx.paging)
|
||||||
implementation(libs.bundles.coroutines)
|
implementation(libs.bundles.coroutines)
|
||||||
implementation(libs.kotlinx.serialization)
|
implementation(libs.kotlinx.serialization)
|
||||||
implementation(libs.hilt.android.core)
|
implementation(libs.hilt.android.core)
|
||||||
|
|
|
@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.Flow
|
||||||
@Dao
|
@Dao
|
||||||
interface EntryDao {
|
interface EntryDao {
|
||||||
@Query("SELECT COUNT(*) FROM Entry")
|
@Query("SELECT COUNT(*) FROM Entry")
|
||||||
fun getCount(): Long
|
suspend fun count(): Long
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT * FROM Entry ORDER BY publishedAt DESC")
|
@Query("SELECT * FROM Entry ORDER BY publishedAt DESC")
|
||||||
|
@ -35,12 +35,20 @@ interface EntryDao {
|
||||||
fun observeUnread(): Flow<List<EntryAndFeed>>
|
fun observeUnread(): Flow<List<EntryAndFeed>>
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT * FROM Entry ORDER BY publishedAt DESC")
|
@Query("SELECT * FROM Entry ORDER BY publishedAt DESC LIMIT :limit OFFSET :offset")
|
||||||
fun getAll(): List<EntryAndFeed>
|
suspend fun load(limit: Int, offset: Int): List<EntryAndFeed>
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT * FROM Entry WHERE status = \"UNREAD\" ORDER BY createdAt DESC")
|
@Query("SELECT * FROM Entry WHERE status = \"READ\" ORDER BY createdAt DESC LIMIT :limit OFFSET :offset")
|
||||||
fun getAllUnread(): List<EntryAndFeed>
|
suspend fun loadRead(limit: Int, offset: Int): List<EntryAndFeed>
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query("SELECT * FROM Entry WHERE status = \"UNREAD\" ORDER BY createdAt DESC LIMIT :limit OFFSET :offset")
|
||||||
|
suspend fun loadUnread(limit: Int, offset: Int): List<EntryAndFeed>
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query("SELECT * FROM Entry WHERE starred = 1 ORDER BY publishedAt DESC LIMIT :limit OFFSET :offset")
|
||||||
|
suspend fun loadStarred(limit: Int, offset: Int): List<EntryAndFeed>
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT * FROM Entry WHERE id = :id")
|
@Query("SELECT * FROM Entry WHERE id = :id")
|
||||||
|
|
Loading…
Reference in a new issue