Created
September 21, 2022 23:51
-
-
Save danielloaizabr/b9ae955b873dff2a1bac04eb3239de68 to your computer and use it in GitHub Desktop.
Paging Implementation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| @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