Android PagedList Scroll to bottom - android

I'm using Android Jetpack Paging library(2.1.2) for a chat view.
Data is served from a local database(room) and network by using PagedList.BoundaryCallback mechanism. Since the Room provide paged data to the RecyclerView adapter I'm unable to scroll to the bottom of list as the list size is unknown.
I also registerd registerAdapterDataObserver for adapter to observe the changes. However
itemCount value in onItemRangeInserted callback changed from time to time.
So how do you guys scroll to bottom of the RecyclerView when you are using PagedListAdapter with BoundaryCallback ?

For this we need to extend the positional datasource, and override loadInitial() method. Basically room uses the limit and offset in sqlite to load specific range of data from the specific position. Codes are in Kotlin but can be easily understandable and convertable in Java
override fun loadInitial(
params: LoadInitialParams,
callback: LoadInitialCallback<ChatListItem>
) {
val newRequestedSize = params.requestedStartPosition + params.requestedLoadSize
val dbMessage = groupMessagesDao.getPagedMessageForGroup(
channelId,
0,
newRequestedSize
)
if (dbMessage.isNotEmpty()) {
callback.onResult(dbMessage, 0)
} else {
callback.onResult(emptyList(), params.requestedStartPosition)
}
}
In the above code we are forcing the room(you will have to implement your own Dao of course, it just uses the offset and limit as I mentioned earlier) to load the initial data from position 0 to the newRequestedSize which is the sum of current offset and load size the paging library wants us to load. Once, we load the data from position 0 to the current page position, we need to set the callback with the data and the position which is 0. Now, the paging library loads the data up to the current page position and our
private fun scrollToBottom() {
val position = adapter.itemCount - 1
if (position > 0) {
recyclerView.scrollToPosition(0)
}
}
function works as expected.
One thing we also need to handle is to listen for table change, which you can again find in the LimitOffsetDatasource on how the room does it.
init {
val tableObserver = object : InvalidationTracker.Observer(tableName) {
override fun onInvalidated(tables: MutableSet<String>) {
// invalidate the dataSource
Log.v("dataSource invalidate requested!")
invalidate()
}
}
database.invalidationTracker.addObserver(tableObserver)
addInvalidatedCallback {
Log.v("invalidation callback received, removing table observer")
database.invalidationTracker.removeObserver(tableObserver)
}
}
The performance implication are there in this approach, but in our case the user is most of the time messaging which is always at the first page. So, when the user is at bottom this implementation is same as that of the LimitOffsetDatasource. Only when the user scrolls to see the old messages, and at the same time a new message arrives for the user only then we reload all the messages up to the user current position. But when new message arrives most probably the user will scroll to bottom. So, the performance penalty is virtually zero.

Related

Control item to add

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

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

Android - MutableSharedFlow failed to collect new values with multi subscribers

I want to create a shared view model for communication between MainActivity to fragments.
I decided to use share flow for managing events.
private val _sharedChannel: MutableSharedFlow<SharedEvent> = MutableSharedFlow(
replay = 0,extraBufferCapacity=0,onBufferOverflow = BufferOverflow.SUSPEND)
val sharedChannel = _sharedChannel.asSharedFlow()
I don't need to cache the last event, not even when orientation changes.. so I set "replay = 0"
When I collect the events only in my main activity - everything works fine:
lifecycleScope.launchWhenStarted {
gamePlaySharedViewModel.sharedChannel.collect { event->
SnappLog.log("GamePlayContainer-> sharedChannel EVENT: $event ")
when(event){
GamePlaySharedViewModel.SharedEvent.OnBackPress -> {
onBackPressed()
}
is GamePlaySharedViewModel.SharedEvent.BlockScreen -> {
blockScreen(event.isBlocked)
}
else -> {
}
}
}
}
}
When adding a second subscriber to another fragment - both of the subscribers stop receiving events after the first one (the first event send successfully.. )
what can I do to subscribe for multi MutableSharedFlow?
I've tried to increase the number of "replay" and to change the "onBufferOverflow" - nothing seems to work..

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

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