I'm trying to update the validate the list and update the value accordingly but when I'm getting an error, the process gets stopped. I'm calling it in ViewModel class as:
fun validateList(list: List<Model>): Single<List< Model >> {
return Observable.fromIterable(list)
.toFlowable(BackpressureStrategy.LATEST)
.flatMapSingle { validate() }
.toList()
.map { list.mapIndexed { index, testModel ->
(if (it[index] != null) {
testModel.isVerified = it[index].toString()
} else throw Exception("ERROR")); testModel }
}
.onErrorResumeNext { error -> }
And I'm calling it from fragment as:
private fun verify() {
showLoading(true)
testViewModel.validateList(arrayList)
.doFinally {
showLoading(false)
}
.asyncToUiSingle()
.subscribe({
adjustCard(it)
}, {
it.printStackTrace()
})
.addTo(disposables)
}
TestModel:
data class TestModel(
val title: String,
var isVerified: String? = null,
var reason: String? = null )
Please help me to understand how I can update value of reason field in list if one element gets failed status and continue the validations for other elements.
onErrorResumeNext require return an ObservableSource and it will be invoke directly after error happened:
. onErrorResumeNext { error ->
val newList = // Code for new list with updated data
Observable.just(newList) // The new list will be emit to subscriber
}
In your case, I think you can use retry, it will be continue the chain after error happened with given condition, let try:
var index = 0
val validatedList = mutableListOf<Model>()
return Observable.fromIterable(list)
//...
.map {
for (i in index until list.size) {
val testModel = list[i];
if (testModel != null) {
testModel. isVerified = testModel.toString()
validatedList.add(testModel);
} else {
throw Exception("ERROR");
}
}
validatedList
}
.retry { error ->
error.printStackTrace()
index++
index < list.size
}
That's mean when error happened, the retry will trigger and continue the chain if u didn't in the end of list. But, I don't think that's best practice, you maybe try another way. Hope can help you.
Related
Hello I'm emitting each item from the List individually in the ViewModel class to perform some validations on a string of each item which returns boolean value and I update that boolean value in each item and return List. But I'm unable to update one more parameter in List as reason which will come as Throwable.stacktrace().
In ViewModel:
fun validateList(list: List<TestModel>): Single<List<TestModel>> {
return Observable.fromIterable(list)
.toFlowable(BackpressureStrategy.LATEST)
.map { it.jsonString }
.map { it?.let { VerifiableObject(it) } ?: throw IllegalStateException("Json must not be null") }
.flatMapSingle { validate() }
.toList()
.map {
list.mapIndexed { index, testModel ->
(if (it[index] != null) {
//Updating boolean value here
testModel.isVerified = it[index].toString()
} else throw Exception("ERROR")); testModel
}
}
}
In Fragment:
viewModel. validateList(arrayList)
.doFinally {
showLoading(false)
}
.asyncToUiSingle()
.subscribe({
//Updating UI
adjustCard(it)
}, {
it.printStackTrace()
})
.addTo(disposables)
TestModel:
data class TestModel(
val title: String,
var isVerified: String? = null,
var reason: String? = null)
Here I need to insert value in reason field once any item gets false value with exception. Please help me if you have any idea about it.
If validate is causing you trouble, apply onErrorResumeNext to it and turn the subsequence back into the original TestModel item:
fun validateList(list: List<TestModel>): Single<List<TestModel>> {
return Observable.fromIterable(list)
.flatMapSingle { value ->
validate(VerifiableObject(value.jsonString))
.map {
value.isVerified = it.toString
value
}
.onErrorResumeNext {
value.reason = it.toString
Single.just(value)
}
}
.toList()
}
I have a problem with liveData in a particular case. When the response from a http service is Code = 2, it means that the session token has expired. In that case I navigate to the LoginFragment to login the user again. If the user logs in, then I return to the fragment which was previously and when I start to observe the liveData in onViewCreated function, it gives me its last value which is: Code = 2, so the Application navigates back to the login, which is wrong.
I have a Sealed Class:
sealed class Resource<T>(
var data: T? = null,
val message: String? = null,
val Code: Int? = null
) {
class Success<T>(data: T?) : Resource<T>(data)
class Error<T>(message: String, code: Int? = null) : Resource<T>(message = message, Code = code)
class Loading<T> : Resource<T>()
}
This is the code on the ViewModel:
val mLiveData: MutableLiveData<Resource<Data>> = MutableLiveData()
fun getData() {
viewModelScope.launch {
mLiveData.postValue(Resource.Loading())
try {
if (app.hasInternetConnection()) {
// get Data From API
val response = repository.getData()
if(response.isSuccessful){
mLiveData.postValue(Resource.Success(parseSuccessResponse(response.body())))
} else {
mLiveData.postValue(Resource.Error(parseErrorResponse(response.body())))
}
} else {
mLiveData.postValue(Resource.Error("Connection Fail"))
}
} catch (t: Throwable) {
when (t) {
is IOException -> mLiveData.postValue(Resource.Error("Connection Fail"))
else -> mLiveData.postValue(Resource.Error("Convertion Fail"))
}
}
}
}
This is the code on the fragment, observeForData() is called in onViewCreated function:
private fun observeForData() {
mLiveData.getData.observe(viewLifecycleOwner, Observer { response ->
when (response) {
is Resource.Success -> {
isLoading = false
updateUI(response.data)
}
is Resource.Error -> {
isLoading = false
if (response.Code == 2) {
// Token Expired
navigateToLogin()
} else {
showErrorMessage(response.message)
}
}
is Resource.Loading -> {
isLoading = true
}
}
})
}
How can i solve this?
Is there a way to remove the last value or state from a liveData when the fragment is destroyed when navigating to the LoginFragment?
Thank you.
One often-suggested solution is SingleLiveEvent, which is a simple class you can copy-paste into your project.
For a framework solution, I suggest SharedFlow. Some Android developers recommend switching from LiveData to Flows anyway to better decouple data from views. If you give SharedFlow a replay value of 0, new Activities and Fragments that observe it will not get the previous value, only newly posted values.
Sample untested ViewModel code:
val dataFlow: Flow<Resource<Data>> = MutableSharedFlow(replay = 0)
init {
viewModelScope.launch {
// Same as your code, but replace mLiveData.postValue with dataFlow.emit
}
}
And in the Fragment:
private fun observeForData() {
isLoading = true
lifecycleScope.launch {
mLiveData.dataFlow
.flowWithLifecycle(this, Lifecycle.State.STARTED)
.collect { onDataResourceUpdate(it) }
}
}
// (Broken out into function to reduce nesting)
private fun onDataResourceUpdate(resource: Resource): Unit = when(resource) {
is Resource.Success -> {
isLoading = false
updateUI(response.data)
}
is Resource.Error -> {
isLoading = false
if (response.Code == 2) {
// Token Expired
navigateToLogin()
} else {
showErrorMessage(response.message)
}
}
is Resource.Loading -> isLoading = true
}
To change last updated value for live data,You can set "Resource" class with default null values when onDestroy().
onDestroy(){
//in java ,it will be new Resource instance
Resource resourceWithNull=new Resource();
mLiveData.setValue(resourceWithNull);
}
when you relaunch the fragment live data will emit Resource with null value as response.
Then You can write your code with in observer
if(response.data!=null)
{
//your code
}
I'd like to verify two things:
if the way I use retryWhen is correct or not. Basically, when an exception is caught, I want to get an updated value and rerun the sequence.
if another function also needs value, how to make it waits for updateValue to complete in the first instance? I've played with .share() and RxReplayingShare but I'm not sure how to use those properly.
val value = 0
#Test
fun test() {
executeFunction()
.retryWhen { errors -> errors.flatMap { error ->
if (error is WrongValue) {
updateValue()
.doOnSuccess { value = it }
.toObservable()
} else {
Observable.error(error)
}
}
}
}
fun executeFunction(): Single<Int> =
if (value == 0) {
Single.error(WrongValue())
} else {
Single.just(value)
}
fun updateValue(): Single<Int> = Single.just(1)
I'm using nested Coroutine blocks in my code. And I'm getting a null value when I tried to get Deferred type's result to a variable. Thus, It causes a casting problem which is kotlin.TypeCastException: null cannot be cast to non-null type kotlin.collections.ArrayList in getNearbyHealthInstitutions() method's return line. I believe, I did the right implementation at some point but what am I missing to get null value from Deferred's result? The funny thing is when I debug it, it does return the expected value. I think it should be the concurrency problem or I don't have any idea why it works in debug mode in the first place. Any ideas fellas?
// Invocation point where resides in a callback
GlobalScope.launch(Dispatchers.Main) {
nearbyHealthInstitutionSites.value = getNearbyHealthInstitutions()
}
private suspend fun getNearbyHealthInstitutions(radius: Meter = DEFAULT_KM_RADIUS) : ArrayList<Hospital> {
return CoroutineScope(Dispatchers.IO).async {
val list = getHealthInstitutions()
val filteredList = list?.filter { it.city == state?.toUpperCase() } as MutableList<Hospital>
Log.i(MTAG, "nearby list is $filteredList")
Log.i(MTAG, "nearby list's size is ${filteredList.size}")
var deferred: Deferred<MutableList<Hospital>>? = null
addAllNearbyLocations(onEnd = { nearbyHealthInstitutions ->
deferred = async {
findNearbyOfficialHealthInstitutions(
officialHealthInstitutionList = filteredList as ArrayList<Hospital>,
nearbyHealthInstitutions = nearbyHealthInstitutions
)
}
})
val result = deferred?.await()
return#async result as ArrayList<Hospital>
}.await()
}
private suspend fun findNearbyOfficialHealthInstitutions(officialHealthInstitutionList: ArrayList<Hospital>, nearbyHealthInstitutions: MutableList<Hospital>): MutableList<Hospital> {
return GlobalScope.async(Dispatchers.Default) {
val result = mutableListOf<Hospital>()
officialHealthInstitutionList.forEach {
nearbyHealthInstitutions.forEach { hospital ->
StringSimilarity.printSimilarity(it.name, hospital.name)
val similarity = StringSimilarity.similarity(it.name, hospital.name.toUpperCase())
if (similarity > SIMILARITY_THRESHOLD) {
Log.i(MTAG, "findNearbyOfficialHealthInstitutions() - ${it.name} and ${hospital.name.toUpperCase()} have %$similarity")
result.add(hospital)
}
}
}
Log.i(TAG, "------------------------------------------")
result.forEach {
Log.i(MTAG, "findNearbyOfficialHealthInstitutions() - hospital.name is ${it.name}")
}
return#async result
}.await()
}
Since addAllNearbyLocations() is asynchonous, your coroutine needs to wait for the callback to be called to continue its execution. You can use suspendCoroutine API for this.
val result = suspendCoroutine { continuation ->
addAllNearbyLocations(onEnd = { nearbyHealthInstitutions ->
findNearbyOfficialHealthInstitutions(
officialHealthInstitutionList = filteredList as ArrayList<Hospital>,
nearbyHealthInstitutions = nearbyHealthInstitutions
).let { found -> continuation.resume(found) }
})
}
On a separate note you should use List instead of ArrayList or MutableList, you should always look to use a generic interface instead of a specific implementation of that interface. This also gets rids of some of the castings (ideally you should have no castings in this code).
I'm new to RxJava and after a few days of trying everything I could find online I see that I really need help with this one.
I fetch a member in my repository with local and remote sources. I added some operators to return my remote source in priority (via debounce), and to filter out errors so it would return only 1 of the 2 if either remote is not available or the database is empty.
It works fine as long as something is returned by one of my 2 sources, but the problem occurs if both sources returns errors: as I filter out the errors, it doesn't return anything, and my subscribe is never called.
Maybe there is a simple solution but I have not found it so far, could someone help?
Here is my fetchMember() in my Repository:
override fun fetchMember(): Observable<MemberModel?> {
return Observable.concatArrayDelayError(memberLocalSource.fetchMember(), memberRemoteSource.fetchMember())
.doOnNext { member ->
saveMember(member!!)
}
.materialize()
.filter { !it.isOnError }
.dematerialize { it -> it }
.debounce(400, TimeUnit.MILLISECONDS)
}
And here is my viewmodel:
fun fetchToken(username: String, password: String) {
val loginDisposable = authApiService.loginWithJWT(username, password)
.flatMap {
isAuthenticated = isTokenValid(username, password, it)
sharedPreferences.setHasValidCredentials(isAuthenticated)
memberRepository.fetchMember()
}
.subscribeOn(Schedulers.io())
.observeOn((AndroidSchedulers.mainThread()))
.doOnError { throwable ->
throwable.printStackTrace()
}
.subscribe(
{ member ->
memberLiveData.value = member
this.memberId = member!!.id.toString()
this.memberName = member.name.split(" ")[0]
if(isAuthenticated) {
authenticationState.value = AuthenticationState.AUTHENTICATED_VALID_MEMBER
} else {
authenticationState.value = AuthenticationState.UNAUTHENTICATED_VALID_MEMBER
}
},
{ error ->
if(isAuthenticated) {
authenticationState.value = AuthenticationState.AUTHENTICATED_INVALID_MEMBER
} else {
authenticationState.value = AuthenticationState.INVALID_AUTHENTICATION
}
})
disposable.add(loginDisposable)
}
private fun isTokenValid(username: String, password: String, authResponse: AuthModel): Boolean {
return if (authResponse.data != null) {
false
} else {
tokenInterceptor.token = authResponse.token
val tokenWithCredentials = AuthModel(authResponse.token, null, null, username, password)
tokenRepository.saveToken(tokenWithCredentials)
true
}
}
In the end I managed to make it work by adding:
.defaultIfEmpty(MemberModel(-1))
and checking against id == -1.