Control item to add - android

I would like to put a check to ensure that if they are already present it does nothing, otherwise it adds the new ones.
this is the code i wrote:
private fun setUpFireStore() {
firestore = FirebaseFirestore.getInstance()
val collectionReference = firestore.collection("quiz")
collectionReference.addSnapshotListener { value, error ->
if(value == null || error != null){
Toast.makeText(this, "Error fetching data", Toast.LENGTH_SHORT).show()
return#addSnapshotListener
}
Log.d("DATA", value.toObjects(Quiz::class.java).toString())
quizList.clear()
quizList.addAll(value.toObjects(Quiz::class.java))
adapter.notifyDataSetChanged()
}
}
every time I go back to the home, this function is called and adds again all the quizzes present in the Firestore db.
The colors and images are randomly generated each time they are added. (correct operation when starting the app) but if you keep refreshing the home screen, even if they are the same they keep changing...
(identification field: "title")
So it seemed more logical to me that quizzes are only added if they are missing on refresh, not every time this screen is reopened.
but i can't figure out how to implement this function

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

How to make case REMOVED fire when the last element is removed from Firestore?

I'm trying to listen for real-time updates and I'm using the following code in onEvent:
override fun onEvent(querySnapshot: QuerySnapshot?, e: FirebaseFirestoreException?) {
if (e != null) return
if (!querySnapshot!!.isEmpty) {
for (change in querySnapshot.documentChanges) {
value = when (documentChange.type) {
Type.ADDED -> change.document.toObject<Item>(Item::class.java)
Type.MODIFIED -> change.document.toObject<Item>(Item::class.java)
Type.REMOVED -> vchange.document.toObject<Item>(Item::class.java)
}
}
} else {
logErrorMessage("querySnapshot.isEmpty")
}
}
Let's assume we have 2 elements added in a collection. For each addition, case Type.ADDED is triggered. If I remove one of them, Type.REMOVED is triggered. The problem comes, when I try to remove the last item. Instead of having case Type.REMOVED triggered for the last time, I get an empty querySnapshot. So the Type.REMOVED case is not triggered anymore. How can I be notified when the last element is removed?
The problem could be that your code is only looking for document changes if the query results are not empty. If the results become empty, you're just not checking to see if there was a change that made it empty. Remove the check for !querySnapshot!!.isEmpty and just list each change regardless.
If your querySnapshot is empty then you won't satisfy the if condition, therefore, you won't enter any of the cases on your when statement.
Also, since your snapshot is empty, there is no data to map to your Item.class

Android LiveData observes stale data after navigating back

Question:
How can I prevent my livedata immediately receiving stale data when navigating backwards? I am using the Event class outlined here which I thought would prevent this.
Problem:
I open the app with a login fragment, and navigate to a registration fragment when a live data email/password is set (and backend call says "this is a new account go register"). If the user hits the back button during the registration Android is popping back to login.
When the login fragment is recreated after a back press, it immediately fires the live data again with the stale backend response and I would like to prevent that.
LoginFragment.kt
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
subscribeToLoginEvent()
}
private fun subscribeToLoginEvent() {
//When a back press occurs, we subscribe again and this instantly
//fires with the same data it used to leave the screen
//(a Resource<User> with status=SUCCESS, data = null)
viewModel.user.observe(viewLifecycleOwner, Observer { response ->
Timber.i("login event observed....status:" + response?.status + ", data: " + response?.data)
binding.userResource = response
response?.let {
val status = it.status
val message = it.message
if (status == Status.SUCCESS && it.data == null) {
//This is a brand new user so we need to register now
navController()
.navigate(LoginFragmentDirections.showUserRegistration()))
}
else if(status == Status.SUCCESS && it.data != null){
goHome()
}
}
})
}
LoginViewModel.kt
private val _loginCredentials: MutableLiveData<Event<Pair<String, String>>> = MutableLiveData()
val user: LiveData<Resource<User>> = Transformations.switchMap(_loginCredentials) {
val data = it.getContentIfNotHandled()
if(data != null && data.first.isNotBlank() && data.second.isNotBlank())
interactor.callUserLoginRepo(data.first, data.second)
else
AbsentLiveData.create()
}
Okay there's two issues here which I hope helps somebody else.
The first is that the Event class does not appear to work with transformations. I think it is because the Event is literally pointing to the wrong live data (_login_credentials vs user)
The second problem is a little bit more fundamental but also blindingly obvious now. We are told all over the place that live data observations emit the latest data when a subscription is made to ensure you get the most up to date data. This means the way I am using live data here is simply incorrect, I can't subscribe to a login event, navigate somewhere, navigate back and re-subscribe because the ViewModel is rightfully giving me the latest data it has (because the login fragment was only detached, never destroyed).
Solution
The solution is to simply move the logic which performs the fetch one fragment deeper. So instead of listening for user credentials + login click -> fetching a user -> and then navigating somewhere, I need to listen for user credentials + login click -> navigate somewhere -> and then start subscribing for my user live data. That way I can head back to the login screen as much as I want and not subscribe to some stale live data that was never destroyed. And if I go back to login and then forwards the subscription and fragment were destroyed so I will appropriately be getting new data in that case.

Firebase AuthInstance.currentUser is null after successfully reload

I'm currently having an issue during some of my users who log-in with email and password. The case is that after send the verification email, I'm calling reload in my current FirebaseUser to update the currentUser an know if the email have been verified or not.
Everything works fine at this point. The thing is that sometimes after call reload over an instance of FirebaseAuth.currentUser.reload(), on the successful callback of reload I'm trying to access again to the already updated FirebaseAuth.currentUser instance, and the funny thing is that this comes to be null and I'm getting a NPE, when the user have been successfully reloaded and the instance should be updated, not deleted.
This is my code right now:
override fun verifyUser() {
if (authInstance.currentUser == null) {
dispatcher.dispatchOnUi(VerifyUserEmailCompleteAction(requestState = requestFailure(FirebaseUserNotFound()),
verified = false))
return
}
RxFirebaseUser.reload(authInstance.currentUser!!)
.subscribeOn(Schedulers.io())
.subscribe({
dispatcher.dispatchOnUi(VerifyUserEmailCompleteAction(requestState = requestSuccess(),
verified = authInstance.currentUser!!.isEmailVerified))
}, { error ->
dispatcher.dispatchOnUi(VerifyUserEmailCompleteAction(requestState = requestFailure(error),
verified = false))
})
}
The RxFirebase call is a Reactive wrapper over Firebase, but I have tried this code also using the normal Promise of the library and this error is happening too.
My NPE is coming on verified= authInstance.currentUser!!.isEmailVerified which should not be possible because I already did a check before start this call and the reload call have been successful.
Someone have suffer this issue and know why this could be happening? How can be the workaround for it? I could control the NPE there, but if the reload have been successful I want to update my user in my app data, not send a null instance.
The authInstance.currentUser sometimes takes some time to be updated. You can move your reload call to the main thread on the observeOn to give authInstance more time to be updated, and also just in case add a retry clause to your code. It would be something like this:
RxFirebaseUser.reload(authInstance.currentUser!!)
.subscribeOn(Schedulers.io())
.repeatUntil { authInstance.currentUser != null }
.defaultTimeout()
.subscribe({
val user = authInstance.currentUser
val emailVerified = user?.isEmailVerified ?: false
dispatcher.dispatchOnUi(VerifyUserEmailCompleteAction(requestState = if (user == null) requestRunning() else requestSuccess(),
verified = emailVerified))
}, { error ->
dispatcher.dispatchOnUi(VerifyUserEmailCompleteAction(requestState = requestFailure(error),
verified = false))
})

Why ion-infinite-scroll keeps calling on scrolling in Android?

I am using 'ion-infinite-scroll' in html to load more items from server for this i am using below code
<ion-infinite-scroll immediate-check="false" on-infinite="getListOfAreas()" distance="1%">
</ion-infinite-scroll>
Here, getListOfAreas() function is called when I scroll screen to bottom and it fetches data from server.This is getListOfAreas() function defined on controller
$scope.getListOfAreas = function (shoudlShowLoader) {
AreaBusiness.getAreasListing(shoudlShowLoader, function(serviceResponse) {
$scope.$broadcast('scroll.infiniteScrollComplete');
if (serviceResponse != null) {
var isSuccess = serviceResponse.Success;
if (isSuccess) {
}
}
}
}
On browser, i have debugged some how
$scope.$broadcast('scroll.infiniteScrollComplete');
above line of code keeps calling and spinner keep rotating.I don't know the reason. Am I missing anything?
$scope.$broadcast('scroll.infiniteScrollComplete'); only lets ionic know that the current page of data has been fetched and that it's safe to now fetch the next page. This does not indicate that all data has finished loading.
Looks like the official recommendation is to add an ng-if to the scroll delegate and remove it from dom once there is not more data to load.
Example:
<ion-infinite-scroll
ng-if="moreDataCanBeLoaded()"
icon="ion-loading-c"
on-infinite="loadMoreData()">
</ion-infinite-scroll>
Source: https://ionicframework.com/docs/api/directive/ionInfiniteScroll/
I have resolved the issue, after spending sometime. The issue which I found, I was not adding those elements into the list which were to be shown on UI. So, list was not being updated on controller that is why it kept calling.So I updated my code and added those fetched items from service to list associated to ui as mentioned in my below code
$scope.$broadcast('scroll.infiniteScrollComplete');
if (serviceResponse != null) {
var isSuccess = serviceResponse.Success;
if (isSuccess) {
if ($scope.areas != undefined && $scope.areas.length > 0) {
pushDataToPOIList(serviceResponse.PointOfInterestData);
} else {
$scope.areas = serviceResponse.PointOfInterestData;
}
pushDataToVehiclesList() method I added that is pushing new data into list to update list and on UI as well. This helped me to sort out the problem.

Categories

Resources