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()
Related
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 am making a network repository that supports multiple data retrieval configs, therefore I want to separate those configs' logic into functions.
However, I have a config that fetches the data continuously at specified intervals. Everything is fine when I emit those values to the original Flow. But when I take the logic into another function and return another Flow through it, it stops caring about its coroutine scope. Even after the scope's cancelation, it keeps on fetching the data.
TLDR: Suspend function returning a flow runs forever when currentCoroutineContext is used to control its loop's termination.
What am I doing wrong here?
Here's the simplified version of my code:
Fragment calling the viewmodels function that basically calls the getData()
lifecycleScope.launch {
viewModel.getLatestDataList()
}
Repository
suspend fun getData(config: MyConfig): Flow<List<Data>>
{
return flow {
when (config)
{
CONTINUOUS ->
{
//It worked fine when fetchContinuously was ingrained to here and emitted directly to the current flow
//And now it keeps on running eternally
fetchContinuously().collect { updatedList ->
emit(updatedList)
}
}
}
}
}
//Note logic of this function is greatly reduced to keep the focus on the problem
private suspend fun fetchContinuously(): Flow<List<Data>>
{
return flow {
while (currentCoroutineContext().isActive)
{
val updatedList = fetchDataListOverNetwork().await()
if (updatedList != null)
{
emit(updatedList)
}
delay(refreshIntervalInMs)
}
Timber.i("Context is no longer active - terminating the continuous-fetch coroutine")
}
}
private suspend fun fetchDataListOverNetwork(): Deferred<List<Data>?> =
withContext(Dispatchers.IO) {
return#withContext async {
var list: List<Data>? = null
try
{
val response = apiService.getDataList().execute()
if (response.isSuccessful && response.body() != null)
{
list = response.body()!!.list
}
else
{
Timber.w("Failed to fetch data from the network database. Error body: ${response.errorBody()}, Response body: ${response.body()}")
}
}
catch (e: Exception)
{
Timber.w("Exception while trying to fetch data from the network database. Stacktrace: ${e.printStackTrace()}")
}
finally
{
return#async list
}
list //IDE is not smart enough to realize we are already returning no matter what inside of the finally block; therefore, this needs to stay here
}
}
I am not sure whether this is a solution to your problem, but you do not need to have a suspending function that returns a Flow. The lambda you are passing is a suspending function itself:
fun <T> flow(block: suspend FlowCollector<T>.() -> Unit): Flow<T> (source)
Here is an example of a flow that repeats a (GraphQl) query (simplified - without type parameters) I am using:
override fun query(query: Query,
updateIntervalMillis: Long): Flow<Result<T>> {
return flow {
// this ensures at least one query
val result: Result<T> = execute(query)
emit(result)
while (coroutineContext[Job]?.isActive == true && updateIntervalMillis > 0) {
delay(updateIntervalMillis)
val otherResult: Result<T> = execute(query)
emit(otherResult)
}
}
}
I'm not that good at Flow but I think the problem is that you are delaying only the getData() flow instead of delaying both of them.
Try adding this:
suspend fun getData(config: MyConfig): Flow<List<Data>>
{
return flow {
when (config)
{
CONTINUOUS ->
{
fetchContinuously().collect { updatedList ->
emit(updatedList)
delay(refreshIntervalInMs)
}
}
}
}
}
Take note of the delay(refreshIntervalInMs).
I've been mulling this over for some time now and I just can't get it to work.
So in brief, I have a Splash Activity from where I call another activity that contains my ViewModel. The ViewModel in simple terms just needs to sequentially run function A(which is getfbdata below; it is a network call.). And only after this function completes, it should run function B (which is dosavefbdata below; save info to DB.). Again, it should wait for function B to complete before running the main thread function, function C(which is confirm first below; it checks whether function B has completed by getting the result from function B (dosavefbdata below). If function C is positive, it closes the Splash activity.
Suffice to say, none of the above works. Println results show all functions were run sequentially without waiting for each to complete. Lastly, SplashActivity().killActivity() call on function C did not work.
Note: withContext does not require to await() on the suspended functions right? I also tried using viewModelScope.async instead of viewModelScope.launch.
I would really appreciate your help here. Thanks in advance.
*Under SplashActivity:
fun killActivity(){
finish()
}
*Under onCreate(SplashActivity):
CoroutingClassViewModel(myc).initialize()
**
class CoroutingClassViewModel(val myc: Context): ViewModel() {
fun initialize() {
viewModelScope.launch(Dispatchers.Main) {
try {
val fbdata = withContext(Dispatchers.IO) { getfbdata() }
val test1 = withContext(Dispatchers.IO) { test1(fbdata) }
val savedfbdata = withContext(Dispatchers.IO) { dosavefbdata(fbdata,myc) }
val confirmfirst = { confirmfunc(savedfbdata,myc) }
println("ran savedfbdata.")
} catch (exception: Exception) {
Log.d(TAG, "$exception handled !")
}
}
}
fun confirmfunc(savedfbdata: Boolean, myc: Context){
if (savedfbdata==true){
SplashActivity().killActivity()
}
}
suspend fun getfbdata(): MutableList<FirebaseClass> {
return withContext(Dispatchers.IO) {
//perform network call
return#withContext fbdata
}
}
suspend fun dosavefbdata(fbdata: MutableList<FirebaseClass>,myc: Context): Boolean{
return withContext(Dispatchers.IO) {
//save to database
return#withContext true
}
}
suspend fun test1(fbdata: MutableList<FirebaseClass>){
return withContext(Dispatchers.IO) {
println("test1: fbdata is: $fbdata")
}
}
}
Use AndroidViewModel if you want to have Context in it:
class CoroutingClassViewModel(myc: Application) : AndroidViewModel(myc) { ... }
In onCreate method of SplashActivity activity instantiate the view model like this:
val vm = ViewModelProvider(this)[CoroutingClassViewModel::class.java]
vm.initialize()
In CoroutingClassViewModel class create LiveData object to notify activity about operations completion:
val completion = MutableLiveData<Boolean>()
fun confirmfunc(savedfbdata: Boolean, myc: Context) {
if (savedfbdata) {
completion.postValue(true)
}
}
In your SplashActivity use this code to observe completion:
vm.completion.observe(this, Observer {
if (it) killActivity()
})
You use withContext(Dispatchers.IO) function two times for the same operation. Don't do that. For example in this code:
val fbdata = withContext(Dispatchers.IO) { getfbdata() }
if we look at getfbdata function we see that function withContext(Dispatchers.IO) is already called there. So get rid of repeated calls:
val fbdata = getfbdata()
I had same issue with withContext(Dispatcher.IO), I thought that switching coroutine context doesn't work, while in fact in splash screen i launched super long operation on Dispatcher.IO, then later when trying to use the same Dispatcher.IO it didn't work or in other words it waited until the first work in splash screen finished then started the new work.
I'm trying to use the Android MVVM pattern with a repository class and Retrofit for network calls. I have the common problem that I can't get the coroutine to wait for the network response to return.
This method is in my ViewModel class:
private fun loadConfigModel() {
val model = runBlocking {
withContext(Dispatchers.IO) {
configModelRepository.getConfigFile()
}
}
configModel.value = model
}
In ConfigModelRepository, I have this:
suspend fun getConfigFile(): ConfigModel {
val configString = prefs.getString(
ConfigViewModel.CONFIG_SHARED_PREF_KEY, "") ?: ""
return if (configString.isEmpty() || isCacheExpired()) {
runBlocking { fetchConfig() }
} else {
postFromLocalCache(configString)
}
}
private suspend fun fetchConfig(): ConfigModel {
return suspendCoroutine { cont ->
dataService
.config() // <-- LAST LINE CALLED
.enqueue(object : Callback<ConfigModel> {
override fun onResponse(call: Call<ConfigModel>, response: Response<ConfigModel>) {
if (response.isSuccessful) {
response.body()?.let {
saveConfigResponseInSharedPreferences(it)
cont.resume(it)
}
} else {
cont.resume(ConfigModel(listOf(), listOf()))
}
}
override fun onFailure(call: Call<ConfigModel>, t: Throwable) {
Timber.e(t, "config fetch failed")
cont.resume(ConfigModel(listOf(), listOf()))
}
})
}
}
My code runs as far as dataService.config(). It never enters onResponse or onFailure. The network call goes and and returns properly (I can see this using Charles), but the coroutine doesn't seem to be listening for the callback.
So, my question is the usual one. How can I get the coroutines to block such that they wait for this callback from Retrofit? Thanks.
The problem must be that response.body() returns null since that is the only case that is missing a call to cont.resume(). Make sure to call cont.resume() also in that case and your code should at least not get stuck.
But like CommonsWare points out, even better would be to upgrade to Retrofit 2.6.0 or later and use native suspend support instead of rolling your own suspendCoroutine logic.
You should also stop using runBlocking completely. In the first case, launch(Dispatchers.Main) a coroutine instead and move configModel.value = model inside of it. In the second case you can just remove runBlocking and call fetchConfig() directly.
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)
}
}