Using realm with PublishSubject - android

I want to map my realm results to an immutable viewmodel, and I want to listen to results changes, so i'm emitting them PublishSubject, However, the data doesn't appear in my recyclerview, until I rotate the device, this issue is fixed when I remove observeOn(AndroidSchedulers.mainThread()).
Repository:
fun notionsChanges(state: Boolean): Observable<Pair<MutableList<Notion>, OrderedCollectionChangeSet?>> {
val notionsChanges = PublishSubject.create<Pair<MutableList<Notion>, OrderedCollectionChangeSet?>>()
val realm = Realm.getDefaultInstance()
val queryResult = realm.where<Notion>()
.equalTo("isArchived", state)
.findAllAsync()
val listener: OrderedRealmCollectionChangeListener<RealmResults<Notion>> = OrderedRealmCollectionChangeListener { realmResults, changeSet ->
if (realmResults.isValid && realmResults.isLoaded) {
val results: MutableList<Notion> = realm.copyFromRealm(realmResults)
notionsChanges.onNext(results to changeSet)
}
}
queryResult.addChangeListener(listener)
notionsChanges.doFinally {
queryResult.removeChangeListener(listener)
closeRealm(realm)
}.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
return notionsChanges
}
in my presenter I use this observable to map the model to a view model, then i show(when subscribe) the data in recyclerview inside a fragment:
private var subscriptions: CompositeDisposable = CompositeDisposable()
override fun onResume() {
super.onResume()
showData()
}
override fun onPause() {
subscriptions.clear()
super.onPause()
}
private fun showData() {
val viewModel = present(idleStates, resources, isIdle)
with(viewModel) {
subscriptions.addAll(
notionsChanges.subscribe(notionsAdapter::handleChanges),
//other subscriptions.
)
}
}
notionsAdapter.handleChanges:
fun handleChanges(collectionChange: Pair<List<NotionCompactViewModel>, OrderedCollectionChangeSet?>) {
val (collection, changeset) = collectionChange
debug("${collection.size}") //correctly prints the actual size of the collection.
replaceAll(collection)
if (changeset == null)
notifyDataSetChanged()
else {
for (change in changeset.changeRanges)
notifyItemRangeChanged(change.startIndex, change.length)
for (insertion in changeset.insertionRanges)
notifyItemRangeInserted(insertion.startIndex, insertion.length)
for (deletion in changeset.deletionRanges)
notifyItemRangeRemoved(deletion.startIndex, deletion.length)
}
}
sorry if the code is unclear.
edit: my onBindViewHolder doesn't get called sometimes(when recyclerview is empty, of course).

Since Realm 5.0, the initial changeset is no longer signaled with changeset == null.
You need to check:
if(changeSet.getState() == State.INITIAL) {
adapter.notifyDataSetChanged()

Related

StateFlow collect not firing for list type

#HiltViewModel
class HistoryViewModel #Inject constructor(private val firebaseRepository: FirebaseRepository) :
ViewModel() {
private val translateList: MutableList<Translate> = mutableListOf()
private val _translateListState: MutableStateFlow<List<Translate>> =
MutableStateFlow(translateList)
val translateListState = _translateListState.asStateFlow()
init {
listenToSnapshotData()
}
private suspend fun addItemToList(translate: Translate) {
Log.d("customTag", "item added adapter $translate")
translateList.add(translate)
_translateListState.emit(translateList)
}
private suspend fun removeItemFromList(translate: Translate) {
Log.d("customTag", "item removed adapter $translate")
val indexOfItem = translateList.indexOfFirst {
it.id == translate.id
}
if (indexOfItem != -1) {
translateList.removeAt(indexOfItem)
_translateListState.emit(translateList)
}
}
private suspend fun updateItemFromList(translate: Translate) {
Log.d("customTag", "item modified adapter $translate")
val indexOfItem = translateList.indexOfFirst {
it.id == translate.id
}
if (indexOfItem != -1) {
translateList[indexOfItem] = translate
_translateListState.emit(translateList)
}
}
private fun listenToSnapshotData() {
viewModelScope.launch {
firebaseRepository.translateListSnapshotListener().collect { querySnapshot ->
querySnapshot?.let {
for (document in it.documentChanges) {
val translateData = document.document.toObject(Translate::class.java)
when (document.type) {
DocumentChange.Type.ADDED -> {
addItemToList(translate = translateData)
}
DocumentChange.Type.MODIFIED
-> {
updateItemFromList(translate = translateData)
}
DocumentChange.Type.REMOVED
-> {
removeItemFromList(translate = translateData)
}
}
}
}
}
}
}
}
Here data comes properly in querySnapshot in listenToSnapshotData function. And post that it properly calls corresponding function to update the list.
But after this line _translateListState.emit(translateList) flow doesn't go to corresponding collectLatest
private fun observeSnapShotResponse() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
historyViewModel.translateListState.collectLatest {
Log.d("customTag", "calling submitList from fragment")
translateListAdapter.submitList(it)
}
}
}
}
calling submitList from fragment is called once at the start, but as & when data is modified in list viewmodel, callback doesn't come to collectLatest
This is from StateFlow documentation:
Values in state flow are conflated using Any.equals comparison in a similar way to distinctUntilChanged operator. It is used to conflate incoming updates to value in MutableStateFlow and to suppress emission of the values to collectors when new value is equal to the previously emitted one.
You are trying to emit the same instance of List all the time, which has no effect because of what is written in the docs. You will have to create new instance of the list every time.

Firestore live update using Kotlin Flow

I want to implement system with live updates (similar to onSnapshotListener). I heard that this can be done with Kotlin Flow.
Thats my function from repository.
suspend fun getList(groupId: String): Flow<List<Product>> = flow {
val myList = mutableListOf<Product>()
db.collection("group")
.document(groupId)
.collection("Objects")
.addSnapshotListener { querySnapshot: QuerySnapshot?,
e: FirebaseFirestoreException? ->
if (e != null) {}
querySnapshot?.forEach {
val singleProduct = it.toObject(Product::class.java)
singleProduct.productId = it.id
myList.add(singleProduct)
}
}
emit(myList)
}
And my ViewModel
class ListViewModel: ViewModel() {
private val repository = FirebaseRepository()
private var _products = MutableLiveData<List<Product>>()
val products: LiveData<List<Product>> get() = _produkty
init {
viewModelScope.launch(Dispatchers.Main){
repository.getList("xGRWy21hwQ7yuBGIJtnA")
.collect { items ->
_products.value = items
}
}
}
What do I need to change to make it work? I know data is loaded asynchronously and it doesn't currently work (the list I emit is empty).
You can use this extension function that I use in my projects:
fun Query.snapshotFlow(): Flow<QuerySnapshot> = callbackFlow {
val listenerRegistration = addSnapshotListener { value, error ->
if (error != null) {
close()
return#addSnapshotListener
}
if (value != null)
trySend(value)
}
awaitClose {
listenerRegistration.remove()
}
}
It uses the callbackFlow builder to create a new flow instance.
Usage:
fun getList(groupId: String): Flow<List<Product>> {
return db.collection("group")
.document(groupId)
.collection("Objects")
.snapshotFlow()
.map { querySnapshot ->
querySnapshot.documents.map { it.toObject<Product>() }
}
}
Note that you don't need to mark getList as suspend.
Starting in firestore-ktx:24.3.0, you can use the Query.snapshots() Kotlin flow to get realtime updates:
suspend fun getList(groupId: String): Flow<List<Product>> {
return db.collection("group")
.document(groupId)
.collection("Objects")
.snapshots().map { querySnapshot -> querySnapshot.toObjects()}
}
As of 2 days ago, firestore has this functionality provided out of the box: https://github.com/firebase/firebase-android-sdk/pull/1252/

observer gets called twice inside activity

In below code my observer gets called multiple time after storing all users from arguments to result arraylist. I am new to observe pattern so I am not sure what I am doing wrong here.
private lateinit var usersObserver: Observer<List<User?>?>
override fun onCreate(savedInstanceState: Bundle?) {
usersObservar = Observer {
userResults = populateResults(it)
}
}
private fun populateResults(users: List<User?>): MutableList<UserModel> {
val results: MutableList<UserModel> = ArrayList()
for (user in users) {
//Ignore potential null predictions
if ((user != null) &&user.isUserNotNull()) {
user.id?.let {
searchResultsViewModel.getUserById(it).observe(
this,
Observer { ud ->
if (ud != null && ud.hasNonNullLatLngOffsetMembers()) {
results.add(
UserModel(
name = user.placeId!!,
address = ud.address
displayed = false
)
)
}
}
)
}
}
}
return results
}
I assume you are calling popoulateResults() multiple times. When you call searchResultsViewModel.getUserById(it).observe() you pass it a new instance of the Observer therefore everytime the observer is called the code inside the observer is getting executed. An easy fix should be defining the observer as a property outside the function like this
val observer = Observer { your code }
and use it like
searchResultsViewModel.getUserById(it).observe(this, observer)

How do I handle too early observation of LiveData that depend on firebase auth state to be AUTHENTICATED?

In my Fragment's onViewCreated, I have a redirect based on auth state, and also bind the Frag's Observers, like so:
class TodoMvvmFragment : Fragment() {
private val loginViewModel by viewModel<LoginViewModel>()
private val todoViewModel by viewModel<TodoViewModel>()
val TAG = "TODO_FRAG"
var rvAdapter:TodoListAdapter? = null
var observersBound = false
///...omitted
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
rvAdapter = TodoListAdapter(todoViewModel, this )
todo_rv.let {
it.adapter = rvAdapter
it.layoutManager = LinearLayoutManager(requireActivity())
}
loginViewModel.authenticationState.observe(this, Observer {
when(it) {
LoginViewModel.AuthenticationState.UNAUTHENTICATED -> {
findNavController().navigate(R.id.action_global_loginFragment)
}
LoginViewModel.AuthenticationState.AUTHENTICATED -> {
// Viewmodel calls aspects of the repository that rely on AUTHSTATE
// to be AUTHENTICATED, so don't bind observers until we are.
if (!observersBound) {
observersBound = true
todoViewModel.init()
bindObservers()
addListeners()
}
}
else -> {
Log.d(TAG, it.toString())
}
}
})
}
Here is the problematic Observer:
fun bindObservers() {
todoViewModel.getFilteredTodos().observe(this, Observer{
if (it.data == null) {
handleException("Error with todos by timestamp query", it.exception)
} else {
val todos: List<Todo> = it.data.map { todoOrException ->
if( todoOrException.data == null) {
handleException("error with individual Todo", todoOrException.exception)
null
} else {
todoOrException.data
}
}.filterNotNull()
rvAdapter?.submitList(todos)
}
})
This percolates down to the repo, which creates a firebase query. Notice the .document(auth.uid.toString())... if this query is created before the user is authorized, no data is returned.
fun allTodosQuery(): Query {
return firestore.collection(PATH_ROOT)
.document(PATH_APPDATA)
.collection(PATH_USERS)
.document(auth.uid.toString())
.collection(PATH_TODOS)
.orderBy(FIELD_TIMESTAMP, Query.Direction.DESCENDING)
Back in the authentication state listener, I am binding the observers ONLY when auth state changes to authenticated. I'm also guarding that with a boolean so that it only happens once. When I don't do that, and just bind the observers in onViewCreated after the auth state listener, I don't get any data on the initial load of the app.
My question is how to keep livedata that depend on authstate being authenticated from omitting nothing when they are bound to before the user is authenticated. Essentially, how do I keep from fetching a bad query with null auth.uid from the repo before the user is authenticated?
Thanks for allowing me to rubber duck. Here's a solution:
var _queryResult = Transformations.switchMap(_authState){
if (_authState.value == LoginViewModel.AuthenticationState.AUTHENTICATED) {
repo.getAllTodosByTimestamp()
} else {
MutableLiveData<ListOrException<TodoOrException>>()
}
}
That way, we don't construct or fire off the query (down in the repo) until authenticated.

How to clear/remove all items in page list adapter

I'm using the android paging library to show search result items, is there any way I can clear/remove all the loaded result items, Calling Invalidate on live Paged List refreshing the list not clear/remove items
In Activity:
fun clearSearchResult() {
if (productSearchResultItemAdapter.itemCount > 0) {
viewModel.invalidateResultList()
}
}
In ViewModel
private fun searchProductByTerm(searchTerm: String): Listing<Item> {
sourceFactory = ProductSearchItemDataSourceFactory(productSearchUC, compositeDisposable, searchTerm, resourceResolver)
val livePagedList = LivePagedListBuilder(sourceFactory, pagedListConfig)
//The executor used to fetch additional pages from the DataSource
.setFetchExecutor(getNetworkExecutor())
.build()
return Listing(
pagedList = livePagedList,
networkState = switchMap(sourceFactory.sourceLiveData) {
it.networkState
},
retry = {
sourceFactory.sourceLiveData.value?.retryAllFailed()
}
)
}
fun invalidateResultList() {
sourceFactory?.sourceLiveData?.value?.invalidate()
}
private val productSearchName = MutableLiveData<String>()
private val repoResult = map(productSearchName) {
searchProductByTerm(it)
}
If you're working with PagingDataAdapter, searchAdapter.submitData(lifecycle, PagingData.empty()) works
submitting null clear the currently loaded page list
productSearchResultItemAdapter.submitList(null)
In Java:
I cleared all items on in PagedListAdapter by calling invalidate() on DataSource instance like that
public void clear(){
movieDataSource.invalidate();
}
Add this method in your ViewModel then call it in your activity
movieViewModel.clear();
movieAdapter.notifyDataSetChanged();
Then Load any data you want
You can see how I made it in my project.
Here is the Link: https://github.com/Marwa-Eltayeb/MovieTrailer
In Fragment
lifecycleScope.launch {
viewModel.currentResult = null
viewModel.getSearchAudio(binding.etxtSearch.text.toString().trim(), 0).collectLatest { it ->
Log.v(mTAG, "Status: New record")
adapterAudioList.submitData(it)
}
}
In ViewModel
var currentResult: Flow<PagingData<AudioModel>>? = null
fun getSearchAudio(trackName: String, lastPageCount: Int): Flow<PagingData<AudioModel>> {
val lastResult = currentResult
if (lastResult != null) {
return lastResult
}
val newResult: Flow<PagingData<AudioModel>> = videoRepository.getAudioSearchPaging(trackName, lastPageCount).cachedIn(viewModelScope)
currentResult = newResult
return newResult
}
In videoRepository
fun getAudioSearchPaging(trackName: String, lastPageCount: Int): Flow<PagingData<AudioModel>> {
return Pager(
config = PagingConfig(pageSize = KeyConstants.AUDIO_PAGE_SIZE, enablePlaceholders = false),
pagingSourceFactory = { AudioSearchPagingSource(context, trackName, lastPageCount) },
).flow
}
Before invalidate, clear your list data item.
Like we did in simple way:
list.clear();
adapter.notifyDataSetChanged();

Categories

Resources