Remember in Composition is forgotten - kotlin

I'm at Google CodeLabs:
And here is the code:
#Composable
fun WaterCounter(
modifier: Modifier = Modifier,
) {
var count by remember { mutableStateOf(0) }
Column(
modifier = modifier.padding(16.dp)
) {
if (count > 0) {
var showTask by remember { mutableStateOf(true) }
if (showTask) {
WellnessTaskItem(
taskName = "Have you taken your 15 minute walk today?",
onClose = { showTask = false })
}
Text(
text = "You've had $count glasses",
modifier = modifier.padding(16.dp)
)
}
Row(
modifier = modifier.padding(top = 8.dp)
) {
Button(
onClick = { count++ },
enabled = count < 10,
) {
Text(text = "add one")
}
Button(
onClick = { count = 0 },
modifier = modifier.padding(start = 8.dp)
) {
Text(text = "Clear water count")
}
}
}
}
I can't get this phrase:
"Press the Clear water count button to reset count to 0 and cause a recomposition. Text showing count, and all code related to WellnessTaskItem, are not invoked and leave the Composition. showTask is forgotten because the code location where remember showTask is called was not invoked."
Please help me understand why show task is forgotten? What does it mean "the code was not invoked" and "it leaves the composition"?

Conditional code blocks enter composition when condition is true and leave composition when condition cease to be true. When count is zero that block is removed and next time it enters composition remember stores showTask with true value.
For instance the code block below
#Composable fun App() {
val result = getData()
if (result == null) {
Loading(...)
} else {
Header(result)
Body(result)
}
}
these blocks also enter composition based on result is null or not.
Entering Composition means it's a node that starts execution and when a State that is read in this block is updated recomposition happens which means same block is updated with new value.
when result is null Loading Composable enters composition, let's say it has a progress bar with value when it's increased Loading Composable is recomposed.
when a result comes Loading Composable exits composition
Header(result)
Body(result)
enters composition
Also, Composable blocks are not necessarily need to be UI related either. You can create a Composable block for with LaunchedEffect to display a SnackBar for instance.
if (count > 0 && count <5) {
// `LaunchedEffect` will cancel and re-launch if
// `scaffoldState.snackbarHostState` changes
LaunchedEffect(scaffoldState.snackbarHostState) {
// Show snackbar using a coroutine, when the coroutine is cancelled the
// snackbar will automatically dismiss. This coroutine will cancel whenever
// if statement is false, and only start when statement is true
// (due to the above if-check), or if `scaffoldState.snackbarHostState` changes.
scaffoldState.snackbarHostState.showSnackbar("count $count")
}
}
This block will enter composition when count is bigger than 0 and stay in composition while count is less than 5 but since it's LaunchedEffect it will trigger once but if count reaches 5 faster than Snackbar duration Snackbar gets canceled because block leaves composition.
You can check when a Composable enters and exits composition with DisposableEffect
And you can check this article for deeper analysis.
https://medium.com/androiddevelopers/under-the-hood-of-jetpack-compose-part-2-of-2-37b2c20c6cdd

Related

Why won't my checkbox UI update on the first click, but will update on every click after that?

I have a Jetpack Compose for Desktop UI application that shows a popup with a list of enums, each of which has a checkbox to toggle them:
Sorry for the long code, this is as small a MCVE as I could make of it.
enum class MyEnum {
A, B, C
}
data class MyFilter(val enums: Collection<MyEnum>)
fun main() = application {
Window(onCloseRequest = ::exitApplication) {
App()
}
}
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun App() {
var filter by remember { mutableStateOf(MyFilter(emptyList())) }
MaterialTheme {
var show by remember { mutableStateOf(false) }
if (show) {
val selected = remember { filter.enums.toMutableStateList() }
AlertDialog({ show = false }, text = {
Column {
MyEnum.values().forEach { e ->
Row {
val isSelected by remember { derivedStateOf { e in selected } }
Checkbox(isSelected, { if (isSelected) selected.remove(e) else selected.add(e) })
Text(e.name)
}
}
}
}, buttons = {
Button({
filter = MyFilter(selected.toList())
show = false
}) { Text("OK") }
})
}
Button({ show = true }) { Text("Open") }
}
}
The problem here is, the very first time after opening the dialog, a click on a checkbox will properly update the underlying selected list, but it won't update the UI. So the checkbox doesn't appear to change state. Every click after that will properly update the UI, including the changed state for the previous click.
This happens reliably, every single time the dialog is opened, but only on the first checkbox click.
Why does this happen, and how can I fix it?

How to handle key events during TextField editing in Compose?

I'm making a chess engine on desktop compose, one of the things I'm trying to implement is a TextField where i can paste in several moves to recreate games.
I'm having problems saving the text that I input into my TextField composable.
My text composable is as follows, I understand that with my current implementation it prints every time the move variable changes, but i just wanted this to happen when I press the ENTER key on my keyboard.
I'm using the print to try some code but what i want to do is to save the String to a list or something, but that i can implement in my own later.
I only find explanations on how to do it with android and android specific methods.
Text(" Play", textAlign = TextAlign.Center, fontSize = 30.sp)
val move = remember { mutableStateOf("Play") }
TextField(
value = move.value,
onValueChange = { move.value = it },
label = { Text("Move") },
maxLines = 1,
textStyle = TextStyle(color = Color.Black, fontWeight = FontWeight.Bold),
modifier = Modifier.padding(20.dp)
)
println(move.value)
It has been pointed out to me a solution to handle the ENTER keybind as been answered here:
How to trigger PC Keyboard inputs in Kotlin Desktop Compose
I'm just having some issues, that I press ENTER unable to handle the String when I press ENTER it prints continuosly isntead of only once based on my research I need to implement something like this:
Box doesn't capture key events in Compose Desktop
but i can't seem to be able to use the KeyEvent.ACTION_UP not sure if it's specific to the editText, if I'm missing some imports, or if there's another way to do it with the TextField composable.
My code after the given suggestions
val requester = remember { FocusRequester() }
LaunchedEffect(Unit) {
requester.requestFocus()
}
Text(" Play", textAlign = TextAlign.Center, fontSize = 30.sp)
val move = remember { mutableStateOf("Play") }
TextField(
value = move.value,
onValueChange = { move.value = it },
label = { Text("Move") },
maxLines = 1,
textStyle = TextStyle(color = Color.Black, fontWeight = FontWeight.Bold),
modifier = Modifier.padding(20.dp)
.onKeyEvent {
//if(keyCode==KeyEvent.KEYCODE_ENTER&&event.action==KeyEvent.ACTION_UP){
if (it.key == Key.Enter) {
println(move.value)
true
} else {
// let other handlers receive this event
false
}
}
.focusRequester(requester)
.focusable()
)
I managed to fix this problem by changing the if condition to this:
if (it.key == Key.Enter && move.value!="") {
println(move.value)
move.value = ""
true
}
So that everytime I write something and press ENTER it prints the String and clears the mutableState, and while ENTER is still pressed it won't print because the mutableState is an empty String.
I'm still looking for a better solution than this
You can capture key events with Modifier.onKeyEvent. With the text field you don't need any focus capturing, because it's already there. If you would need to do the same for a custom view, check out this answer
To check which button was pressed you can use key, and to check where it was released, you can check type:
TextField(
value = text,
onValueChange = { text = it },
modifier = Modifier
.onKeyEvent { keyEvent ->
if (keyEvent.key != Key.Enter) return#onKeyEvent false
if (keyEvent.type == KeyEventType.KeyUp) {
println("Enter released")
}
true
}
)

How to correctly use setInterval for multiple functions being called repeatedly in React Native

I am building a simple app in React Native that aims to flash different colors on the screen at certain time intervals. My implementation is as follows:
useEffect(() => {
var blinkOnValue;
var blinkOffValue;
function blinkOn() {
const colorAndWord = getRandomColor(colorArray);
setBackground(colorAndWord.color);
}
function blinkOff() {
setBackground('#F3F3F3');
}
if (strobeStart) {
if (on) {
blinkOnValue = setInterval(() => {
blinkOn();
setOn(false);
}, info.length * 1000);
} else {
blinkOffValue = setInterval(() => {
blinkOff();
setOn(true);
}, info.delay * 1000);
}
}
return () => {
on ? clearInterval(blinkOnValue) : clearInterval(blinkOffValue);
};
}, [colorArray, info.delay, info.length, on, strobeStart]);
The blinkOn function sets the background a certain color and the blinkOff function sets the background a default light gray-ish color. These functions should alternate back and forth, blinking on and off at different intervals. For example, if info.length is 2 and info.delay is 0.5, then the color should flash on for 2 seconds and then the screen should be light gray for 0.5 seconds and repeat. However, the duration of both of the blinkOn and blinkOff are happening for the same amount of time, no matter what the two values are. Sometimes it uses the value from info.length, and sometimes it uses the value from info.delay which is also quite strange.
I think it has something to do with components mounting and unmounting correctly but honestly I am quite lost. If anyone has any advice on how to make this code consistently work where it flashes appropriately I would really appreciate it.
Instead of trying to time your events just right, I suggest using a single timer and computing the blink state from the current system time.
var oldState = true;
function blink() {
var ms = new Date().getTime();
var t = ms % (info.delay + info.length);
var state = (t < info.length ? true : false);
if (state == oldState) return;
if (state) {
blinkOn();
}
else
{
blinkOff();
}
oldState = state;
}
Now set a short timer to check the time and update the blink state as needed:
setInterval( () => blink(), 100 );

update variable inside componentDidUpdate fails

I'm having a popup in my view and inside the popup there's a countdown timer. I need to hide the popup when countdown timer has value1 . This is what I tried.
componentDidUpdate() {
if (this.state.count === 1) {
clearInterval(this.intervalDuration);
this.setState({popupvisible:false})
}
}
I'm getting an error when the count equals to 1 as follows.
Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
You have to add condition in componentDidUpdate,
The componentDidUpdate is particularly useful when an operation needs to happen after the DOM is updated
componentDidUpdate(prevProps) {
if (prevProps.data !== this.props.data) {
if (this.state.count === 1) {
clearInterval(this.intervalDuration);
this.setState({popupvisible:false})
}
}
}
You can learn more about componentDidUpdate on official document :
https://reactjs.org/docs/react-component.html#componentdidupdate
If you are setting a state variable, then that variable can also be added in condition to avoid recurring update.
componentDidUpdate() {
if (this.state.count === 1) {
clearInterval(this.intervalDuration);
if(this.state.popupvisible){
this.setState({popupvisible:false})}
}
}
This is happening because when count reaches the value 1 yourcomponentDidUpdate sets the state and since there is no check to control when to update your state based on popupVisible's value, your componentDidUpdate is called again and again causing an infinite loop. What you'd what to do is have a check if popupVisible is true, then only set it to false.
componentDidUpdate() {
if (this.state.count === 1) {
clearInterval(this.intervalDuration);
if(this.state.popupvisible) {
this.setState({ popupvisible:false });
}
}
}

Transition with keepScrollPosition and navigateBack

We are using Durandal for our SPA application and came to a, in my opinion, common use case. We have two pages: one page is a list of entities (with filters, sorting, virtual scroll) and another is detail preview of an entity. So, user is on list page and set a filter and a list of results comes out. After scrolling a little bit down user notice an entity which he/she would like to see details for. So clicking on a proper link user is navigated to details preview page.
After "work finished" on preview page user click back button (in app itself or browser) and he/she is back on the list page. However, default 'entrance' transition scroll the page to the top and not to the position on list where user pressed preview. So in order to 'read' list further user have to scroll down where he/she was before pressing preview.
So I started to create new transition which will for certain pages (like list-search pages) keep the scroll position and for other pages (like preview or edit pages) scroll to top on transition complete. And this was easy to do however, I was surprised when I noticed that there are strange behavior on preview pages when I hit navigateBack 'button'. My already long story short, after investigation I found out that windows.history.back is completing earlier then the transition is made and this cause that preview pages are scrolled automatically down to position of previous (list) page when back button is hit. This scrolling have a very unpleasant effect on UI not mentioning that it is 'total catastrophe' for my transition.
Any idea or suggestion what could I do in this case?
Here is the code of transition. It is just a working copy not finished yet as far as I have this problem.
define(['../system'], function (system) {
var fadeOutDuration = 100;
var scrollPositions = new Array();
var getScrollObjectFor = function (node) {
var elemObjs = scrollPositions.filter(function (ele) {
return ele.element === node;
});
if (elemObjs.length > 0)
return elemObjs[0];
else
return null;
};
var addScrollPositionFor = function (node) {
var elemObj = getScrollObjectFor(node);
if (elemObj) {
elemObj.scrollPosition = $(document).scrollTop();
}
else {
scrollPositions.push({element: node, scrollPosition: $(document).scrollTop()});
}
};
var scrollTransition = function (parent, newChild, settings) {
return system.defer(function (dfd) {
function endTransition() {
dfd.resolve();
}
function scrollIfNeeded() {
var elemObj = getScrollObjectFor(newChild);
if (elemObj)
{
$(document).scrollTop(elemObj.scrollPosition);
}
else {
$(document).scrollTop(0);
}
}
if (!newChild) {
if (settings.activeView) {
addScrollPositionFor(settings.activeView);
$(settings.activeView).fadeOut(fadeOutDuration, function () {
if (!settings.cacheViews) {
ko.virtualElements.emptyNode(parent);
}
endTransition();
});
} else {
if (!settings.cacheViews) {
ko.virtualElements.emptyNode(parent);
}
endTransition();
}
} else {
var $previousView = $(settings.activeView);
var duration = settings.duration || 500;
var fadeOnly = !!settings.fadeOnly;
function startTransition() {
if (settings.cacheViews) {
if (settings.composingNewView) {
ko.virtualElements.prepend(parent, newChild);
}
} else {
ko.virtualElements.emptyNode(parent);
ko.virtualElements.prepend(parent, newChild);
}
var startValues = {
marginLeft: fadeOnly ? '0' : '20px',
marginRight: fadeOnly ? '0' : '-20px',
opacity: 0,
display: 'block'
};
var endValues = {
marginRight: 0,
marginLeft: 0,
opacity: 1
};
$(newChild).css(startValues);
var animateOptions = {
duration: duration,
easing : 'swing',
complete: endTransition,
done: scrollIfNeeded
};
$(newChild).animate(endValues, animateOptions);
}
if ($previousView.length) {
addScrollPositionFor(settings.activeView);
$previousView.fadeOut(fadeOutDuration, startTransition);
} else {
startTransition();
}
}
}).promise();
};
return scrollTransition;
});
A simpler approach could be to store the scroll position when the module deactivates and restore the scroll on viewAttached.
You could store the positions in some global app variable:
app.scrollPositions = app.scrollPositions || {};
app.scrollPositions[system.getModuleId(this)] = theCurrentScrollPosition;