2

Hey I am learning stateflow in android kotlin. I am creating a reverse conversation calendar view with the help of reyclerview. In my mainactivity there is one fragment, and inside that I have reyclerview. My goal to is doing paging stuff in my recyclerview so I am loading few months first than adding more and more data in my reyclerview by the help of this answer. I achieved it successfully. But the problem is when I reached the threshold and it trigger the new data . My whole list is refershed and it not being updated it removed previous data. I don't understand why this happening in MutableStateFlow and also I am new in this flow. My Github project link is here. Please guide me what I am doing wrong here. My Goal is to prepend the stateFlow data. Thanks

ConversationFragment.kt

class ConversationFragment(val customManager: CustomManager) : Fragment() {

    companion object {
        private const val DATA = "data"
        fun create(
            data: List<ConversationDate>,
            customManager: CustomManager
        ): ConversationFragment {
            val fragment = ConversationFragment(customManager)
            val bundle = Bundle()
            bundle.putParcelableArrayList(
                DATA,
                ArrayList<Parcelable>(data)
            )
            fragment.arguments = bundle
            return fragment
        }
    }

    private var _binding: ConversationFragmentLayoutBinding? = null
    private val binding get() = _binding!!
    private val visibleThreshold = 7
    private var previousTotalItemCount = 0
    private var loading = true
    private val weeklyAdapter = ConversationWeeklyAdapter()
    private val viewModel by viewModels<FragmentViewModel>()

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        setupViewModel()
        _binding = ConversationFragmentLayoutBinding.inflate(inflater, container, false)
        setupView()
        return binding.root
    }

    private fun setupViewModel() {
        viewModel.data = arguments?.getParcelableArrayList(DATA)
        viewModel.startDate = customManager.startDate()
        viewModel.endDate = customManager.endDate()
    }

    private fun setupView() {
        viewModel.weeklyFormatData()

        activity?.lifecycleScope?.launchWhenCreated {
            activity?.repeatOnLifecycle(Lifecycle.State.CREATED) {
                viewModel.prepareMutableStateFlow.collectLatest {
                    weeklyAdapter.submitList(it)
                }
            }
        }

        binding.conversationView.apply {
            adapter = weeklyAdapter
            layoutManager = LinearLayoutManager(context).apply {
                orientation = LinearLayoutManager.HORIZONTAL
                stackFromEnd = true
            }

            addOnScrollListener(object : RecyclerView.OnScrollListener() {

                override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                    super.onScrolled(recyclerView, dx, dy)
                    val firstVisibleItemPosition =
                        (recyclerView.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
                    val totalItemCount = recyclerView.adapter?.itemCount

                    if (totalItemCount != null) {
                        if (totalItemCount < previousTotalItemCount) {
                            previousTotalItemCount = totalItemCount
                            if (totalItemCount == 0) {
                                loading = true
                            }
                        }
                    }

                    if (totalItemCount != null) {
                        if (loading && (totalItemCount > previousTotalItemCount)) {
                            loading = false
                            previousTotalItemCount = totalItemCount
                        }
                    }

                    if (!loading && firstVisibleItemPosition < visibleThreshold) {
                        customManager.loadMoreData()
                        loading = true
                    }

                }
            })
        }

    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

    fun loadMoreData(item: List<ConversationDate>) {
        viewModel.startDate =
            SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse("2021-11-01")
        viewModel.endDate =
            SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse("2021-11-30")
        val previousData = viewModel.data
        if (previousData != null) {
            viewModel.data = previousData + item
        }
        viewModel.weeklyFormatData()
    }
}

FragmentViewModel.kt

class FragmentViewModel(app: Application) : AndroidViewModel(app) {

    var data: List<ConversationDate>? = null
    var startDate: Date? = null
    var endDate: Date? = null
    private val dateRange: MutableList<Date>
        get() {
            val datesInRange = mutableListOf<Date>()
            val tempCalendar = Calendar.getInstance()
            tempCalendar.time = startDate as Date
            while (tempCalendar.time <= endDate) {
                datesInRange.add(tempCalendar.time)
                tempCalendar.add(Calendar.DATE, 1)
            }
            return datesInRange
        }
    val prepareMutableStateFlow: MutableStateFlow<List<ConversationCount>> =
        MutableStateFlow(emptyList())

    fun weeklyFormatData() {
        val conversationCount = mutableListOf<ConversationCount>()
        viewModelScope.launch {
            dateRange.forEachIndexed { index, dateRangeValue ->
                val findData = data?.find {
                    dateRangeValue == it.dateObject
                }
                conversationCount.add(
                    ConversationCount(
                        setDay(dateRangeValue),
                        index,
                        findData != null
                    )
                )
            }
            prepareMutableStateFlow.value = conversationCount
        }
    }

    private fun setDay(date: Date): String? {
        return SimpleDateFormat("dd", Locale.getDefault()).format(date)
    }
}

My mock api response link

Problem Description

When the application is load the fragment screen will appear and showing some dates on it. I set startDate as 2021-12-01 and endDate as today date i.e. 2022-01-13. So when starting scroll and reach to threshold of loading more data. I passed than startDate as 2021-11-01 and endDate as 2021-11-30. It added to the list but previous data is remove. I don't understand why this is happening. I am attaching my YouTube link

Kotlin Learner
  • 3,995
  • 6
  • 47
  • 127

1 Answers1

1

It's a ton of code to go through and I can't figure out everything you're trying to do. You didn't show what CustomManager.loadMoreData() does. But I'm assuming this is a list that loads additional items when you scroll to the bottom? If you step through the logic of the onScrolled function, it doesn't seem to make sense. Like why would totalItemCount ever be smaller than previousTotalItemCount?

If you scroll down to @Minh Nguyen's answer on the question you linked, it is a much simpler solution. I would take it a step further and create a helper class so you can keep your Fragment code clean. I added a feature where it automatically registers to reset itself when new data arrives in the adapter. Note, I haven't tested this class!

class BottomReachedListener(
    var onBottomReached: () -> Unit = {}
) : RecyclerView.OnScrollListener() {
    private var isTriggered = false
    private var registeredAdapter: RecyclerView.Adapter<*>? = null
    private val observer = object: RecyclerView.AdapterDataObserver() {
        override fun onChanged() = reset()
    }
    
    fun reset() {
        isTriggered = false
    }

    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
        if (isTriggered) {
            return
        }
        val layoutManager = recyclerView.layoutManager as? LinearLayoutManager
            ?: error("No LinearLayoutManager")
        val totalItems = layoutManager.itemCount
        val lastVisibleItem = layoutManager.findLastVisibleItemPosition()

        if (lastVisibleItem == totalItems - 1) {
            registerWithAdapter(recyclerView)
            isTriggered = true
            onBottomReached()
        }
    }

    private fun registerWithAdapter(recyclerView: RecyclerView) {
        registeredAdapter?.unregisterAdapterDataObserver(observer)
        registeredAdapter = recyclerView.adapter
        registeredAdapter?.registerAdapterDataObserver(observer)
    }
}

Now in your Fragment all you need to do is set an instance of this class as your scroll listener and pass it a callback to invoke when new data needs to be loaded:

addOnScrollListener(BottomReachedListener {
    customManager.loadMoreData() // for example
})

I don't know if you have other problems in your downstream logic. It's just too much code to reason through without having the actual app to debug and knowing everything it's supposed to do.

Tenfour04
  • 83,111
  • 11
  • 94
  • 154
  • Sorry I added the wrong link. Instead of going down. My recyclerview is working in reverse order `StackFromEnd` is `true`. So I need to go to top of list then I am fetching more data. Thanks – Kotlin Learner Jan 18 '22 at 16:29