Clean Architecture: Cannot map PagingSource<Int, Entities> to PagingSource<RepositoryModel>
Asked Answered
I

1

8

My requirement is to display the notes in pages using clean architecture along with offline suppport. I am using the Paging library for pagination. And below is the clean architectural diagram for getting notes.

enter image description here

Note: Please open the above image in new tab and zoom to view it clear.

I have four layers UI, UseCase, Repository, and Datasource. I am planning to abstract the internal implementation of the data source. For that, I need to map NotesEntities to another model before crossing the boundary.

class TimelineDao{
    @Transaction
    @Query("SELECT * FROM NotesEntities ORDER BY timeStamp DESC")
    abstract fun getPagingSourceForNotes(): PagingSource<Int, NotesEntities>
}

Current Implementation:

internal class NotesLocalDataSourceImpl @Inject constructor(
    private val notesDao: NotesDao
) : NotesLocalDataSource {
    override suspend fun insertNotes(notes: NotesEntities) {
        notesDao.insert(NotesEntities)
    }

    override fun getNotesPagingSource(): PagingSource<Int, NotesEntities> {
        return notesDao.getPagingSourceForNotes()
    }
}

Expected Implementation:

internal class NotesLocalDataSourceImpl @Inject constructor(
    private val notesDao: NotesDao
) : NotesLocalDataSource {
    override suspend fun insertNotes(notes: NotesRepositoryModel) {
        notesDao.insert(NotesRepositoryModel.toEntity())
    }

    override fun getNotesPagingSource(): PagingSource<Int, NotesRepositoryModel> {
        return notesDao.getPagingSourceForNotes().map{ it.toNotesRepositoryModel() }
    }
}

I am having an issue mapping the PagingSource<Int, NotesEntities> to PagingSource<Int, NotesRespositoryModel>. As for as I have researched, there is no way to map PagingSource<Int, NotesEntities> to PagingSource<Int, NotesRespositoryModel>

Kindly let me know if there is a clean way/ workaround way to map the paging source objects. If anyone is sure if there is no way as of now. Please leave a comment as well.

Please Note: I am aware that paging allows transformation for PagingData. Below is code snippet that gets notes in pages. It maps NotesEntities to NotesDomainModel. But then I want to use NotesRespositoryModel instead of NotesEntities in the NotesRespositoryImpl, abstracting the NotesEntities within NotesLocalDataSourceImpl layer.

override fun getPaginatedNotes(): Flow<PagingData<NotesDomainModel>> {
     return Pager<Int, NotesEntities>(
               config = PagingConfig(pageSize = 10),
               remoteMediator = NotesRemoteMediator(localDataSource,remoteDataSource),
               pagingSourceFactory = localDataSource.getNotesPagingSource()
           ).flow.map{ it.toDomainModel() }
}

The solution I have thought of:

Instead of using the PagingSource in Dao directly, I thought of creating a custom PagingSource, that calls the Dao and maps the NoteEntities to LocalRepositoryModel.

But then I need to understand that any updates to the DB will not be reflected in the PagingSource. I need to handle it internally.

Kindly let me know your thoughts on this.

Isidraisidro answered 18/8, 2022 at 3:53 Comment(3)
I'm facing the same problem hereDromedary
i found this issuetracker.google.com/issues/206697857, i think it will be for the next alpha / beta. otherwise there is no way to keep it clean for the moment.Dromedary
@Dromedary Thank you for providing the issue link.Isidraisidro
I
0

What about creating an implementation of PagingSource that forwards all of the calls to the original PagingSource and performs the mapping, something like this:

class MappingPagingSource<Key: Any, Value: Any, MappedValue: Any>(
  private val originalSource: PagingSource<Key, Value>,
  private val mapper: (Value) -> MappedValue,
) : PagingSource<Key, MappedValue>() {
  
  override fun getRefreshKey(state: PagingState<Key, MappedValue>): Key? {
    return originalSource.getRefreshKey(
      PagingState(
        pages = emptyList(),
        leadingPlaceholderCount = 0,
        anchorPosition = state.anchorPosition,
        config = state.config,
      )
    )
  }

  override suspend fun load(params: LoadParams<Key>): LoadResult<Key, MappedValue> {
    val originalResult = originalSource.load(params)
    return when (originalResult) {
      is LoadResult.Error -> LoadResult.Error(originalResult.throwable)
      is LoadResult.Invalid -> LoadResult.Invalid()
      is LoadResult.Page -> LoadResult.Page(
        data = originalResult.data.map(mapper),
        prevKey = originalResult.prevKey,
        nextKey = originalResult.nextKey,
      )
    }
  }

  override val jumpingSupported: Boolean
    get() = originalSource.jumpingSupported
}

Usage would be like this then:

override fun getNotesPagingSource(): PagingSource<Int, NotesRepositoryModel> {
  return MappingPagingSource(
    originalSource = notesDao.getPagingSourceForNotes(),
    mapper = { it.toNotesRepositoryModel() },
  )
}

Regarding the empty pages in PagingState - mapping all loaded pages back to original value would be too expensive and room's paging implementation is only using anchorPosition and config.initialLoadSize anyway - see here and here.

Inquiring answered 10/9, 2022 at 20:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.