In our fragment we have this code inside onViewCreated.
with(viewModel) {
viewLifecycleOwner.collectShared(newsState, ::onNewsStateChanged)
// Initial fetch
fetchNews(
"some_data",
false
)
swipeRefreshLayout.setOnRefreshListener {
// Reload data
viewModel.fetchNews("some_data", true)
}
}
An extension function
inline fun <T : Any, L : SharedFlow<T>> LifecycleOwner.collectShared(
sharedFlow: L,
crossinline function: (T) -> Unit,
lifecycleState: Lifecycle.State = Lifecycle.State.STARTED
) {
lifecycleScope.launch {
repeatOnLifecycle(lifecycleState) {
sharedFlow.collect { t -> function(t) }
}
}
}
fun <T : Any> defaultMutableSharedFlow() =
MutableSharedFlow<T>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
ViewModel
private val _newsState = defaultMutableSharedFlow<NewsState>()
val newsState = _newsState.asSharedFlow()
fun fetchNews(category: String, isReload: Boolean) {
viewModelScope.launch {
getNewsUseCase(
isReload,
AppConfig.remote.newsLink,
AppConfig.remote.newsField,
category,
if (isReload)
getNewsUseCase.getPageNumber().minus(1)
else
getNewsUseCase.getPageNumber(),
).onEach {
when (it) {
is RequestStatus.Loading -> {
_newsState.tryEmit(NewsState.FetchLoading(it.data))
}
is RequestStatus.Success -> {
_newsState.tryEmit(NewsState.FetchSuccess(it.data))
}
is RequestStatus.Canceled -> {
// No operation is needed
}
is RequestStatus.Failed -> {
_newsState.tryEmit(NewsState.FetchFailed(it.message, it.data))
}
}
}.collect()
_lastSelectedCategory = category
}
}
Repository
override fun fetchNews(
isReload: Boolean,
baseUrl: String,
query: String,
category: String,
page: Int
) = flow {
emit(RequestStatus.Loading())
var domainModel = mapper.mapToDomainModelList(dao.getNewsByCategoryId(category, page))
try {
val cacheList = getProcessedNewsList(domainModel)
// Fetching data from remote can take longer
// Showing the cache first if available
emit(RequestStatus.Loading(cacheList))
val dtoModel = service.getNewsItems(
baseUrl + "posts",
query,
category,
page.toString()
)
val entities = mapper.mapToEntityModelList(dtoModel)
entities.forEach {
it.category = category
}
// Updating cache
dao.insert(entities)
// Get the updated cache
domainModel = mapper.mapToDomainModelList(dao.getNewsByCategoryId(category, page))
val freshData = getProcessedNewsList(domainModel)
// New page was fetched, if API response returns empty list then retain the page number
if (domainModel.isNotEmpty() && isReload.not())
_pageNumber = _pageNumber.plus(1)
emit(RequestStatus.Success(freshData))
} catch (e: HttpException) {
val cacheDomainList = getProcessedNewsList(domainModel)
// Support pagination for offline
if (domainModel.isNotEmpty() && isReload.not())
_pageNumber = _pageNumber.plus(1)
emit(RequestStatus.Failed(e, cacheDomainList))
} catch (e: IOException) {
val cacheDomainList = getProcessedNewsList(domainModel)
// Support pagination for offline
if (domainModel.isNotEmpty() && isReload.not())
_pageNumber = _pageNumber.plus(1)
emit(RequestStatus.Failed(e, cacheDomainList))
}
}
We are sometimes getting <-- HTTP FAILED: java.io.IOException: Canceled specially when triggering the SwipeRefreshLayout. As far as we know, this happens when the request or subscriber if using RxJava was cancelled but we are do not have code that performs cancelling here and leaving all handling to LifeCycle component. It seems the Coroutine is being cancelled. What is the issue here and how to avoid the cancellation?