Zar E Ahmer
Zar E Ahmer

Reputation: 34380

implmenent Paging Library 3.0 Filter/Search functionality

Using paging 3.0 , I am successful in implemented it. Now I want to add search functionality to it.

I simply display photo gallery along with paging functionality. Now I want to invalidate pagination when someone search

But whenever I call invalidate on search. App crashes..

PhotoFragment.kt

@AndroidEntryPoint
class PhotosFragment : BaseFragment<FragmentPhotosBinding,PhotosFragmentViewModel>(R.layout.fragment_photos),
    SearchView.OnQueryTextListener, LifecycleObserver {
    override val mViewModel: PhotosFragmentViewModel by viewModels()

    private lateinit var photoAdapter: PhotoCollectionAdapter

    override fun onAttach(context: Context) {
        super.onAttach(context)
        activity?.lifecycle?.addObserver(this)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        setHasOptionsMenu(true)
        ///mViewModel.setFilter(getString(R.string.search_filter_default_value))
        initAdapter()
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    fun onCreated(){
        mViewModel.trendingPhotos.observe(viewLifecycleOwner, Observer {
            photoAdapter.submitData(lifecycle,it)
        })
    }

    private fun initAdapter() {
        photoAdapter = PhotoCollectionAdapter()
        photoAdapter.stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY

        mBinding.recyclerView.apply {
            layoutManager = LinearLayoutManager(context)
            setHasFixedSize(true)
            adapter = photoAdapter
        }

        photoAdapter.addLoadStateListener { loadState ->
            mBinding.recyclerView.isVisible = loadState.refresh is LoadState.NotLoading

            val errorState = loadState.source.append as? LoadState.Error
                ?: loadState.source.prepend as? LoadState.Error
                ?: loadState.append as? LoadState.Error
                ?: loadState.prepend as? LoadState.Error
            errorState?.let {
            }
        }
    }

    var timer: CountDownTimer? = null
    override fun onQueryTextSubmit(p0: String?): Boolean = false
    override fun onQueryTextChange(newText: String?): Boolean {

        timer?.cancel()
        timer = object : CountDownTimer(1000, 2500) {
            override fun onTick(millisUntilFinished: Long) {}
            override fun onFinish() {
                Timber.d("query : %s", newText)
                if (newText!!.trim().replace(" ", "").length >= 3) {
                    mViewModel.cachedFilter = newText
                    mViewModel.setFilter(newText)
                }
                ///afterTextChanged.invoke(editable.toString())
            }
        }.start()

        return true
    }

    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        super.onCreateOptionsMenu(menu, inflater)
        inflater.inflate(R.menu.search_menu, menu)

        // Get the SearchView and set the searchable configuration
        val searchManager = activity?.getSystemService(Context.SEARCH_SERVICE) as SearchManager
        //val searchManager = activity!!.getSystemService(Context.SEARCH_SERVICE) as SearchManager
        (menu.findItem(R.id.app_bar_search).actionView as SearchView).apply {
            // Assumes current activity is the searchable activity
            setSearchableInfo(searchManager.getSearchableInfo(activity?.componentName))
            setIconifiedByDefault(false) // Do not iconify the widget; expand it by default
            queryHint = getString(R.string.search_view_hint)
            setQuery(
                if (mViewModel.cachedFilter.isEmpty()) getString(R.string.search_filter_default_value) else mViewModel.cachedFilter,
                true
            )
            isSubmitButtonEnabled = true
        }.setOnQueryTextListener(this)
    }
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return view?.let {
            NavigationUI.onNavDestinationSelected(item,it.findNavController())
        }?: kotlin.run {
            super.onOptionsItemSelected(item)
        }
    }
}

PhotosFragmentViewModel.kt

@HiltViewModel
class PhotosFragmentViewModel @Inject constructor(
    private val photoPagingSourceRx: PhotoPagingSourceRx
): BaseViewModel() {

    private val _trendingPhotos = MutableLiveData<PagingData<Models.PhotoResponse>>()
    val trendingPhotos: LiveData<PagingData<Models.PhotoResponse>>
    get() = _trendingPhotos
    var cachedFilter: String = ""

    fun setFilter(filter: String) {
        photoPagingSourceRx.setFilter(if (cachedFilter.isEmpty()) filter else cachedFilter)
    }

    init {
        viewModelScope.launch {
            getPhotosRx().cachedIn(viewModelScope).subscribe {
                    _trendingPhotos.value = it
            }
        }
    }

    private fun getPhotosRx(): Flowable<PagingData<Models.PhotoResponse>> {
        return Pager(
            config = PagingConfig(
                pageSize = 20,
                enablePlaceholders = false,
                prefetchDistance = 5
            ),
            pagingSourceFactory = { photoPagingSourceRx }
        ).flowable
    }
}

PhotoPagingSourceRx.kt

@Singleton
class PhotoPagingSourceRx @Inject constructor(
    private val restApi: RestApi
): RxPagingSource<Int, Models.PhotoResponse>() {

    private var filter: String = "Flowers"
    private var lastFilter = filter
    fun setFilter(filter: String) {
        this.filter = filter
    }

    override fun loadSingle(params: LoadParams<Int>): Single<LoadResult<Int, Models.PhotoResponse>> {
        val page = if(lastFilter == filter) params.key ?: 1 else 1
        lastFilter = filter

        return restApi.getPhotos(filter,20,page).subscribeOn(Schedulers.io()).map {

            Log.v("pagingLog","page -> $page ) ")
            LoadResult.Page(
                data = it.response,
                prevKey = if (page == 1) null else page - 1,
                nextKey = page + 1
            ) as LoadResult<Int, Models.PhotoResponse>
        }.onErrorReturn {
            LoadResult.Error(it)
        }
    }

    override fun getRefreshKey(state: PagingState<Int, Models.PhotoResponse>): Int? {
        return state.anchorPosition
    }
}

Upvotes: 1

Views: 5280

Answers (1)

dlam
dlam

Reputation: 3905

I didn't get a chance to look at your crash yet, getting invalidation working is definitely important as a single instance of PagingSource is meant to represent an immutable snapshot and invalidate when it changes (so setting filter dynamically does not work well here).

Instead try this approach since it looks like you need to pass filter to network api:

ViewModel.kt

val filterFlow = MutableStateFlow<String>("")
val pagingDataFlow = filterFlow.flatMapLatest { filter ->
  Pager(...) {
    PhotoPagingSourceRx(restApi, filter)
  }.flow
}.cachedIn(viewModelScope)

PhotoPagingSourceRx (btw, this cannot be a singleton)

class PhotoPagingSourceRx constructor(
    private val restApi: RestApi,
    private val filter: String,
): RxPagingSource<Int, Models.PhotoResponse>() {

    override fun loadSingle(..): Single<LoadResult<Int, Models.PhotoResponse>> { ... }

    override fun getRefreshKey(..): Int? { ... }
}

Upvotes: 5

Related Questions