Creating OnCompletionListener in Compose - android

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.

Related

The best solution for the main thread to obtain data in the coroutine to avoid null values?

I have the following code, in the click event I need to use the class that is queried from the database.
data class Work(...)
fun Compose(){
var work1: Work? = null
var work2: Work? = null
var work3: Work? = null
LaunchedEffect(true){
CoroutineScope(IO).launch {
work1 = workViewModel.getById(1)
work2 = workViewModel.getById(2)
work3 = workViewModel.getById(3)
}
}
Card(
modifier = Modifier
.clickable(onClick = {
val url = "https://www.google.com/"
when{
url.contains(work1?.baseUrl) -> {...}
url.contains(work2?.baseUrl) -> {...}
url.contains(work3?.baseUrl) -> {...}
}
})
){}
}
this creates a problem, work3?.baseUrl found String? type Required CharSequence type.
So far it seems that only the !! operator can successfully run this code. But this code is based on a database query, using the !! operator is very risky.
And if you add a null operator before this, also not working.
requireNotNull(work1)
when{
url.contains(work1.baseUrl) -> {...}
}
Smart cast to 'Work' is impossible, because 'work1' is a local variable that is captured by a changing closure
Can you tell me what is the best solution?
I suggest not having that logic in the Composable. Try to move that to a function in the ViewModel, something like:
private val url = "https://www.google.com/"
fun validateBaseUrl() {
viewModelScope.launch(Dispatchers.IO) {
workViewModel.getById(1)?.let {
if (url.contains(it)) { .. }
}
work2 = workViewModel.getById(2)?.let {
if (url.contains(it)) { .. }
}
work3 = workViewModel.getById(3)?.let {
if (url.contains(it)) { .. }
}
}
}
And then in the Composable would be something like:
fun Compose() {
Card(
modifier = Modifier
.clickable(onClick = { viewModel.validateBaseUrl() })
){}
}
Remember to use the State Hoisting instead of sending the view model through the Composables.
Finally, you would need to send back a State to the Composable, either using a LiveData or a StateFlow.
You need to create a scope for the work properties :
work1?.run { url.contains(baseUrl) } == true -> {...}
Within the run lambda the accessed object is immutable even if the work properties themselves are mutable.
The == true is needed because the left side of the comparison operator can be either a Boolean or null.
You could also define an extension function like this:
fun Work?.isContainedIn(url: String) = this?.run { url.contains(baseUrl) } == true
and then just do:
work1.isContainedIn(url) -> { }
work2.isContainedIn(url) -> { }
work3.isContainedIn(url) -> { }

How to handle callbacks in Jetpack compose?

I am migrating my multiple activity app to single activity app.
In the activity I am observing a live data from view model. When the observable triggers, I start a payment activity from a third party SDK as shown below.
onCreate() {
viewmodel.orderCreation.observe {
thirdpartysdk.startPaymentWithThisOrder(context)
}
}
onActivityResult() {
// use payment result
}
As I will be using a Composable now,
#Composable
fun PaymentScreen(onOrderCreated: () -> Unit) {
val orderCreation by viewmodel.orderCreation.observeAsState()
// How to use order creation once here to call onOrderCreated here only once as composable is called again and again
}
Here's my suggestion:
In your viewmodel, create a function to reset your orderCreation. And another field + function to store the payment result.
Something like:
fun resetOrderCreation() {
_orderCreation.value = null
}
fun paymentResult(value: SomeType) {
_paymentResult.value = value
}
Now, in your composable, you can do the following:
#Composable
fun PaymentScreen(onOrderCreated: () -> Unit) {
// 1
val orderCreation by viewmodel.orderCreation.observeAsState()
var paymentResult by viewmodel.paymentResult.observeAsState()
// 2
val launcher = rememberLauncherForActivityResult(
PaymentActivityResultContract()
) { result ->
viewModel.paymentResult(result)
}
...
// 3
LaunchedEffect(orderCreation) {
if (orderCreation != null) {
launcher.launch()
viewModel.resetOrderCreation()
}
}
// 4
if (paymentStatus != null) {
// Show some UI showing the payment status
}
}
Explaining the code:
I'm assuming that you're using LiveData. But I really suggest you move to StateFlow instead. See more here.
You will probably need to write a ActivityResultContact to your third party lib. I wrote a post about (it's in Portuguese, but I think you can get the idea translating it to English).
As soon the orderCreation has changed, the LaunchedEffect block will run, then you can start the third party activity using launcher.launch() (the parameters for this call are defined in your ActivityResultContract).
Finally, when the payment status changed, you can show something different to the user.

How to pause emitting Flow in Kotlin?

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}

Hook Compose Slider to MediaPlayer

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

conditional navigation in compose, without click

I am working on a compose screen, where on application open, i redirect user to profile page. And if profile is complete, then redirect to user list page.
my code is like below
#Composable
fun UserProfile(navigateToProviderList: () -> Unit) {
val viewModel: MainActivityViewModel = viewModel()
if(viewModel.userProfileComplete == true) {
navigateToProviderList()
return
}
else {
//compose elements here
}
}
but the app is blinking and when logged, i can see its calling the above redirect condition again and again. when going through doc, its mentioned that we should navigate only through callbacks. How do i handle this condition here? i don't have onCLick condition here.
Content of composable function can be called many times.
If you need to do some action inside composable, you need to use side effects
In this case LaunchedEffect should work:
LaunchedEffect(viewModel.userProfileComplete == true) {
if(viewModel.userProfileComplete == true) {
navigateToProviderList()
}
}
In the key(first argument of LaunchedEffect) you need to specify some key. Each time this key changes since the last recomposition, the inner code will be called. You may put Unit there, in this case it'll only be called once, when the view appears at the first place
The LaunchedEffect did not work for me since I wanted to use it in UI thread but it wasn't for some reason :/
However, I made this for my self:
#Composable
fun <T> SelfDestructEvent(liveData: LiveData<T>, onEvent: (argument: T) -> Unit) {
val previousState = remember { mutableStateOf(false) }
val state by liveData.observeAsState(null)
if (state != null && !previousState.value) {
previousState.value = true
onEvent.invoke(state!!)
}
}
and you use it like this in any other composables:
SingleEvent(viewModel.someLiveData) {
//your action with that data, whenever it was triggered, but only once
}

Categories

Resources