The Android Paging Library does not work when making asynchronous network calls using Retrofit. I am using the Google's sample code for Architecture Components on Github, and modified it for my needs.
I had faced the same issue previously but got around it by making synchronous call since the use-case allowed it. But in the current scenario, there are multiple network calls required and the data repository returns the combined result. I am using RxJava for this purpose.
Initially it seemed like a multi-threading issue, but this answer suggests otherwise. Observing the RxJava call on the Main Thread also does not work.
I have added the relevant code below. I stepped into the callback.onResult while debugging and everything works as expected. But ultimately it does not notify the Recycler View Adapter.
View Model snippet:
open fun search(query : String, init : Boolean = false) : Boolean {
return if(query == searchQuery.value && !init) {
false
} else {
searchQuery.value = query
true
}
}
fun refresh() {
listing.value?.refresh?.invoke()
}
var listing : LiveData<ListingState<T>> = Transformations.map(searchQuery) {
getList() // Returns the Listing State from the Repo snippet added below.
}
Repository snippet:
val dataSourceFactory = EvaluationCandidateDataSourceFactory(queryParams,
Executors.newFixedThreadPool(5) )
val pagelistConfig = PagedList.Config.Builder()
.setEnablePlaceholders(true)
.setInitialLoadSizeHint(5)
.setPageSize(25)
.setPrefetchDistance(25).build()
val pagedList = LivePagedListBuilder<Int, PC>(
dataSourceFactory, pagelistConfig)
.setFetchExecutor(Executors.newFixedThreadPool(5)).build()
val refreshState = Transformations.switchMap(dataSourceFactory.dataSource) {
it.initialState
}
return ListingState(
pagedList = pagedList,
pagingState = Transformations.switchMap(dataSourceFactory.dataSource) {
it.pagingState
},
refreshState = refreshState,
refresh = {
dataSourceFactory.dataSource.value?.invalidate()
},
retry = {
dataSourceFactory.dataSource.value?.retryAllFailed()
}
)
Data Source snippet :
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, PC>) {
try {
queryMap = if (queryMap == null) {
hashMapOf("page" to FIRST_PAGE)
} else {
queryMap.apply { this!!["page"] = FIRST_PAGE }
}
initialState.postValue(DataSourceState.LOADING)
pagingState.postValue(DataSourceState.LOADING)
val disposable : Disposable = aCRepositoryI.getAssignedAC(queryMap)
.subscribeOn(AndroidSchedulers.mainThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
if(it.success) {
// remove possible retries on success
retry = null
val nextPage = it.responseHeader?.let { getNextPage(it, FIRST_PAGE) } ?: run { null }
val previousPage = getPreviousPage(FIRST_PAGE)
callback.onResult(it.response.pcList, previousPage, nextPage)
initialState.postValue(DataSourceState.SUCCESS)
pagingState.postValue(DataSourceState.SUCCESS)
} else {
// let the subscriber decide whether to retry or not
retry = {
loadInitial(params, callback)
}
initialState.postValue(DataSourceState.failure(it.networkError.message))
pagingState.postValue(DataSourceState.failure(it.networkError.message))
Timber.e(it.networkError.message)
}
}, {
retry = {
loadInitial(params, callback)
}
initialState.postValue(DataSourceState.failure(it.localizedMessage))
pagingState.postValue(DataSourceState.failure(it.localizedMessage))
})
} catch (ex : Exception) {
retry = {
loadInitial(params, callback)
}
initialState.postValue(DataSourceState.failure(ex.localizedMessage))
pagingState.postValue(DataSourceState.failure(ex.localizedMessage))
Timber.e(ex)
}
}
Can someone please tell what is the issue here. There is a similar issue I mentioned above, but it recommends using synchronous calls. How can we do it using asynchronous calls or with RxJava.
I don't understand why you want to go to the main thread. The loading methods in the DataSource are running in a background thread. This means you can do synchronous work on this thread without blocking the main thread, which means you could just think of a solution without RxJava. Something like:
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, PC>) {
try {
val result = repository.fetchData(..)
// post result values and call the callback
catch (e: Exception) {
// post error values and log and save the retry
}
}
Then in your repository you can do this because we are not on the main thread.
fun fetchData(...) {
val response = myRetrofitService.someBackendCall(..).execute()
response.result?.let {
return mapResponse(it)
} ?: throw HttpException(response.error)
}
I might have messed up the syntax but I hope you get the point. No callbacks, no subscribing/observing but simple straightforward code.
Also if you start threading inside the loadInitial(..) method your initial list will be empty, so doing things synchronously also avoids seeing empty lists.
Related
I need to read the content of a collection in real-time. Here is what I have tried:
override fun getItems() = callbackFlow {
val listener = db.collection("items").addSnapshotListener { snapshot, e ->
val response = if (snapshot != null) {
val items = snapshot.toObjects(Item::class.java)
Response.Success(items)
} else {
Response.Error(e)
}
trySend(response).isSuccess //???
}
awaitClose {
listener.remove()
}
}
And it works fine. The problem is that I don't understand the purpose of .isSuccess. Is it mandatory to be added?
trySend() returns a ChannelResult object which contains the result of the operation. If ChannelResult.isSuccess returns true then the response had been successfully sent, otherwise the operation has been failed for some reason (maybe because of the buffer overflow) or because of a coroutine had been finished. You may handle it if you want, but usually it's omitted. Or you may log this result.
So basically, on the snackbar action button, I want to Retry API call if user click on Retry.
I have used core MVVM architecture with Flow. I even used Flow between Viewmodel and view as well. Please note that I was already using livedata between view and ViewModel, but now the requirement has been changed and I have to use Flow only. Also I'm not using and shared or state flow, that is not required.
Code:
Fragment:
private fun apiCall() {
viewModel.fetchUserReviewData()
}
private fun setObservers() {
lifecycleScope.launch {
viewModel.userReviewData?.collect {
LogUtils.d("Hello it: " + it.code)
setLoadingState(it.state)
when (it.status) {
Resource.Status.ERROR -> showErrorSnackBarLayout(-1, it.message, {
// Retry action button logic
viewModel.userReviewData = null
apiCall()
})
}
}
}
Viewmodel:
var userReviewData: Flow<Resource<ReviewResponse>>? = emptyFlow<Resource<ReviewResponse>>()
fun fetchUserReviewData() {
LogUtils.d("Hello fetchUserReviewData: " + userReviewData)
userReviewData = flow {
emit(Resource.loading(true))
repository.getUserReviewData().collect {
emit(it)
}
}
}
EDIT in ViewModel:
// var userReviewData = MutableStateFlow<Resource<ReviewResponse>>(Resource.loading(false))
var userReviewData = MutableSharedFlow<Resource<ReviewResponse>>()
fun fetchUserReviewData() {
viewModelScope.launch {
userReviewData.emit(Resource.loading(true))
repository.getUserReviewData().collect {
userReviewData.emit(it)
}
}
}
override fun onCreate() {}
}
EDIT in Activity:
private fun setObservers() {
lifecycleScope.launchWhenStarted {
viewModel.userReviewData.collect {
setLoadingState(it.state)
when (it.status) {
Resource.Status.SUCCESS ->
if (it.data != null) {
val reviewResponse: ReviewResponse = it.data
if (!AppUtils.isNull(reviewResponse)) {
setReviewData(reviewResponse.data)
}
}
Resource.Status.ERROR -> showErrorSnackBarLayout(it.code, it.message) {
viewModel.fetchUserReviewData()
}
}
}
}
}
Now, I have only single doubt, should I use state one or shared one? I saw Phillip Lackener video and understood the difference, but still thinking what to use!
The thing is we only support Portrait orientation, but what in future requirement comes? In that case I think I have to use state one so that it can survive configuration changes! Don't know what to do!
Because of the single responsibility principle, the ViewModel alone should be updating its flow to show the latest requested data, rather than having to cancel the ongoing request and resubscribe to a new one from the Fragment side.
Here is one way you could do it. Use a MutableSharedFlow for triggering fetch requests and flatMapLatest to restart the downstream flow on a new request.
A Channel could also be used as a trigger, but it's a little more concise with MutableSharedFlow.
//In ViewModel
private val fetchRequest = MutableSharedFlow<Unit>(replay = 1, BufferOverflow.DROP_OLDEST)
var userReviewData = fetchRequest.flatMapLatest {
flow {
emit(Resource.loading(true))
emitAll(repository.getUserReviewData())
}
}.shareIn(viewModelScope, SharingStarted.WhlieSubscribed(5000), 1)
fun fetchUserReviewData() {
LogUtils.d("Hello fetchUserReviewData: " + userReviewData)
fetchRequest.tryEmit(Unit)
}
Your existing Fragment code above should work with this, but you no longer need the ?. null-safe call since the flow is not nullable.
However, if the coroutine does anything to views, you should use viewLifecycle.lifecycleScope instead of just lifecycleScope.
I am doing multiple network requests in parallel and monitoring the result using a Stateflow.
Each network request is done in a separate flow, and I use combine to push the latest status on my Stateflow. Here's my code:
Repo class:
fun networkRequest1(id: Int): Flow<Resource<List<Area>>> =
flow {
emit(Resource.Loading())
try {
val areas = retrofitInterface.getAreas(id)
emit(Resource.Success(areas))
} catch (throwable: Throwable) {
emit(
Resource.Error()
)
)
}
}
fun networkRequest2(id: Int): Flow<Resource<List<Area>>> = //same code as above for simplicity
fun networkRequest3(id: Int): Flow<Resource<List<Area>>> = //same code as above for simplicity
fun networkRequest4(id: Int): Flow<Resource<List<Area>>> = //same code as above for simplicity
ViewModel class:
val getDataCombinedStateFlow: StateFlow<Resource<HashMap<String, Resource<out List<Any>>>>?> =
getDataTrigger.flatMapLatest {
withContext(it) {
combine(
repo.networkRequest1(id: Int),
repo.networkRequest2(id: Int),
repo.networkRequest3(id: Int),
repo.networkRequest4(id: Int)
) { a,
b,
c,
d
->
hashMapOf(
Pair("1", a),
Pair("2",b),
Pair("3", c),
Pair("4", d),
)
}.flatMapLatest {
val progress = it
var isLoading = false
flow<Resource<HashMap<String, Resource<out List<Any>>>>?> {
emit(Resource.Loading())
progress.forEach { (t, u) ->
if (u is Resource.Error) {
emit(Resource.Error(error = u.error!!))
// I want to cancel here, as I no longer care if 1 request fails
return#flow
}
if (u is Resource.Loading) {
isLoading = true
}
}
if (isLoading) {
emit(Resource.Loading())
return#flow
}
if (!isLoading) {
emit(Resource.Success(progress))
}
}
}
}
}.stateIn(viewModelScope, SharingStarted.Lazily, null)
View class:
viewLifecycleOwner.lifecycleScope.launchWhenCreated() {
viewModel.getDataCombinedStateFlow.collect {
val result = it ?: return#collect
binding.loadingErrorState.apply {
if (result is Resource.Loading) {
//show smth
}
if (result is Resource.Error) {
//show error msg
}
if (result is Resource.Success) {
//done
}
}
}
}
I want to be able to cancel all work after a Resource.Error is emitted, as I no longer want to wait or do any related work for the response of other API calls in case one of them fails.
How can I achieve that?
I tried to cancel the collect, but the flows that build the Stateflow keep working and emmit results. I know that they won't be collected but still, I find this a waste of resources.
I think this whole situation is complicated by the fact that you have source flows just to precede what would otherwise be suspend functions with a Loading state. So then you're having to merge them and filter out various loading states, and your end result flow keeps repeatedly emitting a loading state until all the sources are ready.
If you instead have basic suspend functions for your network operations, for example:
suspend fun networkRequest1(id: Int): List<Area> =
retrofitInterface.getAreas(id)
Then your view model flow becomes simpler. It doesn't make sense to use a specific context just to call a flow builder function, so I left that part out. (I'm also confused as to why you have a flow of CoroutineContexts.)
I also think it's much cleaner if you break out the request call into a separate function.
private fun makeParallelRequests(id: Int): Map<String, Resource<out List<Any>> = coroutineScope {
val results = listOf(
async { networkRequest1(id) },
async { networkRequest2(id) },
async { networkRequest2(id) },
async { networkRequest4(id) }
).awaitAll()
.map { Resource.Success(it) }
listOf("1", "2", "3", "4").zip(results).toMap()
}
val dataCombinedStateFlow: StateFlow<Resource<Map<String, Resource<out List<Any>>>>?> =
getDataTrigger.flatMapLatest {
flow {
emit(Resource.Loading())
try {
val result = makeParallelRequests(id)
emit(Resource.Success(result))
catch (e: Throwable) {
emit(Resource.Error(e))
}
}
}
I agree with #Tenfour04 that those nested flows are overly complicated and there are several ways to simplify this (#Tenfour04's solution is a good one).
If you don't want to rewrite everything then you can fix that one line that breaks the structured concurrency:
.stateIn(viewModelScope, SharingStarted.Lazily, null)
With this the whole ViewModel flow is started in the ViewModel's scope while the view starts the collect from a separate scope (viewLifecycleOwner.lifecycleScope which would be the Fragment / Activity scope).
If you want to cancel the flow from the view, you need to use either the same scope or expose a cancel function that would cancel the ViewModel's scope.
If you want to cancel the flow from the ViewModel itself (at the return#flow statement) then you can simply add:
viewModelScope.cancel()
I am making a network repository that supports multiple data retrieval configs, therefore I want to separate those configs' logic into functions.
However, I have a config that fetches the data continuously at specified intervals. Everything is fine when I emit those values to the original Flow. But when I take the logic into another function and return another Flow through it, it stops caring about its coroutine scope. Even after the scope's cancelation, it keeps on fetching the data.
TLDR: Suspend function returning a flow runs forever when currentCoroutineContext is used to control its loop's termination.
What am I doing wrong here?
Here's the simplified version of my code:
Fragment calling the viewmodels function that basically calls the getData()
lifecycleScope.launch {
viewModel.getLatestDataList()
}
Repository
suspend fun getData(config: MyConfig): Flow<List<Data>>
{
return flow {
when (config)
{
CONTINUOUS ->
{
//It worked fine when fetchContinuously was ingrained to here and emitted directly to the current flow
//And now it keeps on running eternally
fetchContinuously().collect { updatedList ->
emit(updatedList)
}
}
}
}
}
//Note logic of this function is greatly reduced to keep the focus on the problem
private suspend fun fetchContinuously(): Flow<List<Data>>
{
return flow {
while (currentCoroutineContext().isActive)
{
val updatedList = fetchDataListOverNetwork().await()
if (updatedList != null)
{
emit(updatedList)
}
delay(refreshIntervalInMs)
}
Timber.i("Context is no longer active - terminating the continuous-fetch coroutine")
}
}
private suspend fun fetchDataListOverNetwork(): Deferred<List<Data>?> =
withContext(Dispatchers.IO) {
return#withContext async {
var list: List<Data>? = null
try
{
val response = apiService.getDataList().execute()
if (response.isSuccessful && response.body() != null)
{
list = response.body()!!.list
}
else
{
Timber.w("Failed to fetch data from the network database. Error body: ${response.errorBody()}, Response body: ${response.body()}")
}
}
catch (e: Exception)
{
Timber.w("Exception while trying to fetch data from the network database. Stacktrace: ${e.printStackTrace()}")
}
finally
{
return#async list
}
list //IDE is not smart enough to realize we are already returning no matter what inside of the finally block; therefore, this needs to stay here
}
}
I am not sure whether this is a solution to your problem, but you do not need to have a suspending function that returns a Flow. The lambda you are passing is a suspending function itself:
fun <T> flow(block: suspend FlowCollector<T>.() -> Unit): Flow<T> (source)
Here is an example of a flow that repeats a (GraphQl) query (simplified - without type parameters) I am using:
override fun query(query: Query,
updateIntervalMillis: Long): Flow<Result<T>> {
return flow {
// this ensures at least one query
val result: Result<T> = execute(query)
emit(result)
while (coroutineContext[Job]?.isActive == true && updateIntervalMillis > 0) {
delay(updateIntervalMillis)
val otherResult: Result<T> = execute(query)
emit(otherResult)
}
}
}
I'm not that good at Flow but I think the problem is that you are delaying only the getData() flow instead of delaying both of them.
Try adding this:
suspend fun getData(config: MyConfig): Flow<List<Data>>
{
return flow {
when (config)
{
CONTINUOUS ->
{
fetchContinuously().collect { updatedList ->
emit(updatedList)
delay(refreshIntervalInMs)
}
}
}
}
}
Take note of the delay(refreshIntervalInMs).
I would like my app users to be able to cancel file upload.
My coroutine upload job in ViewModel looks like this
private var uploadImageJob: Job? = null
private val _uploadResult = MutableLiveData<Result<Image>>()
val uploadResult: LiveData<Result<Image>>
get() = _uploadResult
fun uploadImage(filePath: String, listener: ProgressRequestBody.UploadCallbacks) {
//...
uploadImageJob = viewModelScope.launch {
_uploadResult.value = withContext(Dispatchers.IO) {
repository.uploadImage(filePart)
}
}
}
fun cancelImageUpload() {
uploadImageJob?.cancel()
}
Then in the repository the Retrofit 2 request is handled like this
suspend fun uploadImage(file: MultipartBody.Part): Result<Image> {
return try {
val response = webservice.uploadImage(file).awaitResponse()
if (response.isSuccessful) {
Result.Success(response.body()!!)
} else {
Result.Error(response.message(), null)
}
} catch (e: Exception) {
Result.Error(e.message.orEmpty(), e)
}
}
When cancelImageUpload() it called the job gets cancelled and the exception gets caught in the repository but the result won't get assigned to uploadResult.value.
Any ideas please how to make this work?
PS: There is a similar question Cancel file upload (retrofit) started from coroutine kotlin android but it suggests using coroutines call adapter which is depricated now.
Have finally managed to make it work by moving withContext one level up like this
uploadImageJob = viewModelScope.launch {
withContext(Dispatchers.IO) {
_uploadResult.postValue(repository.uploadImage(filePart))
}
}