Kotlin - delay between UI updates - kotlin

I have created a simple tik-tac-toe game and have some bots you can play with. I am trying to have a delay between consecutive bot turns as otherwise all sequential bot moves appear on the screen at the same time
here is what I have tried in my code:
// bot functionality
private fun botTurn(botDifficulty: Int) {
var botMove = 100
when (botDifficulty) {
1 -> botMove = easyBot(confirmedMoves)
2 -> botMove = mediumBot(activePlayer, confirmedMoves, maxPlayers)
3 -> botMove = hardBot(activePlayer, confirmedMoves, maxPlayers)
else -> Toast.makeText(this, "What sort of bot is this??", Toast.LENGTH_SHORT).show()
}
trueCellID = botMove
// colour chosen segment
setSegmentColor(trueCellID, -1, activePlayer)
// TODO add 1-2 second delay after confirming move?
Log.d("Wait", "start of wait $activePlayer")
Thread.sleep(1000)
Log.d("Wait", "end of wait $activePlayer")
confirmMove()
}
using Thread.sleep seems to just delay all the UI updates until after all botPlayer sleeps have happened. I have also tried using Handler.postDelayed, GlobalScope.launch with a delay block and runOnUiThread with a SystemClock.sleep(1000)- these have the same problem of doing all the bot waiting, then the UI updating.
I even tried to adapt this solution a try - how to wait for Android runOnUiThread to be finished? but got the same result - big delay then all the UI updates.
Is there a fix for this or have I missed something simple?

As broot suggested in the comments, putting both setSegmentColor() and confirmMove() inside the postDelayed() block achieved the desired delay between botTurn()
val botDelay = 1500L
Handler().postDelayed(
{
// colour chosen segment then save move
setSegmentColor(botMove, -1, activePlayer)
confirmMove(botMove)
},
botDelay
)

Related

Jetpack Compose - ModalBottomSheet comes up with soft keyboard

I have a problem with ModalBottomSheet and it's on my work computer so I can't record it to you right now. So basically, after I give focus to one of my TextFields, my keyboard comes up and pushes all the content upwards so I can see the TextField that I'm writing to. When I'm hiding my keyboard I can see that my ModalBottomSheet hides too, but I never set it to come up.
So if you are familiar with this bug, please let me know your solutions.
My coworker, so he inserted a boolean that checks if keyboard is up or not and if it is, dont put ap modal bottom sheet.
You can use this method until this problem is fixed with an additional update.
You can use LaunchedEffect for this. Here is an example for you.
The important thing here is to disable the ModalBottomSheetDialog when the keyboard is opened and re-enable it half a second after the keyboard is closed.
You can trigger the required function by assigning a value to this variable when the keyboard is turned on, and then changing and checking this value when the keyboard is closed.
/*Change this value to "keyboard_on" when the keyboard is turned on and "keyboard_off" when the keyboard is closed again. You can give different names for different usage areas. That's why we're using a string, not a Boolean.*/
var taskCodeValue = remember { mutableStateOf("keyboard_off") }
var sheetOpener by remember { mutableStateOf(true) }
if (taskCodeValue.value == "keyboard_off"){
LaunchedEffect(taskCodeValue.value == "keyboard_off"){
delay(500)
sheetOpener = true
}
}else {
sheetOpener = false
}
/*
By adding the Scaffold, which includes ModalBottomSheet and other compose
elements, into a box, we enable them to work independently of each other.
*/
Box(modifier = Modifier.fillMaxSize()) {
Scaffold(
content = {}
)
if (sheetOpener){
ModalBottomSheetLayout(
sheetState = sheetState,
sheetContent = {}
) {}
}
}

Recyclerview list item vanishes after closing and restarting app. KOTLIN

I just implemented an undo function for a snackbar in my to do list app. Everything works perfectly until you close the app and open it again. Once it's opened the list item that was just restored/undone does not show up. However if you exit the app and NOT close it. The list item is still there.
override fun onViewSwiped(position: Int) {
val removedItem = list.removeAt(position)
notifyItemRemoved(position)
updateNotesPositionInDb()
val snackBar = Snackbar
.make((context as Activity).findViewById(R.id.mainLayout), "Task deleted", Snackbar.LENGTH_LONG)
.setAction("Undo"){
undoDeleteTask(removedItem.ID)
list.add(position, removedItem)
notifyItemInserted(position)
updateNotesPositionInDb()
}
.addCallback(object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
override fun onShown(transientBottomBar: Snackbar?) {
}
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
deleteTask(removedItem.ID)
updateNotesPositionInDb()
}
})
snackBar.show()
}
fun undoDeleteTask(id: String) {
val db = mHelper.readableDatabase
db.insert(TaskContract.TaskEntry.TABLE, "id=$id", null)
db.close()
}
Let me explain again. When a user swipes the item and they press undo. The item will be added back to the recyclerview. If a user were to exit the app, but not close it. The item will store be there. However if the user were to close the app then open it, the recently restored item is no longer there.
Your Snackbar callback always deletes the item from the database
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
deleteTask(removedItem.ID)
updateNotesPositionInDb()
}
From the docs:
public void onDismissed (B transientBottomBar, int event)
Called when the given BaseTransientBottomBar has been dismissed, either through a time-out, having been manually dismissed, or an action being clicked.
So even if the user clicks that undo action, the dismissed callback runs and always deletes from the DB. You need to check that event parameter in the callback, and make sure it doesn't do the delete operation if it was a click that dismissed it:
int: The event which caused the dismissal. One of either: DISMISS_EVENT_SWIPE, DISMISS_EVENT_ACTION, DISMISS_EVENT_TIMEOUT, DISMISS_EVENT_MANUAL or DISMISS_EVENT_CONSECUTIVE.
I'm guessing just an if (event != DISMISS_EVENT_ACTION) will do it!
The reason you're only seeing this on a restart is that I'm guessing deleteTask(removedItem.ID) doesn't update your in-memory list or the RecyclerView's adapter - your flow here seems to be
remove from the in-memory list
if the user clicks undo then restore it
if they don't then remove it from the DB as well
So what happens here is it's removed from the in-memory list, then it's restored, then the data is removed from the DB. So long as the app stays in memory you'll be able to see the data that's in the list. But as soon as you restart the app, it has to restore the list by reading the DB, and the item isn't in there anymore

Jetpack Compose: scrolling to the bottom of list on event

I have a composable representing list of results:
#Composable
fun ResultsList(results: List<Pair<Square, Boolean>>) {
val coroutineScope = rememberCoroutineScope()
val listState = rememberLazyListState()
LazyRow(state = listState) {
items(results) { result ->
ResultsItem(result.first, result.second)
coroutineScope.launch {
listState.animateScrollToItem(results.size)
}
}
}
}
Expected behaviour: The list smoothly scrolls to the last item whenever a new item is added
Actual behaviour: All is good, but whenever I manually scroll fast through the list, it is also automatically put on the bottom. Also, the scrolling is not smooth.
Your code gives the following error:
Calls to launch should happen inside a LaunchedEffect and not composition
You should not ignore it by calling the side effect directly from the Composable function. See side effects documentation for more details.
You can use LaunchedEffect instead (as this error suggests). By providing results.size as a key, you guarantee that this will be called only once when your list size changes:
#Composable
fun ResultsList(results: List<Pair<Square, Boolean>>) {
val listState = rememberLazyListState()
LaunchedEffect(results.size) {
listState.animateScrollToItem(results.size)
}
LazyRow(state = listState) {
items(results) { result ->
ResultsItem(result.first, result.second)
}
}
}
Philip's solution will work for you. However, I'm posting this to ensure that you understand why
A.) The scroll was not smooth
B.) The list gets scrolled to the bottom when you scroll through it fast enough.
Explanation for A.)
It is because you are using animateScollTo. I've experienced issues with this method if called too often,
Explanation for this lies in how Lazy scrollers handle their children internally. You see, Lazy scrollers, as you might know, are meant to display only a small window of a large dataset to the user. To achieve this, it uses internal caching. So, the items on the screen, AND a couple of items to the top and bottom of the current window are cached.
Now, since in your code, you are making a call to animateScrollTo(size) inside the Composable's body (the items scope), the code will essentially be executed upon every composition.
Hence, on every recomposition, there is an active clash between the animateScrollTo method, and the users touch input. When the user scrolls past in a not-so-fast manner, this is what happens - user presses down, gently scrolls, then lifts up the finger. Now, remember this, for as long as the finger is actually pressed down, they animateScrollTo will seem to have no effect (because the user is actively holding a position on the scroller, so it won't be scrolled past it by the system). Hence, while the user is scrolling, some items ahead of the list are cached, but the animateScrollTo does not work. Then, because the motion is slow enough, the distance the scroller travels because of inertia is not a problem, since the list already has enough cached items to show for the distance. That also explains the second problem.
B.)
When you are scrolling through the list FAST enough, the exact same thing as the above case (the slow-scroll) happens. Only, this time the inertia carries the list too forward for the scroller to be handled based on the internal cache, and hence there is active recomposition. However, now since there is no active user input (they have lifted their finger off the screen), it actually does animate to the bottom, since their is no clash here for the animateScrollTo method.
For as long as your finger is pressed, no matter how fast you scroll, it won't scroll to the bottom (test that!)
Now to the solution of the actual problem. Philip your answer is brilliant. The only thing is that it might not work if the developer has an item remove implementation as well. Since only the size of the list is monitored, it will scroll to end when an item is added OR deleted. To counteract that, we would actually need some sort of reference value. So, either you can implement something of your own to provide you with a Boolean variable that actually confirms whether an item has been ADDED, or you could just use something like this.
#Composable
fun ResultsList(results: List<Pair<Square, Boolean>>) {
//Right here, create a variable to remember the current size
val currentSize by rememberSaveable { mutableStateOf (results.size) }
//Now, extract a Boolean to be used as a key for LaunchedEffect
var isItemAdded by mutableStateO(results.size > currentSize)
LaunchedEffect (isItemAdded){ //Won't be called upon item deletion
if(isItemAdded){
listState.animateScrollToItem(results.size)
currentSize = results.size
}
}
val listState = rememberLazyListState()
LazyRow(state = listState) {
items(results) { result ->
ResultsItem(result.first, result.second)
}
}
}
This should ensure the proper behaviour. Of course, let me know if there is anything else, happy to help.
Pretty obvious. Why are you calling:
listState.animateScrollToItem(results.size) inside your LazyList? Of course you're going to get extremely bad performance. You shouldn't be messing around with scrolling when items are being rendered. Get rid of this line of code.

Video event issue: User touching screen before time to play

Hi this question is for anyone who may be able to show me how to resolve this user issue. I have a script that plays a video with the user able pause and resume on touch. This was easy enough to do and works fine. At a certain point the script pauses the video for the user for a set time so the user has time to read the information on the screen. The intent is for them to be able to to resume playing the video after reading the information. If the user waits long enough, the touch to resume works fine. But, because some users read faster than others it is obvious I need to detect the touch even when the video was paused by the script - this is the part I need help with. Currently, if the video is paused by the script and the user touches the screen before the set time for the pause expires, the play button will show but pressing it to play will not resume the script until that time expires. I presume I need a listener inside the function the script used to pause the video. I am not quite sure the best way to handle that. Here is a snippet of my approach so far:
var myVideo = document.getElementById("myVideo");
function playPause() {
var el = document.getElementById("playButton");
if (myVideo.paused) {
myVideo.play();
el.className ="";
} else {
myVideo.pause();
el.className = "playButton";
}
}
myVideo.addEventListener("click", playPause, false);
var pause42 = function(){
if(this.currentTime >= 42 && !myVideo.paused) {
this.pause();
// remove the event listener after you paused the playback
this.removeEventListener("timeupdate",pause42);
}
};
myVideo.addEventListener("timeupdate", pause42);
Any help is appreciated!
FYI problem solved. Posting the answer here if it helps others.
var pause42 = function(){
if(this.currentTime >= 15 && !myVideo.paused) {
this.currentTime = 42;
this.pause();
// remove the event listener after you paused the playback
this.removeEventListener("timeupdate",pause42);
}
};
myVideo.addEventListener("timeupdate", pause42);

How to Wait for an element and do not fail if not found after a certain time xcuitest

In my application i have a two tab buttons say Tasks and Worklist. Tasks is always loaded. But Worklist button is dynamic and loaded only after some time.
I want to click Tasks button after a certain time. ie, i need to wait for Worklist button and if it exists after a certain time then click the Tasks button. Also if the timeout exceeds and Worklist button is not loaded then i need to click Tasks button.
i cannot use sleep.
Can i use expectationForPredicate and waitForExpectationsWithTimeout. But waitForExpectationsWithTimeout gets failed if the element is not found after timeout. Even if i write
waitForExpectationsWithTimeout(120) { (error) -> Void in
click Tasks button
}
This gives stall on main thread.
I want to click Tasks button only after worklists is loaded. but if worklist is not loaded after some timeout, then also i need to click Tasks button..
Is there any solution. any help.
You can create your own custom Method to handle this:
func waitForElementToExist(
element: XCUIElement,
timeout: Int = 20,
failTestOnFailure: Bool = true)
-> Bool
{
var i = 0
let message = "Timed out while waiting for element: \(element) after \(timeout) seconds"
while !element.exists {
sleep(1)
i += 1
guard i < timeout else {
if failTestOnFailure {
XCTFail(message)
} else {
print(message)
}
return false
}
}
return true
}
you can call the method like:
if waitForElementToExist(taskButton, timeout: 20, failTestOnFailure: false) {
button.tap()
}
Hope this works for you!