I'm trying to create a Flow that needs to emit values from a callback but I can't call the emit function since the SAM is a normal function
Here's the class with the SAM from a library that I can't really modify it the way I need it to be.
class ValueClass {
fun registerListener(listener: Listener) {
...
}
interface Listener {
fun onNewValue(): String
}
}
And here's my take on creating the Flow object
class MyClass(private val valueClass: ValueClass) {
fun listenToValue = flow<String> {
valueClass.registerListener { value ->
emit(value) // Suspension functions can only be called on coroutine body
}
}
}
I guess it would've been simple if I could change the ValueClass but in this case, I can't. I've been wrapping my head around this and trying to look for implementations.
At least from what I know so far, one solution would be to use GlobalScope like this
class MyClass(private val valueClass: ValueClass) {
fun listenToValue = flow<String> {
valueClass.registerListener { value ->
GlobalScope.launch {
emit(value)
}
}
}
}
Now, this works but I don't want to use GlobalScope since I'll be using viewModelScope to tie it to my app's lifecycle.
Is there any way to work around this?
Thanks in advance. Any help would be greatly appreciated!
You can use callbackFlow to create a Flow from the callback. It will look something like:
fun listenToValue(): Flow<String> = callbackFlow {
valueClass.registerListener { value ->
trySend(value)
channel.close() // close channel if no more values are expected
}
awaitClose { /*unregister listener*/ }
}
Or if only one value is expected from the callback, you can use suspendCoroutine or suspendCancellableCoroutine. It this case listenToValue() function must be suspend and later called from a coroutine(e.g. someScope.launch):
suspend fun listenToValue(): String = suspendCoroutine { continuation ->
valueClass.registerListener { value ->
continuation.resumeWith(value)
}
}
Related
I have method, which returns response from server. For example:
fun uploadVideo(link: String, completionHandler: (Result<String>) -> Unit) {
// some action
completionHandler(Result.success(""))
}
I want to call this method one by one. Wait for a response from the previous one to call the next one. For example
uploadVideo("https://stackoverflow.com/video1.mp4") {
}
// call this only when i have response from preview request
uploadVideo("https://stackoverflow.com/video2.mp4") {
}
// call this only when i have response from preview request
uploadVideo("https://stackoverflow.com/video3.mp4") {
}
I tried use suspendCancellableCoroutine, like this
suspend fun uploadVideo(link: String?): String? = suspendCancellableCoroutine { cont ->
uri?.let {
uploadVideo(link,
completionHandler = {
it.onSuccess { uri ->
cont.resume(uri.toString())
}.onFailure {
cont.resumeWithException(it)
}
}
)
} ?: kotlin.run {
cont.resume(null)
}
}
and then call like this:
uploadVideo("https://stackoverflow.com/video1.mp4")
uploadVideo("https://stackoverflow.com/video2.mp4")
uploadVideo("https://stackoverflow.com/video3.mp4")
but these methods are not called sequentially, but in parallel
Note, the contents of your example API function don't quite make sense. If the callback were simply called inside the body of the function, then that would mean the function was blocking the whole time, which would mean there would be no reason for it to even have a callback. It could just directly return the value.
The actual contents of the API function might look more like this:
fun uploadVideo(link: String, completionHandler: (Result<String>) -> Unit) {
val callbackHandler = Handler(Looper.myLooper())
someOtherHandlerOrThreadPool.run {
// some action
callbackHandler.post {
completionHandler(Result.success(""))
}
}
}
The reason I bring that up is that the alternative to nesting a bunch of callbacks is use suspend functions and coroutines, but the code to convert the above to a suspend function doesn't make sense if it were a blocking function like in your version of it.
The basic pattern to convert a callback-based function into a suspend function is to use suspendCoroutine or suspendCancellableCoroutine. If uploadVideo was a function in some api class, you can define it as an extension function:
suspend fun SomeApiClass.uploadVideo(link: String): Result<String> = withContext(Dispatchers.Main) {
suspendCoroutine { cont ->
uploadVideo(link) { cont.resume(it) }
}
}
Now you can call this suspend function repeatedly in sequence if you're inside a coroutine or another suspend function:
fun foo() {
viewModelScope.launch {
val result1 = uploadVideo("https://stackoverflow.com/video1.mp4")
val result2 = uploadVideo("https://stackoverflow.com/video2.mp4")
val result3 = uploadVideo("https://stackoverflow.com/video3.mp4")
}
}
You could try this. this waits till the previous method called its callback and then runs the next one. Only if you have many images this is not a really nice way to do this.
fun uploadVideo(link: String, completionHandler: () -> Unit) {
// some action
completionHandler()
}
uploadVideo("https://stackoverflow.com/video1.mp4") {
uploadVideo("https://stackoverflow.com/video2.mp4") {
uploadVideo("https://stackoverflow.com/video3.mp4") {}
}
}
Fragment
private fun makeApiRequest() {
vm.getRandomPicture()
var pictureElement = vm.setRandomPicture()
GlobalScope.launch(Dispatchers.Main) {
// what about internet
if (pictureElement != null && pictureElement!!.fileSizeBytes!! < 400000) {
Glide.with(requireContext()).load(pictureElement!!.url)
.into(layout.ivRandomPicture)
layout.ivRandomPicture.visibility = View.VISIBLE
} else {
getRandomPicture()
}
}
}
viewmodel
fun getRandomPicture() {
viewModelScope.launch {
getRandomPictureItemUseCase.build(Unit).collect {
pictureElement.value = it
Log.d("inspirationquotes", "VIEWMODEL $pictureElement")
Log.d("inspirationquotes", "VIEWMODEL VALUE ${pictureElement.value}")
}
}
}
fun setRandomPicture(): InspirationQuotesDetailsResponse? {
return pictureElement.value
}
Flow UseCase
class GetRandomPictureItemUseCase #Inject constructor(private val api: InspirationQuotesApi): BaseFlowUseCase<Unit, InspirationQuotesDetailsResponse>() {
override fun create(params: Unit): Flow<InspirationQuotesDetailsResponse> {
return flow{
emit(api.getRandomPicture())
}
}
}
My flow task from viewmodel doesn't goes on time. I do not know how to achieve smooth downloading data from Api and provide it further.
I was reading I could use runBlocking, but it is not recommended in production as well.
What do you use in your professional applications to achieve nice app?
Now the effect is that that image doesn't load or I have null error beacause of my Log.d before GlobalScope in Fragment (it is not in code right now).
One more thing is definding null object I do not like it, what do you think?
var pictureElement = MutableStateFlow<InspirationQuotesDetailsResponse?>(null)
EDIT:
Viewmodel
val randomPicture: Flow<InspirationQuotesDetailsResponse> = getRandomPictureItemUseCase.build(Unit)
fragment
private fun makeApiRequest() = lifecycleScope.launch {
vm.randomPicture
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect { response ->
if (response.fileSizeBytes < 600000) {
Log.d("fragment", "itGetsValue")
Glide.with(requireContext()).load(response.url)
.into(layout.ivRandomPicture)
layout.ivRandomPicture.visibility = View.VISIBLE
} else {
onFloatingActionClick()
}
}
}
Edit2 problem on production, another topic:
Link -> What is the substitute for runBlocking Coroutines in fragments and activities?
First of all, don't use GlobalScope to launch a coroutine, it is highly discouraged and prone to bugs. Use provided lifecycleScope in Fragment:
lifecycleScope.launch {...}
Use MutableSharedFlow instead of MutableStateFlow, MutableSharedFlow doesn't require initial value, and you can get rid of nullable generic type:
val pictureElement = MutableSharedFlow<InspirationQuotesDetailsResponse>()
But I guess we can get rid of it.
Method create() in GetRandomPictureItemUseCase returns a Flow that emits only one value, does it really need to be Flow, or it can be just a simple suspend function?
Assuming we stick to Flow in GetRandomPictureItemUseCase class, ViewModel can look something like the following:
val randomPicture: Flow<InspirationQuotesDetailsResponse> = getRandomPictureItemUseCase.build(Unit)
And in the Fragment:
private fun makeApiRequest() = lifecycleScope.launch {
vm.randomPicture
.flowWithLifecycle(lifecycle, State.STARTED)
.collect { response ->
// .. use response
}
}
Dependency to use lifecycleScope:
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0'
I have a suspendCoroutine in my repository with which I want to send data back to my ViewModel -
suspend fun sendPasswordResetMail(emailId: String): Boolean {
return withContext(Dispatchers.IO) {
suspendCoroutine { cont ->
firebaseAuth?.sendPasswordResetEmail(emailId)
?.addOnCompleteListener {
cont.resume(it.isSuccessful)
}
?.addOnFailureListener {
cont.resumeWithException(it)
}
}
}
}
However, neither of the listeners are called. Debugger says no executable code found at line where 'cont.resume(it.isSuccessful)' or 'cont.resumeWithException(it)' are.
I tried 'Dispatchers.IO', 'Dispatchers.Main' and 'Dispatchers.Default' but none of them seem to work. What could I be doing wrong?
My ViewModel code -
isEmailSent : LiveData<Boolean> = liveData {
emit(firebaseAuthRepo.sendPasswordResetMail(emailId))
}
and
fragment -
viewModel.isEmailSent.observe(viewLifecycleOwner, { flag ->
onResetMailSent(flag)
})
I believe you are calling
isEmailSent : LiveData<Boolean> = liveData {
emit(firebaseAuthRepo.sendPasswordResetMail(emailId))
}
this piece of code everytime for sending email
and
viewModel.isEmailSent.observe(viewLifecycleOwner, { flag ->
onResetMailSent(flag)
})
this piece only once.
Assuming that's true what you are essentially observing is the initial live data that was created with the model while it is being replaced everytime when resent is called. Instead call
isEmailSent.postValue(firebaseAuthRepo.sendPasswordResetMail(emailId))
from inside of a coroutine.
Also for the debugger not showing anything try adding a log above the cont.resume call and cont.resumeWithException call since it has worked for me in the past.
I think the easier way to achieve this is by using firebase-ktx and the await() function (which does what you are trying under the hood):
suspend fun sendPasswordResetMail(emailId: String): Boolean {
try {
firebaseAuth?.sendPasswordResetEmail(emailId).await()
return true
} catch(e: Exception) {
return false
}
}
Another way would be to use flow:
suspend fun sendPasswordResetMail(emailId: String): Boolean = flow<Boolean {
firebaseAuth?.sendPasswordResetEmail(emailId).await()
emit(true)
}.catch { e: Exception -> handleException(e) }
You could then observe this in your fragment by putting the code inside your viewmodel and calling .asLiveData()
I want to call a suspend function within an apply { } block.
I have a:
private suspend fun retrieve(accountAction: AccountAction): Any
suspend fun login() {
accountEvent.apply {
retrieve(it)
}
I tried to surround it with suspend { retrieve(it) } runblocking { retrieve(it) } but it seems that even if it’s not generating an error (Suspension functions can be called only within coroutine body) the code is not getting inside the retrieve function, but just passes through it and that’s why my unit tests fails.
FYI: this is a class, not an activity or a fragment.
Edit:
This is the actual code (from comment):
override suspend fun login(webView: WebView) = trackingId()
.flatMap { id -> AccountAction(client, id, WeakReference(webView), upgradeAccount) }
.map {
it.apply {
upgradeWebViewProgress(webView)
suspend { retrieve(it) }
}
}
.flatMap { updateAuth(it) }
You can use the Flow-API when you want to do asynchronous (suspend) operations on a list of elements like this. You can read about that API here: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/
Probably the simplest way to get your example working is by converting your list to a Flow, performing the suspending operations, then converting back to a List. Like this:
override suspend fun login(webView: WebView) = trackingId()
.flatMap { id -> AccountAction(client, id, WeakReference(webView), upgradeAccount) }
.asFlow()
.map {
it.apply {
upgradeWebViewProgress(webView)
retrieve(it)
}
}
.toList()
.flatMap { updateAuth(it) }
Note that this might not be the most efficient, because it will perform the retrieve-operations sequentially. You can use other operators on Flow to perform the operations in parallel for example.
Edited:
This shows an alternative without using map as it is not really required in my opionion for this example (except you really wanna chain all your calls)
suspend fun login(webView: WebView) {
val result = trackingId().flatMap { id -> AccountAction(client, id, WeakReference(webView), upgradeAccount) }
upgradeWebViewProgress(webView)
return retrieve(result).flatMap { updateAuth(it) } }
I need to call a suspending function inside a suspendCoroutine block, before I call continuation.resume().
What is the appropriate way of doing that?
private suspend fun someFunction() = suspendCoroutine { cont ->
//...
val myResult = mySuspendingFunction() //<--- The IDE says "Suspension functions can be called only within coroutine body"
cont.resume(myResult)
}
You can't call a suspend function in suspendCoroutine block, because it accepts non suspend block as parameter:
suspend inline fun <T> suspendCoroutine(
crossinline block: (Continuation<T>) -> Unit
): T
'suspendCoroutine' mainly used when we have some legacy code with callbacks, e.g.:
suspend fun getUser(id: String): User = suspendCoroutine { continuation ->
Api.getUser(id) { user ->
continuation.resume(user)
}
}
If function someFunction() doesn't call Api with callbacks then you should reconsider your approach getting rid of 'suspendCoroutine':
private suspend fun someFunction() {
// ...
val myResult = mySuspendingFunction()
// ...
}
If you still want to use suspendCoroutine move call of mySuspendingFunction out of suspendCoroutine block:
private suspend fun someFunction(): String {
val myResult = mySuspendingFunction()
return suspendCoroutine { cont ->
//...
cont.resume(myResult)
}
}
suspend fun mySuspendingFunction(): String {
delay(1000) // simulate request
return "result"
}
It's best to avoid this and call the suspending function before suspendCoroutine, as others have answered. That is possible for the specific case in question.
However, that is not possible if you need the continuation.
(The following is for those, who found this question for the this reason, as #Zordid and I have. chan.send is an example of this.)
In which case, the following is a possible, but error prone way to do it, that I do not recommend:
suspend fun cont1() {
//btw. for correct implementation, this should most likely be at least suspendCancellableCoroutine
suspendCoroutine<Unit> { uCont ->
val x = suspend { chan.send(foo(uCont)) }
x.startCoroutine(Continuation(uCont.context) {
if (it.isFailure)
uCont.resumeWith(it)
// else resumed by whatever reads from chan
})
}
}
(I think the error handling alone illustrates why it's not a great option, despite other problems.)
A better, safer and cheaper way is to use CompletableDeferred if you can.
If you must pass in a Continuation, it's still safer and probably cheaper to do:
suspend fun cont2() {
val rslt = CompletableDeferred<Unit>()
chan.send(foo(Continuation(currentCoroutineContext()) {
rslt.completeWith(it)
}))
rslt.await()
}