Paging 3 with multiSelect and filtering android

52 Views Asked by At

I want to implement the following scenario. I want to have a Paging3 recyclerView with checkboxes and multiselect items and also apply a filter on the request which should trigger the refresh of the data.

I have a request to fetch some transactions and in the request i pass a dateInterval object

DateInterval(
    dateFrom: Date?, 
    dateTo: Date = Calendar.getInstance().time  // which is always the current datetime
)

DateFrom can be chosen through a filter by a list of a RadioButtons from a BottomSheet enter image description here

By default Current month is chosen. So the PagingSource is as the following


class AvailableTransactionsPagingSource(
    ...data...
) : PagingSource<PagingInfoDomainModel, DiffUiItem>() {

    override fun getRefreshKey(state: PagingState<PagingInfoDomainModel, DiffUiItem>): PagingInfoDomainModel? =
        null

    override suspend fun load(params: LoadParams<PagingInfoDomainModel>): LoadResult<PagingInfoDomainModel, DiffUiItem> {
        val nextPageKey = params.key ?: createDefaultPagingInfo()

        when (
            getTransactionsUseCase(...data...)
        ) {
            is DomainResult.Error -> {
                return showEmptyTransactions()
            }

            is DomainResult.Success -> {
                if (params.key == null && response.data.availableTransactions.isEmpty())
                    return showEmptyTransactions()

                val list = mutableListOf<TransactionWithCheckbox>()
                response.data.availableTransactions.forEach {
                    list.add(
                        TransactionWithCheckbox(
                            uniqueId = it.internalId ?: "",
                            title = "it.description",
                            subtitle = it.transactionTimestampLocal,
                            isChecked = false
                        )
                    )
                }

                return LoadResult.Page(
                    data = list,
                    prevKey = null,
                    nextKey = getNextKey(response.data.pageInfo)
                )

            }

            null -> {
                return showEmptyTransactions()
            }
        }
    }

    private var dateInterval = DatePeriodDomainModel(
        dateFrom = null,
        dateTo = Calendar.getInstance().time
    )
    fun setDate(interval: DatePeriodDomainModel) {
        dateInterval = interval
    }


    override val keyReuseSupported: Boolean
        get() = true

    ....

Here i create a list of TransactionWithCheckbox models. The field isChecked will determine if the checkBox inside the onBind will be checked or not. So at the beginning all the checkboxes should be unchecked, thus i pass a false value. The dateInterval holds the dateInterval that i want the requests to be made with.

Inside the ViewModel i create the PagingSource instance and pass it the to the Pager which i collectLatest in my Fragment


@HiltViewModel
class TransactionViewModel @Inject constructor(
    ...data...
) : BaseViewModel() {

    private val _hasNextButtonEnabled = MutableStateFlow(false)
    override val hasNextButtonEnabled = _hasNextButtonEnabled.asStateFlow()


    var timeWindowRadioButtonList = buildTimeWindowList(resources)
    private val _selectedTimeWindow: MutableStateFlow<RadioButtonUiItem?> = MutableStateFlow(timeWindowRadioButtonList.first())
    val selectedTimeWindow = _selectedTimeWindow.asStateFlow()

    private var datePeriod = currentMonthsTransaction()

    val checkedItems = MutableStateFlow<Set<TransactionWithCheckbox>>(setOf())
    var availableTransactionsPagingSource: availableTransactionsPagingSource? = null

    val loadingPagerFlow = Pager(
        config = PagingConfig(
            pageSize = PAGE_SIZE,
            enablePlaceholders = true
        ),
        pagingSourceFactory = {
            getPagingSource()
        }
    ).flow
        .cachedIn(viewModelScope)

        // Combine flow pagingData with checkedItems in order to
        // keep the selected items when PagingSource reloads data
        .combine(checkedItems) { pagingData, checkedSet ->
            pagingData.map { item ->
                if (item is TransactionWithCheckbox)
                    item.isChecked = checkedSet.find { item.uniqueId == it.uniqueId } != null

                item
            }
        }

    /**
     *  Caution!!
     *  checkedItems.value holds a Set of the user-selected models
     *  However, we cannot find an item of the list inside the Set since the
     *  PagingAdapter reloads the items holding different instances of the same data
     *  Thus, we can find-modify-access the desired items through the uniqueId comparison
     */
    val onCheckBoxHandler = object : CheckBoxHandler {
        override fun onItemChecked(item: RowUiItem, isChecked: Boolean) {
            if (item is TransactionWithCheckbox) {
                val set = checkedItems.value.toMutableSet()
                if (isChecked) {
                    val exists = checkedItems.value.find { it.uniqueId == item.uniqueId } != null
                    if (!exists) set.add(item)
                } else {
                    val existing = checkedItems.value.find { it.uniqueId == item.uniqueId }
                    set.remove(existing)
                }
                checkedItems.value = set
                
                validateNextButton()
            }
        }
    }

    

    private fun getPagingSource(): PagingSource<PagingInfoDomainModel, DiffUiItem> {
        availableTransactionsPagingSource = AvailableTransactionsPagingSource(...)
        availableTransactionsPagingSource?.setDate(datePeriod)
        return disputeAvailableTransactionsPagingSource!!
    }


    // !!!User just Selected or Unselected a date Filter!!!
    // Reset values and set datePeriod value for the new pagingSource request

    fun timeWindowSelected(selectedTimeWindow: TimeWindowRowUI?) {
        when (selectedTimeWindow?.id) {
            TIME_WINDOW_CURRENT_MONTH -> {
                checkedItems.value = setOf()
                datePeriod = currentMonthsTransaction()
                _selectedTimeWindow.value = timeWindowRadioButtonList[0]
            }

            TIME_WINDOW_LAST_3_MONTHS -> {
                checkedItems.value = setOf()
                datePeriod = previousMonthsTransactions(months = -3)
                _selectedTimeWindow.value = timeWindowRadioButtonList[1]
            }

            TIME_WINDOW_LAST_6_MONTHS -> {
                checkedItems.value = setOf()
                datePeriod = previousMonthsTransactions(months = -6)
                _selectedTimeWindow.value = timeWindowRadioButtonList[2]
            }

            TIME_WINDOW_LAST_12_MONTHS -> {
                checkedItems.value = setOf()
                datePeriod = getPreviousYearsTransactions(years = -1)
                _selectedTimeWindow.value = timeWindowRadioButtonList[3]
            }
            null -> {
                checkedItems.value = setOf()
                datePeriod = getUnlimitedPreviousTransactions()
                _selectedTimeWindow.value = null
                timeWindowRadioButtonList.map { it.checked = false }
            }
        }


        availableTransactionsPagingSource?.setDate(datePeriod)
        validateNextButton()
    }

    private fun validateNextButton() {
        _hasNextButtonEnabled.value = checkedItems.value.isNotEmpty()       
    }

}

So inside my fragment i observe the pagingFlow and submit it to the adapter.

 private val onTimeWindowApplyClickListener = object : OnRadioBottomSheetButtonClick {
        override fun onButtonClick(item: RadioButtonUiItem) {
            if (item is TimeWindowRowUI) {
                // Here i want to refresh the whole data, 
                // and reset the checkboxes to the false state
                viewModel.timeWindowSelected(item)
                adapter.refresh()   // ???? Is it correct
            }
        }
    }


 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        setUpToolbar()

        viewBinding.transactionRecycler.adapter = adapter
        adapter.addLoadStateListener {
            viewModel.updateListLoadingState(it.source.refresh)
        }

        viewModel.loadingPagerFlow.collectLatest(viewLifecycleOwner) { pagingData ->
            adapter.submitData(pagingData)
        }

        // Depending on the selected filter we appear or disappear the filter Chip
        viewModel.selectedTimeWindow.collect(viewLifecycleOwner) {
            it?.let {
                viewBinding.transactionFilter.containerFilterChip.isVisible = true
                viewBinding.transactionFilter.textFilterName.text = it.getText()
                viewBinding.transactionFilter.buttonFilterCross.isVisible = true
            } ?: run {
                viewBinding.transactionFilter.containerFilterChip.isVisible = false
            }
        }

        // Click on filterChip sets the dateFrom = null
        viewBinding.filter.buttonFilterCross.setOnSafeClickListener {
            viewModel.timeWindowSelected(null)
            viewModel.availableTransactionsPagingSource?.invalidate()  // ??? correct?  
        }

        viewBinding.imageFilter.setOnSafeClickListener {
            openTimeWindowBottomSheet(viewModel.timeWindowRadioButtonList)
        }

    }

So lets say that i choose the first 2 transactions enter image description here

After i select a 12 month Filter, the recycler supposed to refresh the data with the checkedItems = setOf() and show the new data with isChecked = false. However, this happens

enter image description here

The request has been done, the NextButton is disabled (since the checkedItems is an empty list) but the checkboxes are still checked.

Any ideas how can i fix this issue???

0

There are 0 best solutions below