Preview a "screen" in android jetpack compose navigation with a PreviewParameter NavController - kotlin

I discovering android Cetpack Compose (and Navigation) and try to display a preview of a view with a navController as parameter.
To achieve, I use the PreviewParameter and I have no error, but nothing displayed in the Preview window.
Anyone know how pass a fake NavController instance to a Composable?
class FakeNavController : PreviewParameterProvider<NavController> {
override val values: Sequence<NavController>
get() {}
}
#Preview
#Composable
fun Preview(
#PreviewParameter(FakeNavController::class) fakeNavController: NavController
) {
HomeView(fakeNavController)
}

You don't have to make it nullable and pass null to it.
You just need to pass this: rememberNavController()
#Preview
#Composable
fun Preview() {
HomeView(rememberNavController())
}

PreviewParameter is used to create multiple previews of the same view with different data. You're expected to return the needed values from values. In your example you return nothing that's why it doesn't work(it doesn't even build in my case).
That won't help you to mock the navigation controller, as you still need to create it somehow to return from values. I think it's impossible.
Instead you can pass a handler, in this case you don't need to mock it:
#Composable
fun HomeView(openOtherScreen: () -> Unit) {
}
// your Navigation view
composable(Destinations.Home) { from ->
HomeView(
openOtherScreen = {
navController.navigate(Destinations.Other)
},
)
}

Finally, I declare a nullable NavController and it works.
#Composable
fun HomeView(navController: NavController?) {
Surface {
Column(
modifier = Modifier
.padding(all = 4.dp)
) {
Text(
text = "Home View",
style = MaterialTheme.typography.body2
)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = { navController?.navigate("lineRoute") }) {
Text(text = "nav to Line view")
}
}
}
}
#Preview
#Composable
fun Preview (){
HomeView(null)
}

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

Changing the jetpack compose remember variable from within another function

I am developing an application for android devices, using the jetpack compose library.
Example I created inside a compose function
var taskVeriable = remember {mutableStateOf("Hello World")}
I need to update the value of variable from another compose function. Is there any way to achieve this?
#Composable
fun TestComposeA(){
var taskVeriable = remember {mutableStateOf("Hello World")}
TestComposeB(taskVeriable)
}
#Composable
fun TestComposeB(taskVeriable : String){
taskVeriable = "New Value"
}
I want to know if there is a way to do this.
You can pass mutable state and change it value later:
#Composable
fun TestComposeA(){
val taskVeriable = remember {mutableStateOf("Hello World")}
TestComposeB(taskVeriable)
}
#Composable
fun TestComposeB(taskVeriable : MutableState<String>){
taskVeriable.value = "New Value"
}
You can pass a function that changes your state.
This might help you:
#Composable
fun CompA() {
var text by remember { mutableStateOf("") }
CompB(
text = text,
onChange = { newText ->
text = newText
}
)
}
#Composable
fun CompB(
text: String,
onChange: (String) -> Unit
) {
TextField(value = text, onValueChange = { onChange(it) })
}

jetpack Compose row/column onClick navigation

Please bear with me I don't know how to properly phrase my question, but I'll try my best.
I made the screen above by putting muliple composables together.
I gave the composable carrying the row an onClick function;
#Composable
fun MenuGrid(
modifier: Modifier = Modifier,
onMenuCardClick: (Int) -> Unit = {},
) {
LazyHorizontalGrid(
rows = GridCells.Fixed(3),
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier
.height(200.dp)
) {
items(MenuData) { item ->
MenuCard(
drawable = item.drawable,
text = item.text,
modifier = Modifier
.height(56.dp)
.clickable { onMenuCardClick(item.drawable + item.text) }
)
}
}
}
Like I said earlier I put multiple composables together, to form the image above. I arranged the multiple composables in a composable, I called MenuContentScreen;
#Composable
fun MenuContentScreen(modifier: Modifier = Modifier) {
Column(
modifier
.verticalScroll(rememberScrollState())
.padding(vertical = 16.dp)) {
MenuQuote()
MenuContentSection(title = R.string.favorite_collections) {
MenuGrid()
}
}
}
Then I referenced/called the MenuContentScreen on the main Composable of that screen MenuScreen. (The one defined in NavHost)
#Composable
fun MenuScreen() {
MenuContentScreen(Modifier.padding())
}
Which is where the problem is, since the onClick function was defined in another composable, I can't use the onClick function for the MenuScreen;
So MY QUESTION is there a way I can link the onClick function on MenuGrid to MenuScreen, maybe creating an parameter on MenuScreen and assigning it to the MenuGrid's onClick function (which I have tried and got val cannot be assigned), or Any thing at all.
I will greatly appreciate any help. I have been on this like forever. No information is too small please.
Thanks in Advance.
You need to carry onClick lambda till your MenuScreen Composable such as
#Composable
fun MenuContentScreen(modifier: Modifier = Modifier,
onMenuCardClick: () -> Unit
) {
Column(
modifier
.verticalScroll(rememberScrollState())
.padding(vertical = 16.dp)) {
MenuQuote()
MenuContentSection(title = R.string.favorite_collections) {
MenuGrid(){
onMenuCardClick()
}
}
}
#Composable
fun MenuScreen(
onMenuCardClick: () -> Unit
) {
MenuContentScreen(Modifier.padding(), onClick= onMenuCardClick)
}
And in you nav graph without passing navController to MenuScreen
call
MenuContentScreen {
navController.navigate()
}

Navigate back to previous composable screen Lifecycle.Event.ON_CREATE event call again

My question is that when i navigate back/popup to previous composable screen Lifecycle.Event.ON_CREATE event call again. For example i have two composable screen, first show list of item and send one is detail screen of specific item. When i navigate back to list item screen. List item screen load(network call) again. Below is code test sample
Navigation Logic
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "home"){
composable("home") {
RememberLifecycleEvent(event = {
Log.i("check","home event")
// API Call
})
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(onClick = { navController.navigate("blur") }) {
Text(text = "Blur")
}
}
}
composable("blur") {
RememberLifecycleEvent(event = {
Log.i("check","blur event")
})
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(onClick = { navController.navigate("home") }) {
Text(text = "Home")
}
}
}
}
Lifecycle Event Logic
#Composable
fun RememberLifecycleEvent(
event: () -> Unit,
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
) {
val state by rememberUpdatedState(newValue = event)
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { owner, event ->
if (event == Lifecycle.Event.ON_CREATE) {
state()
Log.i("check","event = $event")
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
I want to call api only first time in Lifecycle.Event.ON_CREATE event
This is happening because when you navigate from A to B, the onDispose is called in A. Then, when you return to A from B, the DisposableEffect is called again and since the Activity is already in "resumed" state, the ON_CREATE event is sent again.
My suggestion is controlling this call in your view model since it is kept alive after you go to B from A.
There are a few possibilities depending if you want to call the API once on every 'forward' navigation to your first screen or if you want to call the API just once based on some other criteria.
If former, you can either use a ViewModel and call the API from it when the ViewModel is created. If you use Hilt and call hiltViewModel() inside your Composable the ViewModel will be scoped to the lifecycle of the NavBackStackEntry of your NavHost.
But the same scope can also be achieved by simply using a rememberSaveable, since this will use the saveStateHandle from the NavBackStackEntry of your NavHost.
Another advantage is that both of the above options also ensure that the API won't be called again on orientation change and other configuration changes (when they are enabled).
// Just a sample (suspend) call
suspend fun someApi(): String {
// ...
return "some result"
}
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "home"){
composable("home") {
var apiCalled by rememberSaveable { mutableStateOf(false) }
if (!apiCalled) {
apiCalled = true
// key = Unit is okay here, we only want to launch once when entering the composition
LaunchedEffect(Unit) {
val result = runCatching {
someApi()
}
if (result.isFailure) {
// retry the api call? or report the error
}
}
}
// rest of your code ...
}
}

How to show the bottom sheet with transparent background in jetpack compose?

My app consists of the home screen and on this screen, there is a button when users click on it they navigate to the login bottom sheet.
I am going to use this login bottom sheet elsewhere in the app so I prefer to make it a separate screen and navigate from home to login.
It is desirable to show the home screen as the background for the login screen. I mean the login bottom sheet's main content should be empty and transparent in order to see the home screen as the background. But instead of the home screen for background, the white background shows up.
Here are my codes:
LoginScreen:
#Composable
fun LoginScreen(
loginViewModel: LoginViewModel = hiltViewModel()
) {
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed)
)
val coroutineScope = rememberCoroutineScope()
BottomSheetScaffold(
scaffoldState = bottomSheetScaffoldState,
sheetContent = {
LoginContent()
},
sheetPeekHeight = 400.dp,
sheetShape = RoundedCornerShape(topEnd = 52.dp, topStart = 52.dp),
backgroundColor = Color.Transparent
) {
Box(modifier = Modifier.fillMaxSize().background(color = Color.Transparent)) {
}
}
}
HomeScreen:
#Composable
fun HomeScreen(
modifier: Modifier = Modifier,
viewModel: HomeViewModel = hiltViewModel(),
) {
Column(
modifier = Modifier
.fillMaxSize()
.background(color = Color.White)
) {
somecontent
...
...
...
Button(onClick = {
viewModel.navigate(
LoginDestination.route()
)
}) {
Text("Go to the login screen")
}
}
}
I use navigation like this:
fun interface NavigationDestination {
fun route(): String
val arguments: List<NamedNavArgument>
get() = emptyList()
val deepLinks: List<NavDeepLink>
get() = emptyList()
}
and then Login destination overrides it:
object LoginDestination : NavigationDestination {
override fun route(): String = "login"
}
and here is the implementation of the navigator:
#Singleton
internal class ClientNavigatorImpl #Inject constructor() : ClientNavigator {
private val navigationEvents = Channel<NavigatorEvent>()
override val destinations = navigationEvents.receiveAsFlow()
override fun navigateUp(): Boolean =
navigationEvents.trySend(NavigatorEvent.NavigateUp).isSuccess
override fun popBackStack(): Boolean =
navigationEvents.trySend(NavigatorEvent.PopBackStack).isSuccess
override fun navigate(route: String, builder: NavOptionsBuilder.() -> Unit): Boolean =
navigationEvents.trySend(NavigatorEvent.Directions(route, builder)).isSuccess
}
and the navigator event is:
sealed class NavigatorEvent {
object NavigateUp : NavigatorEvent()
object PopBackStack : NavigatorEvent()
class Directions(
val destination: String,
val builder: NavOptionsBuilder.() -> Unit
) : NavigatorEvent()
}
the way you are trying to show the LoginScreen won't work as you expected because when you navigate to LoginScreen it's like opening a new Screen, HomeScreen is then added to the backstack and not shown behind your LoginScreen. To make it work, try like this:
#Composable
fun HomeScreen(
modifier: Modifier = Modifier,
viewModel: HomeViewModel = hiltViewModel(),
) {
Column(
modifier = Modifier
.fillMaxSize()
.background(color = Color.White)
) {
Button(onClick = {
//TODO: Add functionality
}) {
Text("Go to the login screen")
}
}
}
And change the LoginScreen parameters that you can give it a Composable:
#Composable
fun LoginScreen(
loginViewModel: LoginViewModel = hiltViewModel(),
screen: #Composable (() -> Unit)
) {
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed)
)
val coroutineScope = rememberCoroutineScope()
BottomSheetScaffold(
scaffoldState = bottomSheetScaffoldState,
sheetContent = {
//The Login Content needs to be here
*EDIT*
BackHandler(enabled = true) {
coroutineScope.launch {
bottomSheetScaffoldState.bottomSheetState.collapse()
}
}
*EDIT*
},
sheetPeekHeight = 400.dp,
sheetShape = RoundedCornerShape(topEnd = 52.dp, topStart = 52.dp),
backgroundColor = Color.Transparent
) {
screen() //Adds the content which is shown on the Screen behind bottomsheet
}
}
And then use it somehow like this:
LoginScreen( /*YourLoginViewModel*/) {
HomeScreen(Modifier, /*YourHomeScreenModel*/){
}
}
Now your bottom sheet is shown all the time, to hide it you need to work with the BottomSheetState collapsed/expanded and the sheetPeekHeight = 400.dp, which you need to set to 0 that the sheet is hidden completely at first
In the end you need to implement that the BottomSheetState changes on the ButtonClick where you navigated to the Screen in your first attempt
Edit:
Also don't use backgroundColor. To change the bottomSheets Background you need to use sheetBackgroundColor = Color.Transparent
helpinghand You are right about handling the device back button. But now when the user presses the back button only the Login screen's sheet content collapse and the main content of the login screen which is the main content of the home screen remains. In order to it works I don't need the composable callback screen as a parameter for the login screen function and instead, I replace it with another callback like (callback: () -> Unit) and whenever want to get rid of login screen just invoke it in the login screen (for example when clicking outside the bottom sheet or collapsing it) and then in the home screen create a boolean mutable state for detecting when it needs to show the login screen and so in the trailing lambda make the state false.