Solution 1 :
LiveData is designed to hold the data and every time you subscribe to it returns the data it currently has. Once you come back to your fragment the data that is already held in the LiveData is passed back again.
You can solve this in a couple of different ways:
You can use a SingleLiveEvents to wrap your list and check if the data is used every time you receive new data inside your fragment. If it’s not used means that is fresh new data coming from the ViewModel. I use something like this:
class SingleLiveData<T>(dataToBeConsumed: T? = null) {
private var _data: T? = dataToBeConsumed
val isConsumed
get() = _data == null
fun consumeData(): T {
val curData = _data!!
_data = null
return curData
}
fun consumeDataSafely(): T? {
val curData = _data
_data = null
return curData
}
}
This will cause to modify the ViewModel and have this instead:
val mutableLiveData = MutableLiveData<SingleLiveData<List<News>>>()
And change the way you populate the data like
viewModel.mutableLiveData.value = SingleLiveData(newsList)
In your code, you will check if the data isConsumed
before updating the RecyclerView.
//observe to the viewModel
viewModel.getNewsList(Timestamp.now()).observe(viewLifecycleOwner, Observer {
// Now it is SingleLiveData<List>
if (!it. isConsumed)
newsAdapter.submitList(it.consumeData())
})
You can browse more about the topic: https://proandroiddev.com/singleliveevent-to-help-you-work-with-livedata-and-events-5ac519989c70
Another approach is to use DiffUtil when updating your recyclerView this will cause only to update new objects and not have duplicates.
Ref: https://blog.mindorks.com/the-powerful-tool-diff-util-in-recyclerview-android-tutorial
Unrelated to your problem, I advise not to hold ViewModel reference in your FirebaseRepo but return the data using a callback lambda function. You are creating a cyclic dependency which can cause bugs and problems in your app.
Solution 2 :
The problem is in your destructuring:
Everytime Observer called your ViewModel is re-fetching data from firebase and holding it in variable called mutableLivedata as you defined.
You need to observe mutableLiveData for your recyclerView and call getNewsItem() inside init function as shown below:
ViewModel.kt
val mutableLiveData = MutableLiveData<List<News>>()
fun getNewsList(tm:Timestamp) {
repo.getNewsData(tm)
}
init {
getNewsList(Timestamp.now())
}
MainActivity.kt
viewModel.mutableLiveData.observe(viewLifecycleOwner, Observer {
newsAdapter.submitList(it)
})
Happy Coding..
Problem :
I am using Firebase firestore pagination and ViewModel class in my project, I have setup an onScroll listener for recyclerview, and fetching data on scroll but when i Navigate to another fragment and back to the main fragment, the whole items are duplicated, How can i fix this issue??
Here is my code
NewsViewModel.kt
class NewsViewModel : ViewModel() {
private val repo = FirebaseRepo(this)
val mutableLiveData = MutableLiveData<List<News>>()
fun getNewsList(tm:Timestamp): LiveData<List<News>> {
repo.getNewsData(tm)
return mutableLiveData
}
}
Repository.kt
class FirebaseRepo(private val viewModel: NewsViewModel) {
private val db = FirebaseFirestore.getInstance().collection("news")
fun getNewsData(tm: Timestamp) {
val newsList = ArrayList<News>()
if(viewModel.mutableLiveData.value != null) {
newsList.addAll(viewModel.mutableLiveData.value!!)
}
db
.orderBy("timestamp", Query.Direction.DESCENDING)
.whereLessThan("timestamp",tm)
.limit(6)
.get()
.addOnSuccessListener {
Log.i("CodeCamp", it.toString())
for (doc in it) {
val imgUrl = doc.getString("imageUrl")
val heading = doc.getString("headline")
val timestamp = doc.getTimestamp("timestamp")
val tagline = doc.getString("tagline")
val type = doc.getString("type")
newsList.add(News(doc.id, imgUrl!!, heading!!, tagline!!, type!!, timestamp!!))
}
viewModel.mutableLiveData.value = newsList
}
}
}
MainActivity.kt
viewModel = ViewModelProvider(this).get(NewsViewModel::class.java)
val layoutManager = LinearLayoutManager(view.context)
recyclerView.layoutManager = layoutManager
recyclerView.adapter = newsAdapter
recyclerView.addItemDecoration(DividerItemDecoration(view.context, RecyclerView.VERTICAL))
//observe to the viewModel
viewModel.getNewsList(Timestamp.now()).observe(viewLifecycleOwner, Observer {
newsAdapter.submitList(it)
})
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val visibleItemCount = layoutManager.childCount
val totalItemCount = layoutManager.itemCount
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
if ((visibleItemCount + firstVisibleItemPosition) >= totalItemCount
&& firstVisibleItemPosition >= 0
&& totalItemCount >= PAGE_SIZE && !isLoading
) {
isLoading != isLoading
val list = viewModel.mutableLiveData.value!!
viewModel.getNewsList(list[list.size - 1].timestamp).value
Handler().postDelayed({
isLoading != isLoading
},2000)
}
}
})
My Adapter
class NewsAdapter : ListAdapter<News, NewsAdapter.ViewHolder> (NEWS_COMPARATOR) {
companion object {
private val NEWS_COMPARATOR = object : DiffUtil.ItemCallback<News>() {
override fun areItemsTheSame(old: News, new: News): Boolean = old.id == new.id
override fun areContentsTheSame(old: News, new: News): Boolean = old == new
}
}
class ViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
fun bindView(news: News) {
Glide.with(view).load(news.imageUrl).into(itemView.img)
itemView.news_title.text = news.heading
itemView.news_src.text = news.tagline
itemView.news_type.text = news.type
itemView.news_time.text = DateTime.getTimeAgo(news.timestamp.seconds)
itemView.setOnClickListener {
it.findNavController().navigate(R.id.action_homeFragment_to_newsFragment, bundleOf("id" to news.id))
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_news,parent,false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val news = getItem(position)
holder.bindView(news)
}
}
Comments
Comment posted by nirazverma
can u write the full code, please because i am new in SingleLiveData and the documentation you shared is written in java language please..
Comment posted by Viktor Petrovski
The SingleLiveData implementation is enough to get you started. I’ve edited my answer to show an example of how to observe the data. Let me know if you need any other assistance.
Comment posted by nirazverma
onScroll Recylerviewonscroll listener val list = viewModel.mutableLiveData.value!!.consumeData() viewModel.getNewsList(list[list.size – 1].timestamp) i am getting error kotlin.KotlinNullPointerException
Comment posted by nirazverma
and also where to use consumeDataSafely() and isConsumed?
Comment posted by Viktor Petrovski
I just noticed that you are calling getNewList from inside onScroll. You should change this approach and access the data that is already in the RecyclerView. You can get the last timestamp using: val lastItem = newsAdapter.items[newsAdapter.items.size – 1] (Where items is the ArrayList inside your adapter) You can ignore consumeDataSafely() for now, that is just the null check if you are not sure if your data is null or not. isConsumed is used before calling the newsAdapter as shown in the example if (!it. isConsumed) newsAdapter.submitList(it.consumeData()) @nirazverma