Navigate to another screen without the previous scaffold in Jetpack Compose - android

My app has a main screen with a Scaffold and a BottomNavigation bar:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
MyApplicationTheme {
Scaffold(
bottomBar = {
BottomBar(navController = navController)
}
) {
NavigationGraph(navController = navController)
}
}
}
}
...
#Composable
fun NavigationGraph(navController: NavHostController){
NavHost(navController = navController, startDestination = BottomMenuOption.Home.route) {
composable(route = BottomMenuOption.Home.route) {
HomeScreen(navController = navController)
}
composable(route = BottomMenuOption.Settings.settings) {
SettingsScreen()
}
composable(route = BottomMenuOption.Profile.route) {
ProfileScreen()
}
composable(route = "feature") {
FeatureScreen()
}
}
}
FeatureScreen has it's own Scaffold with a topBar instead of a bottomBar and when I navigate to it from HomeScreen, I want to replace the previous one from the main screen and just see a topBar but instead, I'm seeing the two bars in the screen.
#Composable
fun FeatureScreen() {
Scaffold (
topBar = {
TopBar(" Feature Screen")
}
) {
}
}
Is it possible to accomplish this? I'm thinking it could be done by just using a new Activity but ideally, I would like to keep the Single Activity approach.

I would suggest creating a new function like this:
#Composable
fun MainScaffold(
topBar: #Composable (() -> Unit) = {},
bottomBar: #Composable (() -> Unit) = {},
content: #Composable (PaddingValues) -> Unit){
Scaffold(
bottomBar = bottomBar,
topBar = topBar,
content = content
)
}
then, use this main scaffold in your screens:
#Composable
fun HomeScreen(navController: NavHostController) {
MainScaffold(
bottomBar = { BottomBar(navController = navController) },
content = {
// content
})
}
and in your feature screen:
#Composable
fun FeatureScreen() {
MainScaffold (
topBar = {
TopBar(" Feature Screen")
}
) {
//content
}
}
and in setContent
setContent {
val navController = rememberNavController()
VoiceAssistantJetpackComposeTheme {
NavigationGraph(navController = navController)
}}

Related

Home and Auth navigation flows in Jetpack Compose

I have a problem with navigation in my Compose app. So starting from the beginning I want to have two navigation graphs: one for authentication related things and second for main functionalities, which should be accessible only after login. So typical case.
I want to use Splash Screen API from Android 12, so in my main activity I could do something like this:
class AppMainActivity : ComponentActivity() {
private val viewModel by viewModels<SplashViewModel>()
private var userAuthState: UserAuthState = UserAuthState.UNKNOWN
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.onEvent(SplashEvent.CheckAuthentication)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.isAuthenticated.collect {
userAuthState = it
}
}
}
setContent {
CarsLocalizerTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
val scaffoldState = rememberScaffoldState()
val navController = rememberNavController()
Scaffold(
modifier = Modifier.fillMaxSize(),
scaffoldState = scaffoldState
) {
val splash = installSplashScreen()
splash.setKeepOnScreenCondition {
userAuthState != UserAuthState.UNKNOWN
}
when (userAuthState) {
UserAuthState.UNAUTHENTICATED -> {
AuthNavigation(
navController = navController,
scaffoldState = scaffoldState
)
}
UserAuthState.AUTHENTICATED -> {
HomeScreen(
viewModel = hiltViewModel(),
onLogout = { navController.popBackStack() }
)
}
UserAuthState.UNKNOWN -> {}
}
}
}
}
}
}
}
Here I am collecting StateFlow from view model, which describes if user is authenticated already or no. If authenticated successfully then go to HomeScreen, which has HomeNavigation inside. If not authenticated, go to Authentication nav graph. Problem is that this approach would not work, since activity is created only once, so if I user will login, this when
when (userAuthState) {
UserAuthState.UNAUTHENTICATED -> {
AuthNavigation(
navController = navController,
scaffoldState = scaffoldState
)
}
UserAuthState.AUTHENTICATED -> {
HomeScreen(
viewModel = hiltViewModel(),
onLogout = { navController.popBackStack() }
)
}
UserAuthState.UNKNOWN -> {}
}
Won’t be called again.
I was trying to find some solution for my problem but, can’t find anything helpful. Maybe somebody had such issue before, or saw something useful? Will be very glad for any help.
Rest of my code:
SplashViewModel
#HiltViewModel
class SplashViewModel #Inject constructor(
private val authenticateUseCase: AuthenticateUseCase
): ViewModel() {
private val _isAuthenticated = MutableStateFlow<UserAuthState>(value = UserAuthState.UNKNOWN)
val isAuthenticated: StateFlow<UserAuthState> = _isAuthenticated.asStateFlow()
fun onEvent(event: SplashEvent) {
when (event) {
is SplashEvent.CheckAuthentication -> {
viewModelScope.launch {
val result = authenticateUseCase()
when (result) {
true -> {
_isAuthenticated.emit(UserAuthState.AUTHENTICATED)
}
false -> {
_isAuthenticated.emit(UserAuthState.UNAUTHENTICATED)
}
}
}
}
}
}
}
AuthNavigation
#Composable
fun AuthNavigation(
navController: NavHostController,
scaffoldState: ScaffoldState
) {
NavHost(
navController = navController,
startDestination = Screen.Login.route,
modifier = Modifier.fillMaxSize()
) {
composable(Screen.Login.route) {
LoginScreen(
onNavigate = { navController.navigate(it) } ,
onLogin = {
navController.popBackStack()
},
scaffoldState = scaffoldState,
viewModel = hiltViewModel()
)
}
composable(Screen.Register.route) {
RegisterScreen(
onPopBackstack = { navController.popBackStack() },
scaffoldState = scaffoldState,
viewModel = hiltViewModel()
)
}
composable(Screen.Onboarding.route) {
OnboardingScreen(
onCompleted = { navController.popBackStack() },
viewModel = hiltViewModel()
)
}
}
}
HomeScreen
#Composable
fun HomeScreen(
viewModel: HomeViewModel,
onLogout: () -> Unit
) {
val navController = rememberNavController()
val scaffoldState = rememberScaffoldState()
var appBarTitle by remember {
mutableStateOf("")
}
LaunchedEffect(key1 = true) {
viewModel.userName.collectLatest {
appBarTitle = "Hello $it"
}
}
Scaffold(
scaffoldState = scaffoldState,
topBar = {
if (appBarTitle.isEmpty()) {
AppBarWithName("Hello")
} else {
AppBarWithName(appBarTitle)
}
},
bottomBar = { BottomNavigationBar(navController) },
content = { padding ->
Box(modifier = Modifier.padding(padding)) {
HomeNavigation(
navController = navController,
scaffoldState = scaffoldState,
onLogout = { onLogout() }
)
}
}
)
}
HomeNavigation
#Composable
fun HomeNavigation(
navController: NavHostController,
scaffoldState: ScaffoldState,
onLogout: () -> Unit
) {
NavHost(
navController = navController,
startDestination = Screen.Map.route
) {
composable(Screen.Map.route) {
MapScreen(viewModel = hiltViewModel())
}
composable(Screen.ManageCars.route) {
ManageCarsScreen(
viewModel = hiltViewModel(),
scaffoldState = scaffoldState,
onAddCar = {
navController.navigate(Screen.AddCar.route)
}
)
}
composable(Screen.AddCar.route) {
AddCarScreen(
viewModel = hiltViewModel(),
onPopBackstack = {
navController.popBackStack()
}
)
}
composable(Screen.Logout.route) {
LogoutDialogScreen(
viewModel = hiltViewModel(),
onLogout = {
navController.popBackStack()
onLogout()
},
onCancel = {
navController.popBackStack()
}
)
}
}
}
You need to store the UserAuthState as MutableState, this way, when the value is updated, setContent will automatically re-compose:
private var userAuthState = mutableStateOf(UserAuthState.UNKNOWN)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.isAuthenticated.collect { userAuthState.value = it }
}
}
setContent {
when (userAuthState.value) {
etc....
}
}
From the docs:
mutableStateOf creates an observable MutableState, which is an
observable type integrated with the compose runtime.
Any changes to value schedules recomposition of any composable
functions that read value.

Compose dark mode never shows dark colors

So I used https://m3.material.io/theme-builder#/custom to create a theme, I downloaded it, and I'm using it. But it only ever shows light colors.
AppTheme.kt
#Composable
fun AppTheme(
useDarkTheme: Boolean = isSystemInDarkTheme(),
content: #Composable() () -> Unit
) {
val colors = if (!useDarkTheme) {
LightColors
} else {
DarkColors
}
MaterialTheme(
colorScheme = colors,
content = content
)
}
MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onStart() {
super.onStart()
Koin.init(context = this)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
AppTheme(useDarkTheme = true) {
NavHost(
navController = navController,
startDestination = Screen.Home.route
) {
addHomeTopLevel(navController)
}
}
}
}
}
HomeScreen.kt
#Composable
fun HomeScreen(
navController: NavController
) {
val viewModel: HomeViewModel by inject(HomeViewModel::class.java)
HomeContent()
}
#Composable
private fun HomeContent() {
Scaffold(
backgroundColor = MaterialTheme.colors.background,
topBar = {
TopAppBar {
Text("Hello World!")
}
},
content = {
Text("Hello Wolrd")
}
)
}
My emulator is in dark mode, but it's still showing light colors. The color values are correct, and they're part of LightColors and DarkColors correctly. The boolean is correct, and even if I change both to DarkColors it still shows light ones.
I'm not sure what I'm doing wrong here.

Using Compose navigation with deeplink within LazyColumn

I've read the article about handling deeplinks with Compose,
but I didn't find anything for my use case - for navigation within LazyColumn.
I have the following main Composable which I want to make the root of the graph (I want it to be host for NavHost):
#Composable
private fun MainScreenContainer(
mainState: state,
...
) {
val navController = rememberNavController()
MainTheme {
Box(
modifier = Modifier.fillMaxSize()
) {
val listState = rememberLazyListState()
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize()
) {
mainScreenContent(
state = mainState,
...
)
}
StickyToolbar(
...
)
}
}
}
and here is the remaining code:
private fun LazyListScope.mainScreenContent(
mainState: MainState,
...
) {
header(...)
overview(...)
myCard(...)
footer()
}
private fun LazyListScope.myCard(
...
) {
item {
ContentContainer(
modifier = Modifier.padding(vertical = MainTheme.spacing.medium)
) {
MyCardStateless(
onCardClick = myCardViewModel::onCardClick,
...
)
}
}
}
#Composable
fun MyCardStateless(
onCardClick: () -> Unit,
...
) {
Column(modifier = modifier) {
Card(
modifier = Modifier.fillMaxWidth()
.clickable(onClick = onCardClick)
) { ... }
}
}
And within MyCardViewModel I was using navigation to another fragment via Navigation Component:
fun onCardClick() {
navigate(
MainFragmentDirections.actionMainFragmentToDescriptionFragment()
)
}
#AndroidEntryPoint
class DescriptionFragment : ...() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
setContent {
DescriptionStateful(...)
}
}
...
}
But now I want to add 2 deeplinks ("myapp://mycard" - to MyCardStateless and "myapp://description" - to DescriptionFragment or to DescriptionStateful) and handle navigation using navigation-compose. Is it possible at all?

why it throw an error for NavController as a NullPointerException in jetpack compose?

I try to learning jetpack compose in android, so I want to use navController in a simple project, when I debug the project it throw an error as a
NullPointerException when trying to get NavController
for in this onClick = {navController.navigate("screenB")} line of code. I do not know what I missed in this project?. I was search on internet to solve it, but I am still not get it.
#AndroidEntryPoint
class NavActivity : ComponentActivity() {
private lateinit var viewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this)[MyViewModel::class.java]
setContent {
NavScreen()
}
}
}
#Composable
fun NavScreen() {
val viewModel = hiltViewModel<MyViewModel>()
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "screenA") {
composable(route = "screenA") {
ScreenA(navController)
}
composable(route = "screenB") {
ScreenB(navController)
}
}
}
ScreenA:
#AndroidEntryPoint
class ScreenA : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
ScreenA(navController)
}
}
}
#Composable
fun ScreenA(
navController: NavController
) {
Button(
modifier = Modifier
.width(30.dp)
.height(15.dp),
onClick = {
navController.navigate("screenB")
},
colors = ButtonDefaults.buttonColors(
backgroundColor = Color.Red
),
shape = RoundedCornerShape(20)
) {
Text(
text = "OK",
)
}}
ScreenB:
#OptIn(ExperimentalComposeUiApi::class)
#Composable
fun ScreenB(
navController: NavController
) {
}
ScreenA should be composable not Activity. Try to leave only composable component in ScreenA
#Composable
fun ScreenA(
navController: NavController
) {
Button(
modifier = Modifier
.width(30.dp)
.height(15.dp),
onClick = {
navController.navigate("screenB")
},
colors = ButtonDefaults.buttonColors(
backgroundColor = Color.Red
),
shape = RoundedCornerShape(20)
) {
Text(
text = "OK",
)
}}

How could i run my logic code inside composable function just 1 time?

I'm doing my test project using ViewModel and ComposeView.
My architecture include: one Activity and multi ComposeView, using navigation like this:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MainView()
}
}
}
#Composable
fun MainView() {
TestComposeTheme {
val navController = rememberNavController()
Surface(color = MaterialTheme.colors.background) {
NavHost(
navController = navController,
startDestination = KYCScreen.getName(),
) {
navigation(startDestination = KYCScreen.Preload.name, route = KYCScreen.getName()) {
composable(KYCScreen.Preload.name) { PreloadView(navHostController = navController) }
composable(KYCScreen.StartKYC.name) { StartView(navHostController = navController) }
composable(KYCScreen.Login.name) { LoginView(navHostController = navController) }
}
navigation(startDestination = HomeScreen.Home.name, route = HomeScreen.getName()) {
composable(HomeScreen.Home.name) { HomeView(navHostController = navController) }
}
}
}
}
}
The problem happens when I include logic that executes when a Composable function is executed, my logic code was looped many times. Here my code:
#Composable
fun PreloadView(navHostController: NavHostController) {
val preloadViewModel: PreloadViewModel = viewModel()
preloadViewModel.getSyncData()
val syncState by preloadViewModel.uiStateSync.observeAsState()
syncState?.let { PreloadContent(navHostController = navHostController, it) }
}
#Composable
fun PreloadContent(navHostController: NavHostController, uiState: UiState<SyncResponse>) {
when (uiState.state) {
RequestState.SUCCESS -> {
navHostController.navigate(KYCScreen.StartKYC.name)
}
RequestState.FAIL -> {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
) {
Text(text = "Error", color = Color.Black)
}
}
RequestState.NON -> {
}
}
}
Anyone have a solution to help me with that architecture?
Your composable function should be side-effects free.
If you want to run something just once. You can do something like the following
LaunchedEffect(Unit){
preloadViewModel.getSyncData()
}
or in your case, if it is mandatory to Sync Data when ViewModel initializes you can call this function in the init block inside of your ViewModel.
Check the official doc https://developer.android.com/jetpack/compose/side-effects

Categories

Resources