How can I trigger calling a function when the user clicks outside the ModalBottomSheetLayout, effectively hiding it?
One solution I've found is to implement a LaunchedEffect with the state, but this only changes after it has been hidden, whereas I would like it to happen immediately as the Modal is hiding.
val bottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
LaunchedEffect(bottomSheetState.isVisible) {
if (!bottomSheetState.isVisible) {
doWork()
}
}
ModalBottomSheetLayout(
sheetState = bottomSheetState,
sheetContent = {
Text("Lorem Ipsum")
}
) {
Text("hello worled")
}
You can still use LaunchedEffect but listen to the value of the ModalBottomSheetState.currentValue
LaunchedEffect(modalBottomSheetState.currentValue) {
when (modalBottomSheetState.currentValue) {
ModalBottomSheetValue.Hidden -> {
// Do what you need with a hidden state
}
ModalBottomSheetValue.Expanded -> {
// Do what you need with Expanded state
}
ModalBottomSheetValue.HalfExpanded -> {
// Do what you need with a HalfExpanded state
}
}
You can trigger an action at half expanded so it doesn't wait until all the bottom sheet is hidden.
Note that his state is only enabled if the height of the bottom sheet is more than 50% of the screen height.
Related
I want to show some data and simulate loading. For this reason I use LaunchedEffect on the first screen. It works fine, but when I add navigation, LaunchedEffect launch twice.
Navigation: First (LaunchedEffect) -> Second -> Back to first (LaunchedEffect launch again)
I expect that when I return to the first screen LaunchedEffect won't launch and I will immediately see the data.
Sample of LaunchedEffect:
#Composable
fun FirstScreen(...) {
...
LaunchedEffect(Unit) {
state = State.Loading
delay(2_000L)
state = State.Success(...)
}
}
Sample of navigation:
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = FIRST_ROTE
) {
composable(FIRST_ROTE) { FirstScreen(...) }
composable(SECOND_ROTE) { SecondScreen(...) }
}
See what the documentation says
To call suspend functions safely from inside a composable, use the
LaunchedEffect composable. When LaunchedEffect enters the Composition,
it launches a coroutine with the block of code passed as a parameter.
The coroutine will be cancelled if LaunchedEffect leaves the
composition. If LaunchedEffect is recomposed with different keys, the existing coroutine will be
cancelled and the new suspend function will be launched in a new
coroutine.
When you navigate to another screen, the composable is removed from the composition (which means it is not drawn). This is why the LaunchedEffect is executed again.
You can control this execution using the state in view model. Something like:
#Composable
fun FirstScreen(vm: YourViewModel) {
LaunchedEffect(Unit) {
if (vm.someFlag == true) {
vm.someFlag = false
state = State.Loading
delay(2_000L)
state = State.Success(...)
}
}
}
I need to check when a certain LazyColumn item comes into view, and once it does, make a callback to onItemWithKeyViewed() only once to notify that this item has been viewed.
My attempt:
#Composable
fun SpecialList(
someItems: List<Things>,
onItemWithKeyViewed: () -> Unit
) {
val lazyListState = rememberLazyListState()
if (lazyListState.isScrollInProgress) {
val isItemWithKeyInView = lazyListState.layoutInfo
.visibleItemsInfo
.any { it.key == "specialKey" }
if (isItemWithKeyInView) {
onItemWithKeyViewed()
}
}
LazyColumn(
state = lazyListState
) {
items(items = someItems) { itemData ->
ComposableOfItem(itemData)
}
item(key = "specialKey") {
SomeOtherComposable()
}
}
}
Issue with my method is I notice the list scrolling performance degrades badly and loses frames. I realize this may be because it's checking all visible item keys on every frame?
Also, onItemWithKeyViewed() is currently being called multiple times instead of just the first time it's viewed.
Is there a more efficient way to make a single callback to onItemWithKeyViewed() only the first time "specialKey" item is viewed?
In such cases, when you have a state that is updated often, you should use derivedStateOf: this will cause recomposition only when the result of the calculation actually changes.
You should not call side effects (which is calling onItemWithKeyViewed) directly in the composable builder. You should use one of the special side-effect functions instead, usually LaunchedEffect - this ensures that the action is not repeated. You can find more information on this topic in Thinking in Compose and in side-effects documentation.
val isItemWithKeyInView by remember {
derivedStateOf {
lazyListState.isScrollInProgress &&
lazyListState.layoutInfo
.visibleItemsInfo
.any { it.key == "specialKey" }
}
}
if (isItemWithKeyInView) {
LaunchedEffect(Unit) {
onItemWithKeyViewed()
}
}
I have a login scren and when the login is successful and the view model updates the mutable state variable, my expectation is that a new composable function is called to show a new screen and the login one is removed. The problem is that when the new screen (aka Screen.AccountsScreen) is shown, its content keeps flashing/redrawing and same thing happen with the login form which never gets destroyed (I know this because the log message 'Recomponing...' gets printed endless). I assume this happens because the isLoginSuccessful state is always true. It seems I need an event that can be consumed only once, is this correct? If so, how can I do that?
LoginViewModel.kt
#HiltViewModel
class LoginViewModel #Inject constructor() : ViewModel() {
var isLoginSuccessful by mutableStateOf(false)
var errorMessage by mutableStateOf("")
fun onLoginClick(email: String, password:String) {
errorMessage = ""
if (credentialsValid(email, password)) {
isLoginSuccessful = true
} else {
errorMessage = "Email or password invalid"
isLoginSuccessful = false
}
}
}
LoginScreen.kt
#Composable
fun loginScreen(
navController: NavController,
viewModel: LoginViewModel = hiltViewModel()
) {
println("Recomponing...")
// Here gos the code for the login form
if (viewModel.isLoginSuccessful) {
navController.navigate(Screen.AccountsScreen.route) {
popUpTo(Screen.LoginScreen.route) { inclusive = true }
}
}
}
Composite navigation recomposes both disappearing and appearing views during transition. This is the expected behavior.
You're calling navigate on each recomposition. Your problem lays in these lines:
if (viewModel.isLoginSuccessful) {
navController.navigate(Screen.AccountsScreen.route) {
popUpTo(Screen.LoginScreen.route) { inclusive = true }
}
}
You shouldn't change state directly from view builders. In this case LaunchedEffect should be used:
if (viewModel.isLoginSuccessful) {
LaunchedEffect(Unit) {
navController.navigate(Screen.AccountsScreen.route) {
popUpTo(Screen.LoginScreen.route) { inclusive = true }
}
}
}
Check out more in side effects documentation.
For me, I see flicker because the activity background is white, but I am on dark mode.
Change your app theme to daynight, try adding
implementation 'com.google.android.material:material:1.5.0'
and change your theme to
<style name="Theme.MyStockApp" parent="Theme.Material3.DayNight.NoActionBar" />
Currently I am using ModalBottomSheetLayout to display the bottom sheet.
I don't know is there a way to listen to the bottom page closing event?
In Compose to listen for changes you need to check current values of your mutable states.
In case with ModalBottomSheetLayout you have ModalBottomSheetState, and you can check on currentValue of this state. If you need you can modify your views state depending on this value.
If you wanna perform some action on state changes, you need to use side effects. The most basic one is LaunchedEffect, you can use it in combination with snapshotFlow to watch any state value:
LaunchedEffect(Unit) {
snapshotFlow { modalBottomSheetState.currentValue }
.collect {
println(it.toString())
}
}
Also I'll be called first time when you launch a screen, and you'll have Hidden there too, so depending on your needs it may not be an ideal solution.
To get the most close to listening on becoming hidden, you can use DisposableEffect.
if (modalBottomSheetState.currentValue != ModalBottomSheetValue.Hidden) {
DisposableEffect(Unit) {
onDispose {
println("hidden")
}
}
}
Here I'm launching DisposableEffect when your sheet appears, and when it disappears - I'm removing it from the view hierarchy, which will cause the onDispose to be called.
Full example of basically everything you can do with the state:
val modalBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
LaunchedEffect(modalBottomSheetState.currentValue) {
println(modalBottomSheetState.currentValue)
}
if (modalBottomSheetState.currentValue != ModalBottomSheetValue.Hidden) {
DisposableEffect(Unit) {
onDispose {
println("hidden")
}
}
}
ModalBottomSheetLayout(
sheetState = modalBottomSheetState,
sheetContent = {
Text(
"sheetContent",
modifier = Modifier.fillMaxHeight()
)
}
) {
Column {
Text(modalBottomSheetState.currentValue.toString())
Button(onClick = {
scope.launch {
modalBottomSheetState.show()
}
}) {
Text("Show bottom sheet")
}
}
}
You can just use confirmStateChange from rememberModalBottomSheetState:
val state = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden, confirmStateChange = {
if (it == ModalBottomSheetValue.Hidden) {
println(it)
}
true
})
Using confirmStateChange won't be invoked however if you show/hide the modal yourself with a clickable for example - it seems to only be invoked on clicking the scrim or swiping.
A way around this is to just observe the modalBottomSheetState.targetValue which will change anytime the modal is animating between two different states:
LaunchedEffect(modalBottomSheetState.targetValue) {
if (modalBottomSheetState.targetValue == ModalBottomSheetValue.Hidden) {
// do something when animating to Hidden state
} else {
// expanding
}
}
This is closer to the timing of confirmStateChange which is invoked before the call to show/hide. Observing modalBottomSheetState.currentValue will change at the end of the animation, while modalBottomSheetState.targetValue will change before the animation begins.
I am working on a compose screen, where on application open, i redirect user to profile page. And if profile is complete, then redirect to user list page.
my code is like below
#Composable
fun UserProfile(navigateToProviderList: () -> Unit) {
val viewModel: MainActivityViewModel = viewModel()
if(viewModel.userProfileComplete == true) {
navigateToProviderList()
return
}
else {
//compose elements here
}
}
but the app is blinking and when logged, i can see its calling the above redirect condition again and again. when going through doc, its mentioned that we should navigate only through callbacks. How do i handle this condition here? i don't have onCLick condition here.
Content of composable function can be called many times.
If you need to do some action inside composable, you need to use side effects
In this case LaunchedEffect should work:
LaunchedEffect(viewModel.userProfileComplete == true) {
if(viewModel.userProfileComplete == true) {
navigateToProviderList()
}
}
In the key(first argument of LaunchedEffect) you need to specify some key. Each time this key changes since the last recomposition, the inner code will be called. You may put Unit there, in this case it'll only be called once, when the view appears at the first place
The LaunchedEffect did not work for me since I wanted to use it in UI thread but it wasn't for some reason :/
However, I made this for my self:
#Composable
fun <T> SelfDestructEvent(liveData: LiveData<T>, onEvent: (argument: T) -> Unit) {
val previousState = remember { mutableStateOf(false) }
val state by liveData.observeAsState(null)
if (state != null && !previousState.value) {
previousState.value = true
onEvent.invoke(state!!)
}
}
and you use it like this in any other composables:
SingleEvent(viewModel.someLiveData) {
//your action with that data, whenever it was triggered, but only once
}