pref: Use Paging3 for changelogs instead of custom pagination

This commit is contained in:
Ushie
2026-03-22 19:21:35 +03:00
parent 742e6e8e78
commit 77e73d1ee1
8 changed files with 101 additions and 189 deletions

View File

@@ -38,6 +38,7 @@ dependencies {
implementation(libs.compose.material.icons.extended)
implementation(libs.compose.material3)
implementation(libs.navigation.compose)
implementation(libs.paging3)
// Accompanist
implementation(libs.accompanist.drawablepainter)

View File

@@ -2,6 +2,8 @@ package app.revanced.manager.domain.repository
import android.os.Parcelable
import androidx.core.net.toUri
import androidx.paging.PagingSource
import androidx.paging.PagingState
import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.network.dto.ReVancedAssetHistory
import app.revanced.manager.network.utils.getOrThrow
@@ -18,50 +20,29 @@ sealed interface ChangelogSource : Parcelable {
}
class ChangelogsRepository(
private val api: ReVancedAPI
) {
private var all: List<ReVancedAssetHistory> = emptyList()
private var page = 0
private val api: ReVancedAPI,
private val source: ChangelogSource,
) : PagingSource<Int, ReVancedAssetHistory>() {
suspend fun loadInitial(
source: ChangelogSource,
pageSize: Int
): PageResult<ReVancedAssetHistory> {
all = when (source) {
is ChangelogSource.Manager ->
api.getAppHistory().getOrThrow()
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ReVancedAssetHistory> {
return try {
val items = when (source) {
is ChangelogSource.Manager ->
api.getAppHistory().getOrThrow()
is ChangelogSource.Patches ->
api.getPatchesHistory(source.baseUrl, source.prerelease).getOrThrow()
is ChangelogSource.Patches ->
api.getPatchesHistory(source.baseUrl, source.prerelease).getOrThrow()
}
LoadResult.Page(
data = items,
prevKey = null,
nextKey = null
)
} catch (e: Exception) {
LoadResult.Error(e)
}
page = 1
val items = all.take(pageSize)
return PageResult(
items = items,
hasMore = hasMore(pageSize)
)
}
fun loadNext(pageSize: Int): PageResult<ReVancedAssetHistory> {
val items = all
.drop(page * pageSize)
.take(pageSize)
page++
return PageResult(
items = items,
hasMore = hasMore(pageSize)
)
}
private fun hasMore(pageSize: Int) =
page * pageSize < all.size
}
data class PageResult<T>(
val items: List<T>,
val hasMore: Boolean
)
override fun getRefreshKey(state: PagingState<Int, ReVancedAssetHistory>): Int? = null
}

View File

@@ -31,85 +31,65 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import app.revanced.manager.R
import app.revanced.manager.network.dto.ReVancedAssetHistory
import app.revanced.manager.util.relativeTime
sealed interface ChangelogUiState {
data object Loading : ChangelogUiState
data class Error(val error: String) : ChangelogUiState
data class Success(
val changelogs: List<ReVancedAssetHistory>,
val hasMore: Boolean = false,
val isLoadingMore: Boolean = false,
) : ChangelogUiState
}
@Composable
fun ChangelogList(
state: ChangelogUiState,
onLoadMore: () -> Unit,
changelogs: LazyPagingItems<ReVancedAssetHistory>,
modifier: Modifier = Modifier,
) {
val listState = rememberLazyListState()
val shouldLoadMore by remember {
derivedStateOf {
val lastVisible = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
val total = listState.layoutInfo.totalItemsCount
val canScroll = listState.canScrollForward || listState.canScrollBackward
(lastVisible >= total - 2 || !canScroll) && total > 0
}
}
LaunchedEffect(shouldLoadMore, state) {
if (shouldLoadMore) onLoadMore()
}
Box(
modifier = modifier.then(Modifier.fillMaxSize()),
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
when (state) {
is ChangelogUiState.Loading -> LoadingIndicator()
when {
changelogs.loadState.refresh is LoadState.Loading -> LoadingIndicator()
is ChangelogUiState.Error -> Text(
text = state.error,
changelogs.loadState.refresh is LoadState.Error -> {
val error = changelogs.loadState.refresh as LoadState.Error
Text(
text = error.error.message ?: stringResource(R.string.changelog_download_fail),
style = MaterialTheme.typography.titleLarge
)
}
changelogs.itemCount == 0 -> Text(
text = stringResource(R.string.no_changelogs_found),
style = MaterialTheme.typography.titleLarge
)
is ChangelogUiState.Success -> {
if (state.changelogs.isEmpty()) {
Text(
text = stringResource(R.string.no_changelogs_found),
style = MaterialTheme.typography.titleLarge
)
} else {
LazyColumnWithScrollbar(
modifier = Modifier.fillMaxSize(),
state = listState
) {
items(
items = state.changelogs,
key = { it.version }
) { changelog ->
else -> {
val listState = rememberLazyListState()
LazyColumnWithScrollbar(
modifier = Modifier.fillMaxSize(),
state = listState
) {
items(
count = changelogs.itemCount,
key = { changelogs.peek(it)?.version ?: it }
) { index ->
changelogs[index]?.let { changelog ->
ChangelogItem(
changelog = changelog,
showDivider = changelog != state.changelogs.last()
showDivider = index < changelogs.itemCount - 1
)
}
}
if (state.isLoadingMore) {
item(key = "loading_more") {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
if (changelogs.loadState.append is LoadState.Loading) {
item(key = "loading_more") {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
}

View File

@@ -32,6 +32,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.paging.compose.collectAsLazyPagingItems
import app.revanced.manager.R
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.BottomContentBar
@@ -48,6 +49,7 @@ fun UpdateScreen(
vm: UpdateViewModel = koinViewModel()
) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val changelogs = vm.changelogs.collectAsLazyPagingItems()
val buttonConfig = when (vm.state) {
State.CAN_DOWNLOAD -> Triple(
@@ -123,10 +125,7 @@ fun UpdateScreen(
)
}
ChangelogList(
state = vm.changelogsState,
onLoadMore = vm::loadNextPage,
)
ChangelogList(changelogs = changelogs, modifier = Modifier.padding(paddingValues))
}
}
}

View File

@@ -8,6 +8,7 @@ import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.paging.compose.collectAsLazyPagingItems
import app.revanced.manager.domain.repository.ChangelogSource
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.ChangelogList
@@ -23,6 +24,7 @@ fun ChangelogsSettingsScreen(
vm: ChangelogsViewModel = koinViewModel { parametersOf(source) }
) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val changelogs = vm.changelogs.collectAsLazyPagingItems()
Scaffold(
topBar = {
@@ -34,6 +36,6 @@ fun ChangelogsSettingsScreen(
},
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { paddingValues ->
ChangelogList(modifier = Modifier.padding(paddingValues), state = vm.state, onLoadMore =vm::loadNextPage)
ChangelogList(changelogs = changelogs, modifier = Modifier.padding(paddingValues))
}
}

View File

@@ -1,60 +1,26 @@
package app.revanced.manager.ui.viewmodel
import android.app.Application
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.R
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import app.revanced.manager.domain.repository.ChangelogSource
import app.revanced.manager.domain.repository.ChangelogsRepository
import app.revanced.manager.ui.component.ChangelogUiState
import app.revanced.manager.util.uiSafe
import kotlinx.coroutines.launch
import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.network.dto.ReVancedAssetHistory
import kotlinx.coroutines.flow.Flow
class ChangelogsViewModel(
private val repository: ChangelogsRepository,
private val app: Application,
private val api: ReVancedAPI,
private val source: ChangelogSource,
) : ViewModel() {
var state: ChangelogUiState by mutableStateOf(ChangelogUiState.Loading)
private set
private val pageSize = 2
init {
viewModelScope.launch {
uiSafe(app, R.string.changelog_download_fail, "Failed to download changelog") {
val result = repository.loadInitial(source, pageSize)
state = ChangelogUiState.Success(
changelogs = result.items,
hasMore = result.hasMore
)
}
if (state is ChangelogUiState.Loading) {
state = ChangelogUiState.Error(
app.getString(R.string.changelog_download_fail)
)
}
}
}
fun loadNextPage() {
val current = state as? ChangelogUiState.Success ?: return
if (current.isLoadingMore || !current.hasMore) return
state = current.copy(isLoadingMore = true)
val result = repository.loadNext(pageSize)
state = current.copy(
changelogs = current.changelogs + result.items,
isLoadingMore = false,
hasMore = result.hasMore
)
}
val changelogs: Flow<PagingData<ReVancedAssetHistory>> = Pager(
config = PagingConfig(
pageSize = 10,
enablePlaceholders = false
),
pagingSourceFactory = { ChangelogsRepository(api, source) }
).flow.cachedIn(viewModelScope)
}

View File

@@ -10,6 +10,10 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import app.revanced.manager.R
import app.revanced.manager.data.platform.Filesystem
import app.revanced.manager.data.platform.NetworkInfo
@@ -17,13 +21,14 @@ import app.revanced.manager.domain.repository.ChangelogSource
import app.revanced.manager.domain.repository.ChangelogsRepository
import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.network.dto.ReVancedAsset
import app.revanced.manager.network.dto.ReVancedAssetHistory
import app.revanced.manager.network.service.HttpService
import app.revanced.manager.ui.component.ChangelogUiState
import app.revanced.manager.util.toast
import app.revanced.manager.util.uiSafe
import io.ktor.client.plugins.onDownload
import io.ktor.client.request.url
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
@@ -37,7 +42,8 @@ import ru.solrudev.ackpine.session.await
import ru.solrudev.ackpine.session.parameters.Confirmation
class UpdateViewModel(
private val changelogsRepository: ChangelogsRepository,
private val api: ReVancedAPI,
private val source: ChangelogSource,
private val downloadOnScreenEntry: Boolean
) : ViewModel(), KoinComponent {
private val app: Application by inject()
@@ -66,8 +72,13 @@ class UpdateViewModel(
var releaseInfo: ReVancedAsset? by mutableStateOf(null)
private set
var changelogsState: ChangelogUiState by mutableStateOf(ChangelogUiState.Loading)
private val changelogsPageSize = 2
val changelogs: Flow<PagingData<ReVancedAssetHistory>> = Pager(
config = PagingConfig(
pageSize = 10,
enablePlaceholders = false
),
pagingSourceFactory = { ChangelogsRepository(api, source) }
).flow.cachedIn(viewModelScope)
private val location = fs.tempDir.resolve("updater.apk")
@@ -77,44 +88,14 @@ class UpdateViewModel(
releaseInfo = reVancedAPI.getAppUpdate()
?: throw Exception("No update available")
val result = changelogsRepository.loadInitial(
ChangelogSource.Manager,
changelogsPageSize
)
changelogsState = ChangelogUiState.Success(
changelogs = result.items,
hasMore = result.hasMore
)
if (downloadOnScreenEntry) {
downloadUpdate()
} else {
state = State.CAN_DOWNLOAD
}
}
if (changelogsState is ChangelogUiState.Loading) {
changelogsState = ChangelogUiState.Error(
app.getString(R.string.changelog_download_fail)
)
}
}
}
fun loadNextPage() {
val current = changelogsState as? ChangelogUiState.Success ?: return
if (current.isLoadingMore || !current.hasMore) return
changelogsState = current.copy(isLoadingMore = true)
val result = changelogsRepository.loadNext(changelogsPageSize)
changelogsState = current.copy(
changelogs = current.changelogs + result.items,
isLoadingMore = false,
hasMore = result.hasMore
)
}
fun downloadUpdate(ignoreInternetCheck: Boolean = false) = viewModelScope.launch {
uiSafe(app, R.string.failed_to_download_update, "Failed to download update") {

View File

@@ -40,6 +40,7 @@ binary-compatibility-validator = "0.18.1"
semver-parser = "3.0.0"
ackpine = "0.20.6"
foundation-layout = "1.10.5"
paging3 = "3.4.2"
[libraries]
# AndroidX Core
@@ -62,6 +63,7 @@ compose-livedata = { group = "androidx.compose.runtime", name = "runtime-livedat
compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" }
paging3 = { group = "androidx.paging", name = "paging-compose", version.ref = "paging3" }
# Coil
coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }