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), painter = painterResource(R.drawable.ic_refresh),
message = stringResource(R.string.error), message = stringResource(R.string.error),
actionLabel = stringResource(R.string.action_retry), actionLabel = stringResource(R.string.action_retry),
onAction = { viewModel.fetchNextPage() } onAction = { viewModel.fetch() }
) )
} else { } else {
val bundle = (uiState as? ViewState.Success<*>)?.data as? StreamBundle val bundle = (uiState as? ViewState.Success<*>)?.data as? StreamBundle
@@ -60,7 +60,7 @@ fun CategoryBrowseScreen(
}, },
onAppClick = { onNavigateTo(Destination.AppDetails(it.packageName)) }, onAppClick = { onNavigateTo(Destination.AppDetails(it.packageName)) },
onClusterScrolled = { viewModel.fetchNextCluster(it) }, 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") Log.w(TAG, "Expanded stream returned ${exception.code}, redirecting to Splash")
AuroraApp.events.send(AuthEvent.SessionExpired()) AuroraApp.events.send(AuthEvent.SessionExpired())
emptyList() emptyList()
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch apps for page $page", exception)
emptyList()
} }
PageResult(items) PageResult(items)
}.flow.distinctUntilChanged() }.flow.distinctUntilChanged()

View File

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

View File

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

View File

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

View File

@@ -25,7 +25,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
@HiltViewModel(assistedFactory = CategoryStreamViewModel.Factory::class) @HiltViewModel(assistedFactory = CategoryStreamViewModel.Factory::class)
class CategoryStreamViewModel @AssistedInject constructor( class CategoryStreamViewModel @AssistedInject constructor(
@@ -47,66 +46,62 @@ class CategoryStreamViewModel @AssistedInject constructor(
private var streamBundle = StreamBundle.EMPTY private var streamBundle = StreamBundle.EMPTY
init { init {
fetchNextPage() fetch()
} }
fun fetchNextPage() { fun fetch() {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
supervisorScope { try {
try { if (!streamBundle.hasCluster() || streamBundle.hasNext()) {
if (!streamBundle.hasCluster() || streamBundle.hasNext()) { val newBundle = if (streamBundle.streamClusters.isEmpty()) {
val newBundle = if (streamBundle.streamClusters.isEmpty()) { categoryStreamContract.fetch(browseUrl)
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)
} else { } 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) { fun fetchNextCluster(streamCluster: StreamCluster) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
supervisorScope { try {
try { if (streamCluster.hasNext()) {
if (streamCluster.hasNext()) { val newCluster = categoryStreamContract.nextStreamCluster(
val newCluster = categoryStreamContract.nextStreamCluster( streamCluster.id,
streamCluster.id, streamCluster.clusterNextPageUrl
streamCluster.clusterNextPageUrl )
) val mergedCluster = streamCluster.copy(
val mergedCluster = streamCluster.copy( clusterNextPageUrl = newCluster.clusterNextPageUrl,
clusterNextPageUrl = newCluster.clusterNextPageUrl, clusterAppList =
clusterAppList = streamCluster.clusterAppList + newCluster.clusterAppList
streamCluster.clusterAppList + newCluster.clusterAppList )
)
val newClusters = streamBundle.streamClusters.toMutableMap().apply { val newClusters = streamBundle.streamClusters.toMutableMap().apply {
this[streamCluster.id] = mergedCluster this[streamCluster.id] = mergedCluster
}
streamBundle = streamBundle.copy(streamClusters = newClusters)
_viewState.value = ViewState.Success(streamBundle)
} else {
Log.i(TAG, "End of cluster")
} }
} catch (e: Exception) { streamBundle = streamBundle.copy(streamClusters = newClusters)
Log.e(TAG, "Failed to fetch next cluster", e) _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.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
@HiltViewModel @HiltViewModel
class TopChartViewModel @Inject constructor( class TopChartViewModel @Inject constructor(
@@ -69,21 +68,19 @@ class TopChartViewModel @Inject constructor(
fun nextCluster(type: TopChartsContract.Type, chart: TopChartsContract.Chart) { fun nextCluster(type: TopChartsContract.Type, chart: TopChartsContract.Chart) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
supervisorScope { try {
try { val target = targetCluster(type, chart)
val target = targetCluster(type, chart) if (target.hasNext()) {
if (target.hasNext()) { val newCluster = topChartsContract.getNextStreamCluster(
val newCluster = topChartsContract.getNextStreamCluster( target.id,
target.id, target.clusterNextPageUrl
target.clusterNextPageUrl )
)
updateCluster(type, chart, newCluster) updateCluster(type, chart, newCluster)
_state.value = ViewState.Success(targetCluster(type, chart)) _state.value = ViewState.Success(targetCluster(type, chart))
}
} catch (_: Exception) {
} }
} catch (_: Exception) {
} }
} }
} }