Does offer make the coroutines close or cancelled? - android

I was looking at the flow documentation on the Android Developer site and I have a question.
https://developer.android.com/kotlin/flow#callback
If you look at the above link, you will see code like this.
class FirestoreUserEventsDataSource(
private val firestore: FirebaseFirestore
) {
// Method to get user events from the Firestore database
fun getUserEvents(): Flow<UserEvents> = callbackFlow {
// Reference to use in Firestore
var eventsCollection: CollectionReference? = null
try {
eventsCollection = FirebaseFirestore.getInstance()
.collection("collection")
.document("app")
} catch (e: Throwable) {
// If Firebase cannot be initialized, close the stream of data
// flow consumers will stop collecting and the coroutine will resume
close(e)
}
// Registers callback to firestore, which will be called on new events
val subscription = eventsCollection?.addSnapshotListener { snapshot, _ ->
if (snapshot == null) { return#addSnapshotListener }
// Sends events to the flow! Consumers will get the new events
try {
offer(snapshot.getEvents())
} catch (e: Throwable) {
// Event couldn't be sent to the flow
}
}
// The callback inside awaitClose will be executed when the flow is
// either closed or cancelled.
// In this case, remove the callback from Firestore
awaitClose { subscription?.remove() }
}
}
In the code above, awaitClose is explained to be executed when the coroutine is closed or cancelled.
But, there is no close() in the code except for the try-catch statement that initializes the eventsCollection.
Additionally, says offer does not add the element to the channel and **returns false** immediately at the bottom of the Android Developer page.
My question is, in the code above, when offer(snapshot.getEvents()) is executed, does the coroutine cancel with return false, so awaitClose is executed?

Expectation:
As the documentation says:
When you try to add a new element to a full channel, send suspends the
producer until there's space for the new element, whereas offer does
not add the element to the channel and returns false immediately.
Ergo:
It Immediately adds the specified element to this channel, if this doesn’t violate its capacity restrictions, and returns the successful result. Otherwise, returns failed or closed result. This is synchronous variant of send, which backs off in situations when send suspends or throws.
So when trySend call returns a non-successful result, it guarantees that the element was not delivered to the consumer, and it does not call onUndeliveredElement that was installed for this channel. See “Undelivered elements” section in Channel documentation for details on handling undelivered elements.
Conclusion:
A typical usage for onDeliveredElement is to close a resource that is being transferred via the channel. The following code pattern guarantees that opened resources are closed even if producer, consumer, and/or channel are cancelled. Resources are never lost. So no it doesn't return false.

Related

How to return values only after observing LiveData in Android [Kotlin]

I have been facing this issue for quite sometime and would like to know a better approach to solve this problem. If you are aware of anything about how to solve it then please let me know.
I am building a project which takes data from an API and then following MVVM architecture I take the Retrofit instance to Repository, and then to ViewModel and further observe it from my fragment.
Now what I am working on currently is Login feature. I will send a number to the API and in response I will receive if the number is registered or not. If it is registered then I would move to the next screen.
Now the problem is that using one of the function in ViewModel I send that number to the API to get the response. And using a variable I observe that response.
Now I create a function which checks if the response was true or false and based on that I am using the logic to move to the next screen, but the issue is the returned value from the function. As LiveData works asynchronously in background it takes some time to return the value and in meantime the function returns the initial value which is false.
Function to verify response
private fun checkNumber(): Boolean {
var valid = false
authRiderViewModel.response.observe(viewLifecycleOwner, Observer {
Timber.d("Response: $it")
if (it.success == true) {
valid = true
}
})
Timber.d("Boolean: $valid")
return valid
}
Moving to next screen code:
binding.btnContinue.setOnClickListener {
val number = binding.etMobileNumber.text.toString().toLong()
Timber.d("Number: $number")
authRiderViewModel.authDriver(number)
if (checkNumber()) {
val action = LoginFragmentDirections.actionLoginFragmentToOtpFragment()
findNavController().navigate(action)
} else {
Toast.makeText(requireContext(), "Number not registered", Toast.LENGTH_SHORT).show()
}
}
So in case I received the true response from the server even then I would not move to the next screen because the initial value I received is false. I have spent few hours trying to fix it and any help would be appreciated. If you need any code let me know in comments. Thanks.
You have four distinct states:
The server returned a positive response
The server returned a negative response
The server failed (e.g., returned a 500, timed out)
You are waiting on the server
You are attempting to model that with two states: true and false. This will not work.
Instead, model it with four states. One approach is called "loading-content-error" and uses a sealed class to represent those states:
sealed class LoginState {
object Loading : LoginState()
data class Content(val isSuccess: Boolean) : LoginState()
object Error : LoginState()
}
Your LiveData (or your StateFlow, once you migrate to coroutines) would be a LiveData<LoginState>. Your observer can then use a when to handle Loading, Content, and Error as needed, such as:
For Loading, display a progress indicator
For Content, do whatever you are doing now with your boolean
For Error, display an error message
Actually, live data observation is an asynchronous operation. You have to code accordingly.
Just calling checkNumber() won't return since is asynchronous instead I give you some ideas to implement in a better way.
Just call the checkNumber when button click inside the check number do this instead of return valid
authRiderViewModel.response.observe(viewLifecycleOwner, Observer {
Timber.d("Response: $it")
if (it.success == true) {
val action = LoginFragmentDirections.actionLoginFragmentToOtpFragment()
findNavController().navigate(action)
} else {
Toast.makeText(requireContext(), "Number not registered", Toast.LENGTH_SHORT).show()
}
})

Suspending a Kotlin coroutine until a flow has a specific value

I am currently playing around with Kotlin coroutines and flows. In my scenario, a MutableStateFlow represents a connection state (CONNECTING, CONNECTED, CLOSING, CLOSED). It is also possible to login, logout and login again.
For further use of the connection, I have to check the state and wait until it is CONNECTED. If it is already CONNECTED, I can continue. If not, I have to wait until the state reaches CONNECTED. The connect() call does return immediately, the result is propagated via a callback that updates the MutableStateFlow. My current idea is to do the following:
connect()
if (connectionState.value != State.CONNECTED) { // connectionState = MutableStateFlow(State.CLOSED)
suspendCoroutine<Boolean> { continuation ->
scope.launch { // scope = MainScope()
connectionState.collect {
if (it == State.CONNECTED) {
continuation.resume(true)
}
}
}
}
}
// continue
As I am fairly new to the topic, I don't know if this is good practice and I was also not able to find a more suitable concept in the Kotlin documenation. Is there some better way of doing it?
A while back I had the same question:
It is preferred to use first() to suspend till the predicate is matched.
if (connectionState.value != State.CONNECTED) {
connectionState.first { it == State.CONNECTED }
}

How to remember state with retry operators in RxJava2

I have a network client that is able to resume from interruptions, but needs the last message for doing so when there is a retry.
Example in Kotlin:
fun requestOrResume(last: Message? = null): Flowable<Message> =
Flowable.create({ emitter ->
val connection = if (last != null)
client.start()
else
client.resumeFrom(last.id)
while (!emitter.isDisposed) {
val msg = connection.nextMessage()
emitter.onNext(msg)
}
}, BackpressureStrategy.MISSING)
requestOrResume()
.retryWhen { it.flatMap { Flowable.timer(5, SECONDS) } }
// how to pass the resume data when there is a retry?
Question: as you can see, I need the last received message in order to prepare the resume call. How can I keep track of it so that when there is a retry it is available to make the resume request?
One possible solution may be to create a holder class that just holds a reference to the last message and is updated when a new message is received. This way when there is a retry the last message can be obtained from the holder. Example:
class MsgHolder(var last: Message? = null)
fun request(): Flowable<Message> {
val holder = MsgHolder()
return Flowable.create({ emitter ->
val connection = if (holder.last != null)
client.start()
else
client.resumeFrom(holder.last.id)
while (!emitter.isDisposed) {
val msg = connection.nextMessage()
holder.last = msg // <-- update holder reference
emitter.onNext(msg)
}
}, BackpressureStrategy.MISSING)
}
I think this might work, but it feels like a hack (thread synchronization issues?).
Is there a better way to keep track of the state so it is available for retries?
Note that, unless you rethrow a wrapper around your last element (not too functionally different from your existing "hack"-ish solution but way uglier imo), no error handling operators can recover the last element without some outside help because they only get access to streams of Throwable. Instead, see if the following recursive approach suits your needs:
fun retryWithLast(seed: Flowable<Message>): Flowable<Message> {
val last$ = seed.last().cache();
return seed.onErrorResumeNext {
it.flatMap {
retryWithLast(last$.flatMap {
requestOrResume(it)
})
}
};
}
retryWithLast(requestOrResume());
The biggest distinction is caching the trailing value from the last attempt in an Observable with cache rather than doing so manually in a value. Note also that the recursion in the error handler means retryWithLast will continue to extend the stream if subsequent attempts continue failing.
Take a close look to buffer() operator: link
You could use it like this:
requestOrResume()
.buffer(2)
From now, your Flowable will return List<Message> with two latests objects

Subscribe to observable after dispose

i am building my app on android repository by Fernando Cejas and i have a problem with subscribing to observable after calling dispose.
When i come to dashboard, i call method subscribeOnUserMessages.execute(new Subscriber(), new Params(token)), which is method in UseCase class
public void execute(DisposableObserver<T> observer, Params params) {
Preconditions.checkNotNull(observer);
final Observable<T> observable = this.buildUseCaseObservable(params)
.subscribeOn(Schedulers.from(threadExecutor))
.observeOn(postExecutionThread.getScheduler());
addDisposable(observable.subscribeWith(observer));
}
In child class SubscribeOnUserMessages i simply call repository like this
return messageRepository.subscribeOnUserMessages(params);
In my socket implementation i create like this
return Observable.create(emitter -> {
if (!isThereInternetConnection()) {
Timber.w("Network connection exception");
emitter.onError(new NetworkConnectionException());
return;
}
/*
* Open socket if not opened
*/
openSocket(params.getToken());
String channelName = CHANNEL_PRIVATE_USER + params.getAuthenticated().getUuid();
if (subscribedChannels.contains(channelName)) {
Timber.d("Channel %s is already subscribed", channelName);
return;
}
JSONObject auth;
try {
auth = createAuthJson(CHANNEL, channelName, params.getToken());
} catch (JSONException e) {
Timber.e("Couldn't create auth json");
emitter.onError(e);
return;
}
mSocket.emit(SUBSCRIBE, auth);
Timber.d("Emitted subscribe with channel: %s ", CHANNEL_PRIVATE_USER + params.getAuthenticated().getUuid());
subscribedChannels.add(CHANNEL_PRIVATE_USER + params.getAuthenticated().getUuid());
Timber.d("Subscribing on event: %s\n with user: %s", EVENT_USER_NEW_MESSAGE, params.getAuthenticated().getUuid());
if (mSocket.hasListeners(EVENT_USER_NEW_MESSAGE)) {
Timber.v("Socket already has listener on event: %s", EVENT_USER_NEW_MESSAGE);
return;
}
mSocket.on(EVENT_USER_NEW_MESSAGE, args -> {
if (args[1] == null) {
emitter.onError(new EmptyResponseException());
}
Timber.d("Event - %s %s", EVENT_USER_NEW_MESSAGE, args[1].toString());
try {
MessageEntity messageEntity = messageEntityJsonMapper.transform(args[1]);
emitter.onNext(messageEntity);
} catch (JSONException e) {
Timber.e(e, "Could not parse message json");
emitter.onError(e);
}
});
});
Symptoms are that first time i subscribe everything is going through to presentation layer. When i dispose after going to second screen and come back i only see logs coming to socket implementation, but not going through.
My question is: Is there a method for subscribing to same observable again? I've already tried to save that observable in my use case in singleton and subscribe to that observable, didn't help.
Without additional info and details regrading socket implementation it is hard to spot the problem exactly, but, from the code you've posted, you don't have dispose logic, so while you might properly call dispose() to the Observable at the correct lifecycle event, your socket will actually stay open, and it might not got disconnected/closed properly ever.
That might lead to a problems opening and connecting to the socket at the 2nd time, as you might try to reopen already open socket and depends on your internal socket impl that might be a problem.
(I can see in the comment that openSocket if not already opened, but still there might be problem elsewhere calling some method on the socket multiple times or setting listeners, again depends on the socket impl)
As a general guidelines, you should add dispose logic using emitter.setCancellable()/emitter.setDisposable() in order to dispose properly the socket resources when you no longer need them, thus - when applying subscribe again (whether the same object or not) will invoke your subscription logic again that will reopen the socket and listen to it.
It is not clear to me if you like to keep the socket open when you moving to a different screen (I don't think it is a good practice, as you will keep this resource open and might never get back to the screen again to use it), but if that's the case as #Phoenix Wang mentioned, you can use publish kind operators to multicast the Observable, so every new Subscriber will not try to reopen the socket (i.e. invoking the subscription logic) but will just get notify about messages running in the already opened socket.

Is it possible to send a synchronous request in the Firebase?

I'm using Firebase Android SDK and became interested in sending synchronous request instead of asynchronous. According to the documentation, in any request callbacks are presented. But what about the synchronicity?
Thanks!
There is no way to synchronously load data from the Firebase Database.
While it is common for developers new to Firebase to wish for a synchronous method, it simply doesn't fit with Firebase's data synchronization model. Also see my answer here: Setting Singleton property value in Firebase Listener
It is not possible to load data synchronously with the official SDK. However, you can access all the data in firebase using the REST API. This would allow you to make synchronous calls. As mentioned about, Firebase is a realtime database and you will be missing the feature of updates when your data changes.
I made a simple class to call tasks synchronously in Android.
Note that this is similar to Javascript's async await function.
Check my gist.
TasksManager.class
public class TasksManager {
...
public ExecutorService getExecutor() {
if (mDefaultExecutor == null || mDefaultExecutor.isShutdown() || mDefaultExecutor.isTerminated()) {
// Create a new ThreadPoolExecutor with 2 threads for each processor on the
// device and a 60 second keep-alive time.
int numCores = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
numCores * 2,
numCores * 2,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>()
);
mDefaultExecutor = executor;
}
return mDefaultExecutor;
}
public static <TResult> Task<TResult> call(#NonNull Callable<TResult> callable) {
return Tasks.call(getInstance().getExecutor(), callable);
}
}
Here's a sample code to use it.
TasksManager.call(() -> {
Tasks.await(AuthManager.signInAnonymously());
// You can use multiple Tasks.await method here.
// Tasks.await(getUserTask());
// Tasks.await(getProfileTask());
// Tasks.await(moreAwesomeTask());
// ...
startMainActivity();
return null;
}).addOnFailureListener(e -> {
Log.w(TAG, "signInAnonymously:ERROR", e);
});
While it is not possible to load data from the FirebaseDatabase in a synchronous way, it is possible to wait for the load to finish synchronously.
You can wrap your value listener in a CountDownLatch and count down,
once the onDataChange or onCancelled implementation is called.
This is actually what the Tasks api is doing internally if you call Tasks.await(someTask).
You should use the value listener for single event listening, because in this case I assume you don't want continued updates. And use a proper timeout for the CountDownLatch, since Firebase won't timeout, ever.
reference.addListenerForSingleValueEvent(...);
You also have to take into account, that if you have the FirebaseDatabase
cache enabled, the first result might not be the actual value on the
server.
I have to add: While this might work, it is against the idea how firebase is designed and supposed to be used, as Frank already said.
If you are using Kotlin, add an extension function:
private suspend fun <TResult> Task<TResult>.await(): TResult? {
return try {
Tasks.await(this)
} catch (e: Exception) {
null
}
}
Now you can do
val snapshot = fireStore.collection(USER_ROOT_PATH).document(documentPath)?.get()?.await()

Categories

Resources