I'm currently using the cameraX sample code to record a video with audio . I didn't change anything from the samples but the camera simply won't launch. I have a swipe button in my fragment that is supposed to take me to the videoCaptureFragment but all I get are some logs and the fragments don't change. No errors or warnings , the app doesn't crash but it doesn't load the camera view.
here's my videoCaptureFragment:
class VideoCaptureFragment : Fragment() {
// UI with ViewBinding
private var _captureViewBinding: VideoCaptureFragmentBinding? = null
private val captureViewBinding get() = _captureViewBinding!!
private val captureLiveStatus = MutableLiveData<String>()
private val cameraCapabilities = mutableListOf<CameraCapability>()
private lateinit var videoCapture: VideoCapture<Recorder>
private var currentRecording: Recording? = null
private lateinit var recordingState:VideoRecordEvent
// Camera UI states and inputs
enum class UiState {
IDLE, // Not recording, all UI controls are active.
RECORDING, // Camera is recording, only display Pause/Resume & Stop button.
FINALIZED, // Recording just completes, disable all RECORDING UI controls.
RECOVERY // For future use.
}
private var cameraIndex = 0
private var qualityIndex = DEFAULT_QUALITY_IDX
private var audioEnabled = false
private val mainThreadExecutor by lazy { ContextCompat.getMainExecutor(requireContext()) }
private var enumerationDeferred:Deferred<Unit>? = null
// main cameraX capture functions
/**
* Always bind preview + video capture use case combinations in this sample
* (VideoCapture can work on its own). The function should always execute on
* the main thread.
*/
private suspend fun bindCaptureUsecase() {
val cameraProvider = ProcessCameraProvider.getInstance(requireContext()).await()
val cameraSelector = getCameraSelector(cameraIndex)
// create the user required QualitySelector (video resolution): we know this is
// supported, a valid qualitySelector will be created.
val quality = cameraCapabilities[cameraIndex].qualities[qualityIndex]
val qualitySelector = QualitySelector.from(quality)
captureViewBinding.previewView.updateLayoutParams<ConstraintLayout.LayoutParams> {
val orientation = this#RecordingInterviewFragment.resources.configuration.orientation
dimensionRatio = quality.getAspectRatioString(quality,
(orientation == Configuration.ORIENTATION_PORTRAIT))
}
val preview = Preview.Builder()
.setTargetAspectRatio(quality.getAspectRatio(quality))
.build().apply {
setSurfaceProvider(captureViewBinding.previewView.surfaceProvider)
}
// build a recorder, which can:
// - record video/audio to MediaStore(only shown here), File, ParcelFileDescriptor
// - be used create recording(s) (the recording performs recording)
val recorder = Recorder.Builder()
.setQualitySelector(qualitySelector)
.build()
videoCapture = VideoCapture.withOutput(recorder)
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
viewLifecycleOwner,
cameraSelector,
videoCapture,
preview
)
} catch (exc: Exception) {
// we are on main thread, let's reset the controls on the UI.
Log.e(TAG, "Use case binding failed", exc)
resetUIandState("bindToLifecycle failed: $exc")
}
enableUI(true)
}
/**
* Kick start the video recording
* - config Recorder to capture to MediaStoreOutput
* - register RecordEvent Listener
* - apply audio request from user
* - start recording!
* After this function, user could start/pause/resume/stop recording and application listens
* to VideoRecordEvent for the current recording status.
*/
#SuppressLint("MissingPermission")
private fun startRecording() {
// create MediaStoreOutputOptions for our recorder: resulting our recording!
val name = "CameraX-recording-" +
SimpleDateFormat(FILENAME_FORMAT, Locale.US)
.format(System.currentTimeMillis()) + ".mp4"
val contentValues = ContentValues().apply {
put(MediaStore.Video.Media.DISPLAY_NAME, name)
}
val mediaStoreOutput = MediaStoreOutputOptions.Builder(
requireActivity().contentResolver,
MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
.setContentValues(contentValues)
.build()
// configure Recorder and Start recording to the mediaStoreOutput.
currentRecording = videoCapture.output
.prepareRecording(requireActivity(), mediaStoreOutput)
.apply { if (audioEnabled) withAudioEnabled() }
.start(mainThreadExecutor, captureListener)
Log.i(TAG, "Recording started")
}
/**
* CaptureEvent listener.
*/
private val captureListener = Consumer<VideoRecordEvent> { event ->
// cache the recording state
if (event !is VideoRecordEvent.Status)
recordingState = event
updateUI(event)
if (event is VideoRecordEvent.Finalize) {
// display the captured video
lifecycleScope.launch {
/*navController.navigate(
CaptureFragmentDirections.actionCaptureToVideoViewer(
event.outputResults.outputUri
)
)*/
}
}
}
/**
* Retrieve the asked camera's type(lens facing type). In this sample, only 2 types:
* idx is even number: CameraSelector.LENS_FACING_BACK
* odd number: CameraSelector.LENS_FACING_FRONT
*/
private fun getCameraSelector(idx: Int) : CameraSelector {
if (cameraCapabilities.size == 0) {
Log.i(TAG, "Error: This device does not have any camera, bailing out")
requireActivity().finish()
}
return (cameraCapabilities[idx % cameraCapabilities.size].camSelector)
}
data class CameraCapability(val camSelector: CameraSelector, val qualities:List<Quality>)
/**
* Query and cache this platform's camera capabilities, run only once.
*/
init {
enumerationDeferred = lifecycleScope.async {
whenCreated {
val provider = ProcessCameraProvider.getInstance(requireContext()).await()
provider.unbindAll()
for (camSelector in arrayOf(
CameraSelector.DEFAULT_BACK_CAMERA,
CameraSelector.DEFAULT_FRONT_CAMERA
)) {
try {
// just get the camera.cameraInfo to query capabilities
// we are not binding anything here.
if (provider.hasCamera(camSelector)) {
val camera = provider.bindToLifecycle(requireActivity(), camSelector)
QualitySelector
.getSupportedQualities(camera.cameraInfo)
.filter { quality ->
listOf(Quality.UHD, Quality.FHD, Quality.HD, Quality.SD)
.contains(quality)
}.also {
cameraCapabilities.add(CameraCapability(camSelector, it))
}
}
} catch (exc: java.lang.Exception) {
Log.e(TAG, "Camera Face $camSelector is not supported")
}
}
}
}
}
/**
* One time initialize for CameraFragment (as a part of fragment layout's creation process).
* This function performs the following:
* - initialize but disable all UI controls except the Quality selection.
* - set up the Quality selection recycler view.
* - bind use cases to a lifecycle camera, enable UI controls.
*/
private fun initCameraFragment() {
initializeUI()
Log.i("Camerx","insisde Init Camera")
viewLifecycleOwner.lifecycleScope.launch {
if (enumerationDeferred != null) {
enumerationDeferred!!.await()
enumerationDeferred = null
}
initializeQualitySectionsUI()
bindCaptureUsecase()
}
}
/**
* Initialize UI. Preview and Capture actions are configured in this function.
* Note that preview and capture are both initialized either by UI or CameraX callbacks
* (except the very 1st time upon entering to this fragment in onCreateView()
*/
#SuppressLint("ClickableViewAccessibility", "MissingPermission")
private fun initializeUI() {
captureViewBinding.cameraButton.apply {
setOnClickListener {
cameraIndex = (cameraIndex + 1) % cameraCapabilities.size
// camera device change is in effect instantly:
// - reset quality selection
// - restart preview
qualityIndex = DEFAULT_QUALITY_IDX
initializeQualitySectionsUI()
enableUI(false)
viewLifecycleOwner.lifecycleScope.launch {
bindCaptureUsecase()
}
}
isEnabled = false
}
// audioEnabled by default is disabled.
captureViewBinding.audioSelection.isChecked = audioEnabled
captureViewBinding.audioSelection.setOnClickListener {
audioEnabled = captureViewBinding.audioSelection.isChecked
}
Log.i("Camerx","inside init UI")
// React to user touching the capture button
captureViewBinding.captureButton.apply {
setOnClickListener {
if (!this#RecordingInterviewFragment::recordingState.isInitialized ||
recordingState is VideoRecordEvent.Finalize)
{
enableUI(false) // Our eventListener will turn on the Recording UI.
startRecording()
} else {
when (recordingState) {
is VideoRecordEvent.Start -> {
currentRecording?.pause()
captureViewBinding.stopButton.visibility = View.VISIBLE
}
is VideoRecordEvent.Pause -> currentRecording?.resume()
is VideoRecordEvent.Resume -> currentRecording?.pause()
else -> throw IllegalStateException("recordingState in unknown state")
}
}
}
isEnabled = false
}
captureViewBinding.stopButton.apply {
setOnClickListener {
// stopping: hide it after getting a click before we go to viewing fragment
captureViewBinding.stopButton.visibility = View.INVISIBLE
if (currentRecording == null || recordingState is VideoRecordEvent.Finalize) {
return#setOnClickListener
}
val recording = currentRecording
if (recording != null) {
recording.stop()
currentRecording = null
}
captureViewBinding.captureButton.setImageResource(R.drawable.ic_start)
}
// ensure the stop button is initialized disabled & invisible
visibility = View.INVISIBLE
isEnabled = false
}
captureLiveStatus.observe(viewLifecycleOwner) {
captureViewBinding.captureStatus.apply {
post { text = it }
}
}
captureLiveStatus.value = getString(R.string.Idle)
}
/**
* UpdateUI according to CameraX VideoRecordEvent type:
* - user starts capture.
* - this app disables all UI selections.
* - this app enables capture run-time UI (pause/resume/stop).
* - user controls recording with run-time UI, eventually tap "stop" to end.
* - this app informs CameraX recording to stop with recording.stop() (or recording.close()).
* - CameraX notify this app that the recording is indeed stopped, with the Finalize event.
* - this app starts VideoViewer fragment to view the captured result.
*/
private fun updateUI(event: VideoRecordEvent) {
val state = if (event is VideoRecordEvent.Status) recordingState.getNameString()
else event.getNameString()
when (event) {
is VideoRecordEvent.Status -> {
// placeholder: we update the UI with new status after this when() block,
// nothing needs to do here.
Log.i("Camerx","you're idk tbh")
}
is VideoRecordEvent.Start -> {
showUI(UiState.RECORDING, event.getNameString())
Log.i("Camerx","you're recording")
}
is VideoRecordEvent.Finalize-> {
showUI(UiState.FINALIZED, event.getNameString())
Log.i("Camerx","you're done, you're done")
}
is VideoRecordEvent.Pause -> {
captureViewBinding.captureButton.setImageResource(R.drawable.ic_resume)
Log.i("Camerx","you're paused")
}
is VideoRecordEvent.Resume -> {
captureViewBinding.captureButton.setImageResource(R.drawable.ic_pause)
Log.i("Camerx","you're resumed")
}
}
val stats = event.recordingStats
val size = stats.numBytesRecorded / 1000
val time = java.util.concurrent.TimeUnit.NANOSECONDS.toSeconds(stats.recordedDurationNanos)
var text = "${state}: recorded ${size}KB, in ${time}second"
if(event is VideoRecordEvent.Finalize)
text = "${text}\nFile saved to: ${event.outputResults.outputUri}"
captureLiveStatus.value = text
Log.i(TAG, "recording event: $text")
}
/**
* Enable/disable UI:
* User could select the capture parameters when recording is not in session
* Once recording is started, need to disable able UI to avoid conflict.
*/
private fun enableUI(enable: Boolean) {
Log.i("Camerx","insisde enableuia")
arrayOf(captureViewBinding.cameraButton,
captureViewBinding.captureButton,
captureViewBinding.stopButton,
captureViewBinding.audioSelection,
captureViewBinding.qualitySelection).forEach {
it.isEnabled = enable
}
// disable the camera button if no device to switch
if (cameraCapabilities.size <= 1) {
captureViewBinding.cameraButton.isEnabled = false
}
// disable the resolution list if no resolution to switch
if (cameraCapabilities[cameraIndex].qualities.size <= 1) {
captureViewBinding.qualitySelection.apply { isEnabled = false }
}
}
/**
* initialize UI for recording:
* - at recording: hide audio, qualitySelection,change camera UI; enable stop button
* - otherwise: show all except the stop button
*/
private fun showUI(state: UiState, status:String = "idle") {
Log.i("Camerx","insisde show ui")
captureViewBinding.let {
when(state) {
UiState.IDLE -> {
it.captureButton.setImageResource(R.drawable.ic_start)
it.stopButton.visibility = View.INVISIBLE
it.cameraButton.visibility= View.VISIBLE
it.audioSelection.visibility = View.VISIBLE
it.qualitySelection.visibility=View.VISIBLE
}
UiState.RECORDING -> {
it.cameraButton.visibility = View.INVISIBLE
it.audioSelection.visibility = View.INVISIBLE
it.qualitySelection.visibility = View.INVISIBLE
it.captureButton.setImageResource(R.drawable.ic_pause)
it.captureButton.isEnabled = true
it.stopButton.visibility = View.VISIBLE
it.stopButton.isEnabled = true
}
UiState.FINALIZED -> {
it.captureButton.setImageResource(R.drawable.ic_start)
it.stopButton.visibility = View.INVISIBLE
}
else -> {
val errorMsg = "Error: showUI($state) is not supported"
Log.e(TAG, errorMsg)
return
}
}
it.captureStatus.text = status
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initCameraFragment()
}
override fun onDestroyView() {
_captureViewBinding = null
super.onDestroyView()
}
companion object {
// default Quality selection if no input from UI
const val DEFAULT_QUALITY_IDX = 0
val TAG:String = RecordingInterviewFragment::class.java.simpleName
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
}
Thank you
My goal:
in the view of the fragment I have a button that, when pressed once, launches a method in the viewModel which cyclically calls a suspend function to be repeated every few seconds from its conclusion. Pressing the button again stops this cycle.
My approach:
inside the fragment I set the onclicklistener of the button
binding.demoButton.setOnClickListener {
viewModel.toggleDemo()
}
in the viewModel:
private var startDemo : Boolean = false //I need to know whether to start the loop or stop it
private var isBusy : Boolean = false //I need to know if my task is running or finished
fun toggleDemo(){
val oldValue : Boolean = startDemo
val newValue = !oldValue
startDemo = newValue
if(startDemo){
saveLogLine("** start demo **") //method that passes some log strings to the fragment
startDemo()
}else{
saveLogLine("NO start demo!!")
}
}
private fun startDemo(){
GlobalScope.launch(Dispatchers.IO) {
saveLogLineAsync("before while loop")
while(startDemo){
if(!isBusy){
isBusy = true
Handler(Looper.getMainLooper()).postDelayed({
runBlocking(Dispatchers.IO) {
saveLogLineAsync("inside runBlocking")
initDemo()
}
isBusy = false
saveLogLineAsync("inside handler")
}, 5000)
}
}
saveLogLineAsync("after while loop")
}
}
private suspend fun initDemo(){ //my task
}
Is there a more elegant way to do this?
I would have liked to use a Service () or a BroadcastReceiver () but in both cases I would not know how to make them communicate with the fragment or with the viewModel (more precisely, they should be able to use the 2 methods 'saveLogLineAsync' and 'intDemo')
You can simplify your code with this:
private var demoRunning = false
private var demoJob: Job? = null
fun toggleDemo() {
if (!demoRunning) {
startDemo()
} else {
demoJob?.cancel()
}
demoRunning = !demoRunning
}
private fun startDemo() {
demoJob = viewModelScope.launch(Dispatchers.IO) {
while (true) {
initDemo()
delay(5000)
}
}
}
private suspend fun initDemo() { //my task
Log.e("INIT DEMO", "initDemo Ran")
}
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()
}
}
...
I have a video of 10 seconds. I want to loop a segment from 2 seconds to 6 seconds. Starting the player at the right time is easy:
player?.seekTo(2000)
I don't think there is a functionality available in the ExoPlayer2 library to define an end position. So I tried to add a delay co-routine. With the method seekToPositionAndStartCounter. This works in a separate project. But used in the actual project, onPlayerStateChanged gets triggered a lot of times all of a sudden.
var elapsedTime = 0L // just for testing
private fun showVideoWhenDoneLoading() {
videoView?.player?.addListener(object : Player.EventListener {
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
super.onPlayerStateChanged(playWhenReady, playbackState)
if (playbackState == Player.STATE_READY) {
if (playWhenReady) {
seekToPositionAndStartCounter()
} else {
cancelCounter()
}
}
}
})
}
private fun seekToPositionAndStartCounter() {
elapsedTime = System.currentTimeMillis()
cancelCounter()
job = GlobalScope.launch(Dispatchers.IO) {
Timber.d("starting Thread name = ${Thread.currentThread().name}")
player?.seekTo(startPosition)
if (duration != 0L) {
runBlocking {
delay(duration)
}
GlobalScope.launch(Dispatchers.Main) {
Timber.d("duration = $duration elapsedTime = ${System.currentTimeMillis()-elapsedTime} Thread name = ${Thread.currentThread().name}")
seekToPositionAndStartCounter()
}
}
}
}
private fun cancelCounter() {
job?.cancel()
job = null
}
If you don't try this then think about this.
getCurrentPosition of player and if player reach end position which you want then run player.seekto(2) it loops the video in specific segment of video.
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: