Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
515 views
in Technique[技术] by (71.8m points)

android - 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
    }
}
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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 queryFlow = MutableStateFlow<String>("")
val pagingDataFlow = queryFlow.flatMapLatest { query ->
  Pager(...) {
    PhotoPagingSourceRx(query)
  }.flow
}.cachedIn(viewModelScope)

PhotoPagingSourceRx (btw, this cannot be a singleton)

class PhotoPagingSourceRx @Inject 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? { ... }
}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...