DO NOT MERGE: Switch to adaptive navigation3 statergy

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2025-11-09 18:37:58 +08:00
parent 29dbc18750
commit 4e8c4435d4
5 changed files with 325 additions and 0 deletions

View File

@@ -190,6 +190,7 @@ dependencies {
implementation(libs.androidx.adaptive.core)
implementation(libs.androidx.adaptive.navigation)
implementation(libs.androidx.adaptive.navigation3)
implementation(libs.androidx.adaptive.layout)
implementation(libs.androidx.paging.compose)
implementation(libs.androidx.navigation3.runtime)

View File

@@ -0,0 +1,103 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.ui.details
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
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.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.compose.LocalAsyncImagePreviewHandler
import com.aurora.gplayapi.data.models.App
import com.aurora.store.R
import com.aurora.store.compose.composable.Header
import com.aurora.store.compose.composable.TopAppBar
import com.aurora.store.compose.composable.app.LargeAppListItem
import com.aurora.store.compose.preview.AppPreviewProvider
import com.aurora.store.compose.preview.coilPreviewProvider
import com.aurora.store.viewmodel.details.AppDetailsViewModel
import kotlin.random.Random
@Composable
fun SuggestionsScreen(
packageName: String,
onNavigateToAppDetails: (packageName: String) -> Unit,
actions: @Composable (RowScope.() -> Unit) = {},
viewModel: AppDetailsViewModel = hiltViewModel(key = packageName),
) {
val suggestions by viewModel.suggestions.collectAsStateWithLifecycle()
ScreenContent(
suggestions = suggestions,
onNavigateToAppDetails = onNavigateToAppDetails,
actions = actions
)
}
@Composable
private fun ScreenContent(
suggestions: List<App> = emptyList(),
onNavigateToAppDetails: (packageName: String) -> Unit = {},
actions: @Composable (RowScope.() -> Unit) = {}
) {
Scaffold(
topBar = { TopAppBar(actions = actions) }
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
Row(
modifier = Modifier.padding(dimensionResource(R.dimen.margin_medium)),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(R.drawable.ic_suggestions),
contentDescription = null
)
Header(title = stringResource(R.string.pref_ui_similar_apps))
}
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(vertical = dimensionResource(R.dimen.padding_medium))
) {
items(items = suggestions, key = { item -> item.id }) { app ->
LargeAppListItem(
app = app,
onClick = { onNavigateToAppDetails(app.packageName) }
)
}
}
}
}
}
@Preview
@Composable
private fun SuggestionsScreenPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
CompositionLocalProvider(LocalAsyncImagePreviewHandler provides coilPreviewProvider) {
val apps = List(10) { app.copy(id = Random.nextInt()) }
ScreenContent(suggestions = apps)
}
}

View File

@@ -0,0 +1,197 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.ui.details.navigation
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective
import androidx.compose.material3.adaptive.navigation3.SupportingPaneSceneStrategy
import androidx.compose.material3.adaptive.navigation3.rememberSupportingPaneSceneStrategy
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.ui.NavDisplay
import com.aurora.Constants.SHARE_URL
import com.aurora.extensions.appInfo
import com.aurora.extensions.browse
import com.aurora.extensions.isWindowCompact
import com.aurora.extensions.requiresObbDir
import com.aurora.extensions.share
import com.aurora.gplayapi.data.models.App
import com.aurora.store.compose.navigation.Screen
import com.aurora.store.compose.ui.commons.PermissionRationaleScreen
import com.aurora.store.compose.ui.details.AppDetailsScreen
import com.aurora.store.compose.ui.details.ExodusScreen
import com.aurora.store.compose.ui.details.ManualDownloadScreen
import com.aurora.store.compose.ui.details.MoreScreen
import com.aurora.store.compose.ui.details.PermissionScreen
import com.aurora.store.compose.ui.details.ReviewScreen
import com.aurora.store.compose.ui.details.ScreenshotScreen
import com.aurora.store.compose.ui.details.SuggestionsScreen
import com.aurora.store.compose.ui.details.menu.AppDetailsMenu
import com.aurora.store.compose.ui.details.menu.MenuItem
import com.aurora.store.compose.ui.dev.DevProfileScreen
import com.aurora.store.data.model.PermissionType
import com.aurora.store.data.providers.PermissionProvider.Companion.isPermittedToInstall
import com.aurora.store.util.ShortcutManagerUtil
import com.aurora.store.viewmodel.details.AppDetailsViewModel
/**
* Navigation display for AppDetails Screen
*
* When viewing details related to an app, a user can navigate to several screens among
* which majority are related to the app, directly or indirectly. This navigation display
* is supposed to manage those screens in its own separate backstack.
*/
@Composable
fun NavDisplay(
packageName: String,
onNavigateUp: () -> Unit,
onNavigateToAppDetails: (packageName: String) -> Unit,
forceSinglePane: Boolean = false,
viewModel: AppDetailsViewModel = hiltViewModel(key = packageName),
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo()
) {
val context = LocalContext.current
val app by viewModel.app.collectAsStateWithLifecycle()
// TODO: Make this adaptive so it doesn't looks bad; maybe use BoxWithConstraints
var paneScaffoldDirective = calculatePaneScaffoldDirective(windowAdaptiveInfo)
if (forceSinglePane) {
paneScaffoldDirective = paneScaffoldDirective.copy(maxHorizontalPartitions = 1)
}
val startDestinations = listOfNotNull<NavKey>(
Screen.AppDetails(packageName),
if (windowAdaptiveInfo.isWindowCompact) SupportingScreen.Suggestions else null
)
val backstack = rememberNavBackStack(*startDestinations.toTypedArray())
val supportingPaneSceneStrategy = rememberSupportingPaneSceneStrategy<NavKey>(
directive = paneScaffoldDirective
)
fun onRequestNavigateUp() {
if (!backstack.all { it in startDestinations }) {
backstack.removeLastOrNull()
} else {
onNavigateUp()
}
}
// TODO: Handle manual download on first time without permissions as app will change
fun onInstall(requested: App = app!!) {
if (isPermittedToInstall(context, requested)) {
viewModel.enqueueDownload(requested)
backstack.removeAll(setOf(ExtraScreen.ManualDownload, Screen.PermissionRationale))
} else {
val requiredPermissions = setOfNotNull(
PermissionType.INSTALL_UNKNOWN_APPS,
if (requested.fileList.requiresObbDir()) PermissionType.STORAGE_MANAGER else null,
if (requested.fileList.requiresObbDir()) PermissionType.EXTERNAL_STORAGE else null
)
backstack.add(Screen.PermissionRationale(requiredPermissions = requiredPermissions))
}
}
NavDisplay(
backStack = backstack,
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator()
),
sceneStrategy = supportingPaneSceneStrategy,
entryProvider = entryProvider {
entry<Screen.AppDetails>(metadata = SupportingPaneSceneStrategy.mainPane()) {
AppDetailsScreen(
packageName = packageName,
onNavigateUp = ::onRequestNavigateUp,
onNavigateToAppDetails = onNavigateToAppDetails,
)
}
entry<SupportingScreen.Suggestions>(
metadata = when {
windowAdaptiveInfo.isWindowCompact -> SupportingPaneSceneStrategy.extraPane()
else -> SupportingPaneSceneStrategy.supportingPane()
}
) {
SuggestionsScreen(
packageName = packageName,
onNavigateToAppDetails = onNavigateToAppDetails
)
}
entry<ExtraScreen.Review>(metadata = SupportingPaneSceneStrategy.extraPane()) {
ReviewScreen(
packageName = packageName,
onNavigateUp = ::onRequestNavigateUp
)
}
entry<ExtraScreen.Exodus>(metadata = SupportingPaneSceneStrategy.extraPane()) {
ExodusScreen(
packageName = packageName,
onNavigateUp = ::onRequestNavigateUp
)
}
entry<ExtraScreen.More>(metadata = SupportingPaneSceneStrategy.extraPane()) {
MoreScreen(
packageName = packageName,
onNavigateUp = ::onRequestNavigateUp,
onNavigateToAppDetails = onNavigateToAppDetails
)
}
entry<ExtraScreen.Permission>(metadata = SupportingPaneSceneStrategy.extraPane()) {
PermissionScreen(
packageName = packageName,
onNavigateUp = ::onRequestNavigateUp
)
}
entry<ExtraScreen.Screenshot>(metadata = SupportingPaneSceneStrategy.extraPane()) {
ScreenshotScreen(
packageName = packageName,
index = it.index,
onNavigateUp = ::onRequestNavigateUp
)
}
entry<ExtraScreen.ManualDownload>(metadata = SupportingPaneSceneStrategy.extraPane()) {
ManualDownloadScreen(
packageName = packageName,
onNavigateUp = ::onRequestNavigateUp,
onRequestInstall = { requestedApp -> onInstall(requestedApp) }
)
}
// Independent screens but still navigated as extras as related to app //
entry<Screen.DevProfile>(metadata = SupportingPaneSceneStrategy.extraPane()) {
DevProfileScreen(
publisherId = app!!.developerName,
onNavigateUp = ::onRequestNavigateUp,
onNavigateToAppDetails = onNavigateToAppDetails
)
}
entry<Screen.PermissionRationale>(metadata = SupportingPaneSceneStrategy.extraPane()) {
PermissionRationaleScreen(
onNavigateUp = ::onRequestNavigateUp,
requiredPermissions = it.requiredPermissions,
onPermissionCallback = { onInstall() }
)
}
}
)
}

View File

@@ -0,0 +1,22 @@
/*
* 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
/**
* Supporting destinations for app detail's screen
*/
@Parcelize
@Serializable
sealed class SupportingScreen : NavKey, Parcelable {
@Serializable
data object Suggestions : SupportingScreen()
}

View File

@@ -9,6 +9,7 @@ activity = "1.11.0"
agCoreservice = "13.3.1.300"
androidGradlePlugin = "8.13.0"
adaptive = "1.3.0-alpha02"
adaptive-navigation3 = "1.3.0-alpha03"
androidx-hilt = "1.3.0"
androidx-junit = "1.3.0"
browser = "1.9.0"
@@ -59,6 +60,7 @@ androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "core" }
androidx-adaptive-core = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "adaptive" }
androidx-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout", version.ref = "adaptive" }
androidx-adaptive-navigation = { module = "androidx.compose.material3.adaptive:adaptive-navigation", version.ref = "adaptive" }
androidx-adaptive-navigation3 = { module = "androidx.compose.material3.adaptive:adaptive-navigation3", version.ref = "adaptive-navigation3" }
androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" }
androidx-hilt-viewmodel = { module = "androidx.hilt:hilt-lifecycle-viewmodel-compose", version.ref = "androidx-hilt" }
androidx-junit = { module = "androidx.test.ext:junit-ktx", version.ref = "androidx-junit" }