1

I'm trying to create a timeline list similar to a Twitter client using Android Paging 3 and Room library, but I'm facing some issues.

According to the loadType in the Paging RemoteMediator, it can be divided into three types:

LoadType.Refresh
LoadType.Prepend
LoadType.Append

During the first time when the app requests the timeline list data, the loadType should be in the refresh state (as there is no timeline data in the database yet).

During the second time when the app is used (and the user is expected to be at a certain position in the timeline list, for example, page 4 or 5), I expect to execute LoadType.Prepend to get the data before page 4 or 5, until there is no newer data.

Additionally, the timeline list also has a pull-to-refresh feature, where the operation is not a complete refresh (deleting all data, and then adding data from the latest), but more like prepend, where if there's new data, it will automatically expand the header data of the list.

It should look like this:

[Insert image]

However, I encountered a tricky problem, where I don't know how to insert the data obtained from LoadType.Prepend into the beginning of the Room table. This is because the data obtained from LoadType.Prepend should be inserted before the previous visible item in the timeline list.

From the tutorials I've seen onusing Paging 3 and Room, they didn't seem to use the prepend scenario because LoadType.Append can simply insert data at the end of the Room table.

How can I solve this problem?

This is my RemoteMediator code, and it is not working properly (the main issue is that if the current timeline list is not the latest, when triggering (PullRefresh -> LoadType.Refresh) or restarting the app to execute LoadType.Prepend, the new data obtained will be inserted into the last position of the room table, leading to an infinite execution of LoadType.Prepend)

RemoteMediator.kt

class TimelineRemoteMediator @Inject constructor(
  private val database: MastifyDatabase,
  private val apiRepository: ApiRepository,
  private val preferenceRepository: PreferenceRepository
) : RemoteMediator<Int, Status>() {

  private val timelineDao = database.timelineDao()

  // To override the initialize() method, so that the app doesn't need to execute LoadType.Refresh every time it starts
  override suspend fun initialize(): InitializeAction {
    val lastUpdated = preferenceRepository.timelineDatabaseLastUpdated
    val cacheTimeout = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS)
    return if (System.currentTimeMillis() - (lastUpdated ?: 0) <= cacheTimeout) {
      InitializeAction.SKIP_INITIAL_REFRESH
    } else {
      InitializeAction.LAUNCH_INITIAL_REFRESH
    }
  }

  override suspend fun load(
    loadType: LoadType,
    state: PagingState<Int, Status>
  ): MediatorResult {

    var prevPage: String? = null
    var nextPage: String? = null

    when(loadType) {
      LoadType.REFRESH -> {
        prevPage = state.firstItemOrNull()?.id
      }
      LoadType.PREPEND -> {
        prevPage = state.firstItemOrNull()?.id
          ?: return MediatorResult.Success(endOfPaginationReached = true)
      }
      LoadType.APPEND -> {
        nextPage = state.lastItemOrNull()?.id
          ?: return MediatorResult.Success(endOfPaginationReached = true)
      }
    }

    return try {

      val response = apiRepository.getHomeTimeline(
        instanceName = account.instanceName,
        token = account.accessToken,
        minId = prevPage,
        maxId = nextPage
      )
      val endOfPaginationReached = response.isEmpty()
      database.withTransaction {
        timelineDao.insertAll(
          timelineEntity = response.map { status ->
            TimelineEntity(...)
          }
        )
      }
      MediatorResult.Success(endOfPaginationReached)
    } catch (e: IOException) {
      MediatorResult.Error(e)
    } catch (e: HttpException) {
      MediatorResult.Error(e)
    }

  }
}

Dao.kt

@Dao
interface TimelineDao {


  @Insert(onConflict = OnConflictStrategy.REPLACE)
  suspend fun insertAll(timelineEntity: List<TimelineEntity>)

  @Query("SELECT * FROM timelineentity")
  fun pagingSource(): PagingSource<Int, Status>

  @Query("DELETE FROM timelineentity")
  suspend fun clearAll()

}

BTW, this API doesn't use Int as a parameter for pagination, but instead uses a string type of ID to retrieve the data, with the following behavior:

If prevPage and nextPage are not set, it will retrieve the latest timeline posts (e.g. 20 of them).

If you want to retrieve posts beyond the initial 20, you need to set the nextPage GET request parameter to the ID of the 20th post you just received. This way, the API will return posts with IDs after the nextPage parameter. The prevPage parameter works in the same way.

enter image description here

Afterglow
  • 345
  • 4
  • 12

0 Answers0