I'm trying to implement Paging library in my project but until now it's not successful.
I'm using Marvel API and the requests works well. The app was fully done before but I'm now to implement the library and I'm not getting any results.
Basically, I was trying to collect the data like this:
Marvel API:
#GET("characters")
suspend fun recoverAll(
#Query("limit") limit: Int = PAGE_SIZE,
#Query("offset") offset: Int? = 0,
#Query("orderBy") orderBy: String = TITLE
): Response<CharacterResponseModel>
Repository:
interface MarvelApiRepository {
fun recoverAll(): Flow<PagingData<CharacterModel>>
}
class MarvelApiRepositoryImpl(private val api: MarvelApi): MarvelApiRepository {
override fun recoverAll(): Flow<PagingData<CharacterModel>> {
return Pager(
config = PagingConfig(
pageSize = PAGE_SIZE,
enablePlaceholders = false
),
pagingSourceFactory = { CharacterModelPagingSource(api) }
).flow
}
UseCase:
class RecoverCharactersFromApi(private val repository: MarvelApiRepository) {
operator fun invoke(): Flow<PagingData<CharacterModel>> {
return repository.recoverAll()
}
View Model:
fun fetch(): Flow<PagingData<CharacterModel>> {
return useCases.recoverCharactersFromApi().cachedIn(viewModelScope)
}
Recovering (without success):
private fun configDataCollection() = viewLifecycleOwner.lifecycleScope.launch {
viewModel.fetch().collectLatest { result ->
marvelCharactersAdapter.submitData(result)
}
This is my paging source:
class CharacterModelPagingSource(private val api: MarvelApi):
PagingSource<Int, CharacterModel>() {
override fun getRefreshKey(state: PagingState<Int, CharacterModel>): Int? {
return state.anchorPosition?.let { position ->
val anchorPage = state.closestPageToPosition(position)
anchorPage?.prevKey?.plus(1)?: anchorPage?.nextKey?.minus(1)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, CharacterModel> {
return try {
val position = params.key?: STARTING_PAGE_INDEX
val offset = position * PAGE_SIZE
val response = api.recoverAll(offset = offset)
val nextKey = if(offset >= response.body()?.data!!.total) {
null
} else {
position + 1
}
return LoadResult.Page(
data = response.body()?.data!!.results.toList(),
prevKey = null,
nextKey = nextKey
)
} catch(e: java.lang.Exception) {
LoadResult.Error(e)
}
}
}
And the models works as:
CharacterResponseModel contains the data, which is CharacterData, containing the results and the total number of items. The results are the CharacterModel as a list:
data class CharacterResponseModel(
#SerializedName("data")
val data: CharacterDataModel)
data class CharacterDataModel(
#SerializedName("results")
val results: List<CharacterModel>,
#SerializedName("total")
val total: Int
)
#Entity(tableName = CHARACTER_MODEL)
data class CharacterModel(
#PrimaryKey(autoGenerate = true)
#SerializedName("id")
val id: Int,
#SerializedName("name")
val name: String,
#SerializedName("description")
val description: String,
#SerializedName("thumbnail")
val thumbnail: ThumbnailModel): Serializable
Could anyone help me to achieve successfully this implementation?
P.S.: I'm using Paging 3.1.1 and PagingDataAdapter
Related
I need to write unit tests for doing a paginated network request. First I implemented paging library logic to retrieve the data.
I created a data source class and a repository class to get data from the network request.
This is my data source class
class ListDataSource(
private val networkService: NetworkService,
private val searchKey: String) : PagingSource<Int, ListItem>() {
override fun getRefreshKey(state: PagingState<Int, ListItem>): Int? {
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ListItem> {
val pageNumber: Int = params.key ?: 0
return try {
val response = networkService.getList(
searchTerm = searchKey,
page = pageNumber,
size = 30
)
val listItems = response.response?.list
val nextKey = listItems?.let { nonNullList ->
if (nonNullList.size < 30) {
null
} else {
pageNumber + 1
}
} ?: run {
null
}
LoadResult.Page(
data = listItems.orEmpty(),
prevKey = if (pageNumber == 0) null else pageNumber - 1,
nextKey = nextKey
)
} catch (exception: Exception) {
LoadResult.Error(exception)
}
}}
This is my repository class
class ListPagingRepository(private val service: NetworkService) {
private lateinit var pager: Pager<Int, ListItem>
private lateinit var pagingSource: ListDataSource
fun getListPager(): Pager<Int, ListItem> {
return pager
}
fun isPagerInitialized(): Boolean = this::pager.isInitialized
fun createSource(searchTerm: String) {
pagingSource = ListDataSource(service, searchTerm)
}
fun createPager() {
pager = Pager(
config = PagingConfig(
initialLoadSize = 15,
pageSize = 15,
enablePlaceholders = false,
prefetchDistance = 2
),
pagingSourceFactory = { pagingSource }
)
}}
Inside my viewmodel I the function to do the network call is:
fun getPaginatedList(searchTerm: String): Flow<PagingData<ListItem>> {
listPagingRepository.createSource(searchTerm)
listPagingRepository.createPager()
return if (listPagingRepository.isPagerInitialized()) {
listPagingRepository
.getListPager()
.flow
.cachedIn(viewModelScope)
.map { pagingData -> pagingData.map { listMapper.map(it) } }
} else emptyFlow()
}
How can I test this network request?
Searched for 2 days but nothing that I found helped me.
You should test your implementation, with mocked network responses. An example of such a test with MockK as the mocking framework :
#Test
fun `verify error return from load() when a (types of responses you expect to potentially receive) response is returned` () = runTest {
val mockNetworkService = mockk<NetworkService>(relaxed = true, relaxedUnitFun = true)
val testParams = LoadParams(1, false)
every { mockNetworkService.getList(any(), any(), any()) } returns LoadResponse.Error(YourExpectedException)
val result = ListDataSource(mockNetworkService, "test search").load(testParams)
assertEquals(LoadResponse.Error(YourExpectedException), result)
}
I'm currently working with paging 3 library. I'm able to populate the list and pagination is working as expected. But when I call adapter.refresh() method twice by using pull to refresh, the pagination stops working.
I have gone through the documentation and read many articles but no success yet.
One more thing I'm not able to empty the list when I hit pull to refresh. I tried calling invalidate() method of PagingSource but it crashes the app.
https://www.dropbox.com/s/0k2g9mlktv5gee6/22-05-22-10-26-37.mp4?dl=0
VideoPagingSource.kt
class VideoPagingSource(
private val apiInterface: ApiInterface,
private val schoolId: String,
private val ordering: String?,
private val courseId: String?,
private val moduleId: String?,
private val searchText: String?
) : PagingSource<Int, Video>() {
override fun getRefreshKey(state: PagingState<Int, Video>): Int? = null
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Video> {
val pageNumber = params.key
return try {
val response = apiInterface.getVideos(schoolId, ordering, courseId, moduleId, searchText, pageNumber)
val pagedResponse = response?.body()
var nextPageNumber: Int? = null
if (pagedResponse?.links?.next != null) {
val uri = Uri.parse(pagedResponse.links.next)
val nextPageQuery = uri.getQueryParameter("page")
nextPageNumber = nextPageQuery?.toInt()
}
LoadResult.Page(
data = response?.body()?.objects.orEmpty(),
prevKey = null,
nextKey = nextPageNumber
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
}
PreClassViewModel.kt
class PreClassViewModel #Inject constructor(private val repository: PreClassRepository) :
ViewModel() {
private val queryParamLiveData = MutableLiveData<QueryParams>()
val videosLiveData = queryParamLiveData.switchMap {
repository.getVideos(
schoolId = it.extraArgs[0],
ordering = it.extraArgs[1],
courseId = it.courseId,
moduleId = it.chapterId,
searchText = it.searchText
).cachedIn(viewModelScope)
}
fun setQueryParam(queryParams: QueryParams){
queryParamLiveData.value = queryParams
}
}
I experienced a similar problem where the adapter provided only the first page after refresh(). The reason was a wrong pageSize provided to the PagingConfig.
Hello Guys im using Android Jetpack Paging library 3, I'm creating a news app that implements network + database scenario, and im following the codelab by google https://codelabs.developers.google.com/codelabs/android-paging , im doing it almost like in the codelab i almost matched all the operations shown in the examples https://github.com/android/architecture-components-samples/tree/main/PagingWithNetworkSample.
It works almost as it should...but my backend response is page keyed, i mean response comes with the list of news and the next page url, remote mediator fetches the data, populates the database, repository is set, viewmodel is set...
The problem is :
when recyclerview loads the data , following happens:recyclerview flickers, items jump, are removed , added again and so on.
I dont know why recyclerview or its itemanimator behaves like that , that looks so ugly and glitchy.
More than that, when i scroll to the end of the list new items are fetched and that glitchy and jumping effect is happening again.
I would be very grateful if you could help me, im sitting on it for three days , thank you very much in advance.Here are my code snippets:
#Entity(tableName = "blogs")
data class Blog(
#PrimaryKey(autoGenerate = true)
val databaseid:Int,
#field:SerializedName("id")
val id: Int,
#field:SerializedName("title")
val title: String,
#field:SerializedName("image")
val image: String,
#field:SerializedName("date")
val date: String,
#field:SerializedName("share_link")
val shareLink: String,
#field:SerializedName("status")
val status: Int,
#field:SerializedName("url")
val url: String
) {
var categoryId: Int? = null
var tagId: Int? = null
}
Here's the DAO
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(blogs: List<Blog>)
#Query("DELETE FROM blogs")
suspend fun deleteAllBlogs()
#Query("SELECT * FROM blogs WHERE categoryId= :categoryId ORDER BY id DESC")
fun getBlogsSourceUniversal(categoryId:Int?): PagingSource<Int, Blog>
#Query("SELECT * FROM blogs WHERE categoryId= :categoryId AND tagId= :tagId ORDER BY id DESC")
fun getBlogsSourceUniversalWithTags(categoryId:Int?,tagId:Int?): PagingSource<Int, Blog>
NewsDatabaseKt
abstract class NewsDatabaseKt : RoomDatabase() {
abstract fun articleDAOKt(): ArticleDAOKt
abstract fun remoteKeyDao(): RemoteKeyDao
companion object {
#Volatile
private var INSTANCE: NewsDatabaseKt? = null
fun getDatabase(context: Context): NewsDatabaseKt =
INSTANCE ?: synchronized(this) {
INSTANCE ?: buildDatabase(context).also { INSTANCE = it }
}
private fun buildDatabase(context: Context) =
Room.databaseBuilder(context.applicationContext,
NewsDatabaseKt::class.java,
"news_database_kt")
.build()
}
RemoteMediator
#ExperimentalPagingApi
class BlogsRemoteMediator(private val categoryId: Int,
private val service: NewsAPIInterfaceKt,
private val newsDatabase: NewsDatabaseKt,
private val tagId : Int? = null ,
private val initialPage:Int = 1
) : RemoteMediator<Int, Blog>() {
override suspend fun initialize(): InitializeAction {
return InitializeAction.LAUNCH_INITIAL_REFRESH
}
override suspend fun load(loadType: LoadType, state: PagingState<Int, Blog>): MediatorResult {
try {
val page = when (loadType) {
REFRESH ->{
initialPage
}
PREPEND -> {
return MediatorResult.Success(endOfPaginationReached = true)}
APPEND -> {
val remoteKey = newsDatabase.withTransaction {
newsDatabase.remoteKeyDao().remoteKeyByLatest(categoryId.toString())
}
if(remoteKey.nextPageKey == null){
return MediatorResult.Success(endOfPaginationReached = true)
}
remoteKey.nextPageKey.toInt()
}
}
val apiResponse =
if(tagId == null) {
service.getCategoryResponsePage(RU, categoryId, page.toString())
}else{
service.getCategoryTagResponsePage(RU,categoryId,tagId,page.toString())
}
val blogs = apiResponse.blogs
val endOfPaginationReached = blogs.size < state.config.pageSize
newsDatabase.withTransaction {
// clear all tables in the database
if (loadType == LoadType.REFRESH) {
newsDatabase.remoteKeyDao().deleteByLatest(categoryId.toString())
if(tagId == null) {
newsDatabase.articleDAOKt().clearBlogsByCatId(categoryId)
}else {
newsDatabase.articleDAOKt().clearBlogsByCatId(categoryId,tagId)
}
}
blogs.map {blog ->
blog.categoryId = categoryId
if(tagId != null) {
blog.tagId = tagId
}
}
newsDatabase.remoteKeyDao().insert(LatestRemoteKey(categoryId.toString(),
apiResponse.nextPageParam))
newsDatabase.articleDAOKt().insertAll(blogs)
}
return MediatorResult.Success(
endOfPaginationReached = endOfPaginationReached
)
} catch (exception: IOException) {
return MediatorResult.Error(exception)
} catch (exception: HttpException) {
return MediatorResult.Error(exception)
}
}
PagingRepository
class PagingRepository(
private val service: NewsAPIInterfaceKt,
private val databaseKt: NewsDatabaseKt
){
#ExperimentalPagingApi
fun getBlogsResultStreamUniversal(int: Int, tagId : Int? = null) : Flow<PagingData<Blog>>{
val pagingSourceFactory = {
if(tagId == null) {
databaseKt.articleDAOKt().getBlogsSourceUniversal(int)
}else databaseKt.articleDAOKt().getBlogsSourceUniversalWithTags(int,tagId)
}
return Pager(
config = PagingConfig(
pageSize = 1
)
,remoteMediator =
BlogsRemoteMediator(int, service, databaseKt,tagId)
,pagingSourceFactory = pagingSourceFactory
).flow
}
}
BlogsViewmodel
class BlogsViewModel(private val repository: PagingRepository):ViewModel(){
private var currentResultUiModel: Flow<PagingData<UiModel.BlogModel>>? = null
private var categoryId:Int?=null
#ExperimentalPagingApi
fun getBlogsUniversalWithUiModel(int: Int, tagId : Int? = null):
Flow<PagingData<UiModel.BlogModel>> {
val lastResult = currentResultUiModel
if(lastResult != null && int == categoryId){
return lastResult
}
val newResult: Flow<PagingData<UiModel.BlogModel>> =
repository.getBlogsResultStreamUniversal(int, tagId)
.map { pagingData -> pagingData.map { UiModel.BlogModel(it)}}
.cachedIn(viewModelScope)
currentResultUiModel = newResult
categoryId = int
return newResult
}
sealed class UiModel{
data class BlogModel(val blog: Blog) : UiModel()
}
PoliticsFragmentKotlin
#ExperimentalPagingApi
class PoliticsFragmentKotlin : Fragment() {
private lateinit var recyclerView: RecyclerView
private lateinit var pagedBlogsAdapter:BlogsAdapter
lateinit var viewModelKt: BlogsViewModel
lateinit var viewModel:NewsViewModel
private var searchJob: Job? = null
#ExperimentalPagingApi
private fun loadData(categoryId:Int, tagId : Int? = null) {
searchJob?.cancel()
searchJob = lifecycleScope.launch {
viewModelKt.getBlogsUniversalWithUiModel(categoryId, tagId).collectLatest {
pagedBlogsAdapter.submitData(it)
}
}
}
#ExperimentalPagingApi
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_blogs, container, false)
viewModelKt = ViewModelProvider(requireActivity(),Injection.provideViewModelFactory(requireContext())).get(BlogsViewModel::class.java)
viewModel = ViewModelProvider(requireActivity()).get(NewsViewModel::class.java)
pagedBlogsAdapter = BlogsAdapter(context,viewModel)
val decoration = DividerItemDecoration(context, DividerItemDecoration.VERTICAL)
recyclerView = view.findViewById(R.id.politics_recyclerView)
recyclerView.addItemDecoration(decoration)
initAdapter()
loadData(categoryId)
initLoad()
return view
}
private fun initLoad() {
lifecycleScope.launchWhenCreated {
Log.d("meylis", "lqunched loadstate scope")
pagedBlogsAdapter.loadStateFlow
// Only emit when REFRESH LoadState for RemoteMediator changes.
.distinctUntilChangedBy { it.refresh }
// Only react to cases where Remote REFRESH completes i.e., NotLoading.
.filter { it.refresh is LoadState.NotLoading }
.collect { recyclerView.scrollToPosition(0) }
}
}
private fun initAdapter() {
recyclerView.adapter = pagedBlogsAdapter.withLoadStateHeaderAndFooter(
header = BlogsLoadStateAdapter { pagedBlogsAdapter.retry() },
footer = BlogsLoadStateAdapter { pagedBlogsAdapter.retry() }
)
lifecycleScope.launchWhenCreated {
pagedBlogsAdapter.loadStateFlow.collectLatest {
swipeRefreshLayout.isRefreshing = it.refresh is LoadState.Loading
}
}
pagedBlogsAdapter.addLoadStateListener { loadState ->
// Only show the list if refresh succeeds.
recyclerView.isVisible = loadState.source.refresh is LoadState.NotLoading
// Show loading spinner during initial load or refresh.
progressBar.isVisible = loadState.source.refresh is LoadState.Loading
// Show the retry state if initial load or refresh fails.
retryButton.isVisible = loadState.source.refresh is LoadState.Error
// Toast on any error, regardless of whether it came from RemoteMediator or PagingSource
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 {
Toast.makeText(context, "\uD83D\uDE28 Wooops ${it.error}", Toast.LENGTH_LONG
).show()
}
}
}
companion object {
#JvmStatic
fun newInstance(categoryId: Int, tags : ArrayList<Tag>): PoliticsFragmentKotlin {
val args = Bundle()
args.putInt(URL, categoryId)
args.putSerializable(TAGS,tags)
val fragmentKotlin = PoliticsFragmentKotlin()
fragmentKotlin.arguments = args
Log.d("meylis", "created instance")
return fragmentKotlin
}
}
BlogsAdapter
class BlogsAdapter(var context: Context?, var newsViewModel:NewsViewModel) :
PagingDataAdapter<BlogsViewModel.UiModel.BlogModel, RecyclerView.ViewHolder>
(REPO_COMPARATOR) {
private val VIEW = 10
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
VIEW -> MyViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.card_layout, parent, false))
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val uiModel = getItem(position)
if(uiModel == null){
if(uiModel is BlogsViewModel.UiModel.BlogModel){(holder as MyViewHolder).bind(null)}
}
if(uiModel is BlogsViewModel.UiModel.BlogModel){(holder as
MyViewHolder).bind(uiModel.blog)}
}
override fun getItemViewType(position: Int): Int {
return VIEW
}
companion object {
private val REPO_COMPARATOR = object : DiffUtil.ItemCallback<BlogsViewModel.UiModel.BlogModel>() {
override fun areItemsTheSame(oldItem: BlogsViewModel.UiModel.BlogModel, newItem: BlogsViewModel.UiModel.BlogModel): Boolean =
oldItem.blog.title == newItem.blog.title
override fun areContentsTheSame(oldItem: BlogsViewModel.UiModel.BlogModel, newItem: BlogsViewModel.UiModel.BlogModel): Boolean =
oldItem == newItem
}
}
MyViewHolder
class MyViewHolder(var container: View) : RecyclerView.ViewHolder(container) {
var cv: CardView
#JvmField
var mArticle: TextView
var date: TextView? = null
#JvmField
var time: TextView
#JvmField
var articleImg: ImageView
#JvmField
var shareView: View
var button: MaterialButton? = null
#JvmField
var checkBox: CheckBox
var progressBar: ProgressBar
private var blog:Blog? = null
init {
cv = container.findViewById<View>(R.id.cardvmain) as CardView
mArticle = container.findViewById<View>(R.id.article) as TextView
articleImg = container.findViewById<View>(R.id.imgvmain) as ImageView
//button = (MaterialButton) itemView.findViewById(R.id.sharemain);
checkBox = container.findViewById<View>(R.id.checkboxmain) as CheckBox
time = container.findViewById(R.id.card_time)
shareView = container.findViewById(R.id.shareView)
progressBar = container.findViewById(R.id.blog_progress)
}
fun bind(blog: Blog?){
if(blog == null){
mArticle.text = "loading"
time.text = "loading"
articleImg.visibility = View.GONE
}else {
this.blog = blog
mArticle.text = blog.title
time.text = blog.date
if (blog.image.startsWith("http")) {
articleImg.visibility = View.VISIBLE
val options: RequestOptions = RequestOptions()
.centerCrop()
.priority(Priority.HIGH)
GlideImageLoader(articleImg,
progressBar).load(blog.image, options)
} else {
articleImg.visibility = View.GONE
}
}
}
}
NewsApiInterface
interface NewsAPIInterfaceKt {
#GET("sort?")
suspend fun getCategoryResponsePage(#Header("Language") language: String, #Query("category")
categoryId: Int, #Query("page") pageNumber: String): BlogsResponse
#GET("sort?")
suspend fun getCategoryTagResponsePage(#Header("Language") language: String,
#Query("category") categoryId: Int,#Query("tag") tagId:Int, #Query("page") pageNumber: String)
:BlogsResponse
companion object {
fun create(): NewsAPIInterfaceKt {
val logger = HttpLoggingInterceptor()
logger.level = HttpLoggingInterceptor.Level.BASIC
val okHttpClient = UnsafeOkHttpClient.getUnsafeOkHttpClient()
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(NewsAPIInterfaceKt::class.java)
}
}
}
I have tried setting initialLoadSize = 1
But the problem still persists
EDIT: Thanks for your answer #dlam , yes, it does , my network API returns the list of results ordered by id. BTW, items do this jump when the application is run offline as well.
Videos when refreshing and loading online
online loading and paging
online loading and paging(2)
Videos when refreshing and loading offline
offline loading and refreshing
Thanks again, here is my gist link https://gist.github.com/Aydogdyshka/7ca3eb654adb91477a42128de2f06ea9
EDIT
Thanks a lot to #dlam, when I set pageSize=10, jumping has disappeared...Then i remembered why i set pageSize=1 in the first place... when i refresh , 3 x pageSize of items are loaded, even if i overrided initialLoadSize = 10 , it still loads 3 x pageSize calling append 2x times after refresh , what could i be doing wrong, what's the correct way to only load first page when i refresh ?
Just following up here from comments:
Setting pageSize = 10 fixes the issue.
The issue was with pageSize being too small, resulting in PagingSource refreshes loading pages that did not cover the viewport. Since source refresh replaces the list and goes through DiffUtil, you need to provide an initialLoadSize that is large enough so that there is some overlap (otherwise scroll position will be lost).
BTW - Paging loads additional data automatically based on PagingConfig.prefetchDistance. If RecyclerView binds items close enough to the edge of the list, it will automatically trigger APPEND / PREPEND loads. This is why the default of initialLoadSize is 3 * pageSize, but if you're still experiencing additional loads, I would suggest either adjusting prefetchDistance, or increasing initialLoadSize further.
config = PagingConfig(
pageSize = PAGE_SIZE,
enablePlaceholders = true,
prefetchDistance = 3* PAGE_SIZE,
initialLoadSize = 2*PAGE_SIZE,
)
make sure enablePlaceholders is set to true and set the page size to around 10 to 20
recyclerview flickers becouse from dao you get items not the same order it was responded from network.
I will suggest you my solution.
we will get items from database order by primary key, databaseid, descending.
first of all delete autogenerated = true.
we will set databaseid manualy, in same order we got items from network.
next lets edit remoteMediator load function.
when (loadType) {
LoadType.PREPEND -> {
blogs.map {
val databaseid = getFirstBlogDatabaseId(state)?.databaseid?:0
movies.forEachIndexed{
index, blog ->
blog.databaseid = roomId - (movies.size -index.toLong())
}
}
}
LoadType.APPEND -> {
val roomId = getLastBlogDatabaseId(state)?.databaseid ?:0
blogs.forEachIndexed{
index, blog ->
blog.databaseid = roomId + index.toLong() + 1
}
}
LoadType.REFRESH -> {
blogs.forEachIndexed{
index, blog ->
blog.databaseid = index.toLong()
}
}
}
private fun getFirstBlogDatabaseId(state: PagingState<Int, Blog>): Blog? {
return state.pages.firstOrNull { it.data.isNotEmpty() }?.data?.firstOrNull()
}
private fun getLastBlogDatabaseId(state: PagingState<Int, Blog>): Blog? {
return state.lastItemOrNull()
}
I have one entity "drinks" which have [id;name;thumb] and I`m using these entities for 2 response calls. One response returns me a NonAlcohol list of drinks, another AlcoholList, I'm using Room for caching the data. But when I run the app, I saw that my lists merged, after some thought, I found a solution to this problem, I added one Boolean field to my entity "alcoholStatus".But I can't understand how to set the data into this variable correctly using this AccessDataStrategy. I'm new to Android, and this is my learning project. Please give me the right way how to solve this problem.
https://github.com/YaroslavSulyma/LetsDrink/tree/master/app/src/main/java/com/example/letsdrink
Thanks a lot!
Entity
#Entity(tableName = "drinks")
data class DrinksModel(
#SerializedName("strDrink")
val strDrink: String,
#SerializedName("strDrinkThumb")
val strDrinkThumb: String?,
#SerializedName("idDrink")
#PrimaryKey
val idDrink: Int,
var alcohol: Boolean
)
DataAccessStrategyCode
fun <T, A> performGetOperation(
databaseQuery: () -> LiveData<T>,
networkCall: suspend () -> Resource<A>,
saveCallResult: suspend (A) -> Unit
): LiveData<Resource<T>> =
liveData(Dispatchers.IO) {
emit(Resource.loading())
val source = databaseQuery.invoke().map { Resource.success(it) }
emitSource(source)
val responseStatus = networkCall.invoke()
if (responseStatus.status == SUCCESS) {
saveCallResult(responseStatus.data!!)
} else if (responseStatus.status == ERROR) {
emit(Resource.error(responseStatus.message!!))
emitSource(source)
}
}
Resource
data class Resource<out T>(val status: Status, val data: T?, val message: String?) {
enum class Status {
SUCCESS,
ERROR,
LOADING
}
companion object {
fun <T> success(data: T): Resource<T> {
return Resource(Status.SUCCESS, data, null)
}
fun <T> error(message: String, data: T? = null): Resource<T> {
return Resource(Status.ERROR, data, message)
}
fun <T> loading(data: T? = null): Resource<T> {
return Resource(Status.LOADING, data, null)
}
}
}
Repository
class CocktailsRepository #Inject constructor(
private val remoteDataSource: CocktailsRemoteDataSource,
private val localDataSource: CocktailsDao
) {
fun getAlcoholicCocktails() = performGetOperation(
databaseQuery = { localDataSource.getAlcoholicCocktails() },
networkCall = { remoteDataSource.getAllAlcoholicCocktails()},
saveCallResult = { localDataSource.insertAllDrinks(it.drinks) }
)
fun getNonAlcoholicCocktails() = performGetOperation(
databaseQuery = { localDataSource.getNonAlcoholicCocktails() },
networkCall = { remoteDataSource.getAllNonAlcoholicCocktails() },
saveCallResult = { localDataSource.insertAllDrinks(it.drinks) }
)
}
DAO
#Dao
interface CocktailsDao {
#Query("SELECT * FROM drinks WHERE alcohol = 'true'")
fun getAlcoholicCocktails(): LiveData<List<DrinksModel>>
#Query("SELECT * FROM drinks WHERE alcohol = 'false'")
fun getNonAlcoholicCocktails(): LiveData<List<DrinksModel>>
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAllDrinks(drinks: List<DrinksModel>)
}
RemoteDataSource
class CocktailsRemoteDataSource #Inject constructor(private val iCocktailApisService: ICocktailApisService) :
BaseDataSource() {
suspend fun getAllAlcoholicCocktails() =
getResult { iCocktailApisService.allAlcoholicAndNonAlcoholicCocktails("Alcoholic") }
suspend fun getAllNonAlcoholicCocktails() =
getResult { iCocktailApisService.allAlcoholicAndNonAlcoholicCocktails("Non_Alcoholic") }
}
First: I strongly recommend that you define separate data classes for your remote and local model classes and do the mapping between them when needed, for example:
Remote data model:
data class DrinkRemoteModel(
#SerializedName("idDrink")
val idDrink: Int,
#SerializedName("strDrink")
val strDrink: String,
#SerializedName("strDrinkThumb")
val strDrinkThumb: String?,
#SerializedName("alcohol")
var alcohol: Boolean
)
Local data model:
#Entity(tableName = "drinks")
data class DrinkLocalModel(
#PrimaryKey
#ColumnInfo(name = "idDrink")
val idDrink: Int,
#ColumnInfo(name = "strDrink")
val strDrink: String,
#ColumnInfo(name = "strDrinkThumb")
val strDrinkThumb: String?,
#ColumnInfo(name = "alcohol")
var alcohol: Boolean
)
Back to your implementation: I think what causing the problem is that Room maps Boolean fields in your entity to an integer column, 1 for true, and 0 for false, so try changing your querys in your DAO like following:
#Dao
interface CocktailsDao {
#Query("SELECT * FROM drinks WHERE alcohol = 1")
fun getAlcoholicCocktails(): LiveData<List<DrinksModel>>
#Query("SELECT * FROM drinks WHERE alcohol = 0")
fun getNonAlcoholicCocktails(): LiveData<List<DrinksModel>>
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAllDrinks(drinks: List<DrinksModel>)
}
Alternatively: you can substitute your getAlcoholicCocktails and getNonAlcoholicCocktails with one DAO function, like this:
#Query("SELECT * FROM drinks WHERE alcohol = :isAlcoholic")
fun getCocktails(isAlcoholic : Boolean = true): LiveData<List<DrinksModel>>
In fact, I am trying to migrate from paging 2 to paging 3. Already I have successfully implemented PageKeyedDataSource of Paging 2 with RxpagingSource in my codebase. But when I tried to implement ItemKeyedDataSource of Paging 2 to Paging library 3, I got confused to implement that. I also tried to code. but got stuck. Here is my code.
JobSliderRestApi.kt
#GET("job/list/slides")
fun getDetailOfSelectedJob(
#Query("current_job") currentJodId: Int?,
#Query("limit") jobLimit: Int?,
#Query("search_in") fetchType: String?
): Single<Response<JobViewResponse>>
JobViewResponse.kt
data class JobViewResponse(
#SerializedName("data") val data: ArrayList<JobDetail>?
) : BaseResponse()
JodSliderDataSource.kt
class JodSliderDataSource #Inject constructor(
private val jobSliderRestApi: JobSliderRestApi,
private val currentJobId: Int
): RxPagingSource<Int, JobDetail>() {
override fun loadSingle(params: LoadParams<Int>): Single<LoadResult<Int, JobDetail>> {
return jobSliderRestApi.getDetailOfSelectedJob(currentJobId, 20, "next").toSingle()
.subscribeOn(Schedulers.io())
.map { jobResponse -> jobResponse.data }
.map { jobData -> toLoadResult(jobData, position) } // Don't know what to do
.onErrorReturn { LoadResult.Error(it) }
}
//Don't know what to do
private fun toLoadResult(data: JobDetail, position: Int): LoadResult<Int, JobDetail> {
/**val prevKey = if (position == 1) null else position-1
val nextKey = if (data.hasMore) position+1 else null
return LoadResult.Page(data.jobs, prevKey, nextKey)*/
}
#ExperimentalPagingApi
override fun getRefreshKey(state: PagingState<Int, JobDetail>): Int? {
return super.getRefreshKey(state)
}
}
in fact, previously I used 'currentJodId' to differentiate list in Paging 2. but in paging 3, I did not get any clue t fix these
After a lot of searching I solved the problem. Here is the updated JodSliderDataSource class
class JodSliderDataSource #Inject constructor(
private val jobSliderRestApi: JobSliderRestApi
): RxPagingSource<Int, JobDetail>() {
override val keyReuseSupported = true
#ExperimentalPagingApi
override fun getRefreshKey(state: PagingState<Int, JobDetail>): Int? {
return state.anchorPosition?.let {
state.closestItemToPosition(it)?.jobId
}
}
override fun loadSingle(params: LoadParams<Int>): Single<LoadResult<Int, JobDetail>> {
return jobSliderRestApi.getDetailOfSelectedJob(42673, 2, "next").toSingle()
.subscribeOn(Schedulers.io())
.map { jobResponse -> toLoadResult(jobResponse.data) }
.onErrorReturn { LoadResult.Error(it) }
}
private fun toLoadResult(data: ArrayList<JobDetail>): LoadResult<Int, JobDetail> {
return LoadResult.Page(data = data, prevKey = null, nextKey = data.lastOrNull()?.jobId)
}
}