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)
Related
#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.
I am trying to get list of todos from database with livedata however, while debugging it always shows null for value. I have provided my files below.
My Dao:
#Query("SELECT * FROM todo_table WHERE IIF(:isCompleted IS NULL, 1, isCompleted = :isCompleted)")
fun getTodos(isCompleted: Boolean?): LiveData<List<Todo>>
My ViewModel:
private var _allTodoList = MutableLiveData<List<Todo>>()
var allTodoList: LiveData<List<Todo>> = _allTodoList
init {
viewModelScope.launch(Dispatchers.IO) {
val list = todoRepository.getTodos(null)
_allTodoList.postValue(list.value)
}
}
fun onFilterClick(todoType: Constants.TodoType) {
when (todoType) {
Constants.TodoType.ALL -> {
viewModelScope.launch(Dispatchers.IO) {
val list = todoRepository.getTodos(null)
_allTodoList.postValue(list.value)
}
}
Constants.TodoType.COMPLETED -> {
viewModelScope.launch(Dispatchers.IO) {
val list = todoRepository.getTodos(true)
_allTodoList.postValue(list.value)
}
}
Constants.TodoType.INCOMPLETE -> {
viewModelScope.launch(Dispatchers.IO) {
val list = todoRepository.getTodos(false)
_allTodoList.postValue(list.value)
}
}
}
}
My MainActivity:
val allTodoList = viewModel.allTodoList.observeAsState()
allTodoList.value?.run {//value is always null
if (!isNullOrEmpty()) {
...
} else {
...
}
}
While debugging I found that allTodoList.value is always null however, when I manually run same query in app inspection I the get the desired results.
You can simplify your code, see if it works.
ViewModel only needs this:
val allTodoList: LiveData<List<Todo>> = todoRepository.getTodos(null)
MainActivity:
val allTodoList by viewModel.allTodoList.observeAsState()
if (!allTodoList.isNullOrEmpty()) {
...
} else {
...
}
You are not observing the LiveData you get from Room.
YourDao.getTodos() and LiveData.getValue() are not suspend functions, so you get the current value, which is null because Room has not yet fetched the values from SQLite.
A possible solution would be to set the todo type as a live data itself and use a switchMap transformation in the ViewModel :
private val todoType = MutableLiveData<Constants.TodoType>(Constants.TodoType.ALL)
val allTodoList: LiveData<List<Todo>> = androidx.lifecycle.Transformations.switchMap(todoType) { newType ->
val typeAsBoolean = when(newType) {
Constants.TodoType.ALL -> null
Constants.TodoType.COMPLETED -> true
Constants.TodoType.INCOMPLETE -> false
else -> throw IllegalArgumentException("Not a possible value")
}
// create the new wrapped LiveData
// the transformation takes care of subscribing to it
// (and unsubscribing to the old one)
todoRepository.getTodos(typeAsBoolean)
}
fun onFilterClick(todoType: Constants.TodoType) {
// triggers the transformation
todoType.setValue(todoType)
}
This is in fact the exact use case demonstrated in the reference doc
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.
I'm trying to load data from Firebase into a RecyclerView, however nothing shows up until I reload my fragment.
This is my onCreate method in SubjectsFragment:
viewModel.subjectsListLiveData.observe(
this,
Observer { list ->
subjectsAdapter.swapSubjectsList(list)
if (subject_list != null && list.size != 0) Animations.runLayoutAnimation(
subject_list
)
})
viewModel.lessonsListLiveData.observe(
this,
Observer { list ->
subjectsAdapter.swapLessonsList(list)
if (subject_list != null && list.size != 0) Animations.runLayoutAnimation(
subject_list
)
})
This is SubjectsFragmentViewModel:
private val subjectsList = MutableLiveData<ArrayList<Subject>>()
val subjectsListLiveData: LiveData<ArrayList<Subject>>
get() = subjectsList
private val lessonsList = MutableLiveData<ArrayList<Lesson>>()
val lessonsListLiveData: LiveData<ArrayList<Lesson>>
get() = lessonsList
init {
loadSubjects()
loadLessonsForSubjects()
}
fun loadSubjects() {
GlobalScope.launch {
val subjects = FirebaseUtils.loadAllSubjects()
subjectsList.postValue(subjects)
}
}
fun loadLessonsForSubjects() {
GlobalScope.launch {
val lessons = FirebaseUtils.loadAllLessons()
lessonsList.postValue(lessons)
}
}
I don't have any problems once I reload the fragment. Could someone please explain to me what I'm doing wrong?
Try using setValue directly.
But you may be right, using postValue from a background thread is the way it should be done.
Also, attach your observers in onActivityCreated()
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()