refactor(android): switch over to PagingData transformations

This commit is contained in:
Harsh Shandilya 2024-01-27 16:52:47 +05:30
parent 051b7ab2bb
commit 74a7835a53
19 changed files with 187 additions and 151 deletions

View file

@ -28,11 +28,11 @@ import androidx.glance.text.Text
import androidx.glance.text.TextStyle
import dev.msfjarvis.claw.common.theme.DarkThemeColors
import dev.msfjarvis.claw.common.theme.LightThemeColors
import dev.msfjarvis.claw.database.local.SavedPost
import dev.msfjarvis.claw.model.UIPost
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
class SavedPostsWidget(private val posts: List<SavedPost>) : GlanceAppWidget() {
class SavedPostsWidget(private val posts: List<UIPost>) : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
GlanceTheme(
@ -46,7 +46,7 @@ class SavedPostsWidget(private val posts: List<SavedPost>) : GlanceAppWidget() {
}
@Composable
fun WidgetHost(posts: ImmutableList<SavedPost>, modifier: GlanceModifier = GlanceModifier) {
fun WidgetHost(posts: ImmutableList<UIPost>, modifier: GlanceModifier = GlanceModifier) {
LazyColumn(
modifier =
modifier.fillMaxSize().background(GlanceTheme.colors.background).appWidgetBackground(),

View file

@ -33,13 +33,13 @@ import androidx.glance.text.TextStyle
import dev.msfjarvis.claw.android.MainActivity
import dev.msfjarvis.claw.android.MainActivity.Companion.NAVIGATION_KEY
import dev.msfjarvis.claw.android.R
import dev.msfjarvis.claw.database.local.SavedPost
import dev.msfjarvis.claw.model.UIPost
private val destinationKey = Key<String>(NAVIGATION_KEY)
@Composable
@GlanceComposable
fun WidgetListEntry(post: SavedPost, modifier: GlanceModifier = GlanceModifier) {
fun WidgetListEntry(post: UIPost, modifier: GlanceModifier = GlanceModifier) {
val titleStyle = MaterialTheme.typography.titleMedium
val commentsAction =
actionStartActivity<MainActivity>(actionParametersOf(destinationKey to post.shortId))
@ -68,7 +68,7 @@ fun WidgetListEntry(post: SavedPost, modifier: GlanceModifier = GlanceModifier)
)
Image(
provider = ImageProvider(R.drawable.ic_comment),
contentDescription = "${post.commentCount ?: 0} comments",
contentDescription = "${post.commentCount} comments",
modifier = GlanceModifier.padding(end = 4.dp).clickable(commentsAction),
)
}

View file

@ -1,5 +1,5 @@
/*
* Copyright © 2022-2023 Harsh Shandilya.
* Copyright © 2022-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.
@ -16,9 +16,8 @@ import dev.msfjarvis.claw.android.ui.navigation.Destinations
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
import dev.msfjarvis.claw.common.posts.PostActions
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
import dev.msfjarvis.claw.database.local.SavedPost
import dev.msfjarvis.claw.model.LinkMetadata
import dev.msfjarvis.claw.model.LobstersPostDetails
import dev.msfjarvis.claw.model.UIPost
fun Context.getActivity(): ComponentActivity? {
return when (this) {
@ -59,11 +58,11 @@ fun rememberPostActions(
urlLauncher.openUri(commentsUrl.replaceAfterLast('/', "r"))
}
override fun toggleSave(post: SavedPost) {
override fun toggleSave(post: UIPost) {
viewModel.toggleSave(post)
}
override suspend fun getComments(postId: String): LobstersPostDetails {
override suspend fun getComments(postId: String): UIPost {
return viewModel.getPostComments(postId)
}

View file

@ -27,13 +27,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import dev.msfjarvis.claw.common.posts.PostActions
import dev.msfjarvis.claw.common.ui.decorations.MonthHeader
import dev.msfjarvis.claw.database.local.SavedPost
import dev.msfjarvis.claw.model.UIPost
import kotlinx.collections.immutable.ImmutableMap
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun DatabasePosts(
items: ImmutableMap<String, List<SavedPost>>,
items: ImmutableMap<String, List<UIPost>>,
listState: LazyListState,
postActions: PostActions,
modifier: Modifier = Modifier,
@ -54,12 +54,7 @@ fun DatabasePosts(
items.forEach { (month, posts) ->
stickyHeader(contentType = "month-header") { MonthHeader(label = month) }
items(items = posts, key = { it.shortId }, contentType = { "LobstersItem" }) { item ->
LobstersListItem(
item = item,
isSaved = { true },
isRead = { false },
postActions = postActions,
)
LobstersListItem(item = item, postActions = postActions)
HorizontalDivider()
}
}

View file

@ -11,24 +11,16 @@ import androidx.compose.material.icons.automirrored.filled.Reply
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import dev.msfjarvis.claw.common.posts.LobstersCard
import dev.msfjarvis.claw.common.posts.PostActions
import dev.msfjarvis.claw.database.local.SavedPost
import dev.msfjarvis.claw.model.UIPost
import me.saket.swipe.SwipeAction
import me.saket.swipe.SwipeableActionsBox
@Composable
fun LobstersListItem(
item: SavedPost,
isSaved: (SavedPost) -> Boolean,
isRead: suspend (String) -> Boolean,
postActions: PostActions,
modifier: Modifier = Modifier,
) {
val read by produceState(false, item.shortId) { value = isRead(item.shortId) }
fun LobstersListItem(item: UIPost, postActions: PostActions, modifier: Modifier = Modifier) {
val commentsAction =
SwipeAction(
icon = rememberVectorPainter(Icons.AutoMirrored.Filled.Reply),
@ -36,12 +28,6 @@ fun LobstersListItem(
onSwipe = { postActions.viewCommentsPage(item.commentsUrl) },
)
SwipeableActionsBox(endActions = listOf(commentsAction)) {
LobstersCard(
post = item,
isSaved = isSaved(item),
isRead = read,
postActions = postActions,
modifier = modifier,
)
LobstersCard(post = item, postActions = postActions, modifier = modifier)
}
}

View file

@ -27,9 +27,7 @@ import androidx.paging.compose.itemKey
import dev.msfjarvis.claw.common.posts.PostActions
import dev.msfjarvis.claw.common.ui.NetworkError
import dev.msfjarvis.claw.common.ui.ProgressBar
import dev.msfjarvis.claw.database.local.SavedPost
import dev.msfjarvis.claw.model.LobstersPost
import dev.msfjarvis.claw.model.toSavedPost
import dev.msfjarvis.claw.model.UIPost
import eu.bambooapps.material3.pullrefresh.PullRefreshIndicator
import eu.bambooapps.material3.pullrefresh.pullRefresh
import eu.bambooapps.material3.pullrefresh.rememberPullRefreshState
@ -37,10 +35,8 @@ import eu.bambooapps.material3.pullrefresh.rememberPullRefreshState
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NetworkPosts(
lazyPagingItems: LazyPagingItems<LobstersPost>,
lazyPagingItems: LazyPagingItems<UIPost>,
listState: LazyListState,
isPostSaved: (SavedPost) -> Boolean,
isPostRead: suspend (String) -> Boolean,
postActions: PostActions,
modifier: Modifier = Modifier,
) {
@ -64,13 +60,7 @@ fun NetworkPosts(
) { index ->
val item = lazyPagingItems[index]
if (item != null) {
val dbModel = item.toSavedPost()
LobstersListItem(
item = dbModel,
isSaved = isPostSaved,
isRead = isPostRead,
postActions = postActions,
)
LobstersListItem(item = item, postActions = postActions)
HorizontalDivider()
}

View file

@ -1,5 +1,5 @@
/*
* Copyright © 2023 Harsh Shandilya.
* Copyright © 2023-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.
@ -21,15 +21,13 @@ import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems
import dev.msfjarvis.claw.common.posts.PostActions
import dev.msfjarvis.claw.common.ui.SearchBar
import dev.msfjarvis.claw.database.local.SavedPost
import dev.msfjarvis.claw.model.LobstersPost
import dev.msfjarvis.claw.model.UIPost
import kotlinx.coroutines.flow.Flow
@Composable
fun SearchList(
items: Flow<PagingData<LobstersPost>>,
items: Flow<PagingData<UIPost>>,
listState: LazyListState,
isPostSaved: (SavedPost) -> Boolean,
postActions: PostActions,
searchQuery: String,
setSearchQuery: (String) -> Unit,
@ -50,8 +48,6 @@ fun SearchList(
NetworkPosts(
lazyPagingItems = lazyPagingItems,
listState = listState,
isPostSaved = isPostSaved,
isPostRead = { false },
postActions = postActions,
)
}

View file

@ -228,8 +228,6 @@ fun LobstersPostsScreen(
NetworkPosts(
lazyPagingItems = hottestPosts,
listState = hottestListState,
isPostSaved = viewModel::isPostSaved,
isPostRead = viewModel::isPostRead,
postActions = postActions,
)
}
@ -238,8 +236,6 @@ fun LobstersPostsScreen(
NetworkPosts(
lazyPagingItems = newestPosts,
listState = newestListState,
isPostSaved = viewModel::isPostSaved,
isPostRead = viewModel::isPostRead,
postActions = postActions,
)
}

View file

@ -1,5 +1,5 @@
/*
* Copyright © 2023 Harsh Shandilya.
* Copyright © 2023-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.
@ -47,7 +47,6 @@ fun SearchScreen(
SearchList(
items = viewModel.searchResults,
listState = listState,
isPostSaved = viewModel::isPostSaved,
postActions = postActions,
searchQuery = viewModel.searchQuery,
setSearchQuery = { query -> viewModel.searchQuery = query },

View file

@ -18,6 +18,9 @@ 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
@ -31,8 +34,12 @@ import dev.msfjarvis.claw.android.paging.SearchPagingSource
import dev.msfjarvis.claw.api.LobstersApi
import dev.msfjarvis.claw.core.injection.IODispatcher
import dev.msfjarvis.claw.core.injection.MainDispatcher
import dev.msfjarvis.claw.database.local.SavedPost
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
import java.io.OutputStream
@ -44,9 +51,11 @@ import javax.inject.Inject
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@ -68,34 +77,43 @@ constructor(
@MainDispatcher private val mainDispatcher: CoroutineDispatcher,
@ForScope(ApplicationScope::class) context: Context,
) : AndroidViewModel(context as Application) {
private val hottestPostsPager =
Pager(PagingConfig(pageSize = PAGE_SIZE), initialKey = STARTING_PAGE_INDEX) {
pagingSourceFactory.create(api::getHottestPosts)
}
private val newestPostsPager =
Pager(PagingConfig(pageSize = PAGE_SIZE), initialKey = STARTING_PAGE_INDEX) {
pagingSourceFactory.create(api::getNewestPosts)
}
private val searchResultsPager =
Pager(PagingConfig(pageSize = PAGE_SIZE), initialKey = STARTING_PAGE_INDEX) {
searchPagingSourceFactory.create { searchQuery }
}
val hottestPosts =
Pager(
config = PagingConfig(pageSize = PAGE_SIZE),
initialKey = STARTING_PAGE_INDEX,
pagingSourceFactory = { pagingSourceFactory.create(api::getHottestPosts) },
)
.flow
.map(::mapUIPost)
.cachedIn(viewModelScope)
val hottestPosts
get() = hottestPostsPager.flow
val newestPosts
get() = newestPostsPager.flow
val newestPosts =
Pager(
config = PagingConfig(pageSize = PAGE_SIZE),
initialKey = STARTING_PAGE_INDEX,
pagingSourceFactory = { pagingSourceFactory.create(api::getNewestPosts) },
)
.flow
.map(::mapUIPost)
.cachedIn(viewModelScope)
val searchResults =
Pager(
PagingConfig(pageSize = PAGE_SIZE),
initialKey = STARTING_PAGE_INDEX,
pagingSourceFactory = { searchPagingSourceFactory.create { searchQuery } },
)
.flow
.map(::mapUIPost)
val savedPosts
get() = savedPostsRepository.savedPosts
get() =
savedPostsRepository.savedPosts
.map { it.map(UIPost.Companion::fromSavedPost) }
.shareIn(viewModelScope, started = SharingStarted.Lazily, Int.MAX_VALUE)
val savedPostsByMonth
get() = savedPosts.map(::mapSavedPosts)
val searchResults
get() = searchResultsPager.flow
var searchQuery by mutableStateOf("")
private val _savedPostsMutex = Mutex()
@ -104,12 +122,19 @@ constructor(
init {
viewModelScope.launch {
savedPosts.collectLatest {
_savedPostsMutex.withLock { _savedPosts = it.map(SavedPost::shortId) }
_savedPostsMutex.withLock { _savedPosts = it.map(UIPost::shortId) }
}
}
}
private fun mapSavedPosts(items: List<SavedPost>): ImmutableMap<String, List<SavedPost>> {
private fun mapUIPost(pagingData: PagingData<LobstersPost>): PagingData<UIPost> {
return pagingData.map { post ->
val uiPost = post.toUIPost()
uiPost.copy(isSaved = isPostSaved(uiPost), isRead = isPostRead(uiPost))
}
}
private fun mapSavedPosts(items: List<UIPost>): ImmutableMap<String, List<UIPost>> {
val sorted =
items.sortedWith { post1, post2 ->
val post1Date = post1.createdAt.toLocalDateTime()
@ -130,17 +155,19 @@ constructor(
.toImmutableMap()
}
fun isPostSaved(post: SavedPost): Boolean {
private fun isPostSaved(post: UIPost): Boolean {
return _savedPosts.contains(post.shortId)
}
fun toggleSave(post: SavedPost) {
private fun isPostRead(post: UIPost) = readPostsRepository.isRead(post.shortId)
fun toggleSave(post: UIPost) {
viewModelScope.launch(ioDispatcher) {
val saved = isPostSaved(post)
if (saved) {
savedPostsRepository.removePost(post)
savedPostsRepository.removePost(post.toSavedPost())
} else {
savedPostsRepository.savePost(post)
savedPostsRepository.savePost(post.toSavedPost())
}
val newPosts = savedPosts.first()
withContext(mainDispatcher) {
@ -152,7 +179,7 @@ constructor(
suspend fun getPostComments(postId: String) =
withContext(ioDispatcher) {
when (val result = api.getPostDetails(postId)) {
is Success -> result.value
is Success -> result.value.toUIPost()
is Failure.NetworkFailure -> throw result.error
is Failure.UnknownFailure -> throw result.error
is Failure.HttpFailure -> {
@ -198,8 +225,6 @@ constructor(
viewModelScope.launch { readPostsRepository.markRead(postId) }
}
suspend fun isPostRead(postId: String) = readPostsRepository.isRead(postId)
/**
* Parses a given [String] into a [LocalDateTime]. This method is only intended to be used for
* dates in the format returned by the Lobsters API, and is not a general purpose parsing

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.
@ -23,6 +23,7 @@ constructor(
withContext(dbDispatcher) { readPostsQueries.markRead(postId) }
}
suspend fun isRead(postId: String): Boolean =
withContext(dbDispatcher) { readPostsQueries.isRead(postId).executeAsOneOrNull() != null }
fun isRead(postId: String): Boolean {
return readPostsQueries.isRead(postId).executeAsOneOrNull() != null
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright © 2022-2023 Harsh Shandilya.
* Copyright © 2022-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.
@ -18,6 +18,8 @@ import dev.msfjarvis.claw.android.glance.SavedPostsWidget
import dev.msfjarvis.claw.android.viewmodel.SavedPostsRepository
import dev.msfjarvis.claw.api.LobstersApi
import dev.msfjarvis.claw.model.LobstersPostDetails
import dev.msfjarvis.claw.model.UIPost
import dev.msfjarvis.claw.model.fromSavedPost
import dev.msfjarvis.claw.model.toSavedPost
import javax.inject.Inject
import kotlinx.coroutines.flow.first
@ -44,7 +46,10 @@ constructor(
.filterIsInstance<Success<LobstersPostDetails>>()
.map { result -> result.value.toSavedPost() }
.let { savedPostsRepository.savePosts(it) }
SavedPostsWidget(savedPostsRepository.savedPosts.first().take(50)).updateAll(applicationContext)
SavedPostsWidget(
savedPostsRepository.savedPosts.first().take(50).map(UIPost.Companion::fromSavedPost)
)
.updateAll(applicationContext)
return Result.success()
}
}