Jetpack Compose navigation crashes app when button clicked - kotlin

I am just playing around with navigation compose and trying to figure out how it works. I read some articles and watch tutorials how to implement it in my app. So I choose the simpliest way to do this, but when I clicked the buttot to navigate to second screen, app crashed and exited. What am I doing wrong?
I am not doing any fancy stuff like bottom navigation, splash screens and etc, just navigate to the second screen.
Here I created navigation's logic
#Composable
fun navigationDraft(navController: NavController) {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = ScreenNavigation.Home.routeName
) {
composable(route = ScreenNavigation.Home.routeName) {
Home( navController = navController)
}
composable(route = ScreenNavigation.DetailedScreen.routeName) {
DetailedScreen(navController = navController)
}
}
}
Here I created navigation's route:
sealed class ScreenNavigation(var routeName: String, ){
object Home : ScreenNavigation(routeName = "home")
object DetailedScreen : ScreenNavigation(routeName = "detailed")
}
HomeScreen:
#Composable
fun Home(navController: NavController) {
Button(onClick = {navController.navigate(ScreenNavigation.DetailedScreen.routeName) }) {
}
}
Detailed Screen
#Composable
fun DetailedScreen(navController: NavController) {
Scaffold() {
TopAppBar(elevation = 2.dp, backgroundColor = Color.Magenta) {
Text(text = "Second Screen With Detail", fontStyle = FontStyle.Italic)
}
Column(verticalArrangement = Arrangement.Center) {
Text(text = "Hi", fontSize = 30.sp)
}
}
}
MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Users_plofile_kotlinTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
val navController = rememberNavController()
Home(navController = navController)
nameViewModel.getUserNameList()
}
}
}
The error I have got:
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.users_plofile_kotlin, PID: 24321
java.lang.NullPointerException
at androidx.navigation.NavController.navigate(NavController.kt:1652)
at androidx.navigation.NavController.navigate(NavController.kt:1984)

Ok, I think I found the issue. I created another #Composable MainScreen function instead of default #Composable Greetings function and put there all routes I would like to use, so my code now a little bit fixed but the main idea is still the same:
This should be instead of #Composable Greeting function
#Composable
fun MainScreen(){
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "home"
) {
composable(route = ScreenNavigation.ButtonToCkeck.route) {
Home(navController = navController )
}
composable(route = ScreenNavigation.DetailedSreen.route) {
DetailedSreen()
}
}
}
And put it in the MainAcrivity:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NavigationAppTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
MainScreen()
}
}
}
}
}
Now it works smoothly. Also the version of navigation composable is:
implementation("androidx.navigation:navigation-compose:2.5.1")

Related

Invocations can only happen from the context of an #composable function using Compose Navigation

Hi Im currently struggling with navigation in Jetpack Compose due to #composable invocations can only happen from the context of an #composable function. I have a function:
private fun signInResult(result: FirebaseAuthUIAuthenticationResult) {
val response = result.idpResponse
if (result.resultCode == RESULT_OK) {
user = FirebaseAuth.getInstance().currentUser
Log.e("MainActivity.kt", "Innlogging vellykket")
ScreenMain()
} else {
Log.e("MainActivity.kt", "Feil med innlogging" + response?.error?.errorCode)
}
}
and used with my navigation class shown under I only get the error message shown above, how do I fix it?
#Composable
fun ScreenMain(){
val navController = rememberNavController()
NavHost(navController = navController, startDestination = Routes.Vareliste.route) {
composable(Routes.SignUp.route) {
SignUp(navController = navController)
}
composable(Routes.ForgotPassword.route) { navBackStack ->
ForgotPassword(navController = navController)
}
composable(Routes.Vareliste.route) { navBackStack ->
Vareliste(navController = navController)
}
composable(Routes.Handlekurv.route) { navBackStack ->
Handlekurv(navController = navController)
}
composable(Routes.Profileromoss.route) { navBackStack ->
Profileromoss(navController = navController)
}
}
}
EDIT WITH COMPLETE CODE
Here is the whole code for the class if you guys wanted to see it!
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
JetpackComposeDemoTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
LoginPage()
}
}
}
}
private var user: FirebaseUser? = FirebaseAuth.getInstance().currentUser
private lateinit var auth: FirebaseAuth
#Composable
fun LoginPage() {
Box(modifier = Modifier.fillMaxSize()) {
}
Column(
modifier = Modifier.padding(20.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Velkommen til ITGuys", style = TextStyle(fontSize = 36.sp))
Spacer(modifier = Modifier.height(20.dp))
Box(modifier = Modifier.padding(40.dp, 0.dp, 40.dp, 0.dp)) {
Button(
onClick = { signIn() },
shape = RoundedCornerShape(50.dp),
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
) {
Text(text = "Logg inn")
}
}
}
}
private fun signIn() {
val providers = arrayListOf(
AuthUI.IdpConfig.EmailBuilder().build(),
AuthUI.IdpConfig.GoogleBuilder().build()
)
val signinIntent = AuthUI.getInstance()
.createSignInIntentBuilder()
.setAvailableProviders(providers)
.build()
signInLauncher.launch(signinIntent)
}
private val signInLauncher = registerForActivityResult(
FirebaseAuthUIActivityResultContract()
) {
res -> this.signInResult(res)
}
private fun signInResult(result: FirebaseAuthUIAuthenticationResult) {
val response = result.idpResponse
if (result.resultCode == RESULT_OK) {
user = FirebaseAuth.getInstance().currentUser
Log.e("MainActivity.kt", "Innlogging vellykket")
ScreenMain()
} else {
Log.e("MainActivity.kt", "Feil med innlogging" + response?.error?.errorCode)
}
}
}
I need to add more text to be allowed to post this much code you can ignore this text cause it is just for being able to post.
As #z.y mentioned, you can pass a lambda with a onFirebaseAuthSuccess. I would also add that as you are passing the navController to the signup screen, the lambda callback you need to pass should look something like
onFirebaseAuthSuccess = { navController.navigate(Routes.Profileromoss.route) } - or whatever route you need
Based on
composable(Routes.SignUp.route) {
SignUp(navController = navController)
}
I would assume your signIn screen is called from inside the scope of a composable. If you can add the extract of code containing how you are calling the signInResult function we can be sure about it.
I'm not familiar with Firebase Authentication so I'm not sure where do you call or how you use your signInResult function but you cannot invoke a function that is annotated with #Composable (ScreenMain) from a scope that is not annotated by it such as ordinary function (signInResult).
You can consider adding a lambda callback for signInResult which will be called in the RESULT_OK condition block.
private fun signInResult(result: FirebaseAuthUIAuthenticationResult, onFirebaseAuthSuccess: () -> Unit) {
val response = result.idpResponse
if (result.resultCode == RESULT_OK) {
...
...
onFirebaseAuthSuccess() // this callback
} else {
...
}
}
Edit: #sgtpotatoe has better answer, you can invoke a navigation in your root composable from the lambda callback that will navigate to your target screen.
Ok so, in your MainActivity, you want your navigational component to be at the top:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
JetpackComposeDemoTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
ScreenMain()
}
}
}
}
Then add a route to your navhost for the login page:
composable(Routes.LoginPage.route) {
LoginPage(navController = navController)
}
I think its a bit of a major change, but you would have to rely on a view model to make the authentication, so it can handle the calls, not blocking the ui or showing a loading screen, and communicate with the view
It would look something like this:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModel = MyViewModel()
setContent {
JetpackComposeDemoTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
ScreenMain(viewModel)
}
}
}
}
In the LoginPage you want to access the viewmodel to start the auth service calls
In the LoginPage you want to access the viewmodel to observe if the call is succesfull, and in that case do the navigation
In the MyViewModel you want to have the authentication calls, and to update the variable that triggers the navigation if auth is succesfull
Here is an example of a sample firebase authentication app in compose, I would use it as a guide https://firebase.blog/posts/2022/05/adding-firebase-auth-to-jetpack-compose-app

Text in jetpack compose not showing text?

This class simply emit character after 1-sec delay
class MainViewModel: ViewModel() {
var bouncingName = flow<Char> {
val text="hemant"
for (letter in text){
delay(1000L)
emit(letter)
}
}
}
This main class displays string after concatenating string display on the app
class MainActivity : ComponentActivity() {
#SuppressLint("CoroutineCreationDuringComposition")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CompleteGuideKotlinFlowTheme {
// A surface container using the 'background' color from the theme
var letter by remember {
mutableStateOf(StringBuilder())
}
val viewModel = viewModel<MainViewModel>()
// var currentValue=viewModel.countDownValue.collectAsState(initial = 10)
lifecycleScope.launch {
viewModel.bouncingName.collect {
letter.append(it)
Log.d("TAG", "Character in main $letter")
}
}
Box(modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center) {
Text(
text = letter.toString(),
fontSize = 30.sp
)
}
}
}
}
}
But nothing is showing on the app.
I don't understand why?
StringBuilder is mutable. When you append to it, Compose doesn't get notified and the recomposition doesn't happen. You should use immutable types with MutableState and update it using MutableState's setter:
var letter by remember { mutableStateOf("") }
...
letter = letter + it

JetpackCompose Navigation Nested Graphs cause "ViewModelStore should be set before setGraph call" exception

I am trying to apply Jetpack Compose navigation into my application.
My Screens: Login/Register screens and Bottom navbar screens(call, chat, settings).
I already found out that the best way to do this is using nested graphs.
But I keep getting ViewModelStore should be set before setGraph call exception. However, I don't think this is the right exception.
My navigation is already in the latest version. Probably my nested graph logic is not right.
Requirement:
I want to be able to navigate from the Login or Register screen to any BottomBar Screen & reverse
#Composable
fun SetupNavGraph(
navController: NavHostController,
userViewModel: UserViewModel
) {
NavHost(
navController = navController,
startDestination = BOTTOM_BAR_GRAPH_ROUTE,
route = ROOT_GRAPH_ROUTE
) {
loginNavGraph(navController = navController, userViewModel)
bottomBarNavGraph(navController = navController, userViewModel)
}
}
NavGraph.kt
fun NavGraphBuilder.loginNavGraph(
navController: NavHostController,
userViewModel: UserViewModel
) {
navigation(
startDestination = Screen.LoginScreen.route,
route = LOGIN_GRAPH_ROUTE
) {
composable(
route = Screen.LoginScreen.route,
content = {
LoginScreen(
navController = navController,
loginViewModel = userViewModel
)
})
composable(
route = Screen.RegisterScreen.route,
content = {
RegisterScreen(
navController = navController,
loginViewModel = userViewModel
)
})
}
}
LoginNavGraph.kt
fun NavGraphBuilder.bottomBarNavGraph(
navController: NavHostController,
userViewModel: UserViewModel
) {
navigation(
startDestination = Screen.AppScaffold.route,
route = BOTTOM_BAR_GRAPH_ROUTE
) {
composable(
route = Screen.AppScaffold.route,
content = {
AppScaffold(
navController = navController,
userViewModel = userViewModel
)
})
}
}
BottomBarNavGraph.kt
#Composable
fun AppScaffold(
navController: NavHostController,
userViewModel: UserViewModel
) {
val scaffoldState = rememberScaffoldState()
Scaffold(
bottomBar = {
BottomBar(mainNavController = navController)
},
scaffoldState = scaffoldState,
) {
NavHost(
navController = navController,
startDestination = NavigationScreen.EmergencyCallScreen.route
) {
composable(NavigationScreen.EmergencyCallScreen.route) {
EmergencyCallScreen(
navController = navController,
loginViewModel = userViewModel
)
}
composable(NavigationScreen.ChatScreen.route) { ChatScreen() }
composable(NavigationScreen.SettingsScreen.route) {
SettingsScreen(
navController = navController,
loginViewModel = userViewModel
)
}
}
}
}
AppScaffold.kt
#Composable
fun BottomBar(mainNavController: NavHostController) {
val items = listOf(
NavigationScreen.EmergencyCallScreen,
NavigationScreen.ChatScreen,
NavigationScreen.SettingsScreen,
)
BottomNavigation(
elevation = 5.dp,
) {
val navBackStackEntry by mainNavController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
items.map {
BottomNavigationItem(
icon = {
Icon(
painter = painterResource(id = it.icon),
contentDescription = it.title
)
},
label = {
Text(
text = it.title
)
},
selected = currentRoute == it.route,
selectedContentColor = Color.White,
unselectedContentColor = Color.White.copy(alpha = 0.4f),
onClick = {
mainNavController.navigate(it.route) {
mainNavController.graph.startDestinationRoute?.let { route ->
popUpTo(route) {
saveState = true
}
}
restoreState = true
launchSingleTop = true
}
},
)
}
}
}
BottomBar.kt
const val ROOT_GRAPH_ROUTE = "root"
const val LOGIN_GRAPH_ROUTE = "login_register"
const val BOTTOM_BAR_GRAPH_ROUTE = "bottom_bar"
sealed class Screen(val route: String) {
object LoginScreen : Screen("login_screen")
object RegisterScreen : Screen("register_screen")
object AppScaffold : Screen("app_scaffold")
}
Screen.kt
sealed class NavigationScreen(val route: String, val title: String, #DrawableRes val icon: Int) {
object EmergencyCallScreen : NavigationScreen(
route = "emergency_call_screen",
title = "Emergency Call",
icon = R.drawable.ic_phone
)
object ChatScreen :
NavigationScreen(
route = "chat_screen",
title = "Chat",
icon = R.drawable.ic_chat)
object SettingsScreen : NavigationScreen(
route = "settings_screen",
title = "Settings",
icon = R.drawable.ic_settings
)
}
NavigationScreen.kt
After struggling some time with this issue, I made my way out by using two separated NavHost. It might not be the right way to do it but it works at the moment. You can find the example source code here:
https://github.com/talhaoz/JetPackCompose-LoginAndBottomBar
Hope they make the navigation easier on upcoming releases.
Nesting of NavHost is not allowed. It results in ViewModelStore should be set before setGraph call Exception. Generally, the bottom nav is outside of the NavHost, which is what the docs show. The recommended approach is a single NavHost, where you hide and show your bottom nav based on what destination you are on.
One NavHost, one NavHostController. Create a new NavHostController in front of the nested NavHost on AppScaffold.
Have similar issue when implement this common UI pattern:
HomePage(with BottomNavigationBar), this page is hosted by Inner nav controller
click some links of one page
navigate to a new page (with new Scaffold instance). This page is hosted by Outer nav controller.
Kinda hacked this issue by using 2 NavHost with 2 navController instance.
Basic idea is using some msg channel to tell the outer nav controller, a Channel in my case.
private val _pages: Channel<String> = Channel()
var pages = _pages.receiveAsFlow()
#Composable
fun Route() {
val navController1 = rememberNavController()
LaunchedEffect(true) {
pages.collect { page ->
navController1.navigate("detail")
}
}
NavHost(navController = navController1, startDestination = "home") {
composable("home") { MainPage() }
composable("detail") { DetailPage() }
}
}
#Composable
fun MainPage() {
val navController2 = rememberNavController()
val onTabSelected = { tab: String ->
navController2.navigate(tab) {
popUpTo(navController2.graph.findStartDestination().id) { saveState = true }
launchSingleTop = true
restoreState = true
}
}
Scaffold(topBar = { TopAppBar(title = { Text("Home Title") }) },
bottomBar = {
BottomNavigation {
val navBackStackEntry by navController2.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
BottomNavigationItem(
selected = currentDestination?.hierarchy?.any { it.route == "tab1" } == true,
onClick = { onTabSelected("tab1") },
icon = { Icon(imageVector = Icons.Default.Favorite, "") },
label = { Text("tab1") }
)
BottomNavigationItem(
selected = currentDestination?.hierarchy?.any { it.route == "tab2" } == true,
onClick = { onTabSelected("tab2") },
icon = { Icon(imageVector = Icons.Default.Favorite, "") },
label = { Text("tab2") }
)
BottomNavigationItem(
selected = currentDestination?.hierarchy?.any { it.route == "tab3" } == true,
onClick = { onTabSelected("tab3") },
icon = { Icon(imageVector = Icons.Default.Favorite, "") },
label = { Text("tab3") }
)
}
}
) { value ->
NavHost(navController = navController2, startDestination = "tab1") {
composable("tab1") { Home() }
composable("tab2") { Text("tab2") }
composable("tab3") { Text("tab3") }
}
}
}
class HomeViewModel: ViewModel()
#Composable
fun Home(viewModel: HomeViewModel = HomeViewModel()) {
Button(
onClick = {
viewModel.viewModelScope.launch {
_pages.send("detail")
}
},
modifier = Modifier.padding(all = 16.dp)
) {
Text("Home", modifier = Modifier.padding(all = 16.dp))
}
}
#Composable
fun DetailPage() {
Scaffold(topBar = { TopAppBar(title = { Text("Detail Title") }) }) {
Text("Detail")
}
}
Cons:
App needs to maintain UI stack information.
It's even harder to cope with responsive layout.
use rememberNavController() for your function
fun YourFunction(
navController: NavHostController = rememberNavController()
)
In my case, I had to create nav controller (for bottom bar) with in home screen.
#AndroidEntryPoint
class MainActivity: ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
setContent {
Theme {
Surface(modifier = Modifier.fillMaxSize()) {
AppContainer()
}
}
}
}
}
#Composable
fun AppContainer() {
val mainNavController = rememberNavController()
// This was causing the issue. I moved this to HomeScreen.
// val bottomNavController = rememberNavController()
Box(
modifier = Modifier.background(BackgroundColor)
) {
NavGraph(mainNavController)
}
}
#Composable
fun HomeScreen(mainNavController: NavController) {
val bottomBarNavController = rememberNavController()
}

Jetpack Compose: close application by button

NavController can't pop programmatically the latest #Composable in the stack. I.e. popBackStack() doesn't work if it's a root page. So the application can be closed by tap on "Close" button view and only hardware Back key allows to leave application.
Example: Activity
class AppActivity : ComponentActivity() {
override fun onCreate(state: Bundle?) {
super.onCreate(state)
setContent {
val controller = rememberNavController()
NavHost(controller, startDestination = HOME) {
composable(HOME) { HomePage(controller) }
...
}
}
}
}
HomePage.kt
#Composable
fun HomePage(controller: NavController) {
Button(onClick = {
controller.popBackStack()
}) {
Text("Exit")
}
}
Question:
How to close the app in onClick handler if Compose Navigation is used.
You can use this:
#Composable
fun HomePage(controller: NavController) {
val activity = (LocalContext.current as? Activity)
Button(onClick = {
activity?.finish()
}) {
Text("Exit")
}
}
#AndroidEntryPoint
class MainActivity:AppCompatActivity() {
#ExperimentalAnimatedInsets
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
val activityKiller: () -> Unit = {
this.finish()
}
setContent {
MainAppEntry(activityKiller = activityKiller)
}
}
}
#Composable
fun MainAppEntry(activityKiller: () -> Unit) {
val mainViewModel: MainViewModel = hiltViewModel<MainViewModel>()
//mutableStateOf .......
var isKillRequested = mainViewModel.mainActivityState.isActivityFinishRequested
if (isKillRequested) {
activityKiller()
return
}
Column(Modifier.fillMaxSize()) {
TextButton(onClick = {
mainViewModel.smuaActivityState.requestActivityFinish()
}) {
Text(text = "Kill app")
}
}
}
finishAffinity() closes your app and keeps it in the Recent apps screen
finishAndRemoveTask() closes your app and removes it from the Recent apps screen

cant navigate to other fragment from webview

im trying to navigate from webview to fragment using navController, but it seems not working.
here is my code
class LandingPageCreditCardFragment : Fragment(R.layout.fragment_landing_page_credit_card) {
private val binding by viewBinding(FragmentLandingPageCreditCardBinding::bind)
lateinit var navController: NavController
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
navController = view.findNavController()
Log.i("link url", arguments?.getString("url").toString())
showWebViewContent(requireArguments().getString("url")!!)
}
private fun showWebViewContent(url: String) {
binding.webViewCreditCard.settings.javaScriptEnabled = true
binding.webViewCreditCard.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
if (url != null) {
view?.loadUrl(url)
}
return true
}
}
binding.webViewCreditCard.addJavascriptInterface(object : Any() {
#JavascriptInterface
fun valid() {
Toast.makeText(requireContext(), "test", Toast.LENGTH_SHORT).show()
val builder = AlertDialog.Builder(requireContext())
builder.setTitle("Konfirmasi")
builder.setMessage("navigasi ke pesanan")
builder.setPositiveButton("Ya") { dialog, which ->
navController.navigate(R.id.action_landingPageCreditCardFragment_to_pesananFragment)
dialog.dismiss()
}
builder.setNegativeButton("Tidak") { dialog, which ->
dialog.dismiss()
}
builder.show() }
}, "btn")
binding.webViewCreditCard.loadUrl(url)
} }
im referrring to this answer to get my function working
i try to show toast, its working
then im try to show alertDialog contain button to navigate from webview to next fragment, when i click it, my app crash.
any help appreciated
after much debugging, i found the answer.
call the navController in runOnUiThread wrapped on Thread and Handler
binding.webViewCreditCard.addJavascriptInterface(object : Any() {
#JavascriptInterface
fun valid() {
val handler = Handler(Looper.getMainLooper())
handler.post(Thread {
(activity as MainActivity).runOnUiThread {
navController.navigate(R.id.action_landingPageCreditCardFragment_to_pesananFragment)
}
})
}
}, "btn")