implmenent Paging Library 3.0 Filter/Search functionality
Asked Answered
D

1

1

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
    }
}
Doorstone answered 16/2, 2021 at 10:38 Comment(4)
Can you share the stack trace of the crash?Photocomposition
@Photocomposition full source code is published github.com/IMDroidude/GalleryAppArchitecture/tree/feature/…Doorstone
I can't find where you're calling invalidate - it looks like you're just setting a property in PhotoPositionalDataSource when user types in search? You should actually do this using .switchMap on LivePagedList, passing search query into DataSource and trigger new generations that way.Photocomposition
Have you tested branch feature/paging_3 . Kindly switch to paging_3 and see PhotoPagingSourceRx.ktDoorstone
P
5

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? { ... }
}
Photocomposition answered 5/3, 2021 at 1:38 Comment(2)
I really don't understand how the injection is supposed to work in this example: the construction is expecting 2 parameters to be provided by injection but you are creating an instance of PhotoPagingSourceRx manually and with only one parameter. This could work with assisted injection but the code would be very different I think.Bucksaw
Sorry that's just an honest copy-paste mistake. I didn't check thoroughly enough before posting as I just wanted to reply with the overall idea. You would actually need to inject RestApi into the ViewModel and then pass both into the factory (or inject a factory and re-wrap). Thanks for calling this out! I've updated my answer with some more correct code.Photocomposition

© 2022 - 2024 — McMap. All rights reserved.