Reputation: 34380
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
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