Rapid clicks navigation problem with bottom sheet in Jetpack Compose - kotlin

I have a button from which on click I navigate to bottom sheet, the problem come when I click this button multiple times very fast, I am using accompanist library for navigation, as you see below, here is my ModalBottomSheetLayout and Scaffold
val navController = rememberAnimatedNavController()
val bottomSheetNavigator = rememberFullScreenBottomSheetNavigator()
navController.navigatorProvider += bottomSheetNavigator
ModalBottomSheetLayout(
bottomSheetNavigator = bottomSheetNavigator
) {
Scaffold(
scaffoldState = rememberScaffoldState(snackbarHostState = snackState),
content = { paddingValues ->
AnimatedNavHost(
navController = navController,
startDestination = "bottomsheet",
) {
bottomSheet("bottomsheet") {
CancelOrderBottomSheetUi()
}
}
},
bottomBar = {
BuildBottomBar(navController)
}
)
}
And this is my bottom sheet navigator
fun rememberFullScreenBottomSheetNavigator(
animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec,
skipHalfExpanded: Boolean = true,
): BottomSheetNavigator {
val sheetState = rememberModalBottomSheetState(
ModalBottomSheetValue.Hidden,
animationSpec
)
if (skipHalfExpanded) {
LaunchedEffect(sheetState) {
snapshotFlow { sheetState.isAnimationRunning }
.collect {
with(sheetState) {
val isOpening =
currentValue == ModalBottomSheetValue.Hidden && targetValue == ModalBottomSheetValue.HalfExpanded
val isClosing =
currentValue == ModalBottomSheetValue.Expanded && targetValue == ModalBottomSheetValue.HalfExpanded
when {
isOpening -> animateTo(ModalBottomSheetValue.Expanded)
isClosing -> animateTo(ModalBottomSheetValue.Hidden)
}
}
}
}
}
return remember(sheetState) {
BottomSheetNavigator(sheetState = sheetState)
}
}
I have one screen where there is button, and onClick of this button I want to navigate from screen 1(where the button is clicked) to screen 2(where bottom sheet is) both of this screens have different viewModels.
This is where I click cancel button
onCancel = {
viewModel.cancelButton()
}
and in VM
fun cancelButton() {
viewModelScope.launch {
navigationDispatcher.navigateTo("bottomsheet")
}
}
So the problem is just when I spam button very fast the bottom sheet appears almost on full size and then it collapse again, if I do not spam it, it appears like normal bottom sheet and I cannot click the button on background again, this bug happens only if I spam it.
PS: I tried with button bouncer, with some delays with if cycle and so on, but noting of this worked, I think I am missing something essential for navigation itself.

Related

How to navigate back to the correct one screen before the previous in jetpack compose?

I have four compose screens and by clicking on their items user leads to my AdShowScreen then after watching the ad they lead to the FinalShow screen. like the image below
Now I want to navigate correctly back from finalShowScreen to one of the four compose screens that came from by overriding the back press button in FinalShowScreen.
This is my navGraph.
#SuppressLint("UnrememberedMutableState")
#Composable
fun MyNavGraph(
navController: NavHostController) {
val actions = remember(navController) { MainActions(navController) }
NavHost(
navController = navController,
startDestination = BottomNavItems.First.route
) {
composable(BottomNavItems.First.route) {
FirstScreen(actions)
}
composable(BottomNavItems.Second.route) {
SecondScreen(navController, actions)
}
composable(BottomNavItems.Third.route) {
ThirdScreen()
}
composable(Screens.Fourth.route) {
FourthScreen(navController, actions)
}
composable("${Screens.FinalShow.route}/{maskArg}") {
val maskArg = it.arguments?.getString("maskArg")
if (maskArg != null) {
FinalShowScreen(
maskArg = maskArg, navController,actions
)
}
}
composable("${Screens.RewardedShow.route}/{maskArg}") {
val maskArg = it.arguments?.getString("maskArg")
if (maskArg != null) {
RewardedShowCompose(
maskArg = maskArg, navController = navController, actions = actions
)
}
}
}
}
class MainActions(navController: NavController) {
val goToRoute: (String) -> Unit = { route ->
navController.navigate(route) {
navController.graph.startDestinationRoute?.let { rout ->
popUpTo(rout) {
saveState = true
}
}
launchSingleTop = true
restoreState = true
}
}
}
I'm trying this code below but It doesn't work. it goes back to AdShowScreen
val gotoAdShow: (String, String) -> Unit = { maskArg, route ->
navController.navigate("$route/$maskArg") {
navController.graph.startDestinationRoute?.let { rout ->
popUpTo(rout) {
saveState = true
inclusive = true
}
}
launchSingleTop = true
restoreState = true
}
}
Why are you using
navController.navigate(route) {
navController.graph.startDestinationRoute?.let { rout ->
popUpTo(rout) {
saveState = true
}
}
launchSingleTop = true
restoreState = true
}
What if instead you use navHostController.popBackStack("ad", inclusive = true) directly?
To navigate back from the final screen to the first one, add this to the FourthScreen Composable;
BackHandler {
navController.navigate(BottomNavItems.First.route) // Or to whichever route you want to navigate to
}
Also, it is not a good practice to pass the navController to other Composables because it will make your UI pretty hard to test.

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 can I make two windows on jetpack compose desktop and going from window to another?

How can I make two windows on jetpack compose desktop and going from window to another when I click button for example?
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "Products Manager",
state = rememberWindowState(width = 700.dp, height = 600.dp)
) {
val count = remember { mutableStateOf(0) }
MaterialTheme {
Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) {
Button(modifier = Modifier.align(Alignment.CenterHorizontally),
onClick = {
count.value++
}) {
Text(if (count.value == 0) "Hello World" else "Clicked ${count.value}!")
}
Button(modifier = Modifier.align(Alignment.CenterHorizontally),
onClick = {
count.value = 0
}) {
Text("Reset")
}
}
}
}
}
To create multiple windows, you simply need to have multiple Window composables. Check out Open and close multiple windows documentation section for example.
To switch between windows programmatically, you can use window.toFront() on the window that should become topmost: window is property available in FrameWindowScope inside Window.content.
Here's an example how it can be done with two window "types". You can replace type with any other identifier.
enum class WindowTypes {
First,
Second,
}
fun main() = application {
val windowFocusRequestSharedFlow = remember { MutableSharedFlow<WindowTypes>() }
WindowTypes.values().forEach { windowType ->
key(windowType) {
Window(
title = windowType.toString(),
onCloseRequest = ::exitApplication,
) {
LaunchedEffect(Unit) {
windowFocusRequestSharedFlow
.filter { it == windowType }
.collect {
window.toFront()
}
}
val scope = rememberCoroutineScope()
Button({
scope.launch {
val windowTypeToFocus = WindowTypes.values().run {
get((indexOf(windowType) + 1) % count())
}
windowFocusRequestSharedFlow.emit(windowTypeToFocus)
}
}) {
Text("next window")
}
}
}
}
}

Compose for Desktop LazyRow/LazyColumn not scrolling with mouse click

For some reason LazyColumns do not scroll with a mouse click and move gesture. It only works with the mouse wheel so far. For LazyRows it is also not possible to scroll with the mouse wheel. It seems that lazy row is useless for Compose for desktop.
Are there any possiblities to enable a click and move gesture on LazyRow and LazyColum. And if not is it at least possible to enable to scroll through a LazyRow with the mouse wheel?
I used this minimal reproducible example to test the scrolling
#Composable
#Preview
fun App() {
var text by remember { mutableStateOf("Hello, World!") }
MaterialTheme {
LazyRow(modifier = Modifier.fillMaxSize()) {
repeat(100) {
item {
Text("Test Test Test Test $it ")
}
}
}
}
}
fun main() = application {
Window(onCloseRequest = ::exitApplication) {
App()
}
}
This is the intended behavior.
All scrollable components (including LazyColumn) work (for now) only with mouse wheel scroll events on the desktop.The scrollable components should not respond to mouse drag/move events.
Here's a basic example of how you can add drag support to your components:
val scrollState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
LazyRow(
state = scrollState,
modifier = Modifier
.draggable(
orientation = Orientation.Horizontal,
state = rememberDraggableState { delta ->
coroutineScope.launch {
scrollState.scrollBy(-delta)
}
},
)
) {
items(100) {
Text("Test Test Test Test $it")
}
}

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.