network: hoist status into CompositionLocal and promote sheet to screen

Collect NetworkProvider.status once at the activity root and expose it via
LocalNetworkStatus so any screen can read it without injecting the provider.
Drop the redundant networkProvider field from MainViewModel and AuthViewModel.
NetworkSheet becomes a full-screen NetworkScreen with the same content.
This commit is contained in:
Rahul Patel
2026-05-20 00:53:03 +05:30
parent 0391adf82f
commit 91fc4d714a
8 changed files with 154 additions and 117 deletions

View File

@@ -12,21 +12,29 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.core.content.IntentCompat import androidx.core.content.IntentCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.aurora.extensions.getPackageName import com.aurora.extensions.getPackageName
import com.aurora.store.compose.composition.LocalNetworkStatus
import com.aurora.store.compose.composition.LocalUI import com.aurora.store.compose.composition.LocalUI
import com.aurora.store.compose.composition.UI import com.aurora.store.compose.composition.UI
import com.aurora.store.compose.navigation.NavDisplay import com.aurora.store.compose.navigation.NavDisplay
import com.aurora.store.compose.navigation.Screen import com.aurora.store.compose.navigation.Screen
import com.aurora.store.compose.theme.AuroraTheme import com.aurora.store.compose.theme.AuroraTheme
import com.aurora.store.data.model.NetworkStatus
import com.aurora.store.data.providers.NetworkProvider
import com.aurora.store.data.receiver.MigrationReceiver import com.aurora.store.data.receiver.MigrationReceiver
import com.aurora.store.util.PackageUtil import com.aurora.store.util.PackageUtil
import com.aurora.store.util.Preferences import com.aurora.store.util.Preferences
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class ComposeActivity : ComponentActivity() { class ComposeActivity : ComponentActivity() {
@Inject lateinit var networkProvider: NetworkProvider
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
MigrationReceiver.runMigrationsIfRequired(this) MigrationReceiver.runMigrationsIfRequired(this)
enableEdgeToEdge() enableEdgeToEdge()
@@ -42,7 +50,13 @@ class ComposeActivity : ComponentActivity() {
} }
setContent { setContent {
CompositionLocalProvider(LocalUI provides localUI) { val networkStatus by networkProvider.status.collectAsStateWithLifecycle(
initialValue = NetworkStatus.AVAILABLE
)
CompositionLocalProvider(
LocalUI provides localUI,
LocalNetworkStatus provides networkStatus
) {
AuroraTheme { AuroraTheme {
NavDisplay(startDestination = startDestination) NavDisplay(startDestination = startDestination)
} }

View File

@@ -7,12 +7,10 @@ package com.aurora.store
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.aurora.store.data.helper.UpdateHelper import com.aurora.store.data.helper.UpdateHelper
import com.aurora.store.data.providers.NetworkProvider
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class MainViewModel @Inject constructor( class MainViewModel @Inject constructor(
val networkProvider: NetworkProvider,
val updateHelper: UpdateHelper val updateHelper: UpdateHelper
) : ViewModel() ) : ViewModel()

View File

@@ -0,0 +1,19 @@
/*
* SPDX-FileCopyrightText: 2026 Aurora OSS
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composition
import androidx.compose.runtime.compositionLocalOf
import com.aurora.store.data.model.NetworkStatus
/**
* CompositionLocal carrying the current device network status. Provided once at the
* activity root from a single [com.aurora.store.data.providers.NetworkProvider] subscription,
* so any screen can read `LocalNetworkStatus.current` without injecting the provider
* or duplicating the flow collection.
*
* Uses [compositionLocalOf] (not static) so only readers recompose on change.
*/
val LocalNetworkStatus = compositionLocalOf { NetworkStatus.AVAILABLE }

View File

@@ -0,0 +1,112 @@
/*
* SPDX-FileCopyrightText: 2026 Aurora OSS
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.ui.commons
import android.content.ActivityNotFoundException
import android.content.Intent
import android.provider.Settings
import android.util.Log
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import com.aurora.extensions.isQAndAbove
import com.aurora.store.R
import com.aurora.store.compose.preview.ThemePreviewProvider
private const val TAG = "NetworkScreen"
/**
* Full-screen "no network" placeholder shown while [com.aurora.store.data.model.NetworkStatus]
* is UNAVAILABLE. Replaces the legacy modal sheet. Auto-dismisses by virtue of its caller
* only rendering it while offline.
*/
@Composable
fun NetworkScreen() {
val context = LocalContext.current
Scaffold { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(horizontal = dimensionResource(R.dimen.spacing_xlarge)),
contentAlignment = Alignment.Center
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(
dimensionResource(R.dimen.spacing_medium)
)
) {
Icon(
painter = painterResource(R.drawable.ic_network),
contentDescription = null,
modifier = Modifier.requiredSize(48.dp)
)
Text(
text = stringResource(R.string.title_no_network),
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center
)
Text(
text = stringResource(R.string.check_connectivity),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
)
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
try {
if (isQAndAbove) {
context.startActivity(
Intent(Settings.Panel.ACTION_INTERNET_CONNECTIVITY)
)
} else {
context.startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS))
}
} catch (_: ActivityNotFoundException) {
Log.i(TAG, "Unable to launch network settings")
try {
context.startActivity(Intent(Settings.ACTION_SETTINGS))
} catch (_: Exception) { }
}
}
) {
Text(stringResource(R.string.action_check))
}
}
}
}
}
@PreviewWrapper(ThemePreviewProvider::class)
@Preview
@Composable
private fun NetworkScreenPreview() {
NetworkScreen()
}

View File

@@ -1,96 +0,0 @@
/*
* SPDX-FileCopyrightText: 2026 Aurora OSS
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.ui.commons
import android.content.ActivityNotFoundException
import android.content.Intent
import android.provider.Settings
import android.util.Log
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetValue
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.aurora.extensions.isQAndAbove
import com.aurora.store.R
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NetworkSheet() {
val context = LocalContext.current
ModalBottomSheet(
onDismissRequest = {},
sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true,
confirmValueChange = { it != SheetValue.Hidden }
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = dimensionResource(R.dimen.spacing_xlarge))
.padding(bottom = dimensionResource(R.dimen.spacing_large)),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
painter = painterResource(R.drawable.ic_network),
contentDescription = null,
modifier = Modifier.requiredSize(48.dp)
)
Spacer(Modifier.padding(top = dimensionResource(R.dimen.spacing_large)))
Text(
text = stringResource(R.string.title_no_network),
style = MaterialTheme.typography.titleLarge
)
Spacer(Modifier.padding(top = dimensionResource(R.dimen.spacing_small)))
Text(
text = stringResource(R.string.check_connectivity),
style = MaterialTheme.typography.bodyMedium
)
Spacer(Modifier.padding(top = dimensionResource(R.dimen.spacing_xlarge)))
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
try {
if (isQAndAbove) {
context.startActivity(
Intent(Settings.Panel.ACTION_INTERNET_CONNECTIVITY)
)
} else {
context.startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS))
}
} catch (e: ActivityNotFoundException) {
Log.i("NetworkSheet", "Unable to launch network settings")
try {
context.startActivity(Intent(Settings.ACTION_SETTINGS))
} catch (_: Exception) { }
}
}
) {
Text(stringResource(R.string.action_check))
}
Spacer(Modifier.navigationBarsPadding())
}
}
}

View File

@@ -23,7 +23,6 @@ import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -39,10 +38,11 @@ import com.aurora.extensions.requiresObbDir
import com.aurora.store.MainViewModel import com.aurora.store.MainViewModel
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.compose.composable.TopAppBar import com.aurora.store.compose.composable.TopAppBar
import com.aurora.store.compose.composition.LocalNetworkStatus
import com.aurora.store.compose.navigation.Destination import com.aurora.store.compose.navigation.Destination
import com.aurora.store.compose.ui.apps.AppsGamesScreen import com.aurora.store.compose.ui.apps.AppsGamesScreen
import com.aurora.store.compose.ui.commons.MoreSheet import com.aurora.store.compose.ui.commons.MoreSheet
import com.aurora.store.compose.ui.commons.NetworkSheet import com.aurora.store.compose.ui.commons.NetworkScreen
import com.aurora.store.compose.ui.sheets.AppUpdateSheet import com.aurora.store.compose.ui.sheets.AppUpdateSheet
import com.aurora.store.compose.ui.updates.UpdatesScreen import com.aurora.store.compose.ui.updates.UpdatesScreen
import com.aurora.store.data.model.NetworkStatus import com.aurora.store.data.model.NetworkStatus
@@ -69,9 +69,7 @@ fun MainScreen(
onNavigateTo: (Destination) -> Unit = {} onNavigateTo: (Destination) -> Unit = {}
) { ) {
val context = LocalContext.current val context = LocalContext.current
val networkStatus by mainViewModel.networkProvider.status.collectAsStateWithLifecycle( val networkStatus = LocalNetworkStatus.current
initialValue = NetworkStatus.AVAILABLE
)
val updates by mainViewModel.updateHelper.updates.collectAsStateWithLifecycle( val updates by mainViewModel.updateHelper.updates.collectAsStateWithLifecycle(
initialValue = null initialValue = null
) )
@@ -89,7 +87,6 @@ fun MainScreen(
var showMoreSheet by remember { mutableStateOf(false) } var showMoreSheet by remember { mutableStateOf(false) }
var appUpdateTarget by remember { mutableStateOf<Update?>(null) } var appUpdateTarget by remember { mutableStateOf<Update?>(null) }
var showNetworkDialog by remember { mutableStateOf(false) }
fun handleNavigation(destination: Destination) { fun handleNavigation(destination: Destination) {
when (destination) { when (destination) {
@@ -98,12 +95,9 @@ fun MainScreen(
} }
} }
LaunchedEffect(networkStatus) { if (networkStatus == NetworkStatus.UNAVAILABLE) {
showNetworkDialog = networkStatus == NetworkStatus.UNAVAILABLE NetworkScreen()
} return
if (showNetworkDialog) {
NetworkSheet()
} }
if (showMoreSheet) { if (showMoreSheet) {

View File

@@ -59,6 +59,7 @@ import com.aurora.Constants.PACKAGE_NAME_PLAY_STORE
import com.aurora.gplayapi.helpers.AuthHelper import com.aurora.gplayapi.helpers.AuthHelper
import com.aurora.store.BuildConfig import com.aurora.store.BuildConfig
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.compose.composition.LocalNetworkStatus
import com.aurora.store.compose.navigation.Destination import com.aurora.store.compose.navigation.Destination
import com.aurora.store.data.model.AuthState import com.aurora.store.data.model.AuthState
import com.aurora.store.data.model.NetworkStatus import com.aurora.store.data.model.NetworkStatus
@@ -81,10 +82,7 @@ fun SplashScreen(
val activity = LocalActivity.current as? ComponentActivity val activity = LocalActivity.current as? ComponentActivity
val authState by viewModel.authState.collectAsStateWithLifecycle() val authState by viewModel.authState.collectAsStateWithLifecycle()
val networkStatus by viewModel.networkProvider.status.collectAsStateWithLifecycle( val isOnline = LocalNetworkStatus.current == NetworkStatus.AVAILABLE
initialValue = NetworkStatus.AVAILABLE
)
val isOnline = networkStatus == NetworkStatus.AVAILABLE
val canMicroGLogin = PackageUtil.hasSupportedMicroGVariant(context) && val canMicroGLogin = PackageUtil.hasSupportedMicroGVariant(context) &&
Preferences.getBoolean(context, Preferences.PREFERENCE_MICROG_AUTH, true) Preferences.getBoolean(context, Preferences.PREFERENCE_MICROG_AUTH, true)

View File

@@ -34,7 +34,6 @@ import com.aurora.store.data.model.AccountType
import com.aurora.store.data.model.AuthState import com.aurora.store.data.model.AuthState
import com.aurora.store.data.providers.AccountProvider import com.aurora.store.data.providers.AccountProvider
import com.aurora.store.data.providers.AuthProvider import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.data.providers.NetworkProvider
import com.aurora.store.util.AC2DMTask import com.aurora.store.util.AC2DMTask
import com.aurora.store.util.PackageUtil import com.aurora.store.util.PackageUtil
import com.aurora.store.util.Preferences import com.aurora.store.util.Preferences
@@ -51,7 +50,6 @@ import kotlinx.coroutines.launch
@HiltViewModel @HiltViewModel
class AuthViewModel @Inject constructor( class AuthViewModel @Inject constructor(
val authProvider: AuthProvider, val authProvider: AuthProvider,
val networkProvider: NetworkProvider,
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val aC2DMTask: AC2DMTask private val aC2DMTask: AC2DMTask
) : ViewModel() { ) : ViewModel() {