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()
Related
inside MainActivity I have snapShot listener to document added that calls a function inside a fragment that supose to set and update the adapter of item that stored in fire store
mFireStore.collection(Constans.BOARDS)
.whereArrayContains(Constans.ASSIGNED_TO,FireStore().getCurrentUid())
.orderBy("timeStamp", Query.Direction.DESCENDING)
.addSnapshotListener { value, e ->
Log.d("MainActivity","board listener")
if (e != null) {
Log.w(ContentValues.TAG, "Listen failed.", e)
return#addSnapshotListener
}
val boards = ArrayList<Board>()
Constans.BOARDS_CHATS_LIST = ArrayList()
for (doc in value!!) {
val board = doc.toObject(Board()::class.java)
Constans.BOARDS_CHATS_LIST.add(board)
}
fragment_chat().updateBoardToUi(Constans.BOARDS_CHATS_LIST)
}
and here is the function
fun updateBoardToUi(boardsChatsList: ArrayList<Board>) {
if(boardsChatsList.size > 0){
val context = getContext() ?: return
Log.e("${Constans.BOARDS_CHATS_LIST.size.toString()}","updateBoardToUi")
view?.rv_chats_list?.visibility = View.VISIBLE
view?.no_chats_avlible?.visibility = View.GONE
view?.rv_chats_list?.layoutManager = LinearLayoutManager(context)
view?.rv_chats_list?.setHasFixedSize(true)
//might be an error
adapter = BoardItemsAdapter(context,Constans.BOARDS_CHATS_LIST)
view?.rv_chats_list?.adapter = adapter
adapter.notifyItemInserted(0)
adapter.setOnClickListener(
object :BoardItemsAdapter.OnClickListener{
override fun onClick(position: Int, model: Board) {
Log.i("fragment chat", "on click")
val intent = Intent(context, ChatActivity::class.java)
intent.putExtra(Constans.BOARD_CHAT_DETAILS, model)
intent.putExtra("uid", FirebaseAuth.getInstance().currentUser?.uid )
intent.putExtra(Constans.DOCUMENT_ID, model.documentId)
intent.putExtra("position", position)
startActivity(intent)
}
}
)
}else{
Log.e("inside","updateBoardToUi2")
view?.no_chats_avlible?.visibility = View.VISIBLE
}
}
but the adapter deas not show the new item added even thogh I use adapter.notifyItemInserted(0)
It is because you can not hold and send data with "Constants.BOARDS_CHATS_LIST". Because every time you want to call it, it will return the default value it has. You can do 4 things that come into my mind:
1- Send the data from activity to fragment via Shared Preferences. I do not recommend this method.
2 - Send data from activity to fragment via bundle. This is doable but i do not prefer it.
3 - Move your firestore function to the fragment and declare a global list and put the records there, then use it in updateBoardToUi function. You can do this but if you need this function in other fragment, you need to copy and paste it there too.
4- You can create a new class for firestore functions, and whenever you need it, call it from there. This is the best way and i will try to help you with it.
Create new kotlin class and paste this inside it. You will later call this inside onViewCreated of your fragment, and it will send the array to the updateBoardToUi method.
class FirestoreClass {
private val mFireStore = FirebaseFirestore.getInstance()
private val mFirebaseAuth = FirebaseAuth.getInstance()
fun getBoards(fragment: YourFragmentName) {
mFireStore.collection(Constans.BOARDS)
.whereArrayContains(Constans.ASSIGNED_TO,getCurrentUserID())
.orderBy("timeStamp", Query.Direction.DESCENDING)
.addSnapshotListener { value, e ->
if (e != null) {
Log.w(ContentValues.TAG, "Listen failed.", e)
return#addSnapshotListener
}
val boards = ArrayList<Board>()
for (doc in value!!) {
val board = doc.toObject(Board()::class.java)
boards.add(board)
}
fragment.updateBoardToUi(boards)
}
}
fun getCurrentUserID(): String {
val currentUser = mFirebaseAuth.currentUser
var currentUserID = ""
if (currentUser != null) {
currentUserID = currentUser.uid
}
return currentUserID
}
}
Now we will use the list from your db.
fun updateBoardToUi(boardsChatsList: ArrayList<Board>) {
// fragment.updateBoardToUi(boards) that sent the data and now
// it is in boardsChatsList, you will use this.
if(boardsChatsList.size > 0){
val context = getContext() ?: return
Log.e("${boardsChatsList.size.toString()}","updateBoardToUi")
view?.rv_chats_list?.visibility = View.VISIBLE
view?.no_chats_avlible?.visibility = View.GONE
adapter = BoardItemsAdapter(context,boardsChatsList)
view?.rv_chats_list?.adapter = adapter
view?.rv_chats_list?.layoutManager = LinearLayoutManager(context)
view?.rv_chats_list?.setHasFixedSize(true)
adapter.setOnClickListener(
object :BoardItemsAdapter.OnClickListener{
override fun onClick(position: Int, model: Board) {
Log.i("fragment chat", "on click")
val intent = Intent(context,ChatActivity::class.java)
intent.putExtra(Constans.BOARD_CHAT_DETAILS, model)
intent.putExtra("uid", FirestoreClass().getCurrentUserID())
intent.putExtra(Constans.DOCUMENT_ID, model.documentId)
intent.putExtra("position", position)
startActivity(intent)
}
}
)
}else{
Log.e("inside","updateBoardToUi2")
view?.no_chats_avlible?.visibility = View.VISIBLE
}
}
And finally call that db function in your fragment's onViewCreated to activate all of this. If you do not have onViewCreated just paste this code:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
FirestoreClass().getUsersList(this)
}
All of this can be too much, but this is the best practice. If you learn this convention, you will easily adapt working anywhere.
In my fragment I have a RecyclerView, which displays results from the query I enter in options menu. It is an API from which I receive TV-shows list.
The query needs string with a len of 3 at least. When it's 1 or 2 the adapter is cleared.
override fun onQueryTextChange(newText: String?): Boolean {
if (newText != null && newText.length > 2) {
if (!newText.isNullOrBlank() && newText.length > 2)
viewModel.searchMovies(newText)
}
else {
adapter.setMoviesList(emptyList())
}
return true
}
However, I encountered an issue after entering e.g. "cat" twice. I received a list of shows having cat in it. After removing query from optionmenu and taping it again the adapter was empty. And there was no same search. For me -> because the flow value didn't change.
In ViewModel I have:
private val _moviesStateFlow = MutableStateFlow<List<TvMazeShowResponse>>(emptyList())
val moviesStateFlow = _moviesStateFlow as StateFlow<List<TvMazeShowResponse>>
fun searchMovies(query: String) {
viewModelScope.launch {
val response = api.getApiResponse(query)
_moviesStateFlow.emit(response)
}
}
And this StateFlow I collect in fragment.
lifecycleScope.launch {
viewModel.moviesStateFlow.collect {
adapter.setMoviesList(it)
}
}
To fix the problem I added another function in VM
fun clearFlow() {
viewModelScope.launch {
_moviesStateFlow.emit(emptyList())
}
}
And now in the fragment in onQueryTextChange in else I added.
else {
adapter.setMoviesList(emptyList())
viewModel.clearFlow()
}
Now it works as expected. But is there a better way to achieve that?
To make your code less convoluted, avoid doing logic in your UI classes (Fragment/Activity/Adapter) and make your ViewModel provide the single source of truth.
override fun onQueryTextChange(newText: String?): Boolean {
viewModel.searchMovies(newText.orEmpty())
return true
}
// In ViewModel
fun searchMovies(query: String) {
val trimmedQuery = query.trim()
viewModelScope.launch {
val response = if (trimmedQuery.length <= 2) emptyList() else api.getApiResponse(trimmedQuery)
_moviesStateFlow.emit(response)
}
}
To avoid running multiple obsolete queries if the user is typing quickly, I suggest cancelling previous searches when starting new ones.
private val searchJob? = null
fun searchMovies(query: String) {
val trimmedQuery = query.trim()
searchJob?.cancel()
searchJob = viewModelScope.launch {
val response = if (trimmedQuery.length <= 2) emptyList() else api.getApiResponse(trimmedQuery)
_moviesStateFlow.emit(response)
}
}
I am pulling some data from the firestore and after that, I want to filter it before adding it to the recyclerView. Is that possible?
The data that I have pulled is an ArrayList which has a field called 'order_status'. It may contain many different statuses, but I want to filter it out so that I will be left with only "Pending", "Order Received", "In Process", "Packed".
The following code is used to pull the data from the firestore
fun getOrderStatusList(fragment: OrdersByStatusFragment) {
mFireStore.collection("orders")
.whereIn(
"address.pinCode",
listOf("676767", "652365","679577")
)
.get()
.addOnSuccessListener { document ->
val list: ArrayList<OrderStatus> = ArrayList()
for (i in document.documents) {
val orderStatus = i.toObject(OrderStatus::class.java)!!
orderStatus.id = i.id
list.add(orderStatus)
}
fragment.successOrderStatusList(list)
}
.addOnFailureListener {
fragment.hideProgressDialog()
}
}
The following code is part of the fragment.
fun successOrderStatusList(orderStatusList: ArrayList<OrderStatus>) {
hideProgressDialog()
if (orderStatusList.size > 0) {
rv_order_by_status.visibility = View.VISIBLE
tv_no_orders_by_status_found.visibility = View.GONE
rv_order_by_status.layoutManager = LinearLayoutManager(activity)
rv_order_by_status.setHasFixedSize(true)
val orderStatusListAdapter =
OrderStatusListAdapter(requireActivity(), orderStatusList,this#OrdersByStatusFragment)
rv_order_by_status.adapter = orderStatusListAdapter
} else {
rv_order_by_status.visibility = View.GONE
tv_no_orders_by_status_found.visibility = View.VISIBLE
}
}
In your case, you should just check for status inside the loop.
F.e:
if(document.orderStatus == "Pending")
//addTolist
I am trying to get the SUM of all transaction amounts from my TransactionDatabase but it's always returning null.
Thanks for any help!!
This is my fragment
val transactionViewModelSum = ViewModelProvider(
requireActivity(),
TransactionViewModelFactory(requireActivity().application))
.get(TransactionViewModel::class.java)
transactionViewModelSum.getTransactionByDate().observe(viewLifecycleOwner, Observer {
totalAmount = it.div(10)
})
if(totalAmount == null ) binding.cpbMainExpenses.progress = 15f else binding.cpbMainExpenses.progress = totalAmount!!.toFloat()
My DAO
#Query("SELECT total(amount) FROM `Transaction`")
fun getTransactionByDate(): LiveData<Double>
My Repository
fun getTransactionByDate(): LiveData<Double> {
return transactionDao.getTransactionByDate()
}
My View Model
private val liveTransactionDate = repository.getTransactionByDate()
...
fun getTransactionByDate(): LiveData<Double> = liveTransactionDate
Your Query looks like fine. I think problem in async working of this transactionViewModelSum.getTransactionByDate().observe() code.
Try to put if(totalAmount == null ) binding.cpbMainExpenses.progress = 15f else binding.cpbMainExpenses.progress = totalAmount!!.toFloat() in observer lambda like bellow:
transactionViewModelSum.getTransactionByDate().observe(viewLifecycleOwner, Observer {
totalAmount = it.div(10)
if(totalAmount == null ) binding.cpbMainExpenses.progress = 15f else binding.cpbMainExpenses.progress = totalAmount!!.toFloat()
})
I think that you try to read value of totalAmount before getTransactionByDate emits a value. If if I'am not right please write me about it in comment.
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)