vm: let paging errors surface so retry() re-runs the load

Stream/ExpandedStream/Review/Search paging VMs were swallowing exceptions
inside manualPager and returning emptyList, so the pager never entered
LoadState.Error and the error-placeholder retry buttons were dead. Let
non-Auth exceptions propagate; keep the AuthException → SessionExpired
branch so 401 still hands off to Splash.

Also drop supervisorScope wrappers from CategoryStream/DevProfile/TopChart
VMs that launch no children — the surrounding try/catch already handles
everything.
This commit is contained in:
Rahul Patel
2026-05-25 03:30:03 +05:30
parent 0bcb087be2
commit 71f6538d46
8 changed files with 83 additions and 101 deletions

View File

@@ -45,7 +45,7 @@ fun CategoryBrowseScreen(
painter = painterResource(R.drawable.ic_refresh),
message = stringResource(R.string.error),
actionLabel = stringResource(R.string.action_retry),
onAction = { viewModel.fetchNextPage() }
onAction = { viewModel.fetch() }
)
} else {
val bundle = (uiState as? ViewState.Success<*>)?.data as? StreamBundle
@@ -60,7 +60,7 @@ fun CategoryBrowseScreen(
},
onAppClick = { onNavigateTo(Destination.AppDetails(it.packageName)) },
onClusterScrolled = { viewModel.fetchNextCluster(it) },
onScrolledToEnd = { viewModel.fetchNextPage() }
onScrolledToEnd = { viewModel.fetch() }
)
}
}

View File

@@ -90,9 +90,6 @@ class ExpandedStreamBrowseViewModel @AssistedInject constructor(
Log.w(TAG, "Expanded stream returned ${exception.code}, redirecting to Splash")
AuroraApp.events.send(AuthEvent.SessionExpired())
emptyList()
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch apps for page $page", exception)
emptyList()
}
PageResult(items)
}.flow.distinctUntilChanged()

View File

@@ -27,8 +27,11 @@ import androidx.paging.cachedIn
import com.aurora.extensions.TAG
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.gplayapi.exceptions.GooglePlayException
import com.aurora.gplayapi.helpers.web.WebStreamHelper
import com.aurora.store.AuroraApp
import com.aurora.store.data.PageResult
import com.aurora.store.data.event.AuthEvent
import com.aurora.store.data.paging.GenericPagingSource.Companion.manualPager
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@@ -55,10 +58,10 @@ class StreamBrowseViewModel @AssistedInject constructor(
val apps = _apps.asStateFlow()
init {
fetchApps()
fetch()
}
private fun fetchApps() {
fun fetch() {
var nextPageUrl: String = streamCluster.clusterNextPageUrl
manualPager { page ->
@@ -79,8 +82,9 @@ class StreamBrowseViewModel @AssistedInject constructor(
}
}
}
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch apps for $page: $nextPageUrl", exception)
} catch (exception: GooglePlayException.AuthException) {
Log.w(TAG, "Stream returned ${exception.code}, redirecting to Splash")
AuroraApp.events.send(AuthEvent.SessionExpired())
emptyList()
}
PageResult(items)

View File

@@ -38,7 +38,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
@HiltViewModel
class DevProfileViewModel @Inject constructor(
@@ -55,39 +54,35 @@ class DevProfileViewModel @Inject constructor(
fun getStreamBundle(devId: String) {
viewModelScope.launch(Dispatchers.IO) {
supervisorScope {
try {
devStream = appDetailsHelper.getDeveloperStream(devId)
streamBundle = devStream.streamBundle
liveData.postValue(ViewState.Success(devStream))
} catch (e: GooglePlayException.AuthException) {
Log.w(TAG, "Developer stream fetch returned ${e.code}, redirecting to Splash")
AuroraApp.events.send(AuthEvent.SessionExpired())
} catch (e: Exception) {
liveData.postValue(ViewState.Error(e.message))
}
try {
devStream = appDetailsHelper.getDeveloperStream(devId)
streamBundle = devStream.streamBundle
liveData.postValue(ViewState.Success(devStream))
} catch (e: GooglePlayException.AuthException) {
Log.w(TAG, "Developer stream fetch returned ${e.code}, redirecting to Splash")
AuroraApp.events.send(AuthEvent.SessionExpired())
} catch (e: Exception) {
liveData.postValue(ViewState.Error(e.message))
}
}
}
fun observeCluster(streamCluster: StreamCluster) {
viewModelScope.launch(Dispatchers.IO) {
supervisorScope {
try {
if (streamCluster.hasNext()) {
val newCluster = streamHelper.getNextStreamCluster(
streamCluster.id,
streamCluster.clusterNextPageUrl
)
updateCluster(newCluster)
devStream = devStream.copy(streamBundle = streamBundle)
liveData.postValue(ViewState.Success(devStream))
} else {
Log.i(TAG, "End of cluster")
}
} catch (e: Exception) {
liveData.postValue(ViewState.Error(e.message))
try {
if (streamCluster.hasNext()) {
val newCluster = streamHelper.getNextStreamCluster(
streamCluster.id,
streamCluster.clusterNextPageUrl
)
updateCluster(newCluster)
devStream = devStream.copy(streamBundle = streamBundle)
liveData.postValue(ViewState.Success(devStream))
} else {
Log.i(TAG, "End of cluster")
}
} catch (e: Exception) {
liveData.postValue(ViewState.Error(e.message))
}
}
}

View File

@@ -71,9 +71,6 @@ class ReviewViewModel @AssistedInject constructor(
Log.w(TAG, "Reviews fetch returned ${exception.code}, redirecting to Splash")
AuroraApp.events.send(AuthEvent.SessionExpired(packageName))
emptyList()
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch reviews for $page: $reviewsNextPageUrl", exception)
emptyList()
}
PageResult(items)
}.flow.distinctUntilChanged()

View File

@@ -113,9 +113,6 @@ class SearchViewModel @Inject constructor(
Log.w(TAG, "Search returned ${exception.code}, redirecting to Splash")
AuroraApp.events.send(AuthEvent.SessionExpired())
emptyList()
} catch (exception: Exception) {
Log.e(TAG, "Failed to search results for $query", exception)
emptyList()
}
PageResult(items)
}.flow.distinctUntilChanged()

View File

@@ -25,7 +25,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
@HiltViewModel(assistedFactory = CategoryStreamViewModel.Factory::class)
class CategoryStreamViewModel @AssistedInject constructor(
@@ -47,66 +46,62 @@ class CategoryStreamViewModel @AssistedInject constructor(
private var streamBundle = StreamBundle.EMPTY
init {
fetchNextPage()
fetch()
}
fun fetchNextPage() {
fun fetch() {
viewModelScope.launch(Dispatchers.IO) {
supervisorScope {
try {
if (!streamBundle.hasCluster() || streamBundle.hasNext()) {
val newBundle = if (streamBundle.streamClusters.isEmpty()) {
categoryStreamContract.fetch(browseUrl)
} else {
categoryStreamContract.nextStreamBundle(
streamBundle.id,
StreamContract.Category.NONE,
streamBundle.streamNextPageUrl
)
}
streamBundle = streamBundle.copy(
streamClusters = streamBundle.streamClusters + newBundle.streamClusters,
streamNextPageUrl = newBundle.streamNextPageUrl
)
_viewState.value = ViewState.Success(streamBundle)
try {
if (!streamBundle.hasCluster() || streamBundle.hasNext()) {
val newBundle = if (streamBundle.streamClusters.isEmpty()) {
categoryStreamContract.fetch(browseUrl)
} else {
Log.i(TAG, "End of Bundle")
categoryStreamContract.nextStreamBundle(
streamBundle.id,
StreamContract.Category.NONE,
streamBundle.streamNextPageUrl
)
}
} catch (e: Exception) {
_viewState.value = ViewState.Error(e.message)
streamBundle = streamBundle.copy(
streamClusters = streamBundle.streamClusters + newBundle.streamClusters,
streamNextPageUrl = newBundle.streamNextPageUrl
)
_viewState.value = ViewState.Success(streamBundle)
} else {
Log.i(TAG, "End of Bundle")
}
} catch (e: Exception) {
_viewState.value = ViewState.Error(e.message)
}
}
}
fun fetchNextCluster(streamCluster: StreamCluster) {
viewModelScope.launch(Dispatchers.IO) {
supervisorScope {
try {
if (streamCluster.hasNext()) {
val newCluster = categoryStreamContract.nextStreamCluster(
streamCluster.id,
streamCluster.clusterNextPageUrl
)
val mergedCluster = streamCluster.copy(
clusterNextPageUrl = newCluster.clusterNextPageUrl,
clusterAppList =
streamCluster.clusterAppList + newCluster.clusterAppList
)
try {
if (streamCluster.hasNext()) {
val newCluster = categoryStreamContract.nextStreamCluster(
streamCluster.id,
streamCluster.clusterNextPageUrl
)
val mergedCluster = streamCluster.copy(
clusterNextPageUrl = newCluster.clusterNextPageUrl,
clusterAppList =
streamCluster.clusterAppList + newCluster.clusterAppList
)
val newClusters = streamBundle.streamClusters.toMutableMap().apply {
this[streamCluster.id] = mergedCluster
}
streamBundle = streamBundle.copy(streamClusters = newClusters)
_viewState.value = ViewState.Success(streamBundle)
} else {
Log.i(TAG, "End of cluster")
val newClusters = streamBundle.streamClusters.toMutableMap().apply {
this[streamCluster.id] = mergedCluster
}
} catch (e: Exception) {
Log.e(TAG, "Failed to fetch next cluster", e)
streamBundle = streamBundle.copy(streamClusters = newClusters)
_viewState.value = ViewState.Success(streamBundle)
} else {
Log.i(TAG, "End of cluster")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to fetch next cluster", e)
}
}
}

View File

@@ -33,7 +33,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
@HiltViewModel
class TopChartViewModel @Inject constructor(
@@ -69,21 +68,19 @@ class TopChartViewModel @Inject constructor(
fun nextCluster(type: TopChartsContract.Type, chart: TopChartsContract.Chart) {
viewModelScope.launch(Dispatchers.IO) {
supervisorScope {
try {
val target = targetCluster(type, chart)
if (target.hasNext()) {
val newCluster = topChartsContract.getNextStreamCluster(
target.id,
target.clusterNextPageUrl
)
try {
val target = targetCluster(type, chart)
if (target.hasNext()) {
val newCluster = topChartsContract.getNextStreamCluster(
target.id,
target.clusterNextPageUrl
)
updateCluster(type, chart, newCluster)
updateCluster(type, chart, newCluster)
_state.value = ViewState.Success(targetCluster(type, chart))
}
} catch (_: Exception) {
_state.value = ViewState.Success(targetCluster(type, chart))
}
} catch (_: Exception) {
}
}
}