mirror of
https://github.com/msfjarvis/compose-lobsters
synced 2025-08-14 21:07:04 +05:30
refactor(android): move UIPost rewrite into PagingSource
This commit is contained in:
parent
047d6badb0
commit
71977c5b2c
7 changed files with 71 additions and 47 deletions
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright © 2021-2023 Harsh Shandilya.
|
* Copyright © 2021-2024 Harsh Shandilya.
|
||||||
* Use of this source code is governed by an MIT-style
|
* Use of this source code is governed by an MIT-style
|
||||||
* license that can be found in the LICENSE file or at
|
* license that can be found in the LICENSE file or at
|
||||||
* https://opensource.org/licenses/MIT.
|
* https://opensource.org/licenses/MIT.
|
||||||
|
@ -13,8 +13,12 @@ import com.slack.eithernet.ApiResult.Success
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
import dev.msfjarvis.claw.android.viewmodel.ReadPostsRepository
|
||||||
|
import dev.msfjarvis.claw.android.viewmodel.SavedPostsRepository
|
||||||
import dev.msfjarvis.claw.core.injection.IODispatcher
|
import dev.msfjarvis.claw.core.injection.IODispatcher
|
||||||
import dev.msfjarvis.claw.model.LobstersPost
|
import dev.msfjarvis.claw.model.LobstersPost
|
||||||
|
import dev.msfjarvis.claw.model.UIPost
|
||||||
|
import dev.msfjarvis.claw.model.toUIPost
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@ -24,15 +28,25 @@ class LobstersPagingSource
|
||||||
constructor(
|
constructor(
|
||||||
@Assisted private val remoteFetcher: RemoteFetcher<LobstersPost>,
|
@Assisted private val remoteFetcher: RemoteFetcher<LobstersPost>,
|
||||||
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
|
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||||
) : PagingSource<Int, LobstersPost>() {
|
private val savedPostsRepository: SavedPostsRepository,
|
||||||
|
private val readPostsRepository: ReadPostsRepository,
|
||||||
|
) : PagingSource<Int, UIPost>() {
|
||||||
|
|
||||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, LobstersPost> {
|
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, UIPost> {
|
||||||
val page = params.key ?: STARTING_PAGE_INDEX
|
val page = params.key ?: STARTING_PAGE_INDEX
|
||||||
return when (val result = withContext(ioDispatcher) { remoteFetcher.getItemsAtPage(page) }) {
|
return when (val result = withContext(ioDispatcher) { remoteFetcher.getItemsAtPage(page) }) {
|
||||||
is Success ->
|
is Success ->
|
||||||
LoadResult.Page(
|
LoadResult.Page(
|
||||||
itemsBefore = (page - 1) * PAGE_SIZE,
|
itemsBefore = (page - 1) * PAGE_SIZE,
|
||||||
data = result.value,
|
data =
|
||||||
|
result.value.map {
|
||||||
|
it
|
||||||
|
.toUIPost()
|
||||||
|
.copy(
|
||||||
|
isSaved = savedPostsRepository.isPostSaved(it.shortId),
|
||||||
|
isRead = readPostsRepository.isPostRead(it.shortId),
|
||||||
|
)
|
||||||
|
},
|
||||||
prevKey = if (page == STARTING_PAGE_INDEX) null else page - 1,
|
prevKey = if (page == STARTING_PAGE_INDEX) null else page - 1,
|
||||||
nextKey = page + 1,
|
nextKey = page + 1,
|
||||||
)
|
)
|
||||||
|
@ -43,7 +57,7 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getRefreshKey(state: PagingState<Int, LobstersPost>): Int? {
|
override fun getRefreshKey(state: PagingState<Int, UIPost>): Int? {
|
||||||
return state.anchorPosition?.let { anchorPosition ->
|
return state.anchorPosition?.let { anchorPosition ->
|
||||||
(anchorPosition / PAGE_SIZE).coerceAtLeast(STARTING_PAGE_INDEX)
|
(anchorPosition / PAGE_SIZE).coerceAtLeast(STARTING_PAGE_INDEX)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright © 2021-2023 Harsh Shandilya.
|
* Copyright © 2021-2024 Harsh Shandilya.
|
||||||
* Use of this source code is governed by an MIT-style
|
* Use of this source code is governed by an MIT-style
|
||||||
* license that can be found in the LICENSE file or at
|
* license that can be found in the LICENSE file or at
|
||||||
* https://opensource.org/licenses/MIT.
|
* https://opensource.org/licenses/MIT.
|
||||||
|
@ -14,9 +14,12 @@ import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import dev.msfjarvis.claw.android.paging.LobstersPagingSource.Companion.PAGE_SIZE
|
import dev.msfjarvis.claw.android.paging.LobstersPagingSource.Companion.PAGE_SIZE
|
||||||
import dev.msfjarvis.claw.android.paging.LobstersPagingSource.Companion.STARTING_PAGE_INDEX
|
import dev.msfjarvis.claw.android.paging.LobstersPagingSource.Companion.STARTING_PAGE_INDEX
|
||||||
|
import dev.msfjarvis.claw.android.viewmodel.ReadPostsRepository
|
||||||
|
import dev.msfjarvis.claw.android.viewmodel.SavedPostsRepository
|
||||||
import dev.msfjarvis.claw.api.LobstersSearchApi
|
import dev.msfjarvis.claw.api.LobstersSearchApi
|
||||||
import dev.msfjarvis.claw.core.injection.IODispatcher
|
import dev.msfjarvis.claw.core.injection.IODispatcher
|
||||||
import dev.msfjarvis.claw.model.LobstersPost
|
import dev.msfjarvis.claw.model.UIPost
|
||||||
|
import dev.msfjarvis.claw.model.toUIPost
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@ -33,14 +36,16 @@ constructor(
|
||||||
private val searchApi: LobstersSearchApi,
|
private val searchApi: LobstersSearchApi,
|
||||||
@Assisted private val queryProvider: () -> String,
|
@Assisted private val queryProvider: () -> String,
|
||||||
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
|
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||||
) : PagingSource<Int, LobstersPost>() {
|
private val savedPostsRepository: SavedPostsRepository,
|
||||||
override fun getRefreshKey(state: PagingState<Int, LobstersPost>): Int? {
|
private val readPostsRepository: ReadPostsRepository,
|
||||||
|
) : PagingSource<Int, UIPost>() {
|
||||||
|
override fun getRefreshKey(state: PagingState<Int, UIPost>): Int? {
|
||||||
return state.anchorPosition?.let { anchorPosition ->
|
return state.anchorPosition?.let { anchorPosition ->
|
||||||
(anchorPosition / PAGE_SIZE).coerceAtLeast(STARTING_PAGE_INDEX)
|
(anchorPosition / PAGE_SIZE).coerceAtLeast(STARTING_PAGE_INDEX)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, LobstersPost> {
|
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, UIPost> {
|
||||||
val searchQuery = queryProvider()
|
val searchQuery = queryProvider()
|
||||||
// If there is no query, we don't need to call the API at all.
|
// If there is no query, we don't need to call the API at all.
|
||||||
if (searchQuery.isEmpty()) {
|
if (searchQuery.isEmpty()) {
|
||||||
|
@ -55,7 +60,15 @@ constructor(
|
||||||
val nextKey = if (result.value.isEmpty()) null else page + 1
|
val nextKey = if (result.value.isEmpty()) null else page + 1
|
||||||
LoadResult.Page(
|
LoadResult.Page(
|
||||||
itemsBefore = (page - 1) * PAGE_SIZE,
|
itemsBefore = (page - 1) * PAGE_SIZE,
|
||||||
data = result.value,
|
data =
|
||||||
|
result.value.map {
|
||||||
|
it
|
||||||
|
.toUIPost()
|
||||||
|
.copy(
|
||||||
|
isSaved = savedPostsRepository.isPostSaved(it.shortId),
|
||||||
|
isRead = readPostsRepository.isPostRead(it.shortId),
|
||||||
|
)
|
||||||
|
},
|
||||||
prevKey = if (page == STARTING_PAGE_INDEX) null else page - 1,
|
prevKey = if (page == STARTING_PAGE_INDEX) null else page - 1,
|
||||||
nextKey = nextKey,
|
nextKey = nextKey,
|
||||||
)
|
)
|
||||||
|
|
|
@ -18,9 +18,7 @@ import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.paging.Pager
|
import androidx.paging.Pager
|
||||||
import androidx.paging.PagingConfig
|
import androidx.paging.PagingConfig
|
||||||
import androidx.paging.PagingData
|
|
||||||
import androidx.paging.cachedIn
|
import androidx.paging.cachedIn
|
||||||
import androidx.paging.map
|
|
||||||
import com.deliveryhero.whetstone.app.ApplicationScope
|
import com.deliveryhero.whetstone.app.ApplicationScope
|
||||||
import com.deliveryhero.whetstone.viewmodel.ContributesViewModel
|
import com.deliveryhero.whetstone.viewmodel.ContributesViewModel
|
||||||
import com.slack.eithernet.ApiResult.Failure
|
import com.slack.eithernet.ApiResult.Failure
|
||||||
|
@ -35,10 +33,8 @@ import dev.msfjarvis.claw.api.LobstersApi
|
||||||
import dev.msfjarvis.claw.core.injection.IODispatcher
|
import dev.msfjarvis.claw.core.injection.IODispatcher
|
||||||
import dev.msfjarvis.claw.core.injection.MainDispatcher
|
import dev.msfjarvis.claw.core.injection.MainDispatcher
|
||||||
import dev.msfjarvis.claw.model.Comment
|
import dev.msfjarvis.claw.model.Comment
|
||||||
import dev.msfjarvis.claw.model.LobstersPost
|
|
||||||
import dev.msfjarvis.claw.model.UIPost
|
import dev.msfjarvis.claw.model.UIPost
|
||||||
import dev.msfjarvis.claw.model.fromSavedPost
|
import dev.msfjarvis.claw.model.fromSavedPost
|
||||||
import dev.msfjarvis.claw.model.toSavedPost
|
|
||||||
import dev.msfjarvis.claw.model.toUIPost
|
import dev.msfjarvis.claw.model.toUIPost
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
@ -79,7 +75,6 @@ constructor(
|
||||||
pagingSourceFactory = { pagingSourceFactory.create(api::getHottestPosts) },
|
pagingSourceFactory = { pagingSourceFactory.create(api::getHottestPosts) },
|
||||||
)
|
)
|
||||||
.flow
|
.flow
|
||||||
.map(::mapToUIPost)
|
|
||||||
.cachedIn(viewModelScope)
|
.cachedIn(viewModelScope)
|
||||||
val newestPosts =
|
val newestPosts =
|
||||||
Pager(
|
Pager(
|
||||||
|
@ -88,7 +83,6 @@ constructor(
|
||||||
pagingSourceFactory = { pagingSourceFactory.create(api::getNewestPosts) },
|
pagingSourceFactory = { pagingSourceFactory.create(api::getNewestPosts) },
|
||||||
)
|
)
|
||||||
.flow
|
.flow
|
||||||
.map(::mapToUIPost)
|
|
||||||
.cachedIn(viewModelScope)
|
.cachedIn(viewModelScope)
|
||||||
val searchResults =
|
val searchResults =
|
||||||
Pager(
|
Pager(
|
||||||
|
@ -97,7 +91,6 @@ constructor(
|
||||||
pagingSourceFactory = { searchPagingSourceFactory.create { searchQuery } },
|
pagingSourceFactory = { searchPagingSourceFactory.create { searchQuery } },
|
||||||
)
|
)
|
||||||
.flow
|
.flow
|
||||||
.map(::mapToUIPost)
|
|
||||||
val savedPosts = savedPostsRepository.savedPosts.map { it.map(UIPost.Companion::fromSavedPost) }
|
val savedPosts = savedPostsRepository.savedPosts.map { it.map(UIPost.Companion::fromSavedPost) }
|
||||||
val savedPostsByMonth
|
val savedPostsByMonth
|
||||||
get() = savedPosts.map(::groupSavedPosts)
|
get() = savedPosts.map(::groupSavedPosts)
|
||||||
|
@ -112,13 +105,6 @@ constructor(
|
||||||
viewModelScope.launch { readPostsRepository.readPosts.collectLatest { _readPosts = it } }
|
viewModelScope.launch { readPostsRepository.readPosts.collectLatest { _readPosts = it } }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mapToUIPost(pagingData: PagingData<LobstersPost>): PagingData<UIPost> {
|
|
||||||
return pagingData.map { post ->
|
|
||||||
val uiPost = post.toUIPost()
|
|
||||||
uiPost.copy(isSaved = isPostSaved(uiPost), isRead = isPostRead(uiPost))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun groupSavedPosts(items: List<UIPost>): ImmutableMap<String, List<UIPost>> {
|
private fun groupSavedPosts(items: List<UIPost>): ImmutableMap<String, List<UIPost>> {
|
||||||
val sorted =
|
val sorted =
|
||||||
items.sortedWith { post1, post2 ->
|
items.sortedWith { post1, post2 ->
|
||||||
|
@ -140,22 +126,9 @@ constructor(
|
||||||
.toImmutableMap()
|
.toImmutableMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isPostSaved(post: UIPost): Boolean {
|
|
||||||
return _savedPosts.contains(post.shortId)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isPostRead(post: UIPost): Boolean {
|
|
||||||
return _readPosts.contains(post.shortId)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toggleSave(post: UIPost) {
|
fun toggleSave(post: UIPost) {
|
||||||
viewModelScope.launch(ioDispatcher) {
|
viewModelScope.launch {
|
||||||
val saved = isPostSaved(post)
|
savedPostsRepository.toggleSave(post)
|
||||||
if (saved) {
|
|
||||||
savedPostsRepository.removePost(post.toSavedPost())
|
|
||||||
} else {
|
|
||||||
savedPostsRepository.savePost(post.toSavedPost())
|
|
||||||
}
|
|
||||||
withContext(mainDispatcher) { SavedPostsWidget(savedPosts).updateAll(getApplication()) }
|
withContext(mainDispatcher) { SavedPostsWidget(savedPosts).updateAll(getApplication()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,4 +25,8 @@ constructor(
|
||||||
suspend fun markRead(postId: String) {
|
suspend fun markRead(postId: String) {
|
||||||
withContext(dbDispatcher) { readPostsQueries.markRead(postId) }
|
withContext(dbDispatcher) { readPostsQueries.markRead(postId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun isPostRead(shortId: String): Boolean {
|
||||||
|
return withContext(dbDispatcher) { readPostsQueries.isPostRead(shortId).executeAsOne() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright © 2021-2023 Harsh Shandilya.
|
* Copyright © 2021-2024 Harsh Shandilya.
|
||||||
* Use of this source code is governed by an MIT-style
|
* Use of this source code is governed by an MIT-style
|
||||||
* license that can be found in the LICENSE file or at
|
* license that can be found in the LICENSE file or at
|
||||||
* https://opensource.org/licenses/MIT.
|
* https://opensource.org/licenses/MIT.
|
||||||
|
@ -11,6 +11,8 @@ import app.cash.sqldelight.coroutines.mapToList
|
||||||
import dev.msfjarvis.claw.core.injection.DatabaseDispatcher
|
import dev.msfjarvis.claw.core.injection.DatabaseDispatcher
|
||||||
import dev.msfjarvis.claw.database.local.SavedPost
|
import dev.msfjarvis.claw.database.local.SavedPost
|
||||||
import dev.msfjarvis.claw.database.local.SavedPostQueries
|
import dev.msfjarvis.claw.database.local.SavedPostQueries
|
||||||
|
import dev.msfjarvis.claw.model.UIPost
|
||||||
|
import dev.msfjarvis.claw.model.toSavedPost
|
||||||
import io.github.aakira.napier.Napier
|
import io.github.aakira.napier.Napier
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
@ -24,9 +26,14 @@ constructor(
|
||||||
) {
|
) {
|
||||||
val savedPosts = savedPostQueries.selectAllPosts().asFlow().mapToList(dbDispatcher)
|
val savedPosts = savedPostQueries.selectAllPosts().asFlow().mapToList(dbDispatcher)
|
||||||
|
|
||||||
suspend fun savePost(post: SavedPost) {
|
suspend fun toggleSave(post: UIPost) {
|
||||||
Napier.d(tag = TAG) { "Saving post: ${post.shortId}" }
|
if (post.isSaved) {
|
||||||
withContext(dbDispatcher) { savedPostQueries.insertOrReplacePost(post) }
|
Napier.d(tag = TAG) { "Removing post: ${post.shortId}" }
|
||||||
|
withContext(dbDispatcher) { savedPostQueries.deletePost(post.shortId) }
|
||||||
|
} else {
|
||||||
|
Napier.d(tag = TAG) { "Saving post: ${post.shortId}" }
|
||||||
|
withContext(dbDispatcher) { savedPostQueries.insertOrReplacePost(post.toSavedPost()) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun savePosts(posts: List<SavedPost>) {
|
suspend fun savePosts(posts: List<SavedPost>) {
|
||||||
|
@ -38,9 +45,8 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun removePost(post: SavedPost) {
|
suspend fun isPostSaved(postId: String): Boolean {
|
||||||
Napier.d(tag = TAG) { "Removing post: ${post.shortId}" }
|
return withContext(dbDispatcher) { savedPostQueries.isPostSaved(postId).executeAsOne() }
|
||||||
withContext(dbDispatcher) { savedPostQueries.deletePost(post.shortId) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
|
|
|
@ -2,6 +2,13 @@ CREATE TABLE ReadPosts(
|
||||||
id TEXT NOT NULL PRIMARY KEY
|
id TEXT NOT NULL PRIMARY KEY
|
||||||
);
|
);
|
||||||
|
|
||||||
|
isPostRead:
|
||||||
|
SELECT EXISTS(
|
||||||
|
SELECT 1
|
||||||
|
FROM ReadPosts
|
||||||
|
WHERE id = ?
|
||||||
|
) AS isRead;
|
||||||
|
|
||||||
selectAllPosts:
|
selectAllPosts:
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM ReadPosts;
|
FROM ReadPosts;
|
||||||
|
|
|
@ -14,6 +14,13 @@ CREATE TABLE IF NOT EXISTS SavedPost(
|
||||||
description TEXT NOT NULL DEFAULT ""
|
description TEXT NOT NULL DEFAULT ""
|
||||||
);
|
);
|
||||||
|
|
||||||
|
isPostSaved:
|
||||||
|
SELECT EXISTS(
|
||||||
|
SELECT 1
|
||||||
|
FROM SavedPost
|
||||||
|
WHERE shortId = ?
|
||||||
|
) AS isSaved;
|
||||||
|
|
||||||
insertOrReplacePost:
|
insertOrReplacePost:
|
||||||
INSERT OR REPLACE
|
INSERT OR REPLACE
|
||||||
INTO SavedPost
|
INTO SavedPost
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue