using android pagination library with items in Memory - android

i try to implement the pagination library , using rxJava , first of all , i call the NetworkApi to load the full data , then i want to use the pagiantion with the full loaded data, how can i do it with the library , i am trying to use the ItemKeyedDataSource Class , but please , do i need always to pass the element size to my api call or i can work only with the in memory loaded data ?
this is my api call :
public fun getMembersPagination(): MutableLiveData<ResultContainer<PagedList<MembersModel.Data.Member?>>> {
disposable = client.getMembersPaged()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.retry(0)
.subscribe(
{ result ->
onRetrieveUserData(result)
},
{ error -> onRetrieveUserDataError(error) }
)
return pagedList
}
i don't treat the pagiantion from my Api
this is the api :
#GET("members")
fun getMembersPaged(): Observable<PagedList<MembersModel.Data.Member?>>
ItemKeyedDataSource code :
class MembersPaginationDataSource(private val memberId: Int)
: ItemKeyedDataSource<Int, MembersModel.Data.Member?>() {
val client by lazy {
RetrofitClient.RetrofitClient()
}
var disposable: Disposable? = null
private var allMembers = MutableLiveData<PagedList<MembersModel.Data.Member?>>()
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<MembersModel.Data.Member?>) {
getMembersPagination().observe()
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<MembersModel.Data.Member?>) {
}
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<MembersModel.Data.Member?>) {
}
override fun getKey(item: MembersModel.Data.Member): Int = item.id!!
public fun getMembersPagination(): MutableLiveData<PagedList<MembersModel.Data.Member?>> {
disposable = client.getMembersPaged()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.retry(0)
.subscribe(
{ result ->
onRetrieveUserData(result)
},
{ error -> onRetrieveUserDataError(error) }
)
return allMembers
}
private fun onRetrieveUserData(membersModel: PagedList<MembersModel.Data.Member?>?) {
allMembers.postValue(membersModel)
}
private fun onRetrieveUserDataError(error: Throwable) {
allMembers.postValue(null)
}
}
i stop at that point

Related

Espresso Idling Resource setup while using RxJava in Paging library v2

I am trying to write an Espresso test while I am using Paging library v2 and RxJava :
class PageKeyedItemDataSource<T>(
private val schedulerProvider: BaseSchedulerProvider,
private val compositeDisposable: CompositeDisposable,
private val context : Context
) : PageKeyedDataSource<Int, Character>() {
private var isNext = true
private val isNetworkAvailable: Observable<Boolean> =
Observable.fromCallable { context.isNetworkAvailable() }
override fun fetchItems(page: Int): Observable<PeopleWrapper> =
wrapEspressoIdlingResource {
composeObservable { useCase(query, page) }
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Character>) {
if (isNext) {
_networkState.postValue(NetworkState.LOADING)
isNetworkAvailable.flatMap { fetchItems(it, params.key) }
.subscribe({
_networkState.postValue(NetworkState.LOADED)
//clear retry since last request succeeded
retry = null
if (it.next == null) {
isNext = false
}
callback.onResult(it.wrapper, params.key + 1)
}) {
retry = {
loadAfter(params, callback)
}
initError(it)
}.also { compositeDisposable.add(it) }
}
}
override fun loadInitial(
params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, Character>,
) {
_networkState.postValue(NetworkState.LOADING)
isNetworkAvailable.flatMap { fetchItems(it, 1) }
.subscribe({
_networkState.postValue(NetworkState.LOADED)
if (it.next == null) {
isNext = false
}
callback.onResult(it.wrapper, null, 2)
}) {
retry = {
loadInitial(params, callback)
}
initError(it)
}.also { compositeDisposable.add(it) }
}
}
Here is my wrapEspressoIdlingResource :
inline fun <T> wrapEspressoIdlingResource(task: () -> Observable<T>): Observable<T> = task()
.doOnSubscribe { EspressoIdlingResource.increment() } // App is busy until further notice
.doFinally { EspressoIdlingResource.decrement() } // Set app as idle.
But it does not wait until data delivered from network. When I Thread.Sleep before data delivered, Espresso test will be passed, so it is related to my Idling Resource setup.
I believe it could be related to Paging library, since this method works perfectly fine for Observable types when I use them in other samples without Paging library.
Full source code is available at : https://github.com/AliRezaeiii/StarWarsSearch-Paging
What am I missing?
I needed to override the fetchDispatcher on the builder :
class BasePageKeyRepository<T>(
private val scheduler: BaseSchedulerProvider,
) : PageKeyRepository<T> {
#MainThread
override fun getItems(): Listing<T> {
val sourceFactory = getSourceFactory()
val rxPagedList = RxPagedListBuilder(sourceFactory, PAGE_SIZE)
.setFetchScheduler(scheduler.io()).buildObservable()
...
}
}

How to force (return) to not work until after (retrofit) finished

hello I'm trying to study dataBinding, mvvm, retrofit and rxjava
in viewModel I used this code
private var mainRepository: MainRepository = MainRepository(NetManager(getApplication()))
val isLoading = ObservableField(false)
var mainModel = MutableLiveData<ArrayList<MainModel>>()
private val compositeDisposable = CompositeDisposable()
fun loadRepositories(id: Int, mainContract: MainContract) {
isLoading.set(true)
compositeDisposable += mainRepository
.getData(id, mainContract)
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(object : DisposableObserver<ArrayList<MainModel>>() {
override fun onError(e: Throwable) {
//if some error happens in our data layer our app will not crash, we will
// get error here
}
override fun onNext(data: ArrayList<MainModel>) {
mainModel.value= data
}
override fun onComplete() {
isLoading.set(false)
}
})
}
and in the MainRepository I used the retrofit with RxJava code
private val model = ArrayList<MainModel>()
fun getData(id: Int, mainContract: MainContract): Observable<ArrayList<MainModel>> {
Api.getData.getMainCategory(id)
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe ({
model.clear()
model.addAll(it)
AppLogger.log("testingModel1", model.toString())
}, {
AppLogger.log("error", "Failed to load Category : $it")
mainContract.toast("Failed to load Category")
})
AppLogger.log("testingModel2", model.toString())
return Observable.just(model)
}
if you notified that I'm using log to see the output data
but what I see is that
AppLogger.log("testingModel2", model.toString())
and
return Observable.just(model)
are running before
Api.getData.getMainCategory(id)
so the output in Logcat testingModel2 first and it is empty then testingModel1 and it is have data
so the result data in
return Observable.just(model)
is nothing
I hope you understand ^_^
Thank you for help
do like:
fun getData(id: Int, mainContract: MainContract): Observable<ArrayList<MainModel>> {
return Api.getData.getMainCategory(id)
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
}
but remember to subscribe to it later and add ErrorHandling
And about logs: the problem that actions in subscribe block runs only when Api.getData.getMainCategory(id) emit something, which could take a time.

request in another request called several times with rxJava and retrofit

I'm using MVVM and rxJava and retrofit to send my request.
I have a bottom navigation view which has 5 fragments and in one of them, I have to send a request and after it, the response is delivered, I have to send another request to my server.
this is my ViewModel class :
class MyViewModel: ViewModel() {
val compositeDisposable = CompositeDisposable()
val myFirstReqLiveData = MutableLiveData<myFirstReqModel>()
val mySecondReqLiveData = MutableLiveData<mySecondReqModel>()
fun getFirstReq(token:String){
val firstReqDisposable = RetrofitClientInstance.getRetrofitInterface()
.getFirstReq(token)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()).singleElement()
.subscribe({
it-> myFirstReqLiveData.value = it
},{
errorFirstReqLiveData.value = it
},{
})
compositeDisposable.add(firstReqDisposable)
}
fun getSecondReq(token:String){
val secondReqDisposable = RetrofitClientInstance.getRetrofitInterface()
.getSecondReq(token)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()).singleElement()
.subscribe({
it-> mySecondReqLiveData.value = it
},{
errorSecondReqLiveData.value = it
},{
})
compositeDisposable.add(SecondReqDisposable)
}
override fun onCleared() {
super.onCleared()
compositeDisposable.clear()
}
}
and in my fragment, I implement this way:
class FirstTabFragment : Fragment() {
private lateinit var myViewModel: MyViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
myViewModel = ViewModelProviders.of(activity!!).get(MyViewModel::class.java)
getFirstReq(myViewModel, token!!)
observeFirstReq(myViewModel)
observeFirstReqError(myViewModel)
observeSecondReq(myViewModel)
observeSecondReqError(myViewModel)
}
fun getFirstReq(viewModel: MyViewModel, token: String) {
viewModel.getFirstReq(token)
}
fun observeFirstReq(viewModel: MyViewModel) {
viewModel.getFirstReqLiveData().observe(this, Observer { myFirstReqModel ->
getSecondReq(myViewModel)
}
}
fun getSecondReq(viewModel: MyViewModel, token: String) {
viewModel.getSecondReq(token)
}
fun observeSecondReq(viewModel: MyViewModel) {
viewModel.getSecondReqLiveData().observe(this, Observer { mySecondReqModel ->
//do some work with my data
}
}
my problem is when I switch my tabs, my second request called several times.
I think I assign a new subscribe every time i reopen my fragment, so it called several times.
how can I fix this issue?!
Create below class
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
in Viewmodel change like this
val myFirstReqLiveData = MutableLiveData<Event<myFirstReqModel>>()
val mySecondReqLiveData = MutableLiveData<Event<mySecondReqModel>>()
in Fragment class
fun observeFirstReq(viewModel: MyViewModel) {
viewModel.getFirstReqLiveData().observe(this, EventObserver { myFirstReqModel ->
getSecondReq(myViewModel)
}
}
change
it-> myFirstReqLiveData.value = it to
it-> myFirstReqLiveData.value = Event(it)
try using this way, if this helps you.
You can also remove getSecondReq(myViewModel) from observer and combine or chain your requests.
https://github.com/ReactiveX/RxJava/wiki/Combining-Observables
Something like this:
val disposable = RetrofitClientInstance.getRetrofitInterface()
.getFirstReq(token)
.doOnError { errorFirstReqLiveData.value = it }
.doOnNext { myFirstReqLiveData.value = it }
.flatMap { t -> getSecondReq(token) }
.doOnError { errorSecondReqLiveData.value = it }
.doOnNext { mySecondReqLiveData.value = it }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()).singleElement()
.subscribe()
compositeDisposable.add(disposable)

How I can test what paged list return error during response?

I am trying to test error in PagedList's data source (when load new group of data). My DataSource looks like that:
package com.ps.superheroapp.ui.character_screen.list
import androidx.paging.PositionalDataSource
import com.ps.superheroapp.api.MarvelApiService
import com.ps.superheroapp.objects.SchedulerNames
import io.reactivex.Scheduler
import io.reactivex.disposables.CompositeDisposable
import javax.inject.Named
class CharactersDataSource(
private val compositeDisposable: CompositeDisposable,
private val marvelApi: MarvelApiService,
#Named(SchedulerNames.MAIN) private val scheduler: Scheduler,
private val filter: Filter
) : PositionalDataSource<Character>() {
var events: SourcedDataEventsHandler? = null
override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<Character>) {
compositeDisposable.add(marvelApi.searchCharacter(params.pageSize, 0, filter.searchQuery)
.observeOn(scheduler)
.doOnSubscribe {
events?.onLoadStarted()
}
.subscribe({
callback.onResult(it.data.results ?: arrayListOf(), 0)
events?.onLoadFinishedSuccessfully()
}, {
events?.onLoadFinishedWithError(it)
})
)
}
override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<Character>) {
compositeDisposable.add(
marvelApi.searchCharacter(params.loadSize, params.startPosition, filter.searchQuery)
.observeOn(scheduler)
.doOnSubscribe {
events?.onLoadStarted()
}
.subscribe({
callback.onResult(it.data.results ?: arrayListOf())
events?.onLoadFinishedSuccessfully()
}, {
events?.onLoadFinishedWithError(it)
})
)
}
}
If I will just mock events handler class and during test will call methods from it, this test will not test anything.
I searched ways or best practice to test this type of behaviour but I didn't found something.
My test looks like that:
#Test
fun should_show_network_error_when_screen_data_cannot_be_loaded_because_of_internet_connection() {
`when`(connectivityChecker.isOffline()).thenReturn(true)
//logic to imitate error during loading from data source
vm.fetchCharacters()
Assert.assertEquals(ErrorType.NETWORK, vm.error.get())
}
Could you please give me some advice or architectural example, or example of unit test to test this.
Thanks in advance
I found next solution which is work:
I changed callback interface on PublishSubject in Data Source:
class CharactersDataSource(
private val compositeDisposable: CompositeDisposable,
private val marvelApi: MarvelApiService,
#Named(SchedulerNames.MAIN) private val scheduler: Scheduler,
val filter: Filter
) : PositionalDataSource<Character>() {
private val onEvent = PublishSubject.create<CharacterLoadEvent>()
override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<Character>) {
compositeDisposable.add(marvelApi.searchCharacter(params.pageSize, 0, filter.searchQuery)
.observeOn(scheduler)
.doOnSubscribe {
onEvent.onNext(CharacterLoadEvent.LOAD_STARTED)
}
.subscribe({
callback.onResult(it.data.results ?: arrayListOf(), 0)
onEvent.onNext(CharacterLoadEvent.LOADED)
}, {
onEvent.onNext(CharacterLoadEvent.ERROR)
})
)
}
override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<Character>) {
compositeDisposable.add(
marvelApi.searchCharacter(params.loadSize, params.startPosition, filter.searchQuery)
.observeOn(scheduler)
.doOnSubscribe {
onEvent.onNext(CharacterLoadEvent.LOAD_STARTED)
}
.subscribe({
callback.onResult(it.data.results ?: arrayListOf())
onEvent.onNext(CharacterLoadEvent.LOADED)
}, {
onEvent.onNext(CharacterLoadEvent.ERROR)
})
)
}
fun observeCharactersLoadEvents(): Observable<CharacterLoadEvent> = onEvent
}
and in tests mock behaviour of this publisher.
Couple of tests looks like:
#Test
fun should_show_network_error_when_screen_data_cannot_be_loaded_because_of_internet_connection() {
`when`(connectivityChecker.isOffline()).thenReturn(true)
`when`(interactor.observeCharactersLoadEvents()).thenReturn(Observable.just(CharacterLoadEvent.ERROR))
`when`(interactor.getCharacters()).then {
TestPageList.get<Character>(listOf())
}
vm.fetchCharacters()
Assert.assertEquals(ErrorType.NETWORK, vm.error.get())
}
#Test
fun should_show_general_error_when_screen_data_cannot_be_loaded_because_of_unknown_error() {
`when`(connectivityChecker.isOffline()).thenReturn(false)
`when`(interactor.observeCharactersLoadEvents()).thenReturn(Observable.just(CharacterLoadEvent.ERROR))
vm.fetchCharacters()
Assert.assertEquals(ErrorType.GENERAL, vm.error.get())
}
#Test
fun should_filter_character_list_when_user_enter_text_in_search_field() {
`when`(interactor.observeCharactersLoadEvents()).thenReturn(Observable.just(CharacterLoadEvent.LOAD_STARTED))
`when`(interactor.getCharacters()).then {
TestPageList.get<Character>(listOf())
}
vm.searchQuery.value = "Hulk"
Mockito.verify(interactor, times(1)).getCharacters("Hulk")
}
This can help to test behaviour of DataSource and it works for me.
I will also be very appreciate for any idea how to improve this Architecture/Approach/Test

MutableLiveData won't trigger loadAfter to fetch from Android ROM using PagedList

I have 70 itens stored on my ROM and I would like to fetch a paged amount of 15. I read many posts so far with related issues, however none of them were useful for me.
Some possible causes for loadAfter not being triggered:
Solution 1 : call getItem inside onBindViewHolder
Solution 2 : call submitList to PagedListAdapter
Solution 3 : replace ListAdapter with PagedListAdapter
I assume DataBinding is fine since everything works without trying to paging.
I'm mocking my data source to understand what's happening. Some functions are suspended 'cause they should have data coming from ROM which requires it. My code state be like:
ADAPTER
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
getItem(position).let { wkda ->
with(holder) {
wkda?.apply { bind(createOnClickListener(this)) }
}
}
}
FRAGMENT
vm.manufacturers.observe(viewLifecycleOwner) { manufacturers ->
adapter.submitList(manufacturers)
}
VIEWMODEL
var manufacturers: MutableLiveData<PagedList<WKDA>> = MutableLiveData()
init {
viewModelScope.launch {
repository.getManufacturers(manufacturers)
}
}
REPOSITORY
suspend fun getManufacturers(manufacturers: MutableLiveData<PagedList<WKDA>>) {
withContext(Dispatchers.IO) {
manufacturers.postValue(ManufacturerPagedList.
getInstance().
fetchPage())
}
}
MANUFACTURER PAGED LIST
private val executor = ManufacturerExecutor()
private val paginationConfig: PagedList.Config = PagedList.Config.Builder()
.setPageSize(PAGE_SIZE)
.setPrefetchDistance(FETCH_DISTANCE)
.setEnablePlaceholders(false)
.build()
companion object {
#Volatile
private var instance: ManufacturerPagedList? = null
fun getInstance() = instance ?: synchronized(this) {
ManufacturerPagedList().also {
instance = it
}
}
}
fun fetchPage(): PagedList<WKDA> = PagedList.Builder<Int, WKDA>(
MockDataSource(),
paginationConfig)
.setInitialKey(INITIAL_KEY)
.setFetchExecutor(executor)
.setNotifyExecutor(executor)
.build()
}
DATASOURCE
class MockDataSource : PageKeyedDataSource<Int, WKDA>() {
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, WKDA>) {
callback.onResult(List(20) { generatePost(params.requestedLoadSize) }.toList(), -1, 1)
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, WKDA>) {
callback.onResult(List(20) { generatePost(params.key) }.toList(), params.key + 1)
}
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, WKDA>) {
callback.onResult(List(20) { generatePost(params.key) }.toList(), params.key - 1)
}
private fun generatePost(key: Int): WKDA {
return WKDA("name", "author $key")
}
}
CONSTANTS
const val INITIAL_KEY: Int = 0
const val PAGE_SIZE: Int = 15
const val FETCH_DISTANCE: Int = 1
What am I missing here?
After check: loadAfter was called properly. The problem was model itself:
wkda.id had always the same "name" value
DiffCallback compared old list of objects with the new one and didn't see differences, so the item "duplicates" weren't added to the adapter

Categories

Resources