I have the MediaPlayer configured inside my ViewModel. All I want is a way to observe the mediaPlayer.currentPosition in a Composable. However, I cannot find a way to store it in a MutableState<T> value for Compose to observe. I just have a simple slider:
Slider(value = someMutableStateValue, onValueChange = { someMutableStateValue = it }
In my ViewModel, I declare var someMutableStateValue by mutableStateOf(0f)
Now, this app is being built for TV so I cannot provide touch input whatsoever. I don't think it would be required anyway, but I'm informing just in case.
All I want, is a method that will update the value of someMutableStateValue every time the MediaPlayer.currentPosition changes. Maybe there is some listener, but I can't find it as of now. I'm comfortable with using LiveData too, but I'd like to avoid that if possible.
PS: I actually do not even need to pass anything to the onValueChange listener, since it would never be called on a TV.
PS MArk II: Also, I cannot find out why the Slider take up all the width of my Screen? Is it the default implementation? Even hardcoding the width or for that matter, using the fillMaxWidth(...) Modifier with a restricted fraction doesn't seem to work.
It appears MediaPlayer doesn't offer a callback mechanism for the playback progress. I would guess this is because technically the progress would change with every frame but running a callback on every frame would impose a lot of CPU overhead, so they just omitted it alltogether. Instead, you need to resort to polling the progress. If you want to do this on the composable side, you could use LaunchedEffect:
var progress by remember { mutableStateOf(0f) }
LaunchedEffect(Unit){
while(isActive){
progress = mediaPlayer.currentPosition / mediaPlayer.duration
delay(200) // change this to what feels smooth without impacting performance too much
}
}
This is based on #Adriak K's answer above with a small improvement for ExoPlayer(v2.16.1) in case anyone else is looking into this:
var isPlaying by remember {
mutableStateOf(false)
}
isPlaying can be updated as below to keep track of player pause/resume state:
exoplayer.addListener(object : Player.Listener {
override fun onPlaybackStateChanged(playbackState: Int) {
super.onPlaybackStateChanged(playbackState)
isPlaying = playbackState == Player.STATE_READY && isPlaying()
}
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
super.onPlayWhenReadyChanged(playWhenReady, reason)
isPlaying = playWhenReady
}
})
LaunchedEffect(key1 = player, key2 = isPlaying) {
while (isActive && isPlaying) {
sliderPosition = (player.currentPosition + 0.0f) / player.duration
delay(200)
}
}
Related
My current Android Jetpack Compose application employs snapShotFlow to convert mutableStateOf() to flow and trigger user actions as follows
In ViewModel:-
var displayItemState by mutableStateOf(DisplayItemState())
#Immutable
data class DisplayItemState(
val viewIntent: Intent? = null
)
In composable:-
val displayItemState = viewModel.displayItemState
LaunchedEffect(key1 = displayItemState) {
snapshotFlow { displayItemState }
.distinctUntilChanged()
.filter { it.viewIntent != null }
.collectLatest { displayItemState ->
context.startActivity(displayItemState.viewIntent)
}
}
everything works as expected while I keep my test device in portrait or landscape.
However when I change the device orientation the last collected snapShotFlow value is resent.
If I reset the displayItemState as follows in the snapShotFlow this fixes the issue
however this feels like the wrong fix. What am i doing wrong? what is the correct approach to stop the snapShotFlow from re triggering on orientation change
val displayItemState = viewModel.displayItemState
LaunchedEffect(key1 = displayItemState) {
snapshotFlow { displayItemState }
.distinctUntilChanged()
.filter { it.viewIntent != null }
.collectLatest { displayItemState ->
context.startActivity(displayItemState.viewIntent)
viewModel.displayItemState = DisplayItemState()
}
}
That's intended behavior, you are not doing anything wrong. Compose's (Mutable)State holds the last value, similarly to StateFlow, so new collection from them always starts with the last value.
Your solution is ok, something very similar is actually recommended in Android's app architecture guide here:
For example, when showing transient messages on the screen to let the user know that something happened, the UI needs to notify the ViewModel to trigger another state update when the message has been shown on the screen.
Another possibility would be to use SharedFlow instead of MutableState in your viewModel - SharedFlow doesn't keep the last value so there won't be this problem.
I am a Java/Andriod programmer new to Kotlin and Jetpack Compose. I am creating a simple sound-board app with three buttons that will play a unique sound when pressed. All is going well, but I am struggling with creating the OnCompletionListener for the Mediaplayer instances (so I can release resources and change the button on the UI)
Within my button Composable I create the instance of the Mediaplayer
`val mediaPlayer:MediaPlayer by remember {
mutableStateOf(MediaPlayer.create(context,soundID))
}`
which works great in the OnClick of the Image composable:
Image (
painter = painterResource(id = (imageID)),
contentDescription = null,
contentScale = ContentScale.FillBounds,
modifier = Modifier
.size(250.dp)
.aspectRatio(16f / 9f)
.clip(RoundedCornerShape(cornerDiameter.dp))
.border(
BorderStroke(4.dp, Color.Yellow),
RoundedCornerShape(cornerDiameter.dp)
)
.clickable(
enabled = true,
onClick = {
if (isPlaying) {
println("STOPPING player")
mediaPlayer.pause()
isPlaying = false
} else {
println("starting player")
mediaPlayer.start()
isPlaying = true
}
}
)
)
When the respective audio is done playing I want to call a routine to clean up and update the UI. When I create the onCompletionLIstener I send it the instance of the MediaPlayer:
val onCompletionListener =
MediaPlayer.OnCompletionListener(trackDone(mediaPlayer))
mediaPlayer.setOnCompletionListener(onCompletionListener)
which expects the function trackDone to be (MediaPlayer!) → Unit, which it auto creates:
fun trackDone(mediaPlayer: MediaPlayer): (MediaPlayer) -> Unit {
if(mediaPlayer != null)
{
mediaPlayer!!.stop()
mediaPlayer!!.release()
}
}
However, I now get an error for trackDone saying "A 'return' expression required in a function with a block body ('{...}')". But I can't figure out what type of return I can provide to satisfy this. Returning mediaPlayer does not work.
Any help is appreciated. I hope I have given enough information.
The compiler expects trackDone to return a lambda that accepts a MediaPlayer and returns nothing. I'm not sure what you want to accomplish with this function signature but the code below will fix the compile error.
fun trackDone(mediaPlayer: MediaPlayer): (MediaPlayer) -> Unit {
if(mediaPlayer != null)
{
mediaPlayer!!.stop()
mediaPlayer!!.release()
}
return {}
}
I assume this is not exactly your intention and instead you want some callback from this function.
First, please clean it up, you don't need to check for nullability and you don't need to shout !! in your code, because the function already expects a non-nullable MediaPlayer argument.
Next, simply add an additional function type parameter that will be used as a callback inside trackDone.
Putting them all together, your trackDone function should look like this
fun trackDone(mediaPlayer: MediaPlayer, onComplete : (MediaPlayer) -> Unit) {
// remove unnecessary nullability checking
mediaPlayer.stop()
mediaPlayer.release()
// on complete callback
onComplete(mediaPlayer)
}
or if you don't want to return the MediaPlayer instance then this,
fun trackDone(mediaPlayer: MediaPlayer, onComplete : () -> Unit) {
// remove unnecessary nullability checking
mediaPlayer.stop()
mediaPlayer.release()
// on complete callback
onComplete()
}
and this is how it would be used 'ideally'.
fun someFunction() {
val mediaPlayer: MediaPlayer
...
...
...
trackDone(mediaPlayer) { stoppedMediaPlayer ->
// DONE
}
// or
trackDone(mediaPlayer) {
// DONE
}
}
Now I have no idea if this would "work" for a "completeListener" callback like you want to achieve, but the "fix" will surely compile.
In an Android project, we are currently trying to switch from LiveData to StateFlow in our viewmodels. But for some rare cases, we need to update our state without notifying the collectors about the change. It might sound weird when we think of the working mechanism of flows, but I want to learn if it's a doable thing or not. Any real solution or workaround would be appreciated.
If you don't need to react to the true state anywhere, but only the publicly emitted state, I would store the true state in a property directly instead of a MutableStateFlow.
private var trueState: MyState = MyState(someDefault)
private val _publicState = MutableStateFlow<MyState>()
val publicstate = _publicState.asStateFlow()
fun updateState(newState: MyState, shouldEmitPublicly: Boolean) {
trueState = newState
if (shouldEmitPublicly) {
_publicState.value = newState
}
}
If you do need to react to it, one alternative to a wrapper class and filtering (#broot's solution) would be to simply keep two separate StateFlows.
Instead of exposing the state flow directly, we can expose another flow that filters the items according to our needs.
For example, we can keep the shouldEmit flag inside emitted items. Or use any other filtering logic:
suspend fun main(): Unit = coroutineScope {
launch {
stateFlow.collect {
println("Collected: $it")
}
}
delay(100)
setState(1)
delay(100)
setState(2)
delay(100)
setState(3, shouldEmit = false)
delay(100)
setState(4)
delay(100)
setState(5)
delay(100)
}
private val _stateFlow = MutableStateFlow(EmittableValue(0))
val stateFlow = _stateFlow.filter { it.shouldEmit }
.map { it.value }
fun setState(value: Int, shouldEmit: Boolean = true) {
_stateFlow.value = EmittableValue(value, shouldEmit)
}
private data class EmittableValue<T>(
val value: T,
val shouldEmit: Boolean = true
)
We can also keep the shouldEmit flag in the object and switch it on/off to temporarily disable emissions.
If you need to expose StateFlow and not just Flow, this should also be possible, but you need to decide if ignored emissions should affect its value or not.
Suppose I have some data that I need to transfer to the UI, and the data should be emitted with a certain delay, so I have a Flow in my ViewModel:
val myFlow = flow {
listOfSomeData.forEachIndexed { index, data ->
//....
emit(data.UIdata)
delay(data.requiredDelay)
}
}
Somewhere in the UI flow is collected and displayed:
#Composable
fun MyUI(viewModel: ViewModel) {
val data by viewModel.myFlow.collectAsState(INITIAL_DATA)
//....
}
Now I want the user to be able to pause/resume emission by pressing some button. How can i do this?
The only thing I could come up with is an infinite loop inside Flow builder:
val pause = mutableStateOf(false)
//....
val myFlow = flow {
listOfSomeData.forEachIndexed { index, data ->
emit(data.UIdata)
delay(data.requiredDelay)
while (pause.value) { delay(100) } //looks ugly
}
}
Is there any other more appropriate way?
You can tidy up your approach by using a flow to hold pause value then collect it:
val pause = MutableStateFlow(false)
//....
val myFlow = flow {
listOfSomeData.forEachIndexed { index, data ->
emit(data.UIdata)
delay(data.requiredDelay)
if (pause.value) pause.first { isPaused -> !isPaused } // suspends
}
}
Do you need mutableStateOf for compose? Maybe you can transform it into a flow but I'm not aware how it looks bc I don't use compose.
A bit of a creative rant below:
I actually was wondering about this and looking for more flexible approach - ideally source flow should suspend during emit. I noticed that it can be done when using buffered flow with BufferOverflow.SUSPEND so I started fiddling with it.
I came up with something like this that lets me suspend any producer:
// assume source flow can't be accessed
val sourceFlow = flow {
listOfSomeData.forEachIndexed { index, data ->
emit(data.UIdata)
delay(data.requiredDelay)
}
}
val pause = MutableStateFlow(false)
val myFlow = sourceFlow
.buffer(Channel.RENDEZVOUS, BufferOverflow.SUSPEND)
.transform {
if (pause.value) pause.first { isPaused -> !isPaused }
emit(it)
}
.buffer()
It does seem like a small hack to me and there's a downside that source flow will still get to the next emit call after pausing so: n value gets suspended inside transform but source gets suspended on n+1.
If anyone has better idea on how to suspend source flow "immediately" I'd be happy to hear it.
If you don't need a specific delay you can use flow.filter{pause.value != true}
I need to check when a certain LazyColumn item comes into view, and once it does, make a callback to onItemWithKeyViewed() only once to notify that this item has been viewed.
My attempt:
#Composable
fun SpecialList(
someItems: List<Things>,
onItemWithKeyViewed: () -> Unit
) {
val lazyListState = rememberLazyListState()
if (lazyListState.isScrollInProgress) {
val isItemWithKeyInView = lazyListState.layoutInfo
.visibleItemsInfo
.any { it.key == "specialKey" }
if (isItemWithKeyInView) {
onItemWithKeyViewed()
}
}
LazyColumn(
state = lazyListState
) {
items(items = someItems) { itemData ->
ComposableOfItem(itemData)
}
item(key = "specialKey") {
SomeOtherComposable()
}
}
}
Issue with my method is I notice the list scrolling performance degrades badly and loses frames. I realize this may be because it's checking all visible item keys on every frame?
Also, onItemWithKeyViewed() is currently being called multiple times instead of just the first time it's viewed.
Is there a more efficient way to make a single callback to onItemWithKeyViewed() only the first time "specialKey" item is viewed?
In such cases, when you have a state that is updated often, you should use derivedStateOf: this will cause recomposition only when the result of the calculation actually changes.
You should not call side effects (which is calling onItemWithKeyViewed) directly in the composable builder. You should use one of the special side-effect functions instead, usually LaunchedEffect - this ensures that the action is not repeated. You can find more information on this topic in Thinking in Compose and in side-effects documentation.
val isItemWithKeyInView by remember {
derivedStateOf {
lazyListState.isScrollInProgress &&
lazyListState.layoutInfo
.visibleItemsInfo
.any { it.key == "specialKey" }
}
}
if (isItemWithKeyInView) {
LaunchedEffect(Unit) {
onItemWithKeyViewed()
}
}