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

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

Related

How to show rewardedInterstitial ad which was loaded in mainActivity in a compose?

I try to load rewarded interestitial ad in Mainactivity and then show it in one composable screen in jetpack compose. I loaded it successfully but in my RewardedShow compose screen rewardedInterstitialAd is null? I used my code from https://developers.google.com/admob/android/rewarded-interstitial
This is my code in MainActivity
class MainActivity : ComponentActivity(){
var rewardedInterstitialAd: RewardedInterstitialAd? = null
private var TAG = "MainActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyBeautyAppInJetpackComposeTheme {
MyNavGraph()
MobileAds.initialize(this) {
loadAd()
}
}
}
}
private fun loadAd() {
RewardedInterstitialAd.load(this, "ca-app-pub-
3940256099942544/5354046379",
AdRequest.Builder().build(), object :
RewardedInterstitialAdLoadCallback() {
override fun onAdLoaded(ad: RewardedInterstitialAd) {
Log.d(TAG, "Ad was loaded.")
rewardedInterstitialAd = ad
}
override fun onAdFailedToLoad(adError: LoadAdError) {
Log.d(TAG, adError?.toString())
rewardedInterstitialAd = null
}
})
}
}
and this the RewardedShowCompose screen
#SuppressLint("UnrememberedMutableState")
#Composable
fun RewardedShowCompose( actions: MainActions, maskArg: String) {
val context = LocalContext.current
val loading = mutableStateOf(true)
Surface(modifier = Modifier.fillMaxSize())
{
if (MainActivity.rewardedInterstitialAd != null){
MainActivity.rewardedInterstitialAd?.show(context as Activity, OnUserEarnedRewardListener { actions.gotoFinalShow.invoke(maskArg) })
}
}
}
I made rewardedInterstitialAd Companion and now it works well. like this
companion object {
var rewardedInterstitialAd: RewardedInterstitialAd? = null
}

Jetpack Compose navigation crashes app when button clicked

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")

Will the operation of collect from Flow cost many system resources when I use Compose with Kotlin?

The SoundViewModel is a ViewModel class, and val listSoundRecordState may be used by some modules in the App.
In Code A, I invoke fun collectListSoundRecord() when I need to use the data listSoundRecordState. But fun collectListSoundRecord() may be launched again and again because of Jetpack Compose recomposition, I don't know if it will cost many system resources?
In Code B, I launch private fun collectListSoundRecord() in init { }, collectListSoundRecord() will be launched only one time, but it will persist in memory until the App code closed even if I needn't to use the data listSoundRecordState, will the way cost many system resources?
Code A
#HiltViewModel
class SoundViewModel #Inject constructor(
...
): ViewModel() {
private val _listSoundRecordState = MutableStateFlow<Result<List<MRecord>>>(Result.Loading)
val listSoundRecordState = _listSoundRecordState.asStateFlow()
init { }
//It may be launched again and again
fun collectListSoundRecord(){
viewModelScope.launch {
listRecord().collect {
result -> _listSoundRecordState.value =result
}
}
}
private fun listRecord(): Flow<Result<List<MRecord>>> {
return aSoundMeter.listRecord()
}
}
Code B
#HiltViewModel
class SoundViewModel #Inject constructor(
...
): ViewModel() {
private val _listSoundRecordState = MutableStateFlow<Result<List<MRecord>>>(Result.Loading)
val listSoundRecordState = _listSoundRecordState.asStateFlow()
init { collectListSoundRecord() }
private fun collectListSoundRecord(){
viewModelScope.launch {
listRecord().collect {
result -> _listSoundRecordState.value =result
}
}
}
private fun listRecord(): Flow<Result<List<MRecord>>> {
return aSoundMeter.listRecord()
}
}
You would probably benefit from collecting the original flow (from listRecord()) only when there is a subscriber to your intermediate flow (the one you keep in your SoundViewModel) and cache the results.
A subscriber, in your case, would be a Composable function that collects the values (and may recompose often).
You can achieve this using the non-suspending variant of stateIn(), since you have a default value.
#HiltViewModel
class SoundViewModel #Inject constructor(
...
): ViewModel() {
val listSoundRecordState = listRecord().stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Result.Loading)
private fun listRecord(): Flow<Result<List<MRecord>>> {
return aSoundMeter.listRecord()
}
}
In order to use the StateFlow from the UI layer (a #Composable function), you will have to transform it into a State, like so:
val viewModel: SoundViewModel = viewModel()
val listSoundRecord = viewModel.listSoundRecordState.collectAsState()
I use the below implementation of composable with view-model using LaunchedEffect
PokemonDetailScreen.kt
#Composable
fun PokemonDetailScreen(
dominantColor: Color,
pokemonName: String,
navController: NavController,
topPadding: Dp = 20.dp,
pokemonImageSize: Dp = 200.dp,
viewModel: PokemonDetailVm = hiltViewModel()
) {
var pokemonDetailData by remember { mutableStateOf<PokemonDetailView>(PokemonDetailView.DisplayLoadingView) }
val pokemonDetailScope = rememberCoroutineScope()
LaunchedEffect(key1 = true){
viewModel.getPokemonDetails(pokemonName)
viewModel.state.collect{ pokemonDetailData = it }
}
Box(
modifier = Modifier
.fillMaxSize()
.background(dominantColor)
.padding(bottom = 16.dp)
) {
PokemonHeader(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.2f)
.align(Alignment.TopCenter)
) {
navController.popBackStack()
}
PokemonBody(
pokemonInfo = pokemonDetailData,
topPadding = topPadding,
pokemonImageSize = pokemonImageSize
) {
pokemonDetailScope.launch {
viewModel.getPokemonDetails(pokemonName)
}
}
Box(
contentAlignment = Alignment.TopCenter,
modifier = Modifier
.fillMaxSize()
) {
if(pokemonDetailData is PokemonDetailView.DisplayPokemonView){
val data = (pokemonDetailData as PokemonDetailView.DisplayPokemonView).data
data.sprites.let {
// Image is available
val url = PokemonUtils.formatPokemonDetailUrl(it.frontDefault)
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(url)
.crossfade(true)
.build(),
contentDescription = data.name,
contentScale = ContentScale.Fit,
modifier = Modifier
// Set the default size passed to the composable
.size(pokemonImageSize)
// Shift the image down from the top
.offset(y = topPadding)
)
}
}
}
}
}
PokemonDetailVm.kt
#HiltViewModel
class PokemonDetailVm #Inject constructor(
private val repository: PokemonRepositoryFeature
): ViewModel(){
private val _state = MutableSharedFlow<PokemonDetailView>()
val state = _state.asSharedFlow()
suspend fun getPokemonDetails(pokemonName:String) {
viewModelScope.launch {
when(val pokemonInfo = repository.getPokemonInfo(pokemonName)){
is Resource.Error -> {
pokemonInfo.message?.let {
_state.emit(PokemonDetailView.DisplayErrorView(message = it))
}
}
is Resource.Loading -> {
_state.emit(PokemonDetailView.DisplayLoadingView)
}
is Resource.Success -> {
pokemonInfo.data?.let {
_state.emit(PokemonDetailView.DisplayPokemonView(data = it))
}
}
}
}
}
}

In Android jetpack compose, how to move the screen when I log in?

MainActivity
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
#Inject
lateinit var connectivityManager: ConnectivityManager
#Inject
lateinit var settingsDataStore: SettingsDataStore
override fun onStart() {
super.onStart()
connectivityManager.registerConnectionObserver(this)
}
override fun onDestroy() {
super.onDestroy()
connectivityManager.unregisterConnectionObserver(this)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = Screen.Login.route) {
composable(
route = Screen.Login.route
) { navBackStackEntry ->
val factory = HiltViewModelFactory(LocalContext.current, navBackStackEntry)
val viewModel: LoginViewModel = viewModel(key = "LoginViewModel", factory = factory)
LoginScreen(
isNetworkAvailable = connectivityManager.isNetworkAvailable.value,
onNavigateToHomeScreen = navController::navigate,
viewModel = viewModel
)
}
composable(
route = Screen.Home.route
) { navBackStackEntry ->
val factory = HiltViewModelFactory(LocalContext.current, navBackStackEntry)
val viewModel: HomeViewModel = viewModel(key = "HomeViewModel", factory = factory)
HomeScreen(
)
}
}
}
}
}
LoginScreen
#Composable
fun LoginScreen(
isNetworkAvailable: Boolean,
onNavigateToHomeScreen: (String) -> Unit,
viewModel: LoginViewModel
) {
val loading = viewModel.loading.value
val user = viewModel.user.value
Scaffold(
modifier = Modifier.fillMaxSize()
) {
if (!loading && user == null) {
Button(onClick = { viewModel.onTriggerEvent(LoginEvent.AuthStateEvent) }) {
Column(
) {
Text(text = "Log in")
}
}
}else {
user?.let {
val route = Screen.Home.route
onNavigateToHomeScreen(route)
}
}
}
}
LoginViewModel
#HiltViewModel
class LoginViewModel #Inject constructor(
private val postLogin: PostLogin,
private val connectivityManager: ConnectivityManager
) : ViewModel() {
val user: MutableState<User?> = mutableStateOf(null)
val loading = mutableStateOf(false)
val onLoad: MutableState<Boolean> = mutableStateOf(false)
fun onTriggerEvent(event: LoginEvent) {
viewModelScope.launch {
try {
when(event) {
is LoginEvent.AuthStateEvent -> {
Log.d(TAG,"Phase 1")
if (user.value == null) {
val auth: Auth = Auth([***AuthData***])
postAuth(auth)
}
}
}
}catch (e: Exception) {
Log.d(TAG, "launchJob: Exception: ${e}, ${e.cause}")
e.printStackTrace()
}
}
}
private fun postAuth(auth: Auth) {
Log.d(TAG,"Phase 2")
postLogin.execute(auth, connectivityManager.isNetworkAvailable.value).onEach { dataState ->
loading.value = dataState.loading
dataState.data?.let { data ->
user.value = data
Log.d(TAG,"Phase 3")
Log.d(TAG,"User Data File")
}
dataState.error?.let { error ->
Log.d(TAG, "postAuth: ${error}")
}
}.launchIn(viewModelScope)
}
}
LoginEvent
sealed class LoginEvent {
object AuthStateEvent: LoginEvent()
}
PostLogin
class PostLogin(
private val apiService: ApiService,
private val authDtoMapper: AuthDtoMapper,
private val userDtoMapper: UserDtoMapper
) {
fun execute(
auth: Auth,
isNetworkAvailable: Boolean
): Flow<DataState<User>> = flow {
//auth -> authDto -> InterActions -> userDto -> user
try {
emit(DataState.loading())
delay(1000)
if (isNetworkAvailable) {
val networkUser = postLoginFromNetwork(auth)
if (networkUser != null) {
emit(DataState.success(networkUser))
}else {
emit(DataState.error<User>("User data is null"))
}
}else {
emit(DataState.error<User>("Unable to connect network"))
}
}catch (e: Exception) {
emit(DataState.error<User>(e.message?: "Unknown Error"))
}
}
private suspend fun postLoginFromNetwork(auth: Auth): User {
return userDtoMapper.mapToDomainModel(apiService.login(authDtoMapper.fromDomain(auth)))
}
}
HomeScreen
#Composable
fun HomeScreen() {
Card(
modifier = Modifier.fillMaxSize()
) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "HomeScreen",
fontWeight = FontWeight.Bold,
fontSize = 40.sp
)
}
}
}
Hello, I made the above code go to HomeScreen when login is successful by pressing the Login button on LoginScreen.
It was confirmed that the communication was smooth and the value was also brought.
However, when I tried debugging, I confirmed that this part was repeatedly done on Login Screen.
user?.let {
val route = Screen.Home.route
onNavigateToHomeScreen(route)
}
As you can see, when the user has a value, the screen is moved, but that part is repeatedly done, so the HomeScreen screen glitters like a flash.
That's why I'm asking you this question. When the value changes after communication, please tell me how to move the screen.
Use the LauchedEffect
LaunchedEffect(user){
user.let {
navController.navigate("")
}
}

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