compose: details: Encapsulate app-dependent navigation logic

Make it harder to accidentally navigate to an extra screen dependent upon
an app object which is unavailable.

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2025-08-27 14:31:01 +08:00
parent c3a4e4a4f0
commit 8f2a673c6f
9 changed files with 86 additions and 82 deletions

View File

@@ -6,31 +6,23 @@
package com.aurora.store.compose.navigation
import android.os.Parcelable
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.navigation3.runtime.NavKey
import com.aurora.store.R
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
/**
* Destination (Screen) for navigation in compose
* @param label Label of the screen
* @param icon Optional icon for the screen; Must not be null if screen is a top-level destination
* Destinations for navigation in compose
*/
@Parcelize
@Serializable
sealed class Screen(
@StringRes val label: Int? = null,
@DrawableRes val icon: Int? = null
) : NavKey, Parcelable {
sealed class Screen : NavKey, Parcelable {
companion object {
const val PARCEL_KEY = "SCREEN"
}
@Serializable
data object Blacklist : Screen(label = R.string.title_blacklist_manager)
data object Blacklist : Screen()
@Serializable
data class DevProfile(val developerId: String): Screen()
@@ -40,40 +32,4 @@ sealed class Screen(
@Serializable
data object Search : Screen()
/**
* Child screen of [AppDetails]; Avoid navigating to this screen directly.
*/
@Serializable
data object DetailsMore : Screen()
/**
* Child screen of [AppDetails]; Avoid navigating to this screen directly.
*/
@Serializable
data class DetailsScreenshot(val index: Int) : Screen()
/**
* Child screen of [AppDetails]; Avoid navigating to this screen directly.
*/
@Serializable
data object DetailsExodus : Screen()
/**
* Child screen of [AppDetails]; Avoid navigating to this screen directly.
*/
@Serializable
data object DetailsReview : Screen()
/**
* Child screen of [AppDetails]; Avoid navigating to this screen directly.
*/
@Serializable
data object DetailsPermission : Screen()
/**
* Child screen of [AppDetails]; Avoid navigating to this screen directly.
*/
@Serializable
data object DetailsManualDownload : Screen()
}

View File

@@ -43,6 +43,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation3.runtime.NavKey
import coil3.compose.LocalAsyncImagePreviewHandler
import com.aurora.Constants.SHARE_URL
import com.aurora.extensions.appInfo
@@ -74,6 +75,7 @@ import com.aurora.store.compose.ui.details.components.AppRatingAndReviews
import com.aurora.store.compose.ui.details.components.AppScreenshots
import com.aurora.store.compose.ui.details.components.AppTags
import com.aurora.store.compose.ui.details.components.AppTesting
import com.aurora.store.compose.ui.details.navigation.ExtraScreen
import com.aurora.store.compose.ui.dev.DevProfileScreen
import com.aurora.store.data.installer.AppInstaller
import com.aurora.store.data.model.AppState
@@ -212,7 +214,7 @@ private fun ScreenContentApp(
scaffoldDirective = scaffoldDirective.copy(maxHorizontalPartitions = 1)
}
val scaffoldNavigator = rememberSupportingPaneScaffoldNavigator<Screen>(
val scaffoldNavigator = rememberSupportingPaneScaffoldNavigator<NavKey>(
scaffoldDirective = scaffoldDirective,
adaptStrategies = SupportingPaneScaffoldDefaults.adaptStrategies(
supportingPaneAdaptStrategy = AdaptStrategy
@@ -224,13 +226,13 @@ private fun ScreenContentApp(
val shouldShowMenuOnMainPane = scaffoldNavigator
.scaffoldValue[SupportingPaneScaffoldRole.Supporting] == PaneAdaptedValue.Hidden
fun showMainPane() {
fun onNavigateBack() {
coroutineScope.launch {
scaffoldNavigator.navigateBack()
}
}
fun showExtraPane(screen: Screen) {
fun showExtraPane(screen: NavKey) {
coroutineScope.launch {
scaffoldNavigator.navigateTo(SupportingPaneScaffoldRole.Extra, screen)
}
@@ -242,7 +244,7 @@ private fun ScreenContentApp(
when (menuItem) {
AppDetailsMenuItem.FAVORITE -> onFavorite()
AppDetailsMenuItem.MANUAL_DOWNLOAD -> {
showExtraPane(Screen.DetailsManualDownload)
showExtraPane(ExtraScreen.ManualDownload)
}
AppDetailsMenuItem.SHARE -> context.share(app)
AppDetailsMenuItem.APP_INFO -> context.appInfo(app.packageName)
@@ -295,7 +297,7 @@ private fun ScreenContentApp(
primaryActionDisplayName = primaryActionName,
secondaryActionDisplayName = stringResource(R.string.title_manual_download),
onPrimaryAction = onDownload,
onSecondaryAction = { showExtraPane(Screen.DetailsManualDownload) }
onSecondaryAction = { showExtraPane(ExtraScreen.ManualDownload) }
)
}
}
@@ -336,18 +338,18 @@ private fun ScreenContentApp(
HeaderComposable(
title = stringResource(R.string.details_more_about_app),
subtitle = app.shortDescription,
onClick = { showExtraPane(Screen.DetailsMore) }
onClick = { showExtraPane(ExtraScreen.More) }
)
AppScreenshots(
screenshots = app.screenshots,
onNavigateToScreenshot = { showExtraPane(Screen.DetailsScreenshot(it)) }
onNavigateToScreenshot = { showExtraPane(ExtraScreen.Screenshot(it)) }
)
AppRatingAndReviews(
rating = app.rating,
featuredReviews = featuredReviews,
onNavigateToDetailsReview = { showExtraPane(Screen.DetailsReview) }
onNavigateToDetailsReview = { showExtraPane(ExtraScreen.Review) }
)
if (!isAnonymous && app.testingProgram?.isAvailable == true) {
@@ -370,7 +372,7 @@ private fun ScreenContentApp(
stringResource(R.string.details_no_permission)
},
onClick = if (app.permissions.isNotEmpty()) {
{ showExtraPane(Screen.DetailsPermission) }
{ showExtraPane(ExtraScreen.Permission) }
} else {
null
}
@@ -386,7 +388,7 @@ private fun ScreenContentApp(
AppPrivacy(
report = exodusReport,
onNavigateToDetailsExodus = if (!exodusReport?.trackers.isNullOrEmpty()) {
{ showExtraPane(Screen.DetailsExodus) }
{ showExtraPane(ExtraScreen.Exodus) }
} else {
null
}
@@ -447,35 +449,41 @@ private fun ScreenContentApp(
scaffoldNavigator.currentDestination?.contentKey?.let { screen ->
AnimatedPane {
when (screen) {
is Screen.DetailsReview -> DetailsReviewScreen(
is ExtraScreen.Review -> ReviewScreen(
packageName = app.packageName,
onNavigateUp = ::showMainPane
onNavigateUp = ::onNavigateBack
)
is Screen.DetailsExodus -> DetailsExodusScreen(
is ExtraScreen.Exodus -> ExodusScreen(
packageName = app.packageName,
onNavigateUp = ::showMainPane
onNavigateUp = ::onNavigateBack
)
is Screen.DetailsMore -> DetailsMoreScreen(
is ExtraScreen.More -> MoreScreen(
packageName = app.packageName,
onNavigateUp = ::showMainPane,
onNavigateUp = ::onNavigateBack,
onNavigateToAppDetails = onNavigateToAppDetails
)
is Screen.DetailsPermission -> DetailsPermissionScreen(
is ExtraScreen.Permission -> PermissionScreen(
packageName = app.packageName,
onNavigateUp = ::showMainPane
onNavigateUp = ::onNavigateBack
)
is Screen.DetailsScreenshot -> DetailsScreenshotScreen(
is ExtraScreen.Screenshot -> ScreenshotScreen(
packageName = app.packageName,
index = screen.index,
onNavigateUp = ::showMainPane
onNavigateUp = ::onNavigateBack
)
is Screen.DetailsManualDownload -> DetailsManualDownloadScreen(
is ExtraScreen.ManualDownload -> ManualDownloadScreen(
packageName = app.packageName,
onNavigateUp = ::showMainPane
onNavigateUp = ::onNavigateBack
)
is Screen.DevProfile -> DevProfileScreen(
publisherId = app.developerName,
onNavigateUp = ::showMainPane,
onNavigateUp = ::onNavigateBack,
onNavigateToAppDetails = { onNavigateToAppDetails(it) }
)

View File

@@ -37,7 +37,7 @@ import com.aurora.store.viewmodel.details.AppDetailsViewModel
import com.aurora.store.viewmodel.details.DetailsExodusViewModel
@Composable
fun DetailsExodusScreen(
fun ExodusScreen(
packageName: String,
onNavigateUp: () -> Unit,
appDetailsViewModel: AppDetailsViewModel = hiltViewModel(key = packageName),
@@ -110,7 +110,7 @@ private fun ScreenContent(
@Preview
@Composable
private fun DetailsExodusScreenPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
private fun ExodusScreenPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
ScreenContent(
topAppBarTitle = app.displayName,
version = app.versionName

View File

@@ -55,7 +55,7 @@ import com.aurora.store.viewmodel.details.AppDetailsViewModel
import kotlinx.coroutines.android.awaitFrame
@Composable
fun DetailsManualDownloadScreen(
fun ManualDownloadScreen(
packageName: String,
onNavigateUp: () -> Unit,
viewModel: AppDetailsViewModel = hiltViewModel(key = packageName),
@@ -182,7 +182,7 @@ private fun ScreenContent(
@Preview
@Composable
private fun DetailsManualDownloadScreenPreview(
private fun ManualDownloadScreenPreview(
@PreviewParameter(AppPreviewProvider::class) app: App
) {
ScreenContent(

View File

@@ -47,7 +47,7 @@ import com.aurora.store.viewmodel.details.DetailsMoreViewModel
import java.util.Locale
@Composable
fun DetailsMoreScreen(
fun MoreScreen(
packageName: String,
onNavigateUp: () -> Unit,
onNavigateToAppDetails: (packageName: String) -> Unit,
@@ -192,7 +192,7 @@ private fun AppInfoMore(app: App) {
@Preview
@Composable
private fun DetailsMoreScreenPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
private fun MoreScreenPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
CompositionLocalProvider(LocalAsyncImagePreviewHandler provides coilPreviewProvider) {
ScreenContent(app = app)
}

View File

@@ -37,7 +37,7 @@ import com.aurora.store.viewmodel.details.DetailsPermissionViewModel
import java.util.Locale
@Composable
fun DetailsPermissionScreen(
fun PermissionScreen(
packageName: String,
onNavigateUp: () -> Unit,
appDetailsViewModel: AppDetailsViewModel = hiltViewModel(key = packageName),
@@ -117,7 +117,7 @@ private fun ScreenContent(
@Preview
@Composable
private fun DetailsPermissionScreenPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
private fun PermissionScreenPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
ScreenContent(
topAppBarTitle = app.displayName
)

View File

@@ -54,7 +54,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlin.random.Random
@Composable
fun DetailsReviewScreen(
fun ReviewScreen(
packageName: String,
onNavigateUp: () -> Unit,
appDetailsViewModel: AppDetailsViewModel = hiltViewModel(key = packageName),
@@ -182,7 +182,7 @@ private fun FilterHeader(onClick: (filter: Review.Filter) -> Unit) {
@Preview
@Composable
private fun DetailsReviewScreenPreview(@PreviewParameter(ReviewPreviewProvider::class) review: Review) {
private fun ReviewScreenPreview(@PreviewParameter(ReviewPreviewProvider::class) review: Review) {
val reviews = List(10) { review.copy(commentId = Random.nextInt().toString()) }
val reviewsFlow = MutableStateFlow(PagingData.from(reviews)).collectAsLazyPagingItems()

View File

@@ -30,7 +30,7 @@ import com.aurora.store.compose.composables.details.ScreenshotComposable
import com.aurora.store.viewmodel.details.AppDetailsViewModel
@Composable
fun DetailsScreenshotScreen(
fun ScreenshotScreen(
packageName: String,
index: Int,
onNavigateUp: () -> Unit,
@@ -96,7 +96,7 @@ private fun ScreenContent(
@Preview
@Composable
private fun DetailsScreenshotScreenPreview() {
private fun ScreenshotScreenPreview() {
ScreenContent(
topAppBarTitle = stringResource(R.string.app_name)
)

View File

@@ -0,0 +1,40 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.ui.details.navigation
import android.os.Parcelable
import androidx.navigation3.runtime.NavKey
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
/**
* Extra destinations for app detail's screen
*
* All of these destinations require and show information related to an app and thus aren't part of
* the main navigation display class.
*/
@Parcelize
@Serializable
sealed class ExtraScreen : NavKey, Parcelable {
@Serializable
data object More : ExtraScreen()
@Serializable
data class Screenshot(val index: Int) : ExtraScreen()
@Serializable
data object Exodus : ExtraScreen()
@Serializable
data object Review : ExtraScreen()
@Serializable
data object Permission : ExtraScreen()
@Serializable
data object ManualDownload : ExtraScreen()
}