From 7452a18bf9d830b56f22c4a63f8e03744eecb171 Mon Sep 17 00:00:00 2001 From: Aayush Gupta Date: Sat, 23 Mar 2024 14:20:43 +0530 Subject: [PATCH] NodeScreen: Implement paging support Signed-off-by: Aayush Gupta --- app/build.gradle.kts | 5 ++ .../io/aayush/relabs/network/XDAInterface.kt | 5 +- .../io/aayush/relabs/network/XDARepository.kt | 11 +++- .../network/paging/GenericPagingSource.kt | 51 +++++++++++++++++++ .../relabs/ui/screens/node/NodeScreen.kt | 39 +++++++------- .../relabs/ui/screens/node/NodeViewModel.kt | 33 ++---------- 6 files changed, 96 insertions(+), 48 deletions(-) create mode 100644 app/src/main/java/io/aayush/relabs/network/paging/GenericPagingSource.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fc59e59..58d5825 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -150,4 +150,9 @@ dependencies { val coroutinesVersion = "1.8.0" implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion") + + // Paging3 + val pagingVersion = "3.2.1" + implementation("androidx.paging:paging-runtime-ktx:$pagingVersion") + implementation("androidx.paging:paging-compose:$pagingVersion") } diff --git a/app/src/main/java/io/aayush/relabs/network/XDAInterface.kt b/app/src/main/java/io/aayush/relabs/network/XDAInterface.kt index ebc3b02..f2f0369 100644 --- a/app/src/main/java/io/aayush/relabs/network/XDAInterface.kt +++ b/app/src/main/java/io/aayush/relabs/network/XDAInterface.kt @@ -57,7 +57,10 @@ interface XDAInterface { suspend fun getWatchedThreads(): Response @GET("forums/{id}/threads/") - suspend fun getThreadsByNode(@Path("id") nodeID: Int): Response + suspend fun getThreadsByNode( + @Path("id") nodeID: Int, + @Query("page") page: Int? = null, + ): Response @POST("threads/{id}/audapp-watch") suspend fun watchThread(@Path("id") threadID: Int): Response diff --git a/app/src/main/java/io/aayush/relabs/network/XDARepository.kt b/app/src/main/java/io/aayush/relabs/network/XDARepository.kt index a1960f5..601cdc9 100644 --- a/app/src/main/java/io/aayush/relabs/network/XDARepository.kt +++ b/app/src/main/java/io/aayush/relabs/network/XDARepository.kt @@ -1,6 +1,7 @@ package io.aayush.relabs.network import android.util.Log +import androidx.paging.PagingData import io.aayush.relabs.network.data.common.Success import io.aayush.relabs.network.data.alert.Alerts import io.aayush.relabs.network.data.expo.ExpoData @@ -9,9 +10,12 @@ import io.aayush.relabs.network.data.post.PostInfo import io.aayush.relabs.network.data.post.PostReply import io.aayush.relabs.network.data.react.PostReact import io.aayush.relabs.network.data.react.React +import io.aayush.relabs.network.data.thread.Thread import io.aayush.relabs.network.data.thread.ThreadInfo import io.aayush.relabs.network.data.thread.Threads import io.aayush.relabs.network.data.user.Me +import io.aayush.relabs.network.paging.GenericPagingSource.Companion.createPager +import kotlinx.coroutines.flow.Flow import java.util.UUID import okhttp3.MultipartBody import retrofit2.Response @@ -71,8 +75,11 @@ class XDARepository @Inject constructor( return safeExecute { xdaInterface.getWatchedThreads() } } - suspend fun getThreadsByNode(nodeID: Int): Threads? { - return safeExecute { xdaInterface.getThreadsByNode(nodeID) } + fun getThreadsByNode(nodeID: Int): Flow> { + return createPager { page -> + val threads = safeExecute { xdaInterface.getThreadsByNode(nodeID, page) } + threads?.let { if (page == 1) it.sticky + it.threads else it.threads }.orEmpty() + }.flow } suspend fun watchThread(threadID: Int): Success? { diff --git a/app/src/main/java/io/aayush/relabs/network/paging/GenericPagingSource.kt b/app/src/main/java/io/aayush/relabs/network/paging/GenericPagingSource.kt new file mode 100644 index 0000000..4de0070 --- /dev/null +++ b/app/src/main/java/io/aayush/relabs/network/paging/GenericPagingSource.kt @@ -0,0 +1,51 @@ +package io.aayush.relabs.network.paging + +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingSource +import androidx.paging.PagingState +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class GenericPagingSource( + private val totalPages: Int? = null, + private val block: suspend (Int) -> List +) : PagingSource() { + + companion object { + private const val DEFAULT_PAGE_SIZE = 20 + + fun createPager( + totalPages: Int? = null, + pageSize: Int = DEFAULT_PAGE_SIZE, + enablePlaceholders: Boolean = false, + block: suspend (Int) -> List + ): Pager = Pager( + config = PagingConfig(enablePlaceholders = enablePlaceholders, pageSize = pageSize), + pagingSourceFactory = { GenericPagingSource(totalPages, block) } + ) + } + + override suspend fun load(params: LoadParams): LoadResult { + val page = params.key ?: 1 + return try { + withContext(Dispatchers.IO) { + val response = block(page) + LoadResult.Page( + data = response, + prevKey = if (page == 1) null else page - 1, + nextKey = if (totalPages != null && page == totalPages) null else page + 1 + ) + } + } catch (e: Exception) { + LoadResult.Error(e) + } + } + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { anchorPosition -> + state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1) + ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/aayush/relabs/ui/screens/node/NodeScreen.kt b/app/src/main/java/io/aayush/relabs/ui/screens/node/NodeScreen.kt index e3eff9b..b507a74 100644 --- a/app/src/main/java/io/aayush/relabs/ui/screens/node/NodeScreen.kt +++ b/app/src/main/java/io/aayush/relabs/ui/screens/node/NodeScreen.kt @@ -1,21 +1,19 @@ package io.aayush.relabs.ui.screens.node -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController -import io.aayush.relabs.network.data.thread.Thread +import androidx.paging.LoadState +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.itemContentType +import androidx.paging.compose.itemKey import io.aayush.relabs.ui.components.MainTopAppBar import io.aayush.relabs.ui.components.ThreadPreviewItem import io.aayush.relabs.ui.navigation.Screen @@ -27,12 +25,8 @@ fun NodeScreen( nodeTitle: String = String(), viewModel: NodeViewModel = hiltViewModel() ) { - val loading: Boolean by viewModel.loading.collectAsStateWithLifecycle() - val threads: List? by viewModel.threads.collectAsStateWithLifecycle() - LaunchedEffect(key1 = Unit) { - viewModel.getThreads(nodeID) - } + val threads = viewModel.getThreads(nodeID).collectAsLazyPagingItems() Scaffold( modifier = Modifier.fillMaxSize(), @@ -44,16 +38,27 @@ fun NodeScreen( ) } ) { - Column(modifier = Modifier.padding(it)) { - if (loading) { - LazyColumn(modifier = Modifier.fillMaxHeight()) { + LazyColumn( + modifier = Modifier + .fillMaxHeight() + .padding(it) + ) { + when (threads.loadState.refresh) { + is LoadState.Error -> { + // TODO: Handle first load error + } + is LoadState.Loading -> { items(20) { ThreadPreviewItem(modifier = Modifier.padding(10.dp), loading = true) } } - } else { - LazyColumn(modifier = Modifier.fillMaxHeight()) { - items(items = threads ?: emptyList(), key = { t -> t.id }) { thread -> + else -> { + items( + count = threads.itemCount, + key = threads.itemKey { t -> t.id }, + contentType = threads.itemContentType { "Threads" } + ) { index -> + val thread = threads[index] ?: return@items ThreadPreviewItem( modifier = Modifier.padding(10.dp), avatarURL = thread.user.avatar?.data?.medium ?: String(), diff --git a/app/src/main/java/io/aayush/relabs/ui/screens/node/NodeViewModel.kt b/app/src/main/java/io/aayush/relabs/ui/screens/node/NodeViewModel.kt index 224e7be..bc20d0f 100644 --- a/app/src/main/java/io/aayush/relabs/ui/screens/node/NodeViewModel.kt +++ b/app/src/main/java/io/aayush/relabs/ui/screens/node/NodeViewModel.kt @@ -2,13 +2,12 @@ package io.aayush.relabs.ui.screens.node import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import androidx.paging.cachedIn import dagger.hilt.android.lifecycle.HiltViewModel import io.aayush.relabs.network.XDARepository import io.aayush.relabs.network.data.thread.Thread -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.Flow import javax.inject.Inject @HiltViewModel @@ -16,29 +15,7 @@ class NodeViewModel @Inject constructor( private val xdaRepository: XDARepository ) : ViewModel() { - private val _loading = MutableStateFlow(false) - val loading = _loading.asStateFlow() - - private val _threads = MutableStateFlow?>(emptyList()) - val threads = _threads.asStateFlow() - - fun getThreads(nodeID: Int) { - if (!threads.value.isNullOrEmpty()) return - viewModelScope.launch(Dispatchers.IO) { - fetch { - _threads.value = xdaRepository.getThreadsByNode(nodeID)?.let { - it.sticky + it.threads - }?.distinctBy { it.id } - } - } - } - - private inline fun fetch(block: () -> T): T? { - return try { - _loading.value = true - block() - } finally { - _loading.value = false - } + fun getThreads(nodeID: Int): Flow> { + return xdaRepository.getThreadsByNode(nodeID).cachedIn(viewModelScope) } }