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

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 * 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,
) )

View file

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

View file

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

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

View file

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

View file

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