0

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

}
nirazverma
  • 1,001
  • 10
  • 15

2 Answers2

2

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.

Viktor Petrovski
  • 796
  • 5
  • 14
  • can u write the full code, please because i am new in SingleLiveData and the documentation you shared is written in java language please.. – nirazverma Aug 07 '20 at 09:20
  • 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. – Viktor Petrovski Aug 07 '20 at 09:33
  • onScroll Recylerviewonscroll listener val list = viewModel.mutableLiveData.value!!.consumeData() viewModel.getNewsList(list[list.size - 1].timestamp) i am getting error kotlin.KotlinNullPointerException – nirazverma Aug 07 '20 at 09:50
  • and also where to use consumeDataSafely() and isConsumed? – nirazverma Aug 07 '20 at 10:10
  • 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 – Viktor Petrovski Aug 07 '20 at 11:00
  • i can't use val lastItem = newsAdapter.items[newsAdapter.items.size - 1] because i am using DiffUtil in adapter – nirazverma Aug 07 '20 at 12:12
  • How is that stopping you from accessing the items inside the adapter? Even if you are using DiffUtil which is not shown in the example, the items should be in the adapter – Viktor Petrovski Aug 07 '20 at 12:28
  • E/AndroidRuntime: FATAL EXCEPTION: main Process: com.morchhattisgarh.india, PID: 23329 kotlin.KotlinNullPointerException at com.morchhattisgarh.india.data.SingleLiveData.consumeData(SingleLiveData.kt:12) at com.morchhattisgarh.india.fragments.HomeFragment$onViewCreated$2.onScrolled(HomeFragment.kt:77) at androidx.recyclerview.widget.RecyclerView.dispatchOnScrolled(RecyclerView.java:4961) – nirazverma Aug 07 '20 at 13:37
  • edited post -> adapter code added in this post check – nirazverma Aug 07 '20 at 13:38
  • @nirazverma in your code you are calling newsAdapter.submitList(it), I don't see that in your news adapter – Viktor Petrovski Aug 07 '20 at 13:56
  • submitList is a built it function when we are using Diff.Utils in recyclerView aDAPTER – nirazverma Aug 07 '20 at 14:25
1

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..