jetpack compose Scaffold Possible to override the standard durations of SnackBar? - kotlin

How can override the standard durations of the Scaffold SnackBar to apply my own durations in MS. I can't see a way to do it
is EventsToAddAlbumScreen.ShowSnackbarEventToAddAlbumScreen -> scaffoldState.snackbarHostState.showSnackbar(
message = event.message,
duration = SnackbarDuration.Short // <-- want to change this to 500ms for example
)

You can use SnackbarDuration.Indefinite and cancel it manually after the necessary delay:
LaunchedEffect(Unit) {
val job = launch {
scaffoldState.snackbarHostState.showSnackbar("Hi", duration = SnackbarDuration.Indefinite)
}
delay(500)
job.cancel()
}

You can use this trick:
val scope = rememberCoroutineScope()
val snackBarMessage = stringResource(id = R.string.snackbar_message)
scope.launch {
val job = scope.launch {
snackbarHostState.showSnackbar(
message = snackBarMessage,
duration = SnackbarDuration.Indefinite,
)
}
delay(500)
job.cancel()
}

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

Can I use one Job instead of two Jobs when I use Flow in Kotlin?

In Code A, there are two Flows, and I assign two jobs for them, I collect the two Flows in fun beginSoundDensity() and stop collecting the two Flows in fun resetSoundDensity().
I think there are many repeated codes in Code A, so I hope to improve it, but Code B doesn't work.
Can I use one Job in my case?
Code A
private val _soundDensityState = MutableStateFlow(initialMSoundDensity)
val soundDensityState = _soundDensityState.asStateFlow()
private val _timeX = MutableStateFlow(0)
val timeX = _timeX.asStateFlow()
private var myJob1: Job?=null
private var myJob2: Job?=null
val myFlow: Flow<Int> = flow {
var i = 0
while (true) {
emit(i)
i = i + 15
delay(5000)
}
}
fun beginSoundDensity() {
myJob1?.cancel()
myJob2?.cancel()
myJob1 = viewModelScope.launch {
aSoundMeter.startSoundDensity {
pauseSoundDensity()
}.cancellable()
.collect {
_soundDensityState.value = it
}
}
myJob2 = viewModelScope.launch {
myFlow.collect {
_timeX.value = it
}
}
}
}
fun resetSoundDensity(){
myJob1?.cancel()
myJob2?.cancel()
}
Code B
//The same
private var myJob: Job?=null
val myFlow: Flow<Int> = flow {
var i = 0
while (true) {
emit(i)
i = i + 15
delay(5000)
}
}
fun beginSoundDensity() {
myJob?.cancel()
myJob = viewModelScope.launch {
aSoundMeter.startSoundDensity {
pauseSoundDensity()
}.cancellable()
.collect {
_soundDensityState.value = it
}
myFlow.collect {
_timeX.value = it //It will not be launched
}
}
}
}
fun resetSoundDensity(){
myJob?.cancel()
}
Yes and no. You need two separate coroutines running concurrently to collect from two flows. In your Code B myFlow will be collected only after aSoundMeter finishes collecting. Collections need to run at the same time, so you need two concurrent coroutines for this purpose.
However, if you always start and cancel both collections together, then I think it would be better to group them into a single coroutine like this:
fun beginSoundDensity() {
myJob?.cancel()
myJob = viewModelScope.launch {
coroutineScope {
launch {
aSoundMeter.startSoundDensity {
pauseSoundDensity()
}.cancellable()
.collect {
_soundDensityState.value = it
}
}
launch {
myFlow.collect {
_timeX.value = it //It will not be launched
}
}
}
}
}
fun resetSoundDensity(){
myJob?.cancel()
}

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

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.

coroutine execution problem some coroutines won't launch

I'm trying to transform and flush some data to a database in a streaming application using spring and kotlin. performance constraints made me to use coroutine and chunk to execute the transformation and persist procedure faster.
The problem is the size of input data is not equal to the persisted data!
My Job Scheduled to run with a fixed delay:
#Scheduled(fixedDelay = 10_000)
fun flushToDb() {
// some operations
CoroutineScope(getDispatcherExec()).launch {
flush(oldStat, isFlushing)
}
}
and this is my suspend func:
private suspend fun flush(oldStat: Map<String, AppDailyStatsModel>, isFlushing: AtomicBoolean) {
logger.info("RedisFlush ${oldStat.size} starts")
val start = System.currentTimeMillis()
val count = AtomicLong(0)
val startCount = AtomicLong(0)
val newStat = AtomicLong(0)
val updateStat = AtomicLong(0)
val input = oldStat.values
coroutineScope {
input
.chunked(1000)
.forEach { models ->
launch {
try {
startCount.addAndGet(models.size.toLong())
// Transform
// Persist
count.addAndGet(models.size.toLong())
logger.info("Flushed ${count.get()}")
} catch (e: Exception) {
logger.error("PROBLEM IN SAVE ", e)
}
}
}
}
}
The issue is count and startCount are equal and they're less than input !!
any help would be much appreciated!

How to use runBlocking to await the completion of a CoroutineScope

I'm trying to test a class that declares itself as a CoroutineScope. The class has some methods that launch jobs within its scope and I must assert the effects of those jobs from a test method.
Here's what I tried:
import kotlinx.coroutines.*
class Main : CoroutineScope {
override val coroutineContext get() = Job()
var value = 0
fun updateValue() {
this.launch {
delay(1000)
value = 1
}
}
}
fun main() {
val main = Main()
val mainJob = main.coroutineContext[Job]!!
main.updateValue()
runBlocking {
mainJob.children.forEach { it.join() }
}
require(main.value == 1)
}
My expectation was that updateValue() will create a child of the root job in coroutineContext. but it turns out that mainJob.children is empty, so I can't await on the completion of launch and the require statement fails.
What is the proper way to make this work?
The error in my code was simple:
override val coroutineContext get() = Job()
I accidentally left a custom getter, which means each access to coroutineContext created a new job. Naturally, the job I got in the test code had no children. Removing get() makes the code work:
override val coroutineContext = Job()
modify your coroutine builder launch to
this.launch(start = CoroutineStart.LAZY)
and change your job object initialisation to direct
override val coroutineContext : Job = Job()
and it should produce the desired result
here's the example i tried, it's producing the desired result
import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
class Main : CoroutineScope {
val scope: CoroutineScope = this
override val coroutineContext = Job()
//Dispatchers.Default +
var value = 0
fun updateValue(csc : CoroutineScope) {
csc.launch(context = coroutineContext, start = CoroutineStart.LAZY) { println(this.coroutineContext[Job]!!.toString() + " job 2") }
csc.launch (context = coroutineContext, start = CoroutineStart.LAZY){ println(this.coroutineContext[Job]!!.toString() + " job 3") }
csc.launch (start = CoroutineStart.LAZY){
println(this.coroutineContext[Job]!!.toString() + " job 1")
//delay(1000)
value = 1
}
}
fun something() {
launch (start = CoroutineStart.LAZY){
println(this.coroutineContext[Job]!!.toString() + " something 1")
}
launch (start = CoroutineStart.LAZY){
println(this.coroutineContext[Job]!!.toString() + " something 2")
}
launch(start = CoroutineStart.LAZY) {
println(this.coroutineContext[Job]!!.toString() + " something 3")
delay(2000)
value = 1
}
}
}
fun main() {
val main = Main()
val mainJob = main.coroutineContext[Job]!!
main.updateValue(main.scope)
//main.something()
runBlocking {
//println(mainJob.children.count())
println(mainJob.children.count())
mainJob.children.forEach {
//println("in run blocking")
println(it.toString())
it.join()
}
}
println(main.value)
}
`