Is there any feature that is equivalent to Single in sandwich library? - android

I need to call sequential APIs. Is there any way to make it simple?
It's maybe related to 'map' feature or this one
suspend fun reCheckout(
orderData: OrderData
) = flow {
loginAndRegister(orderData.phoneNumber).suspendOnSuccess {
val jwt = "jwt ${data.auth.token}"
Client.saveAuth(data)
cancelOrder(
orderData.orderId,
OrderCancelRequestBody("store", "re-checkout")
).suspendOnSuccess {
addMultiCartItem(jwt, orderData.cartItems).suspendOnSuccess {
checkOut(orderData.checkoutBody!!).suspendOnSuccess {
emit(this.data)
}.suspendOnError {
emit(this)
}.suspendOnException {
emit(this)
}
}.suspendOnError {
emit(this)
}.suspendOnException {
emit(this)
}
}.suspendOnError {
emit(this)
}.suspendOnException {
emit(this)
}
}.suspendOnError {
emit(this)
}.suspendOnException {
emit(this)
}
}
I don't know if it's the right way or not. I'd like to have only one onError and onException here like Single in RxJava. Is that possible?
I also want to handle the data in ViewModel with onSuccess, onException, onError, etc. How can I implement it?
At this moment, since all the response are different, emits different types. So, I can get them as Any and then use is Keyword. In this way, viewModel will be messy. Is there any better way?

Related

Why does Flow (kotlinx.coroutines.flow) not working with Retry even though I manually set as null in Android?

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.

Chaining and mapping kotlin flow

I'm trying to get a result from a flow, that retrieves a list from a room database, and then trying to map the list with another flow inside from another database operation, but I don't know if it is possible and if it is, how to make it, at this time I'm trying to make something like this
fun retrieveOperationsWithDues(client: Long): Flow<List<ItemOperationWithDues>> {
return database.operationsDao.getOperationCliente(client)
.flatMapMerge {
flow<List<ItemOperationWithDues>> {
it.map { itemOperation ->
database.duesDao.cuotasFromOperation(client, itemOperation.id).collectLatest { listDues ->
itemOperation.toItemOperationWithDues(listDues)
}
}
}
}
}
but looks like is not retrieving anything from the collect. Thanks in advice for any help
I think you don't need to use flow builder in flatMapMerge block. For each itemOperation you can call the cuotasFromOperatio() function from the Dao, which returns Flow and use combine() to combine retrieved flows:
fun retrieveOperationsWithDues(client: Long): Flow<List<ItemOperationWithDues>> {
return database.operationsDao.getOperationCliente(client)
.flatMapMerge {
val flows = it.map { itemOperation ->
database.duesDao.cuotasFromOperation(client, itemOperation.id).map { listDues ->
itemOperation.toItemOperationWithDues(listDues)
}
}
combine(flows) { flowArray -> flowArray.toList() }
}
}

I want to call several api and combine the response values of different object types

I want to call several api and combine the response values of different object types.
val itemList : MutableList<BaseItem>
private fun fetchAllData() {
viewModelScope.launch{
val deferreds = listOf(
async { loadData1()},
async { loadData2()}
)
deferreds.awaitAll().forEach {
itemList.add(it)
}
}
}
I want to get a return by combining datatype1 and datatype2 into BaseItem.
Unable to return the callback data from the repository.
I think there's a way to do it using live data. What should I do?
fun loadData1(): ArrayList<DataType1> {
repository.getData1(param, callback) {
onSuccess(List<DataType1>) {
return
}
}
}
fun loadData2(): ArrayList<DataType2> {
repository.getData1(param, callback) {
onSuccess(List<DataType2>) {
return
}
}
}
I'll be waiting for your help.
Well, what I would do is I would switch repository functions to be suspend functions and write the code in synchronized way:
val itemsLiveData = MutableLiveData<BaseItem>()
private fun fetchAllData() = viewModelScope.launch{
try {
val itemList : MutableList<BaseItem>
itemsList.addAll(repository.loadData1(param))
itemsList.addAll(repository.loadData2(param))
itemsLiveData.postValue(itemsList)
} catch(e: Exception) {
// do something with exception
}
}
And if you want to call several Rest API for example, I would go with Retrofit which has built-in support for suspend functions since ver. 2.6.0
https://github.com/square/retrofit/blob/master/CHANGELOG.md#version-260-2019-06-05

Android Paging library does not work with Asynchronous requests

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.

RxJava Alternate to CompletableFuture

I am new to Rx java and trying to find a solution to my problem. I want to return a Single to caller but the data is not available when that method is called, it will get filled either before that call is made or after that call is made. A naive example of what I am trying to do is below. CompletableFuture partially solves it but I am looking for Rx solution, possibly with backpressure.
val receiver = Receiver()
class Receiver {
var data = ""
// this returns a single but does not have data, but will be available after call to onComplete
request(): Single<Data> {
return Single.fromFuture(completableFuture)
}
onNext(data: String) {
data.append(data)
}
onComplete() {
completableFuture.complete(data)
}
}
class Processor {
fun process() {
receiver.onNext("1")
receiver.onNext("2")
receiver.onComplete()
}
}
class Caller {
fun call() {
// This should get "12" result
// Processor().process() can be called before or after caller subscibe
receiver.request()..subscribe(...)
}
}
Instead of CompletableFuture, you can use PublishSubject. In order to be able to supply the value either before or after the subscribe call, you should also used .cache() on the Single. Put it all together like this:
class Receiver {
var data = ""
val subject = PublishSubject.create<Data>()
val single = Single.fromObservable(subject).cache()
// this ensures there is at least one subscription when the event is published - otherwise because there is no subscriber, the event is lost, despite cache().
val dummyObserver = single.subscribe()
// this returns a single but does not have data, but will be available after call to onComplete
fun request(): Single<Data> {
return single
}
fun onNext(data: String) {
// as before
}
fun onComplete() {
subject.onNext(data)
subject.onComplete(data)
}
}

Categories

Resources