Code after `pagingAdapter.submitData()` is not executed - android

I am writing a toy Android app using Kotlin flow and Android Paging 3 library. The app calls some remote API to get a list of photos, and display them using a RecyclerView with a PagingDataAdapter.
I find that the code after pagingAdapter.submitData() is not executed.
Here is the code snippet (this function is in a Fragment):
fun refreshList() {
lifecycleScope.launch {
photosViewModel.listPhotos().collect {
// `it` is PagingData<Photo>
pagingAdapter.submitData(it)
Log.e(TAG, "After submitData")
}
}
}
The log After submitData is not printed.
However, if I put the logging in front of the pagingAdapter.submitData() line, it is printed, like this:
fun refreshList() {
lifecycleScope.launch {
photosViewModel.listPhotos().collect {
// `it` is PagingData<Photo>
Log.e(TAG, "Before submitData")
pagingAdapter.submitData(it)
}
}
}
The log Before submitData is printed with no problem.
Why does this happen, please?

.submitData is a suspending function which does not return until invalidation or refresh. As long as Paging is actively loading (collecting) from the PagingData you provided, it will not finish. This is why it must be done in a launched job.
For the same reason, make sure to use collectLatest instead of collect to make sure you cancel and start displaying new generations as soon as possible.

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

Code inside subscribe() is not getting executed

So what I want to do is get data from the API and depending on the result change the status so the observer of the status gets notified and can act accordingly.
fun getQuestionsByRelatedCategoriesFromApi(categoryId: Int, language: String){
faqFetchStatus.onNext(NetworkStatus.STARTED)
faqService.getQuestionsOfCategory(categoryId, language)
.subscribeOn(Schedulers.io())
.subscribe(
{
Log.d("FaqRepository", "Fetching ${it.size} questions from API")
//adding the language to the question for DB calls
for (element in it) {
element.language = language
}
insertQuestionsInDb(it)
faqFetchStatus.onNext(NetworkStatus.FINISHED)
},
{
faqFetchStatus.onNext(NetworkStatus.FAILED)
}
)
}
The View calls the ViewModel which in return calls the above function in the Repository. As expected the status gets changed to "STARTED" and a loading circle is shown. However the subscribe code never gets called which results in an infinite loading loop (form users perspective).
If I remove subscribeOn(Schedulers.io()) the code in subscribe() gets executed but since it's now a call on the main Thread I recieve the following error.
android.os.NetworkOnMainThreadException
What am I doing wrong here?

Disable Espresso IdlingResource base on condition

I have following Espresso test :
#Test
fun testSearchItemFound() {
onView(withId(R.id.action_search)).perform(click())
onView(isAssignableFrom(EditText::class.java))
.perform(typeText("Harry Potter"),
pressImeActionButton())
// Check error message view is not displayed
onView(withId(R.id.error_msg)).check(matches(not<View>(isDisplayed())))
// Check the item we are looking for is in the search result list.
onData(allOf(`is`<Any>(instanceOf<Any>(String::class.java)),
withItemContent("Harry Potter")))
.check(matches(isDisplayed()))
}
The first line of code :
onView(withId(R.id.action_search)).perform(click())
will be executed properly when EspressoIdlingResource is idle.
But last line of code:
onData(allOf(`is`<Any>(instanceOf<Any>(String::class.java)),
withItemContent("Harry Potter")))
.check(matches(isDisplayed()))
Will be executed when we have following code in MainFragment:
// The network request might be handled in a different thread so make sure Espresso knows
// that the app is busy until the response is handled.
EspressoIdlingResource.increment() // App is busy until further notice
model.movies.observe(this#MainFragment, Observer<PagedList<Movie>> {
// This callback may be called twice, once for the cache and once for loading
// the data from the server API, so we check before decrementing, otherwise
// it throws "Counter has been corrupted!" exception.
if (!EspressoIdlingResource.getIdlingResource().isIdleNow) {
EspressoIdlingResource.decrement() // Set app as idle.
}
adapter.submitList(it)
})
If I disable :
EspressoIdlingResource.increment() // App is busy until further notice
And :
if (!EspressoIdlingResource.getIdlingResource().isIdleNow) {
EspressoIdlingResource.decrement() // Set app as idle.
}
First line works properly :
onView(withId(R.id.action_search)).perform(click())
Is there any solution to handle this exceptional case?
Source can be found at : https://github.com/Ali-Rezaei/TMDb-Paging/tree/master/app/src/androidTest/java/com/sample/android/tmdb

Single.concat report error and continue to the next

I'm trying to use RxJava with Android to asynchronously update my view. When user clicks the movie from the list in the RecyclerView, I want to present him first with the movie from the database, if present. Then I want to fetch the latest information and update the database as well as UI. I'm trying to use concat method and its variant but it does not work.
I have skipped other codes only to post the relevant RxJava methods that are fetching data as the rest is working fine.
When I disable network connection with the code below (hence remote returns error), the code below does not display data from the database at all. Only it reports the error. Which means the local is not resolving.
public Flowable<Movie> getMovie(final int id) {
return Single.concat(mLocal.getMovie(id), mRemote.getMovie(id).doOnSuccess(data -> {
mLocal.save(data);
})).onErrorResumeNext(error->{
return Flowable.error(error);
});
}
And in this code, it works fine, except now that I don't get the error message (and rightly so, since I have replaced it with new stream from the database)
public Flowable<Movie> getMovie(final int id) {
return Single.concat(mLocal.getMovie(id), mRemote.getMovie(id).doOnSuccess(data -> {
mLocal.save(data);
})).onErrorResumeNext(error->{
return mLocal.getMovie(id).toFlowable();
});
}
Now, how can I get database data first and then fire network call next to update data and get errors from the database or network call?
UPDATE
The latest method code
// calling getMovie on mLocal or mRemote returns Single
public Flowable<Movie> getMovie(final int id) {
return Single.concat(mLocal.getMovie(id), mRemote.getMovie(id).doOnSuccess(data -> {
mLocal.insertMovie(data);
})).onErrorResumeNext(error -> {
return Flowable.error(error);
});
}
Here is how I call them
public void loadMovie(int id)
{
Disposable d = mRepo.getMovie(id)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread(), true)
.doOnSubscribe(subscription -> {
subscription.request(Long.MAX_VALUE);
//post progress here
})
.subscribe(data -> {
//onNext
},
error -> {
//onError
},
() -> {
//onComplete
}
);
mDisposables.add(d);
}
With affirmation that my code works and guides on troubleshooting from #akarnokd I found the latest code (see OP) works flawlessly. The result of RxJava chain is posted to LiveData object which should update View. Unfortunately it only posts the latest data (which is an error) and skips the first (which is the data from the database).
I will deal with that but since the post deals with RxJava, I will consider this solved!

Terminate Observable chain when one of them errors

I have a chain of Rx Completables that I want to run one after another. I am using concat() to do this since I do not want them all to start at the same time.
view.welcome_message_edittext.verifyNotEmpty(getString(R.string.enter_your_email_address))
.concatWith(view.welcome_message_edittext.verifyEmailAddress())
.concatWith(sendMessageToBot())
.subscribe({
// The user has successfully entered data into the edittext, entered an email into the edittext, and sent message to bot.
}, { error -> })
The code above is saying this, "Assert the user has entered text into the EditText. If that is true, assert the user has entered an email into the EditText. If both of those are true, send a message to the bot." If the user enters text into the EditText but it is not an email, I expect the chain of Completables to break and onError() gets called.
This is what I want to happen ^^^. When any of the Completables calls onError() (as verifyNotEmpty() and verifyEmailAddress() do if user leaves EditText empty or does not enter email address) then I expect the entire chain to terminate and call the .subscribe() onError() function.
But, looking at the docs for .concat() this is the actual behavior of it:
concat() will simply move onto the next Completable when onError is called. The chain continues.
So my question is, what do I need to use in order to break the chain when any of the Completables call onError()?
Thanks to #Buckstabue in the comments for helping me debug this issue. His comment:
Let me guess. It's absolutely normal that the method verifyEmailAddress() is called and I suspect you are doing some business logic right there outside of an observable. You can put that logic inside the observable and it will be calculated lazily It's similar to difference between Observable.just(getMyInteger()) and Observable.fromCallable(() -> getMyInteger()). In the second case getMyInteger() will be lazily called after subscribing while the first one is called immediately
Went back to my code and viewed my verifyEmailAddress() and sendMessageToBot() functions:
private fun sendMessageToBot(): Completable {
insertChatMessageIntoConversation(ChatMessage(view!!. welcome_message_edittext.text.toString()))
return Completable.complete()
}
fun EditText.verifyEmailAddress(): Completable {
if (!android.util.Patterns.EMAIL_ADDRESS.matcher(text.trim()).matches()) {
return Completable.error(RuntimeException("Enter a valid email"))
} else {
return Completable.complete()
}
}
The logic of the functions were not inside of a Completable block. I did not think that this mattered when I wrote the code because I thought that Rx's behavior was that it executed each Completable and waited for them to complete or error completely before moving onto the next Completeable. Therefore, skipping the sendMessageToBot() and verifyEmailAddress() functions entirely. Not the case.
This works:
fun EditText.verifyEmailAddress(): Completable {
return Completable.fromCallable({
if (!android.util.Patterns.EMAIL_ADDRESS.matcher(text.trim()).matches()) {
val errorMessage = context.getString(R.string.enter_email_address)
error = errorMessage
throw RuntimeException(errorMessage)
}
})
}
private fun sendMessageToBot(): Completable {
return Completable.fromCallable {
insertChatMessageIntoConversation(sage(view!!. welcome_message_edittext.text.toString()))
}
}

Categories

Resources