How to change picture in ImageView every second using Kotlin - android

I would like to have ImageView change picture every 1 second. I've tried using Java-specific solutions using Handler but weren't able to translate them correctly.
Basically, I have a list of image resources:
val loopImages = listOf(R.drawable.one, R.drawable.two, R.drawable.three, R.drawable.four)
and I want to change the pictures in a loop every 1 second. That was my current attempt that didn't work:
val handler = Handler()
val runnable = Runnable() {
var i = 0
fun run() {
binding.slideImage.setImageResource(loopImages[i])
i++
if (i > loopImages.size - 1) {
i = 0
}
handler.postDelayed({ run() }, 2000)
}
}
handler.postDelayed(runnable, 2000)
Is there any way to do it completely in Kotlin?

private fun startSlider() {
Handler().apply {
val runnable = object : Runnable {
var index = 0
var imageView = findViewById<ImageView>(R.id.imageView)
override fun run() {
imageView.setImageResource(loopImages[index])
index++
if (index > loopImages.size - 1) {
index = 0
}
postDelayed(this, 1000)
}
}
postDelayed(runnable, 1000)
}
}

Is there any way to do it completely in Kotlin?
Not really sure what you mean by this - your code is written in Kotlin syntax.
You can achieve a similar result using coroutines which removes the requirement for runnables and handlers, if that is closer to what you meant.
Untested code but something like this should work :
private fun imageChanger(scope: CoroutineScope, images: List<Int>, target: ImageView, periodMillis : Long = 2_000): Job =
scope.launch(Dispatchers.Main.immediate) {
if (images.isNotEmpty()) {
var idx = 0
while (isActive) {
if(idx >= images.size) idx = 0
images[idx].let(target::setImageResource)
idx++
delay(periodMillis)
}
}
}
Its a bit scrappy but can be refined.

Related

How to stop/cancel a task in activity?.runOnUiThread in Android?

I have created StopWatch function for Android app in Kotlin using Timer class. I am using activity?.runOnUiThread to display the time in the App Bar (not in View). Is there any simple way to stop timer and set it back to 0. Is the multithreading necessary?
Here is my function:
private fun stopwatch(isCanceled: Boolean) {
val timer = Timer()
val tt: TimerTask = object : TimerTask() {
override fun run() {
num += 1000L
val runnable = Runnable { setModeTitle(getString(R.string.chipper_title) + TimerUtil.timerDisplay(num)) }
activity?.runOnUiThread(runnable)
}
}
timer.schedule(tt, 0L, 1000)
if (isCanceled) {
//what to implement here, if possible?
}
}
You need to store a resulting TimerTask object and later call cancel() on it. Something like:
private var tt: TimerTask? = null
private fun stopwatch() {
val timer = Timer()
tt = object : TimerTask() {
override fun run() {
num += 1000L
val runnable = Runnable { setModeTitle(getString(R.string.chipper_title) + TimerUtil.timerDisplay(num)) }
activity?.runOnUiThread(runnable)
}
}
timer.schedule(tt, 0L, 1000)
}
private fun cancelStopwatch() {
tt?.cancel()
}
Just make sure not to call stopwatch() twice, without first cancelling an already running stopwatch.

How to sequentially update view using threads Android

I'm making an app where I have a row of 3 colored rectangles and have to change the color of each one then revert back to its original color. I have to do this for each rectangle in order so that they do not change colors simultaneously, but in sequence. I've been trying to do this with handlers since I need to wait 1 second before changing it back to its original color, but for some reason it only does the action for a single rectangle rather than all 3. Here's my code:
private fun start() {
val r1: Runnable = object : Runnable {
override fun run() {
// Lighting up the rectangle to be white
val highlightColor = Color.argb(160, 255, 255, 255)
box.setBackgroundColor(highlightColor)
}
}
val r2: Runnable = object : Runnable {
override fun run() {
// Changing back to original color
box.setBackgroundColor(Color.parseColor("#7c4dff"))
}
}
var i=0
while (i<3) {
val handler = Handler(Looper.getMainLooper())
val tableRow = tableLayout.getChildAt(0) as TableRow
box = tableRow.getChildAt(i) as TextView
val handler = Handler(Looper.getMainLooper())
handler.postDelayed(r1, 1000)
handler.postDelayed(r2, 2000)
i+=1
}
}
This only lights up the last rectangle rather than a sequence of all 3 in order. How can I fix it so that it lights up the first, then lights up the second once the first reverts back to the original color, etc.?
I suggest you use GlobalScope as it is still a CoroutineScope and you won't have to worry about migrating your project to androidx. Then to delay the thread, just use withContext followed by your delay time, like so below
private fun start() {
GlobalScope.launch(Dispatchers.Main) {
val boxes = with(tableLayout.getChildAt(0) as TableRow) {
(0..2).map { getChildAt(it) }
}
withContext(Dispatchers.Default) { delay(800) }
boxes.forEach { it.setBackgroundColor(Color.argb(160, 255, 255, 255)) }
withContext(Dispatchers.Default) { delay(800) }
boxes.forEach { it.setBackgroundColor(Color.parseColor("#7c4dff")) }
}
}
The reason it doesn't work is that your entire while loop that queues up the runnables finishes before they even start running, so they are all looking at the same box instance. For this strategy to work, you would either need to create two runnable instances for each of the three boxes, with each of them viewing a different box; or have each of the two runnables have a while loop that goes through all three boxes. You can also use the view to post your runnable so you don't need to create a Handler instance. Like this:
private fun start() {
val boxes = with(tableLayout.getChildAt(0) as TableRow) {
(0..2).map { getChildAt(it) }
}
tableLayout.postDelayed(1000L) {
boxes.forEach { it.setBackgroundColor(Color.argb(160, 255, 255, 255)) }
}
tableLayout.postDelayed(2000L) {
boxes.forEach { it.setBackgroundColor(Color.parseColor("#7c4dff")) }
}
}
This would be simpler using a coroutine like this, which will not leak your views like the Runnables will.
private fun start() = lifecycleScope.launch {
val boxes = with(tableLayout.getChildAt(0) as TableRow) {
(0..2).map { getChildAt(it) }
}
delay(1000L)
boxes.forEach { it.setBackgroundColor(Color.argb(160, 255, 255, 255)) }
delay(1000L)
boxes.forEach { it.setBackgroundColor(Color.parseColor("#7c4dff")) }
}

Progressbar not updating after Redux Event

I know this question has been asked quite often here, but non of the answers helped me.
I am writting a gallery app with a thumbes-regeneration feature. In oder to show the progress i added the progressbar which should count the number of created thumbnails. After each finished thumbnail-generation i dispatch a Redux event and listen to it in my Fragement, in order to change the progressbar.
Generating all thumbnails for all visible photos/videos
private fun onMenuRefreshThumbs(activity: Activity) {
val mediaPath = Redux.store.currentState.mediaPath
val fileRepository = FileRepository(context = activity, mediaPath = mediaPath)
activity.runOnUiThread {
fileRepository.regenerateThumbs(activity)
}
}
Functions inside the above used FileRepository:
fun regenerateThumbs(context: Context) {
val success = File(getAbsoluteThumbsDir(context, mediaPath)).deleteRecursively()
getMediaItems()
}
fun getMediaItems(): MediaItemList {
val success = File(thumbPath).mkdirs()
val isThumbsEmpty = File(thumbPath).listFiles().isEmpty()
val mediaFileList = File(mediaPath).listFiles().
.sortedByDescending { it.lastModified() }
val list = MediaItemList()
mediaFileList.apply {
forEach {
list.add(MediaItem(it.name, 0, 0))
if (isThumbsEmpty) {
getOrCreateThumb(it)
Redux.store.dispatch(FileUpdateAction(it))
}
}
}
return list
}
Subscribing to Redux in the Fragement:
private fun subscribeRedux() {
val handler = Handler(Looper.getMainLooper())
val activity = requireActivity()
subscriber = { state: AppState ->
when (state.action) {
...
is ClearSelection -> {
progressCounter = 0
// fragment_gallery_progress.visibility = View.GONE
}
is FileUpdateAction -> {
Handler().post {
progressCounter++
fragment_gallery_progress.visibility = View.VISIBLE
fragment_gallery_progress.progress = progressCounter
// fragment_gallery_progress.invalidate()
log.d("test: Thumb Index $progressCounter ${state.action.mediaItem.name} was created")
}
Unit
}
}
}.apply {
Redux.store.subscribe(this)
}
}
I tried all difference version of calling a thread in both cases. But no matter if its done with the handler or by activity.runOnUiThread, the progressbar never changes untill all thumbs are finished and the progressbar jumps from 0 to the maximum number. I can see the logs which are written in the right time, but not the progressbar changing.
I could fix my problem with following steps:
Removing the runOnUiThread() call
private fun onMenuRefreshThumbs(activity: Activity) {
val mediaPath = Redux.store.currentState.mediaPath
val fileRepository = FileRepository(context = activity, mediaPath = mediaPath)
fileRepository.regenerateThumbs(activity)
}
Adding a thread for each Thumbs-Generation:
fun getMediaItems(): MediaItemList {
val success = File(thumbPath).mkdirs()
val isThumbsEmpty = File(thumbPath).listFiles().isEmpty()
val mediaFileList = File(mediaPath).listFiles().
.sortedByDescending { it.lastModified() }
val list = MediaItemList()
mediaFileList.apply {
forEach {
list.add(MediaItem(it.name, 0, 0))
if (isThumbsEmpty) {
Thread {
getOrCreateThumb(it)
Redux.store.dispatch(FileUpdateAction(it))
}.start()
}
}
...

Kotlin: call a function every second

I want to create a simple countdown for my game, when the game starts I want this function to be called every second:
fun minusOneSecond(){
if secondsLeft > 0{
secondsLeft -= 1
seconds_thegame.text = secondsLeft.toString()
}
}
I tried this:
var secondsLeft = 15
timer.scheduleAtFixedRate(
object : TimerTask() {
override fun run() {
minusOneSecond()
}
},0, 1000
) // 1000 Millisecond = 1 second
But the app unfortunately stops, the 2nd time the run function is called
I just started with android development and Kotlin 3 weeks ago and so far I understand the most out of it.
With swift in Xcode I use this line and I thought something similar would work with Kotlin
setTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(minusOneSecond), userInfo: nil, repeats: true)
Problem: Timer class uses a background thread with a queue to queue and execute all tasks sequentially. From your code, because you update UI (changing TextView content in minusOneSecond function). That why the app throws the following exception and make your app crash.
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the
original thread that created a view hierarchy can touch its views.
Solution: There are many ways to achieve your task, but I prefer using post() and postDelayed() method from Handler class. Because it's simple and easy to understand.
val mainHandler = Handler(Looper.getMainLooper())
mainHandler.post(object : Runnable {
override fun run() {
minusOneSecond()
mainHandler.postDelayed(this, 1000)
}
})
Update: From author's comment about how to pause/resume the task from Handler. Here is an example.
class MainActivityKt : AppCompatActivity() {
lateinit var mainHandler: Handler
private val updateTextTask = object : Runnable {
override fun run() {
minusOneSecond()
mainHandler.postDelayed(this, 1000)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Your logic code
...
mainHandler = Handler(Looper.getMainLooper())
}
override fun onPause() {
super.onPause()
mainHandler.removeCallbacks(updateTextTask)
}
override fun onResume() {
super.onResume()
mainHandler.post(updateTextTask)
}
fun minusOneSecond() {
if secondsLeft > 0 {
secondsLeft -= 1
seconds_thegame.text = secondsLeft.toString()
}
}
}
I am using this code to update a clock every minute
fixedRateTimer("timer", false, 0L, 60 * 1000) {
this#FullscreenActivity.runOnUiThread {
tvTime.text = SimpleDateFormat("dd MMM - HH:mm", Locale.US).format(Date())
}
}
so you have to run it with paratemer 1000 instead of 60*1000
val timer = object: CountDownTimer(10000, 1000) {
override fun onTick(millisUntilFinished: Long) {
// do something
}
override fun onFinish() {
// do something
}
}
timer.start()
You can also use CountDownTimer for this purpose. As this takes two parameters (the total time and the interval time)
Plus it also provides an on finish method to perform any task when the total time is finished.
please use
inline fun Timer.schedule(
time: Date,
period: Long,
crossinline action: TimerTask.() -> Unit
): TimerTask
reference: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.concurrent/java.util.-timer/schedule.html
I am calling my function every second like this
val handler = Handler()
handler.postDelayed(object : Runnable {
override fun run() {
//Call your function here
handler.postDelayed(this, 1000)//1 sec delay
}
}, 0)
My solution
viewModelScope.launch(Dispatchers.IO) {
while(isActive) {
when(val response = repository.getApi()) {
is NetworkState.Success -> {
getAllData.postValue(response.data)
}
is NetworkState.Error -> this#MainViewModel.isActive = false
}
delay(API_CALL_DELAY)
}
}
if you use any background task or background service try this code
val timer = Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate({
Log.d("RUNNING ","Thread")
},0,10,TimeUnit.SECONDS)
if you work with UI thers like update UI layout try this code
val timer = Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate({
Log.d("RUNNING ","BACKGROUN Thread")
runOnUiThread {
Log.d("RUNNING ","Update UI Thread")
btnUpdate.setText(System.currentTimeMillis().toString())
}
},0,1,TimeUnit.SECONDS)
I'm using recursion with Coroutine its very simple
private fun loop() {
CoroutineScope(IO).launch {
delay(5000)
CoroutineScope(Main).launch {
ManagerToWorker()
loop()
}
}
}
var isActionAchieved = false
var secondsPassed = 0
fun cDTimer(){
if (!isActionAchieved && secondsPassed < 10){ // repeat check if Action NOT Achieved for max of 10 seconds
Handler(Looper.getMainLooper()).postDelayed({
repeatThisFunction()
repeater()
secondsPassed++
}, 1000) //one second till next execution
}
}
fun repeater(){
cDTimer()
}

How to implement timer with Kotlin coroutines

I want to implement timer using Kotlin coroutines, something similar to this implemented with RxJava:
Flowable.interval(0, 5, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
.map { LocalDateTime.now() }
.distinctUntilChanged { old, new ->
old.minute == new.minute
}
.subscribe {
setDateTime(it)
}
It will emit LocalDateTime every new minute.
Edit: note that the API suggested in the original answer is now marked #ObsoleteCoroutineApi:
Ticker channels are not currently integrated with structured concurrency and their api will change in the future.
You can now use the Flow API to create your own ticker flow:
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun tickerFlow(period: Duration, initialDelay: Duration = Duration.ZERO) = flow {
delay(initialDelay)
while (true) {
emit(Unit)
delay(period)
}
}
And you can use it in a way very similar to your current code:
tickerFlow(5.seconds)
.map { LocalDateTime.now() }
.distinctUntilChanged { old, new ->
old.minute == new.minute
}
.onEach {
setDateTime(it)
}
.launchIn(viewModelScope) // or lifecycleScope or other
Note: with the code as written here, the time taken to process elements is not taken into account by tickerFlow, so the delay might not be regular (it's a delay between element processing). If you want the ticker to tick independently of the processing of each element, you may want to use a buffer or a dedicated thread (e.g. via flowOn).
Original answer
I believe it is still experimental, but you may use a TickerChannel to produce values every X millis:
val tickerChannel = ticker(delayMillis = 60_000, initialDelayMillis = 0)
repeat(10) {
tickerChannel.receive()
val currentTime = LocalDateTime.now()
println(currentTime)
}
If you need to carry on doing your work while your "subscribe" does something for each "tick", you may launch a background coroutine that will read from this channel and do the thing you want:
val tickerChannel = ticker(delayMillis = 60_000, initialDelayMillis = 0)
launch {
for (event in tickerChannel) {
// the 'event' variable is of type Unit, so we don't really care about it
val currentTime = LocalDateTime.now()
println(currentTime)
}
}
delay(1000)
// when you're done with the ticker and don't want more events
tickerChannel.cancel()
If you want to stop from inside the loop, you can simply break out of it, and then cancel the channel:
val ticker = ticker(500, 0)
var count = 0
for (event in ticker) {
count++
if (count == 4) {
break
} else {
println(count)
}
}
ticker.cancel()
A very pragmatic approach with Kotlin Flows could be:
// Create the timer flow
val timer = (0..Int.MAX_VALUE)
.asSequence()
.asFlow()
.onEach { delay(1_000) } // specify delay
// Consume it
timer.collect {
println("bling: ${it}")
}
another possible solution as a reusable kotlin extension of CoroutineScope
fun CoroutineScope.launchPeriodicAsync(
repeatMillis: Long,
action: () -> Unit
) = this.async {
if (repeatMillis > 0) {
while (isActive) {
action()
delay(repeatMillis)
}
} else {
action()
}
}
and then usage as:
var job = CoroutineScope(Dispatchers.IO).launchPeriodicAsync(100) {
//...
}
and then to interrupt it:
job.cancel()
another note: we consider here that action is non-blocking and does not take time.
You can create a countdown timer like this
GlobalScope.launch(Dispatchers.Main) {
val totalSeconds = TimeUnit.MINUTES.toSeconds(2)
val tickSeconds = 1
for (second in totalSeconds downTo tickSeconds) {
val time = String.format("%02d:%02d",
TimeUnit.SECONDS.toMinutes(second),
second - TimeUnit.MINUTES.toSeconds(TimeUnit.SECONDS.toMinutes(second))
)
timerTextView?.text = time
delay(1000)
}
timerTextView?.text = "Done!"
}
Here's a possible solution using Kotlin Flow
fun tickFlow(millis: Long) = callbackFlow<Int> {
val timer = Timer()
var time = 0
timer.scheduleAtFixedRate(
object : TimerTask() {
override fun run() {
try { offer(time) } catch (e: Exception) {}
time += 1
}
},
0,
millis)
awaitClose {
timer.cancel()
}
}
Usage
val job = CoroutineScope(Dispatchers.Main).launch {
tickFlow(125L).collect {
print(it)
}
}
...
job.cancel()
Edit: Joffrey has edited his solution with a better approach.
Old :
Joffrey's solution works for me but I ran into a problem with the for loop.
I have to cancel my ticker in the for loop like this :
val ticker = ticker(500, 0)
for (event in ticker) {
if (...) {
ticker.cancel()
} else {
...
}
}
}
But ticker.cancel() was throwing a cancellationException because the for loop kept going after this.
I had to use a while loop to check if the channel was not closed to not get this exception.
val ticker = ticker(500, 0)
while (!ticker.isClosedForReceive && ticker.iterator().hasNext()) {
if (...) {
ticker.cancel()
} else {
...
}
}
}
Timer with START, PAUSE and STOP functions.
Usage:
val timer = Timer(millisInFuture = 10_000L, runAtStart = false)
timer.start()
Timer class:
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
enum class PlayerMode {
PLAYING,
PAUSED,
STOPPED
}
class Timer(
val millisInFuture: Long,
val countDownInterval: Long = 1000L,
runAtStart: Boolean = false,
val onFinish: (() -> Unit)? = null,
val onTick: ((Long) -> Unit)? = null
) {
private var job: Job = Job()
private val _tick = MutableStateFlow(0L)
val tick = _tick.asStateFlow()
private val _playerMode = MutableStateFlow(PlayerMode.STOPPED)
val playerMode = _playerMode.asStateFlow()
private val scope = CoroutineScope(Dispatchers.Default)
init {
if (runAtStart) start()
}
fun start() {
if (_tick.value == 0L) _tick.value = millisInFuture
job.cancel()
job = scope.launch(Dispatchers.IO) {
_playerMode.value = PlayerMode.PLAYING
while (isActive) {
if (_tick.value <= 0) {
job.cancel()
onFinish?.invoke()
_playerMode.value = PlayerMode.STOPPED
return#launch
}
delay(timeMillis = countDownInterval)
_tick.value -= countDownInterval
onTick?.invoke(this#Timer._tick.value)
}
}
}
fun pause() {
job.cancel()
_playerMode.value = PlayerMode.PAUSED
}
fun stop() {
job.cancel()
_tick.value = 0
_playerMode.value = PlayerMode.STOPPED
}
}
I took inspiration from here.
Here is Flow version of Observable.intervalRange(1, 5, 0, 1, TimeUnit.SECONDS) based on Joffrey's answer:
fun tickerFlow(start: Long,
count: Long,
initialDelayMs: Long,
periodMs: Long) = flow<Long> {
delay(initialDelayMs)
var counter = start
while (counter <= count) {
emit(counter)
counter += 1
delay(periodMs)
}
}
//...
tickerFlow(1, 5, 0, 1_000L)
Made a copy of Observable.intervalRange(0, 90, 0, 1, TimeUnit.SECONDS) ( will emit item in 90 sec each 1 sec ):
fun intervalRange(start: Long, count: Long, initialDelay: Long = 0, period: Long, unit: TimeUnit): Flow<Long> {
return flow<Long> {
require(count >= 0) { "count >= 0 required but it was $count" }
require(initialDelay >= 0) { "initialDelay >= 0 required but it was $initialDelay" }
require(period > 0) { "period > 0 required but it was $period" }
val end = start + (count - 1)
require(!(start > 0 && end < 0)) { "Overflow! start + count is bigger than Long.MAX_VALUE" }
if (initialDelay > 0) {
delay(unit.toMillis(initialDelay))
}
var counter = start
while (counter <= count) {
emit(counter)
counter += 1
delay(unit.toMillis(period))
}
}
}
Usage:
lifecycleScope.launch {
intervalRange(0, 90, 0, 1, TimeUnit.SECONDS)
.onEach {
Log.d(TAG, "intervalRange: ${90 - it}")
}
.lastOrNull()
}
Used this recently to chunk values based on a timer and max buffer size.
private object Tick
#Suppress("UNCHECKED_CAST")
fun <T : Any> Flow<T>.chunked(size: Int, initialDelay: Long, delay: Long): Flow<List<T>> = flow {
if (size <= 0) throw IllegalArgumentException("invalid chunk size $size - expected > 0")
val chunkedList = mutableListOf<T>()
if (delay > 0L) {
merge(this#chunked, timerFlow(initialDelay, delay, Tick))
} else {
this#chunked
}
.collect {
when (it) {
is Tick -> {
if (chunkedList.isNotEmpty()) {
emit(chunkedList.toList())
chunkedList.clear()
}
}
else -> {
chunkedList.add(it as T)
if (chunkedList.size >= size) {
emit(chunkedList.toList())
chunkedList.clear()
}
}
}
}
if (chunkedList.isNotEmpty()) {
emit(chunkedList.toList())
}
}
fun <T> timerFlow(initialDelay: Long, delay: Long, o: T) = flow {
if (delay <= 0) throw IllegalArgumentException("invalid delay $delay - expected > 0")
if (initialDelay > 0) delay(initialDelay)
while (currentCoroutineContext().isActive) {
emit(o)
delay(delay)
}
}
It's not using Kotlin coroutines, but if your use case is simple enough you can always just use something like a fixedRateTimer or timer (docs here) which resolve to JVM native Timer.
I was using RxJava's interval for a relatively simple scenario and when I switched to using Timers I saw significant performance and memory improvements.
You can also run your code on the main thread on Android by using View.post() or it's mutliple variants.
The only real annoyance is you'll need to keep track of the old time's state yourself instead of relying on RxJava to do it for you.
But this will always be much faster (important if you're doing performance critical stuff like UI animations etc) and will not have the memory overhead of RxJava's Flowables.
Here's the question's code using a fixedRateTimer:
var currentTime: LocalDateTime = LocalDateTime.now()
fixedRateTimer(period = 5000L) {
val newTime = LocalDateTime.now()
if (currentTime.minute != newTime.minute) {
post { // post the below code to the UI thread to update UI stuff
setDateTime(newTime)
}
currentTime = newTime
}
}
enter image description here
enter code here
private val updateLiveShowTicker = flow {
while (true) {
emit(Unit)
delay(1000L * UPDATE_PROGRAM_INFO_INTERVAL_SECONDS)
}
}
private val updateShowProgressTicker = flow {
while (true) {
emit(Unit)
delay(1000L * UPDATE_SHOW_PROGRESS_INTERVAL_SECONDS)
}
}
private val liveShow = updateLiveShowTicker
.combine(channelId) { _, channelId -> programInfoRepository.getShow(channelId) }
.catch { emit(LiveShow(application.getString(R.string.activity_channel_detail_info_error))) }
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(), replay = 1)
.distinctUntilChanged()
My solution,You can now use the Flow API to create your own ticker flow:

Categories

Resources