refactor(android): move UIPost rewrite into PagingSource

This commit is contained in:
Harsh Shandilya 2024-03-16 23:41:43 +05:30
parent 047d6badb0
commit 71977c5b2c
7 changed files with 71 additions and 47 deletions

View file

@ -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
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
@ -13,8 +13,12 @@ import com.slack.eithernet.ApiResult.Success
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
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.model.LobstersPost
import dev.msfjarvis.claw.model.UIPost
import dev.msfjarvis.claw.model.toUIPost
import java.io.IOException
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
@ -24,15 +28,25 @@ class LobstersPagingSource
constructor(
@Assisted private val remoteFetcher: RemoteFetcher<LobstersPost>,
@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
return when (val result = withContext(ioDispatcher) { remoteFetcher.getItemsAtPage(page) }) {
is Success ->
LoadResult.Page(
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,
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 ->
(anchorPosition / PAGE_SIZE).coerceAtLeast(STARTING_PAGE_INDEX)
}

View file

@ -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
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
@ -14,9 +14,12 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
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.viewmodel.ReadPostsRepository
import dev.msfjarvis.claw.android.viewmodel.SavedPostsRepository
import dev.msfjarvis.claw.api.LobstersSearchApi
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 kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
@ -33,14 +36,16 @@ constructor(
private val searchApi: LobstersSearchApi,
@Assisted private val queryProvider: () -> String,
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
) : PagingSource<Int, LobstersPost>() {
override fun getRefreshKey(state: PagingState<Int, LobstersPost>): Int? {
private val savedPostsRepository: SavedPostsRepository,
private val readPostsRepository: ReadPostsRepository,
) : PagingSource<Int, UIPost>() {
override fun getRefreshKey(state: PagingState<Int, UIPost>): Int? {
return state.anchorPosition?.let { anchorPosition ->
(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()
// If there is no query, we don't need to call the API at all.
if (searchQuery.isEmpty()) {
@ -55,7 +60,15 @@ constructor(
val nextKey = if (result.value.isEmpty()) null else page + 1
LoadResult.Page(
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,
nextKey = nextKey,
)

View file

@ -18,9 +18,7 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import androidx.paging.map
import com.deliveryhero.whetstone.app.ApplicationScope
import com.deliveryhero.whetstone.viewmodel.ContributesViewModel
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.MainDispatcher
import dev.msfjarvis.claw.model.Comment
import dev.msfjarvis.claw.model.LobstersPost
import dev.msfjarvis.claw.model.UIPost
import dev.msfjarvis.claw.model.fromSavedPost
import dev.msfjarvis.claw.model.toSavedPost
import dev.msfjarvis.claw.model.toUIPost
import java.io.IOException
import java.io.InputStream
@ -79,7 +75,6 @@ constructor(
pagingSourceFactory = { pagingSourceFactory.create(api::getHottestPosts) },
)
.flow
.map(::mapToUIPost)
.cachedIn(viewModelScope)
val newestPosts =
Pager(
@ -88,7 +83,6 @@ constructor(
pagingSourceFactory = { pagingSourceFactory.create(api::getNewestPosts) },
)
.flow
.map(::mapToUIPost)
.cachedIn(viewModelScope)
val searchResults =
Pager(
@ -97,7 +91,6 @@ constructor(
pagingSourceFactory = { searchPagingSourceFactory.create { searchQuery } },
)
.flow
.map(::mapToUIPost)
val savedPosts = savedPostsRepository.savedPosts.map { it.map(UIPost.Companion::fromSavedPost) }
val savedPostsByMonth
get() = savedPosts.map(::groupSavedPosts)
@ -112,13 +105,6 @@ constructor(
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>> {
val sorted =
items.sortedWith { post1, post2 ->
@ -140,22 +126,9 @@ constructor(
.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) {
viewModelScope.launch(ioDispatcher) {
val saved = isPostSaved(post)
if (saved) {
savedPostsRepository.removePost(post.toSavedPost())
} else {
savedPostsRepository.savePost(post.toSavedPost())
}
viewModelScope.launch {
savedPostsRepository.toggleSave(post)
withContext(mainDispatcher) { SavedPostsWidget(savedPosts).updateAll(getApplication()) }
}
}

View file

@ -25,4 +25,8 @@ constructor(
suspend fun markRead(postId: String) {
withContext(dbDispatcher) { readPostsQueries.markRead(postId) }
}
suspend fun isPostRead(shortId: String): Boolean {
return withContext(dbDispatcher) { readPostsQueries.isPostRead(shortId).executeAsOne() }
}
}

View file

@ -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
* license that can be found in the LICENSE file or at
* 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.database.local.SavedPost
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 javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
@ -24,9 +26,14 @@ constructor(
) {
val savedPosts = savedPostQueries.selectAllPosts().asFlow().mapToList(dbDispatcher)
suspend fun savePost(post: SavedPost) {
Napier.d(tag = TAG) { "Saving post: ${post.shortId}" }
withContext(dbDispatcher) { savedPostQueries.insertOrReplacePost(post) }
suspend fun toggleSave(post: UIPost) {
if (post.isSaved) {
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>) {
@ -38,9 +45,8 @@ constructor(
}
}
suspend fun removePost(post: SavedPost) {
Napier.d(tag = TAG) { "Removing post: ${post.shortId}" }
withContext(dbDispatcher) { savedPostQueries.deletePost(post.shortId) }
suspend fun isPostSaved(postId: String): Boolean {
return withContext(dbDispatcher) { savedPostQueries.isPostSaved(postId).executeAsOne() }
}
private companion object {