Why can't I cancel a Flow using Job.cancel() when I start it for two times? - kotlin

In the App, I click "Start" button to display new information every 100ms, then click "Stop" button to stop display new information. it's work as my expection.
But If I click "Start" button for two times, then I click "Stop" button to hope to stop display new information, but the new information keep displaying, why? It seems that myJob.cancel() doesn't work.
class HandleMeter: ViewModel() {
var myInfo = mutableStateOf("Hello")
private lateinit var myJob: Job
private fun soundDbFlow(period: Long = 100) = flow {
while (true) {
emit(soundDb())
delay(period)
}
}
fun calCurrentAsynNew() {
myJob = viewModelScope.launch(Dispatchers.IO) {
soundDbFlow().collect { myInfo.value = it.toString() + " OK Asyn " + a++.toString() }
}
}
fun cancelJob(){
myJob.cancel()
}
...
}
#Composable
fun Greeting(handleMeter: HandleMeter) {
var info = handleMeter.myInfo
Column(
modifier = Modifier.fillMaxSize()
) {
Text(text = "Hello ${info.value}")
Button(
onClick = { handleMeter.calCurrentAsynNew() }
) {
Text("Start")
}
Button(
onClick = { handleMeter.cancelJob() }
) {
Text("Stop")
}
}
}

When you call handleMeter.calCurrentAsynNew() it starts a new coroutine and assigns the returned Job to myJob. When you click Start second time, it again starts a new coroutine and updates the value of myJob to this new Job instance but this doesn't cancel the previous coroutine, it's still running.
If you want to cancel previous coroutine when calCurrentAsynNew() is called, you will have to manually cancel it using myJob.cancel()
fun calCurrentAsynNew() {
if(::myJob.isInitialized)
myJob.cancel()
myJob = viewModelScope.launch {
soundDbFlow().collect { myInfo.value = it.toString() + " OK Asyn " + a++.toString() }
}
}
Instead of using lateinit var myJob: Job you could have also used var myJob: Job? = null and then to cancel it, use myJob?.cancel()
Dispatchers.IO is also not required in this case.

Related

Why won't my UI update while a background task is running?

I have this code that should show a counter while a background task is running:
#Composable fun startIt() {
val scope = rememberCoroutineScope()
val running = remember { mutableStateOf(false) }
Button({ scope.launch { task(running) } }) {
Text("start")
}
if (running.value) Counter()
}
#Composable private fun Counter() {
val count = remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
while (true) {
delay(100.milliseconds)
count.value += 1
}
}
Text(count.toString())
}
private suspend fun task(running: MutableState<Boolean>) {
running.value = true
coroutineScope {
launch {
// some code that blocks this thread
}
}
running.value = false
}
If I understand correctly, the coroutineScope block in task should unblock the main thread, so that the LaunchedEffect in Counter can run. But that block never gets past the first delay, and never returns or gets cancelled. The counter keeps showing 0 until the task finishes.
How do I allow Compose to update the UI properly?
coroutineScope doesn't change the coroutine context, so I think you're launching a child coroutine that runs in the same thread.
The correct way to synchronously do blocking work in a coroutine without blocking the thread is by using withContext(Dispatchers.IO):
private suspend fun task(running: MutableState<Boolean>) {
running.value = true
withContext(Dispatchers.IO) {
// some code that blocks this thread
}
running.value = false
}
If the blocking work is primarily CPU-bound, it is more appropriate to use Dispatchers.Default instead, I think because it helps prevent the backing thread pool from spawning more threads than necessary for CPU work.
This was a small issue of the way count was being modified, and not of coroutines. To fix your code, the remember for count in Counter() needed to be updated to :
#OptIn(ExperimentalTime::class)
#Composable private fun Counter() {
val count = remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
while (true) {
delay(Duration.milliseconds(100))
count.value += 1
}
}
Text(count.value.toString())
}
Remember can be done with delegation to remove the need of using the .value such as:
#OptIn(ExperimentalTime::class)
#Composable private fun Counter() {
var count by remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
while (true) {
delay(Duration.milliseconds(100))
count += 1
}
}
Text(count.toString())
}
Compose does coroutines slightly differently than Kotlin would by default, this is a small example that shows a bit more of how Compose likes Coroutines to be done:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Compose_coroutinesTheme {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
BaseComponent()
}
}
}
}
}
// BaseComponent holds most of the state, child components respond to its values
#Composable
fun BaseComponent() {
var isRunning by remember { mutableStateOf(false) }
val composableScope = rememberCoroutineScope()
val count = remember { mutableStateOf(0) }
Column(verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) {
Text("Count: ${count.value}")
// Using the async context of a button click, we can toggle running off and on, as well as run our background task for incrementing the counter
ToggleCounter(isRunning) {
isRunning = !isRunning
composableScope.launch {
while (isRunning) {
delay(100L)
count.value += 1
}
}
}
}
}
// Accepting an onTap function and passing it into our button, allows us to modify state as a result of the button, without the button needing to know anything more
#Composable
fun ToggleCounter(isRunning: Boolean, onTap: () -> Unit) {
val buttonText = if (isRunning) "Stop" else "Start"
Button(onClick = onTap) {
Text(buttonText)
}
}

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 ...
}
}

Why is the app crashed when I try to use Timer to launch nativeCanvas.drawText periodically with Jetpack Compose?

I hope to call nativeCanvas.drawText periodically to draw a text with Jetpack Compose.
But I get Result A when I run Code A.
1: What's wrong with my code?
2: How can I nativeCanvas.drawText a text periodically ?
Code A
#Composable
fun ScreenHome_Watch(
){
Box(
) {
Canvas(
) {
startTimer{
drawIntoCanvas {
it.nativeCanvas.drawText("Hello "+Calendar.getInstance().time.toString() ,10f,10f, paintText)
}
}
}
}
}
private lateinit var timer: Timer
fun startTimer(block: ()->Unit) {
timer = Timer()
timer.scheduleAtFixedRate(timerTask {
block()
}, 0, 1000)
}
Result A
java.lang.ClassCastException: androidx.compose.ui.graphics.drawscope.EmptyCanvas cannot be cast to androidx.compose.ui.graphics.AndroidCanvas
No need to break the mechanism of the composition. You must change the state and compose will process the changes
#Composable
fun ScreenHome_Watch() {
var currentTime by remember { mutableStateOf(Calendar.getInstance().time) }
val paint = remember {
Paint().apply {
isAntiAlias = true
textSize = 24f
typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
}
}
Canvas(modifier = Modifier.size(height = 20.dp, width = 180.dp)) {
drawIntoCanvas {
it.nativeCanvas.drawText(
"Hello $currentTime", 30f, 30f, paint
)
}
}
LaunchedEffect(Unit) {
while (true) {
currentTime = Calendar.getInstance().time
delay(1000)
}
}
}

Need help completing the kotlinlang suggested exercise supporting Webhooks with javafx

In the tutorial, they teach how to support real-time p2p command-line messaging using websockets by implementing both client and server. I'm trying to finish an exercise where I have the client input messages via a javafx gui and receive messages inside the gui in the form of a chat log (basically a chat room)
I'm having trouble simply starting up the gui and the websocket together. I tried GlobalScope.launch in hopes that both would get run, but only the GUI gets launched. If I use runBlocking instead, only the websocket is active.
Here's what I have so far.
Other issues:
Don't know how to reference the javafx label variable inside the outputMessages function, so that we can update the chatlog. I try placing the label variable in the global scope, but it only results in a compile error, so I put it back inside SAKApplication.
How to update the label field to move to the next line (tried adding "/n" but it literally added "\n")
import java.util.Queue
import java.util.LinkedList
//var a = Label("s")
val messagesToSend: Queue<String> = LinkedList<String>()
class SAKApplication : Application() {
val l = Label("no text")
override fun start(primaryStage: Stage) {
val btn = Button()
btn.text = "Say 'Hello World'"
btn.onAction = EventHandler<ActionEvent> { println("Hello World!") }
val root = StackPane()
root.children.add(btn)
val textField = TextField()
// a = l
// action event
val event: EventHandler<ActionEvent> =
EventHandler {
l.text += "/n" + textField.getText()
messagesToSend.add(textField.getText())
}
// when enter is pressed
textField.setOnAction(event)
// add textfield
root.children.add(textField)
root.children.add(l)
val scene = Scene(root, 300.0, 250.0)
if (primaryStage != null) {
primaryStage.title = "Hello World!"
primaryStage.scene = scene
primaryStage.show()
}
val client = HttpClient {
install(WebSockets)
}
GlobalScope.launch {
client.webSocket(method = HttpMethod.Get, host = "127.0.0.1", port = 8080, path = "/chat") {
while(true) {
val messageOutputRoutine = launch { outputMessages() }
val userInputRoutine = launch { inputMessages() }
userInputRoutine.join() // Wait for completion; either "exit" or error
messageOutputRoutine.cancelAndJoin()
}
}
}
client.close()
}
}
fun main(args: Array<String>) {
Application.launch(SAKApplication::class.java, *args)
}
suspend fun DefaultClientWebSocketSession.outputMessages() {
try {
for (message in incoming) {
message as? Frame.Text ?: continue
// a.text += "/n" + message.readText()
println(message.readText())
}
} catch (e: Exception) {
println("Error while receiving: " + e.localizedMessage)
}
}
suspend fun DefaultClientWebSocketSession.inputMessages() {
val name = readLine() ?: ""
send(name)
while (true) {
sleep(1)
if (messagesToSend.isEmpty()) { continue }
val message = messagesToSend.remove()
if (message.equals("exit", true)) return
try {
send(message)
} catch (e: Exception) {
println("Error while sending: " + e.localizedMessage)
return
}
}
}

Cancel current co routine kotlin

How do i cancel the current co routine if the current coroutine is active ?
Here is the code
fun coRoutine2(interval: Long) {
val coroutineScope = CoroutineScope(Dispatchers.IO)
if (coroutineScope.isActive) {
coroutineScope.cancel()
} else {
coroutineScope.launch {
for (i in 1..progressBar.max) {
delay(interval)
progressBar.progress = i
println(i)
}
}
}
}
If you want to cancel couroutine you should cancel the Job object returned when you call launch method
val job = coroutineScope.launch {
for (i in 1..progressBar.max) {
delay(interval)
progressBar.progress = i
println(i)
}
}
job.cancel()
see more examples in official documentation