onTap - detectTapGestures not working properly as clickable(s) with same code and state hoisting - android-jetpack-compose

I have a method 'activity' which hosts a 'screen' and since state hoisting is cool, I did just that but with a problem.
While the code works perfectly (incrementing the value) using Modifier.clickable, the same code does not properly work using detectTapGestures::onTap as can be observed through Log.d("ExampleScreen", "onClick Multi Count is $multiCount")
#Composable
fun ExampleActivity() {
var multiCount by remember {
mutableStateOf(0)
}
Log.d("ExampleActivity", "Multi Count is $multiCount") //this works either way
ExampleScreen(
multiCount = multiCount,
incrementMultiCount = {
multiCount = ++multiCount
}
)
}
#Composable
fun ExampleScreen(
modifier: Modifier = Modifier,
multiCount: Int,
incrementMultiCount: () -> Unit
) {
Log.d("ExampleScreen", "Example Multi Count is $multiCount") //this works either way
Column(
modifier = modifier
.fillMaxSize()
.padding()
) {
Text(
modifier = Modifier
.size(100.dp, 50.dp)
.background(MaterialTheme.colorScheme.primaryContainer)
.clickable {//this works
incrementMultiCount()
Log.d("ExampleScreen", "onClick Multi Count is $multiCount")
}
/* .pointerInput(Unit) { //this does not work
detectTapGestures(
onTap = {
incrementMultiCount()
Log.d("ExampleScreen", "onClick Multi Count is $multiCount") //this stays 0
},
onLongPress = {
}
)
}*/, text = "Click to confirm"
)
}
}
Partially, it works the same (incrementing works but not reading the
value) as shown in logs defined in ExampleActivity and function body of
ExampleScreen but doesn't in the onTapGesture.
If the remember value is directly in the ExampleScreen composable, onTap and clickable works perfectly but not what I wanted.
Finally, before suggesting I use what works, I wanted to use detectTapGestures because I really need the LongPress method for a secondary work.
Plus I will really appreciate an explanation since I thought both works the same.

You have to wrap your callback in rememberUpdatedState, something like this:
val onTap: () -> Unit = {
incrementMultiCount()
Log.d("ExampleScreen", "onClick Multi Count is $multiCount")
}
val updatedOnTap = rememberUpdatedState(onTap)
detectTapGestures(onTap = { updatedOnTap.value.invoke() })
This is what .clickable modifier does with its onClick argument as well, you can see that in its source code. Explanation can be found in rememberUpdatedState documentation:
rememberUpdatedState should be used when parameters or values computed during composition are referenced by a long-lived lambda or object expression. Recomposition will update the resulting State without recreating the long-lived lambda or object, allowing that object to persist without cancelling and resubscribing, or relaunching a long-lived operation that may be expensive or prohibitive to recreate and restart.
By the way, there is also Modifier.combinedClickable that can be used for long click detection.

Related

Awesome WM adding a keybinding to the Super_L key blocks other key commands from working properly

I'm trying to implement some Super-Tab functionality into awesome-wm so that it acts in a similar way to alt tab, going through tags in order of last used rather than just a set order.
However I've run into an issue, which is that binding something to only Super_L will cause other keybinds to not work as before.
For testing right now I just have it set to this
awful.key({}, "Super_L", function () naughty.notify{text = "aaa"} end, function () naughty.notify{text = "bbb"} end)
The issue is that if I have this set, then a key binding like Super-Shift-C (to close the current window) doesn't work, at least not if you press it in that order. It will work if you press it Shift-Super-C.
Is there a reason for this/a way to fix it?
Extra question: Why doesn't the key release function work, when I press Super_L with this awful.key setting you would expect it to show "aaa" when I press, and "bbb" when I release, but only "aaa" shows up.
I'm not super sure what to try, I've been messing around with the key configs and can't figure out what's going on.
Some of the Super key bindings still work, like Super-H to change the size of a window in tiling mode.
There's some way to make your code work, but more generally, this is not the best way to implement Super-Tab. The easier way is to use awful.keygrabber.
awful.keygrabber {
keybindings = {
awful.key {
modifiers = {"Mod4"},
key = "Tab",
on_press = function () naughty.notify{text = "aaa"} end
},
awful.key {
modifiers = {"Mod4", "Shift"},
key = "Tab",
on_press = function () naughty.notify{text = "ccc"} end
},
},
stop_key = "Super_L",
stop_event = "release",
start_callback = function () naughty.notify{text = "ddd"} end,
stop_callback = function () naughty.notify{text = "bbb"} end,
export_keybindings = true,
}
This code (cop pasted from the doc link above) will create 2 keybindings (Super_L+Tab and Super_L+Shift+Tab). However, once one is executed, instead of just calling the callback, it will start to grab all keys. When Super_L is released, it will stop the keygrabber.

LaunchedEffect vs rememberCoroutineScope. This explanation makes me confused. Please make it clear to me

I am following the codelab Advanced State and Side Effects in Jetpack Compose. It says that if we use rememberCoroutineScope instead of LaunchEffect in this case, it seems to work, but it is not correct. "As explained in the Thinking in Compose documentation, composables can be called by Compose at any moment. LaunchedEffect guarantees that the side-effect will be executed when the call to that composable makes it into the Composition. If you use rememberCoroutineScope and scope.launch in the body of the LandingScreen, the coroutine will be executed every time LandingScreen is called by Compose regardless of whether that call makes it into the Composition or not. Therefore, you'll waste resources and you won't be executing this side-effect in a controlled environment."
#Composable
private fun MainScreen(onExploreItemClicked: OnExploreItemClicked) {
Surface(color = MaterialTheme.colors.primary) {
var showLandingScreen by remember { mutableStateOf(true) }
if (showLandingScreen) {
LandingScreen(onTimeout = { showLandingScreen = false })
} else {
CraneHome(onExploreItemClicked = onExploreItemClicked)
}
}
}
#Composable
fun LandingScreen(modifier: Modifier = Modifier, onTimeout: () -> Unit) {
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
// TODO Codelab: LaunchedEffect and rememberUpdatedState step
// TODO: Make LandingScreen disappear after loading data
val onCurrentTimeout by rememberUpdatedState(newValue = onTimeout)
LaunchedEffect(Unit) {
delay(SplashWaitTime)
onCurrentTimeout()
}
Image(painterResource(id = R.drawable.ic_crane_drawer), contentDescription = null)
}
}
I do not understand the explanation. The phrases "when the call to that composable makes it into the Composition" and "regardless of whether that call makes it into the Composition or not" make me confused. I think when a composable is called by Compose, it will be in the Composition. How can it be not in the Composition after being called? Please show me what I miss here.
When a Composable enters composition LaunchedEffect() gets triggered. If LaunchedEffect has key or keys it can be triggered on recompositions when key values change.
#Composable
private fun MyTestComposable() {
var counter by remember { mutableStateOf(0) }
Column(modifier = Modifier.fillMaxSize()) {
LaunchedEffect(key1 = counter>4) {
println("counter: $counter")
}
Button(onClick = { counter++ }) {
Text("counter: $counter")
}
}
}
For instance, LaunchedEffect block gets triggered when key change. First when MyTestComposable enters composition with false flag and when counter is 5 with true flag.
rememberCoroutineScope on the other hand launches every time your function is run. It's very very likely to enter composition but there might be times state that triggers composition change fast so current composition gets canceled and new one is scheduled
Recomposition starts whenever Compose thinks that the parameters of a
composable might have changed. Recomposition is optimistic, which
means Compose expects to finish recomposition before the parameters
change again. If a parameter does change before recomposition
finishes, Compose might cancel the recomposition and restart it with
the new parameter.
When recomposition is canceled, Compose discards the UI tree from the
recomposition. If you have any side-effects that depend on the UI
being displayed, the side-effect will be applied even if composition
is canceled. This can lead to inconsistent app state.
Ensure that all composable functions and lambdas are idempotent and
side-effect free to handle optimistic recomposition.
https://developer.android.com/jetpack/compose/mental-model#optimistic

Compose - pass composable block in function after coroutine result

I have a problem that I still can't solve and it just doesn't want to work. Basically I have to convert a function into a composable.
In the old function I launched a Coroutine and at the result, I changed context and then continued with my processes. In compose I don't understand how I have to "change context" in order to continue.
Old code:
fun getMyView( activity: Activity
) {
backgroundCoroutineScope.launch {
//some stuff here
withContext(coroutineContext) {
startSearchView(
activity
)
}
}
}
New not working code:
#Composable
fun getMyView( content: #Composable() () -> Unit) {
LaunchedEffect(key1 = Unit) {
//some stuff here like old funciont
//here I don't know how to change context, wait the end and go ahead. startSearchViewis a composable function too
// i want to use it to populate my screen
startSearchView(
content
)
}
}
How can I solve it? Thanks
Seems like you are trying to asynchronously "create" composable function, but UI emitting doesn't work this way. Like #PylypDukhov suggested, you should keep a mutable state holding nullable result of your async action. After loading the data set this state. Then in composable just do something like:
if (data != null) {
SearchComposable(data)
}
This way the composable will be emitted after the data is loaded

Jetpack Compose LazyColumn - How to update values of each Item seperately?

I'm working on a shopping cart feature for my app. I'm looking to add/decrease quantity of each list item in LazyColumn individually. I'm using only one "remember" so if I click on add/decrease they all update at the same time. How do I control each Item individually?
Screenshot
#Composable
fun InventoryCartScreen(
mainViewModel: MainViewModel = hiltViewModel()
) {
val multiSelectValue = mutableStateOf(0)// This is the value I want to change
//random list
val shopList = listOf(
ShoppingList(id = 0,itemNumber = "1",itemDescription = "1",currentInventory = 0,optimalInventory = 0,minInventory = 0),
ShoppingList(id = 0,itemNumber = "2",itemDescription = "2",currentInventory = 0,optimalInventory = 0,minInventory = 0)
)
Column(...) {
LazyColumn(...) {
items(items = shopList, key = { it.id }) { item ->
InventoryCartScreenContents(
onaddClick= { multiSelectValue.value ++ }, //adds for all
onDecreaseClick = { multiSelectValue.value -- }, //decreases for all
value = multiSelectValue.value //setting the initial value for all
)
}
}
}
}
Below is the contents composable to help you reproduce the issue.
#Composable
fun InventoryCartScreenContents(
onAddClick: (Int) -> Unit,
onDecreaseClick: () -> Unit,
value: Int,
) {
Row(...) {
Button(
onClick = { onAddClick(itemId) }
) {
Text(text = "+")
}
Button(
onClick = onDecreaseClick
) {
Text(text = "-")
}
}
}
Create a mutableStateListOf(...) (or mutableStateOf(listOf(...)) object if the former does not support your data type) in your ViewModel. Now, access this state from the composable you wish to read it from, i.e., your LazyColumn.
Inside your ViewModel, you can set the values however you wish, and they will be updated in the Composable as and when they are updated. Now, the composable, i.e., you column could use the index of the list item as the index of the lazycolumn's item. So you can store different data for different items in the viewmodel and it'll work fine.
The problem seems to be that you are missing the concept of state-hoisting here. I thought I had some good posts explaining it but it seems this one's the best I've posted. Anyway, I recommend checking this official reference, since that's where I basically found that (with a little bit of headhunting, so to speak.)
The basic idea is that everything is stored in the viewModel. Then, you divide that state into "setters" and "getters" and pass them down the heirarchy.
For example, the ViewModel may have a item called text, ok?
class VM : ViewModel(){
var text by mutableStateOf("Initial Text")
private set // This ensures that it cannot be directly modified by any class other than the viewmodel
fun setText(newText: Dtring){ // Setter
text = newText
}
}
If you wish to update the value of text on the click of a button, this is how you will hook up that button with the viewModel
MainActivity{
onCreate{
setContent{
StateButton(
getter = viewModel.text, // To get the value
setter = viewModel::setText // Passing the setter directly
)
}
}
}
In your Button Composable declaration
#Composable
private fun ComposeButton(
getter: String,
setter: (String) -> Unit // (receive 'String') -> return 'Unit' or 'void'
){
Button(
onClick = { setter("Updated Text") } // Calls the setter with value "Updated Text", updating the value in the viewModel
){
Text(text = getter)
}
}
This button reads the value 'get' from the viewModel, i.e., it reads the value of text, as it is passed down the model to the composable. Then, when we receive a click event from the button (in the onClick), we invoke the setter that we received as a parameter to the Composable, and we invoke it with a new value, in this case "Updated Text", this will go upwards all the way to the viewModel, and change the value of text in there.
Now, text was originally initialized as a state-holder, by using mutableStateOf(...) in initialization, which will trigger recompositions upon its value being changed. Now, since the value actually did change (from "Initial Text" to "Updated Text"), recompositions will be triggered on all the Composables which read the value of the text variable. Now, our ComposeButton Composable does indeed read the value of text, since we are directly passing it to the getter parameter of that Composable, that right? Hence, all of this will result in a Composable, that will read a value from a single point in the heirarchy (the viewmodel), and only update when that value changes. The Viewmodel, therefore, acts as a single source of truth for the Composable(s).
What you'll get when you run this is a Composable that reads "Initial Text" at first, but when you click it, it changes to "Updated Text". We are connecting the Composables to the main viewModel with the help of getters and setters. When Composables receive events, we invoke setters we receive from the models, and those setters continue the chain up to the viewModel, and change the value of the variables (state-holder) inside the model. Then, the Composables are already reading those variables through 'getters', hence, they recompose to reflect the updated value. This is what state-hoisting is. All the state is stored in the viewModel, and is passed down to the Composables. When a value needs to change, the Composables pass 'events' up to the viewModel (up the heirarchy), and then upon the updating of the value, the updated state is passed down to the Composables ('state' flows down the heirarchy).
All you need, is to use this method, but with a list. You can keep track of the items by using their index, and update their properties in the viewModel like this example demonstrates updating the value of a. You can store all the properties of an item in a single list.
Just create a data-class, like so
data class Item(p1: ..., p2:..., p3 : ...){}
Then, val list by mutableStateOf(listOf<Item>())
Clear?
Ok here is the explanation SPECIFIC to your use-case.
Your code seems excessively large but here's what I got down:
You have two items in a lazycolumn, both of them have three buttons each. Your question is about two buttons, increase and decrease. You wish to have the buttons modify the properties of only the item they belong to, right?
Ok so again, use state-hoisting as explained above
ViewModel{
val list by mutableStateOf(listOf<Item>()) // Main (central) list
val updateItem(itemIndex: Int, item: Item){
list.set(itemIndex, item) // sets the element at 'itemIndex' to 'item'
} // You can fill the list with initial values if you like, for testing
}
Getters and Setters being created, you will use them to read and update ENTIRE ITEMS, even if you have to modify a single property of them. We can use convenience methods like copy() to make our lives easier here.
MainScreen(){
//Assuming here is the LazyColumn
list = viewModel.list // Get the central list here
LazyColumn(...){ // Try to minimize code like this in your questions, since it does not have to compile, just an illustration.
items(list){ item ->
// Ditch the multiSelectValue, we have the state in the model now.
Item( // A Composable, like you demonstrated, but with a simpler name for convenience
item: Item,
onItemUpdate: (Item) -> Unit // You don't need anything else
)
}
}
}
Just create a data class and store everything in there. The class Item (not the Composable) I've demonstrated is the data-class. It could have a value like so
data class Item(var quantity: Int, ...){} // We will increase/decrease this
Now, in your Item Composable, where you receive the 'add' event (in the onClick of the 'Add' Button), just update the value using the setter like this
onClick = {
updateItem(
item // Getter
.copy( //Convenience Method
quantity = item.quantity + 1 // Increase current Quantity
)
)
}
Just do the same for decrease and delete (updateItem(0)), and you should have accomplished this well... Finally. If you have any more doubts, just ask. I could assist over facetime if nothing works.
Based on #MARSK answer I've managed to achieve the goal (Thank you!)
Add a function to update the items value:
//Creating a function to update a certain item with a new value
fun updateShoppingItem(shoppingItem: ShoppingList, newValue: Int) {
val newInventoryMultiValue = shoppingItem.copy(currentInventory = shoppingItem.currentInventory.plus(newValue))
updateShoppingList(newInventoryMultiValue)
}
//Actually updating the room item with the function above
private fun updateShoppingList(shoppingItem: ShoppingList) {
viewModelScope.launch {
repository.updateShoppingItem(shoppingItem)
}
}
Then, in the Composable screen add the functions to the add and decrease buttons
val shoppingList by mainViewModel.getShoppingList.collectAsState(initial = emptyList())
LazyColumn() {
items(items = shoppingList)
{ item ->
InventoryCartScreenContents(
onAddClick = {
val newValue = 1
mainViewModel.updateShoppingItem(item, newValue)
},
onDecreaseClick = {
val newValue = -1
mainViewModel.updateShoppingItem(item, newValue)
},
}
)
}
}
}
Result

How to verify state change in Compose?

Say in a composable I have two states:
var stateA by remember { mutableStateOf(varA) }
var stateB by remember { mutableStateOf(varB) }
varA and varB are class variables of type Int and are set elsewhere in the code.
Then somewhere in the composable, in the same scope, I have
processA(stateA)
processB(stateB)
processA and processB are not composable functions.
So after initial rendering, if neither state changes, then nothing is further processed, that is cool.
Then if say stateB is changed, then both process statements get called. But I hope only to call processB in the case and not processA. How can I detect which of the states has changed?
You should not run heavy processing directly from Composable functions. More information can be found in side effects documentation.
In this case, you can use LaunchedEffect. Using snapshotFlow, you can create a flow that emits values every time a state changes, so you can process it. You can have a flow for each state, so they will be processed independently.
LaunchedEffect(Unit) {
launch {
snapshotFlow { stateA }
.collect(::processA)
}
launch {
snapshotFlow { stateB }
.collect(::processB)
}
}

Resources