Android Retrofit handle response that could be JSON OR String - android

An API we use to login a user recently changed where if they need to take an action, a string is sent in the body instead of a JSON (which gets converted to an object). Therefore, I need to be able to handle this.
I changed my observable response to handle ANY like so:
#POST("api/v1/user/login")
fun postLogin(#Body body: LoginBody): Observable<Response<Any>>
but when I go to subscribe on this observable I am getting an exception that JSON has not been fully consumed.
postLogin(LoginBody(username, password))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
if (it.isSuccessful) {
val res = LoginRemote.parseResponse(it as Response<LoginRemote>)
if (res.result is Result.Ok) {
res.result.data.uid?.let { sessionData?.uid = it }
res.result.data.username?.let { sessionData?.username = it }
res.result.data.email?.let { sessionData?.email = it }
res.result.data.phoneType?.let { sessionData?.phoneType = it }
res.result.data.phone?.let { sessionData?.phone = it }
res.result.data.verified?.let { sessionData?.verified = it }
res.result.data.role?.let { sessionData?.role = it }
res.result.data.gender?.let { sessionData?.gender = it }
res.result.data.countryOfResidence?.let { sessionData?.countryOfResidence = it }
}
acceptTermsResponse.value = res
} else {
acceptTermsResponse.value = LoginResponse(listOf(LoginResponse.ErrorType.Generic()))
}
}, {
ERRORS HERE --> acceptTermsResponse.value = LoginResponse(listOf(LoginResponse.ErrorType.Generic()))
})
Is there a way to subscribe to a two types of response data?
I found this post and attempted the solution of adding the Scalar Converter Factory to my retrofit but that didn't solve the problem.

Related

How to combine two serial flows which input depends on other output?

I have sophisticated scenario where a set of mutually dependent coroutine flows depends on each other and chained:
viewModelScope.launch {
repository.cacheAccount(person)
.flatMapConcat { it->
Log.d(App.TAG, "[2] create account call (server)")
repository.createAccount(person)
}
.flatMapConcat { it ->
if (it is Response.Data) {
repository.cacheAccount(it.data)
.collect { it ->
// no op, just execute the command
Log.d(App.TAG, "account has been cached")
}
}
flow {
emit(it)
}
}
.catch { e ->
Log.d(App.TAG, "[3] get an exception in catch block")
Log.e(App.TAG, "Got an exception during network call", e)
state.update { state ->
val errors = state.errors + getErrorMessage(PersonRepository.Response.Error.Exception(e))
state.copy(errors = errors, isLoading = false)
}
}
.collect { it ->
Log.d(App.TAG, "[4] collect the result")
updateStateProfile(it)
}
}
cache an account on the local disk
create an account on the backend
in positive scenario, cache the newly create account in the local disk
Now I have to add more calls to a new API endpoint and the scenario become even more sophisticated. This endpoint is a ethereum chain.
4a. In the positive scenario, put in the local disk (cache) initiated transaction cacheRepository.createChainTx()
4b. In the negative scenario, just emit further the response from the backend
4a.->5. Register user on the 2nd endpoint repository.registerUser()
The response from 2nd endpoint put in the cache by updating existing row. Even negative case except of exception should be cached to update status of tx.
viewModelScope.launch {
lateinit var newTx: ITransaction
cacheRepository.createChainTxAsFlow(RegisterUserTransaction(userWalletAddress = userWalletAddress))
.map { it ->
newTx= it
repository.registerUserOnSwapMarket(userWalletAddress)
}
.onEach { it -> preProcessResponse(it, newTx) }
.flowOn(backgroundDispatcher)
.collect { it -> processResponse(it) }
}
This a scenario which should be integrated into the 1st Flow chain.
The issue is I do not see how to do it clear in Flow chain. I can rewrite code without chaining, but it also bring variety if else statements.
How would you do this scenario in human readable way?
I'll ended up with this code for transition period:
viewModelScope.launch(backgroundDispatcher) {
try {
var cachedPersonProfile = repository.cacheAccount(person)
var createAccountResponse = repository.createAccount(person)
when(createAccountResponse) {
is Response.Data -> {
repository.cacheAccount(createAccountResponse.data)
val cachedTx = cacheRepository.createChainTx(RegisterUserTransaction(userWalletAddress = person.userWalletAddress))
val chainTx = walletRepository.registerUserOnSwapMarket(userWalletAddress = person.userWalletAddress)
when(chainTx) {
is ru.home.swap.core.network.Response.Data -> {
if (chainTx.data.isStatusOK()) {
cachedTx.status = TxStatus.TX_MINED
} else {
cachedTx.status = TxStatus.TX_REVERTED
}
}
is ru.home.swap.core.network.Response.Error.Message -> {
cachedTx.status = TxStatus.TX_EXCEPTION
}
is ru.home.swap.core.network.Response.Error.Exception -> {
cachedTx.status = TxStatus.TX_EXCEPTION
}
}
cacheRepository.createChainTx(cachedTx)
withContext(Dispatchers.Main) {
state.update { state ->
if (cachedTx.status == TxStatus.TX_MINED) {
state.copy(
isLoading = false,
profile = createAccountResponse.data,
status = StateFlagV2.PROFILE
)
} else {
val txError = "Failed register the profile on chain with status ${TxStatus.TX_MINED}"
state.copy(
isLoading = false,
errors = state.errors + txError
)
}
}
}
}
else -> { updateStateProfile(createAccountResponse) }
}
} catch (ex: Exception) {
withContext(Dispatchers.Main) {
state.update { state ->
val errors = state.errors + getErrorMessage(PersonRepository.Response.Error.Exception(ex))
state.copy(errors = errors, isLoading = false)
}
}
}
}
If you have a better alternative, please share it in the post as an answer.

Proper way of handle sealed class property in kotlin

Hey I am working in Android Kotlin. I am learning this LatestNewsUiState sealed class example from Android doc. I made my own sealed class example. But I am confused little bit, how can I achieved this. Is I am doing right for my scenario or not?
DataState.kt
sealed class DataState {
data class DataFetch(val data: List<Xyz>?) : DataState()
object EmptyOnFetch : DataState()
object ErrorOnFetch : DataState()
}
viewmodel.kt
var dataMutableStateFlow = MutableStateFlow<DataState>(DataState.EmptyOnFetch)
fun fetchData() {
viewModelScope.launch {
val result = repository.getData()
result.handleResult(
onSuccess = { response ->
if (response?.items.isNullOrEmpty()) {
dataMutableStateFlow.value = DataState.EmptyOnFetch
} else {
dataMutableStateFlow.value = DataState.DataFetch(response?.items)
}
},
onError = {
dataMutableStateFlow.value = DataState.ErrorOnFetch
}
)
}
}
fun fetchMoreData() {
viewModelScope.launch {
val result = repository.getData()
result.handleResult(
onSuccess = { response ->
if (response?.items.isNullOrEmpty()) {
dataMutableStateFlow.value = DataState.EmptyOnFetch
} else {
dataMutableStateFlow.value = DataState.DataFetch(response?.items)
}
},
onError = {
dataMutableStateFlow.value = DataState.ErrorOnFetch
}
)
}
}
Activity.kt
lifecycleScope.launchWhenStarted {
viewModel.dataMutableStateFlow.collectLatest { state ->
when (state) {
is DataState.DataFetch -> {
binding.group.visibility = View.VISIBLE
}
DataState.EmptyOnFetch,
DataState.ErrorOnFetch -> {
binding.group.visibility = View.GONE
}
}
}
}
}
I have some points which I want to achieve in the standard ways.
1. When your first initial api call fetchData() if data is not null or empty then we need to show view. If data is empty or null then we need to hide the view. But if api fail then we need to show an error message.
2. When view is visible and view is showing some data. Then we call another api fetchMoreData() and data is empty or null then I don't want to hide view as per code is written above. And If api fails then we show error message.
Thanks in advance

How to Wait response from Server in forEach with Coroutines

I recently started working with coroutines.
The task is that I need to check the priority parameter from the List and make a request to the server, if the response from the server is OK, then stop the loop.
var minPriority = 0
list.forEach { model ->
if (model.priority > minPriority) {
makeRequest(model.value)
minPriority = model.priority
}
}
private fun makeRequest(value: String) {
scope.launch() {
val response = restApi.makeRequest()
if response.equals("OK") {
**stop list foreach()**
}
}
}
In RxJava, this was done using the retryWhen() operator, tell me how to implement this in Coroutines?
I suggest making your whole code suspendable, not only the body of makeRequest() function. This way you can run the whole operation in the background, but internally it will be sequential which is easier to code and maintain.
It could be something like this:
scope.launch() {
var minPriority = 0
list.forEach { model ->
if (model.priority > minPriority) {
val response = restApi.makeRequest()
if response.equals("OK") {
return#forEach
}
minPriority = model.priority
}
}
}
Of if you need to keep your makeRequest() function separate:
fun myFunction() {
scope.launch() {
var minPriority = 0
list.forEach { model ->
if (model.priority > minPriority) {
if (makeRequest(model.value)) {
return#forEach
}
minPriority = model.priority
}
}
}
}
private suspend fun makeRequest(value: String): Boolean {
val response = restApi.makeRequest()
return response.equals("OK")
}

RxJava ConcatArrayDelayError and filters: returning an error only if both sources fail

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.

RxJava map produces a different return type in Kotlin

I'm using RxJava with retrofit in order to do my network calls. In my code I have a retrofit call that returns a Single<Result<List<SpoonacularResult>>>. I have another function that calls the retrofit function and uses map to transform it into a Kotlin.Result. Another function takes the Kotlin.Result and changes it into another Kotlin.Result so that my UI can take the results and do whatever. The Problem I' running into is that after getRecipesViewModel calls getRecipes, the type of processedResult is Single<Result<Result<List<SpoonacularResult>>>. This only happens when my getRecipesRetrofit returns an error, it works completely fine when it returns a success. I'm not sure where the extra Result wrapping the inner Result comes from. Below is my code and some pictures to help illustrate what I'm talking about since the bug is a bit weird to explain.
returnResult type from getRecipes
processedResult from getRecipesViewModel
#GET("recipes/findByIngredients?number=50&ranking=2")
fun getRecipesRetrofit(#Query("ingredients", encoded = true) ingredients: String, #Query("apiKey") apiKey: String)
: Single<RxResult<List<SpoonacularResult>>>
fun getRecipes(ingredients: String): Single<KtResult<List<SpoonacularResult>>> {
return spoonService.getRecipesRetrofit(ingredients, "ApiKey").map { result ->
val processedResult = responseProcessor.process(result)
val returnResult = if (processedResult.isFailure()) {
KtResult.failure(processedResult.error!!)
} else {
KtResult.success(processedResult.body!!)
}
returnResult
}
}
fun getRecipesViewModel(set: MutableSet<String>): Single<Result<Int>> {
val ingredients = setUpSet(set)
return service.getRecipes(ingredients).map { processedResult ->
val networkResult = if (processedResult.isFailure) {
val error = processedResult.exceptionOrNull()!!
Result.failure(error)
} else {
val resultBody = processedResult.getOrNull()!!
if (resultBody.isNotEmpty()) {
CoroutineScope(Dispatchers.IO).launch {
recipeDao.clearRecipes()
resultBody.forEach {
recipeDao.insertRecipe(it)
}
}
Result.success(1)
} else {
Result.success(2)
}
}
networkResult
}
}

Categories

Resources