Skip to content

Instantly share code, notes, and snippets.

@danielloaizabr
Created September 21, 2022 23:51
Show Gist options
  • Select an option

  • Save danielloaizabr/b9ae955b873dff2a1bac04eb3239de68 to your computer and use it in GitHub Desktop.

Select an option

Save danielloaizabr/b9ae955b873dff2a1bac04eb3239de68 to your computer and use it in GitHub Desktop.
Paging Implementation
@Composable
fun AnalyzedCallsScreen(
viewModel: AnalyzedCallsViewModel = hiltViewModel()
) {
val calls = viewModel.data.collectAsLazyPagingItems()
val revealedCardIds by viewModel.revealedCardIdsList.collectAsState()
LazyColumn {
items(calls.itemCount) {
calls[it]?.let { item ->
when (item) {
is AnalyzedCallsState.CallItemModel -> {
return@items Box(Modifier.fillMaxWidth()) {
ActionsRow(
modifier = Modifier.align(Alignment.CenterEnd),
onDelete = {
viewModel.deleteItem(item.item.id)
},
)
DraggableCard(
callItem = item.item,
isRevealed = revealedCardIds.contains(item.item.id),
cardHeight = CARD_HEIGHT.dp,
cardOffset = CARD_OFFSET.dp(),
onExpand = {
viewModel.onItemExpanded(item.item.id)
},
onCollapse = {
viewModel.onItemCollapsed(item.item.id)
},
)
}
}
is AnalyzedCallsState.SeparatorModel -> {
return@items TitleItem(item.title)
}
}
}
}
}
}
@HiltViewModel
class AnalyzedCallsViewModel @Inject constructor(
private val remoteAPIService: RemoteAPIService,
application: Application,
analyzedCallsRepository: AnalyzedCallsRepository
) : AndroidViewModel(application) {
@SuppressLint("StaticFieldLeak")
private val context = getApplication<Application>().applicationContext
private val _revealedCardIdsList = MutableStateFlow(listOf<Int>())
val revealedCardIdsList: StateFlow<List<Int>> get() = _revealedCardIdsList
private var query: MutableStateFlow<String> = MutableStateFlow("")
val data = query.flatMapLatest {
analyzedCallsRepository.getCallsPaging(it)
}.cachedIn(viewModelScope)
.map {
it.map { callEntity ->
AnalyzedCallsState.CallItemModel(mapCallItem(callEntity, context))
}
}
.map {
it.insertSeparators(TerminalSeparatorType.SOURCE_COMPLETE) { before, after ->
if (before == null) {
AnalyzedCallsState.SeparatorModel(after?.item?.callDate.orEmpty())
} else if (before.item?.callDate != after?.item?.callDate) {
AnalyzedCallsState.SeparatorModel(after?.item?.callDate.orEmpty())
} else {
null
}
}
}
private fun mapCallItem(item: AnalyzedCallEntity, context: Context): CallItem {
val phoneNumberInstance = PhoneNumberUtil.createInstance(context)
return CallItem(
item.id,
context.getContactName(item.phoneNumberFrom) ?: phoneNumberInstance.format(
phoneNumberInstance.parse(item.phoneNumberFrom, "US"),
PhoneNumberUtil.PhoneNumberFormat.NATIONAL
),
hourFormat.format(getDateFromISOString(item.createdAt)).uppercase(),
getHumanReadableString(getDateFromISOString(item.createdAt)),
item.voicemail.transcription.orEmpty(),
when (item.result) {
"allowed" -> R.drawable.ic_call_allowed
"blocked" -> R.drawable.ic_call_blocked
else -> R.drawable.ic_silenced
}
)
}
fun onItemExpanded(callId: Int) {
if (_revealedCardIdsList.value.contains(callId)) return
_revealedCardIdsList.value = _revealedCardIdsList.value.toMutableList().also { list ->
list.add(callId)
}
}
fun onItemCollapsed(callId: Int) {
if (!_revealedCardIdsList.value.contains(callId)) return
_revealedCardIdsList.value = _revealedCardIdsList.value.toMutableList().also { list ->
list.remove(callId)
}
}
fun deleteItem(id: Int) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
when (remoteAPIService.deleteCall(
id
)) {
is NetworkResponse.Success -> {
query.emit(id.toString())
}
else -> {}
}
}
}
}
}
class AnalyzedCallsRepository @Inject constructor(
private val appDatabase: AppDatabase,
private val remoteAPIService: RemoteAPIService,
private val callsDao: AnalyzedCallsDao
) {
@OptIn(ExperimentalPagingApi::class)
fun getCallsPaging(query: String) = Pager(
config = PagingConfig(pageSize = 25, initialLoadSize = 300),
remoteMediator = CallsRemoteMediator(query, appDatabase, remoteAPIService)
) {
callsDao.pagingSource()
}.flow
}
@OptIn(ExperimentalPagingApi::class)
class CallsRemoteMediator(
private val query: String,
private val database: AppDatabase,
private val networkService: RemoteAPIService
) : RemoteMediator<Int, AnalyzedCallEntity>() {
val callsDao = database.callsDao()
override suspend fun initialize(): InitializeAction {
return InitializeAction.LAUNCH_INITIAL_REFRESH
}
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, AnalyzedCallEntity>
): MediatorResult {
return try {
// The network load method takes an optional String
// parameter. For every page after the first, pass the String
// token returned from the previous page to let it continue
// from where it left off. For REFRESH, pass null to load the
// first page.
val loadKey = when (loadType) {
LoadType.REFRESH -> null
LoadType.PREPEND -> {
val firstItem = state.firstItemOrNull()
// You must explicitly check if the last item is null when
// appending, since passing null to networkService is only
// valid for initial load. If lastItem is null it means no
// items were loaded after the initial REFRESH and there are
// no more items to load.
if (firstItem == null) {
return MediatorResult.Success(
endOfPaginationReached = true
)
}
firstItem.id
}
// Get the last User object id for the next RemoteKey.
LoadType.APPEND -> {
val lastItem = state.lastItemOrNull()
// You must explicitly check if the last item is null when
// appending, since passing null to networkService is only
// valid for initial load. If lastItem is null it means no
// items were loaded after the initial REFRESH and there are
// no more items to load.
if (lastItem == null) {
return MediatorResult.Success(
endOfPaginationReached = true
)
}
lastItem.currentPage + 1
}
}
// Suspending network load via Retrofit. This doesn't need to
// be wrapped in a withContext(Dispatcher.IO) { ... } block
// since Retrofit's Coroutine CallAdapter dispatches on a
// worker thread.
val response = when (loadType) {
LoadType.APPEND -> {
networkService.getAnalyzedCalls(page = loadKey, afterId = null)
}
LoadType.REFRESH -> {
if (query.isEmpty()) {
networkService.getAnalyzedCalls(afterId = null, null)
} else {
networkService.getAnalyzedCalls(afterId = loadKey, null)
}
}
else -> {
networkService.getAnalyzedCalls(afterId = loadKey, null)
}
}
when (response) {
is NetworkResponse.Success -> {
// Insert new users into database, which invalidates the
// current PagingData, allowing Paging to present the updates
// in the DB.
if (loadType == LoadType.REFRESH) {
if (query.isNotEmpty()) {
callsDao.delete(query.toInt())
}
}
callsDao.insertAll(response.body.data.map {
it.toEntity(response.body.meta.currentPage)
})
// End of pagination has been reached if no users are returned from the
// service
MediatorResult.Success(
endOfPaginationReached = response.body.data.isEmpty()
)
}
else -> MediatorResult.Error(IOException())
}
// Store loaded data, and next key in transaction, so that
// they're always consistent.
} catch (e: IOException) {
MediatorResult.Error(e)
} catch (e: HttpException) {
MediatorResult.Error(e)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment