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

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.

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

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

android modern development: how can I understand user did not change any data in a screen?

I built a fragment, using jetpack compose for adding views on screen and state and view model classes for saving state.
first time when I navigate to this fragment, fetch API is called and If I have any value on this API, I filled text fields with them. It is possible that the API is empty and do not return any value.
Users can enter any data on this text fields or can ignore them because filling the text fields is optional and when he/she clicks on submit button; the data is saved in server by calling save API and shows a successful message that data is saved.
my question is that when user navigates to this fragment and does not enter or change any value,
when clicks on submit button, I do not want to show that toast.
how can I handle it? thanks.
First you will need to create a data class to hold the values for your screen as a mutable state with a default value.
data class SampleState(
val isLoading: Boolean = true,
val someText1: String = "",
val someText2: String = ""
)
It is also necessary to create a sealed class with the possible screen actions, such as changing someText1 and someText2 and also the click of a button.
sealed class SampleAction {
object TestButton : SampleAction()
data class ChangeSomeText1(val text1: String) : SampleAction()
data class ChangeSomeText2(val text2: String) : SampleAction()
}
And finally, a sealed class to contain the possible one-time-event that can be triggered through some action, like after clicking on the button with the fields properly filled in.
sealed class SampleChannel {
object ValidFields : SampleChannel()
}
The view model will be responsible for managing all the logic:
#HiltViewModel
class SampleViewModel #Inject constructor() : ViewModel() {
var state by mutableStateOf(value = SampleState())
private set
private val _channel = Channel<SampleChannel>()
val channel = _channel.receiveAsFlow()
init {
loadInitialFakeData()
}
fun onAction(action: SampleAction) {
when (action) {
is SampleAction.TestButton -> testButtonClicked()
is SampleAction.ChangeSomeText1 -> state = state.copy(someText1 = action.text1)
is SampleAction.ChangeSomeText2 -> state = state.copy(someText2 = action.text2)
}
}
private fun loadInitialFakeData() = viewModelScope.launch {
delay(timeMillis = 1000)
if (Random.nextBoolean()) state = state.copy(
someText1 = "fake text 1 from api",
someText2 = "fake text 2 from api"
)
state = state.copy(isLoading = false)
}
private fun testButtonClicked() {
val hasBlankField = listOf(
state.someText1,
state.someText2
).any { string ->
string.isBlank()
}
if (hasBlankField) return
viewModelScope.launch {
_channel.send(SampleChannel.ValidFields)
}
}
}
In this case there is a loadInitialFakeData function that is executed as soon as the view model is instantiated, which simulates a delay only for testing purposes and according to a random boolean assigns or not values in the state class.
There is also a function testButtonClicked that will do the logic if any of the fields is empty, if any of them is empty nothing happens (I left it that way so as not to extend the code too much) and if it is not empty we notify the channel with the ValidFields.
The only public function in the view model is the onAction, which is responsible for receiving actions through the composable screen.
Now on the composable screen we can do it like this:
#Composable
fun SampleScreen(
viewModel: SampleViewModel = hiltViewModel()
) {
val state = viewModel.state
val scaffoldState = rememberScaffoldState()
LaunchedEffect(key1 = Unit) {
viewModel.channel.collect { channel ->
when (channel) {
SampleChannel.ValidFields -> {
scaffoldState.snackbarHostState.showSnackbar(
message = "all right!"
)
}
}
}
}
Scaffold(
modifier = Modifier.fillMaxSize(),
scaffoldState = scaffoldState
) {
Column(modifier = Modifier.fillMaxSize()) {
ComposeLoading(isLoading = state.isLoading)
ComposeForm(
changeSomeText1 = {
viewModel.onAction(SampleAction.ChangeSomeText1(it))
},
changeSomeText2 = {
viewModel.onAction(SampleAction.ChangeSomeText2(it))
},
testButtonClick = {
viewModel.onAction(SampleAction.TestButton)
},
state = state
)
}
}
}
#Composable
private fun ComposeLoading(
isLoading: Boolean
) = AnimatedVisibility(
modifier = Modifier.fillMaxWidth(),
visible = isLoading,
enter = expandVertically(animationSpec = tween()),
exit = shrinkVertically(animationSpec = tween())
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
Spacer(modifier = Modifier.height(height = 16.dp))
Text(
text = "loading...",
color = MaterialTheme.colors.onBackground,
style = MaterialTheme.typography.body2
)
Spacer(modifier = Modifier.height(height = 16.dp))
}
}
#Composable
private fun ComposeForm(
changeSomeText1: (String) -> Unit,
changeSomeText2: (String) -> Unit,
testButtonClick: () -> Unit,
state: SampleState
) = Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
ComposeSimpleTextField(
value = state.someText1,
onValueChange = {
changeSomeText1(it)
},
label = "text field 1"
)
Spacer(modifier = Modifier.height(height = 16.dp))
ComposeSimpleTextField(
value = state.someText2,
onValueChange = {
changeSomeText2(it)
},
label = "text field 2"
)
Spacer(modifier = Modifier.height(height = 16.dp))
Button(
modifier = Modifier.align(alignment = Alignment.End),
onClick = testButtonClick
) {
Text(text = "Test Button")
}
}
#Composable
private fun ComposeSimpleTextField(
value: String,
onValueChange: (String) -> Unit,
label: String
) = TextField(
modifier = Modifier.fillMaxWidth(),
value = value,
onValueChange = { onValueChange(it) },
label = {
Text(text = label)
}
)
The main thing here is the LaunchedEffect to collect all possible events from the SampleChannel (on this example there is only one) and the calls to screen events when the text of some field is changed or when the button is clicked... The rest is just a compose boilerplate that doesn't matter much for the main logic.
Note: I used Hilt for dependency injection in this example, so I had access to #HiltViewModel and hiltViewModel()
implementation 'com.google.dagger:hilt-android:2.42'
kapt 'com.google.dagger:hilt-android-compiler:2.42'
implementation 'androidx.hilt:hilt-navigation-compose:1.0.0'

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 ...
}
}

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

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