Multiple API call with Observable.zip - android

I develop a RSS reader Android app. I want to API call more than one but I can't. My code is here, What am I doing wrong ?
Note;
Response will be XML as String dasd
Request size may increase (maybe 10)
ArticleServisGenerator;
object ArticleServiceGenerator {
private const val FAKE_URL = "https://api.github.com"
private val interceptor: HttpLoggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BASIC
}
private val client: OkHttpClient = OkHttpClient.Builder().apply {
addInterceptor(interceptor)
}.build()
private val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(FAKE_URL)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(client)
.build()
val service: ArticleService = retrofit.create(ArticleService::class.java)}
ArticleService;
interface ArticleService {
#GET
fun getArticlesFromRss(#Url url: String): Observable<ResponseBody>}
And Repository
class Repository {
fun getArticlesFromRss() {
val request: ArrayList<Observable<*>> = ArrayList()
// The number may increase here.
request.add(ArticleServiceGenerator.service.getArticlesFromRss("https://commonsware.com/blog/feed.atom"))
request.add(ArticleServiceGenerator.service.getArticlesFromRss("https://jfenn.me/blog/feeds/android.xml"))
request.add(ArticleServiceGenerator.service.getArticlesFromRss("https://arunkumar.dev/feed.xml"))
Observable.zip(request) { Any() }
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.newThread())
.subscribe({
print(it.toString())
}) {
print(it.toString())
}
}}

You can try something like below:
fun getArticlesFromRss() {
val request: ArrayList<Observable<ResponseBody>> = ArrayList()
request.add(ArticleServiceGenerator.service.getArticlesFromRss("https://commonsware.com/blog/feed.atom").subscribeOn(Schedulers.io()))
request.add(ArticleServiceGenerator.service.getArticlesFromRss("https://jfenn.me/blog/feeds/android.xml").subscribeOn(Schedulers.io()))
request.add(ArticleServiceGenerator.service.getArticlesFromRss("https://arunkumar.dev/feed.xml").subscribeOn(Schedulers.io()))
Observable.zip(request){ args -> Arrays.asList(args) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
print(it.toString())
}) {
print(it.toString())
}
}}

Each Observable inside zip is executed sequentially. Add subscribeOn(Schedulers.io()) to each getArticles Observable and it will execute them concurrently.

Observable<List<String>> result = Observable.zip(
observable1.subscribeOn(Schedulers.io()),
observable2.subscribeOn(Schedulers.io()),
observable3.subscribeOn(Schedulers.io()),
new Function3<ResponseType1, ResponseType2, ResponseType3, List<String>>() {
#Override
public List<String> apply(ResponseType1 type1, ResponseType2 type2, ResponseType3 type3) {
List<String> list = new ArrayList();
list.add(type1.data);
list.add(type2.data);
list.add(type3.data);
return list;
}
}
);

You just need to specify default scheduler.
.addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io))

Related

How to store the return value of suspended function to a variable?

I'm trying to understand Kotlin couroutine. So here's my code (based on this tutorial). To keep the code relatively simple, I deliberately avoid MVVM, LiveData, etc. Just Kotlin couroutine and Retrofit.
Consider this login process.
ApiInterface.kt
interface ApiInterface {
// Login
#POST("/user/validate")
suspend fun login(#Body requestBody: RequestBody): Response<ResponseBody>
}
ApiUtil.kt
class ApiUtil {
companion object {
var API_BASE_URL = "https://localhost:8100/testApi"
fun getInterceptor() : OkHttpClient {
val logging = HttpLoggingInterceptor()
logging.level = HttpLoggingInterceptor.Level.BODY
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(logging)
.build()
return okHttpClient
}
fun createService() : ApiInterface {
val retrofit = Retrofit.Builder()
.client(getInterceptor())
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(OJIRE_BASE_URL)
.build()
return retrofit.create(ApiInterface::class.java)
}
}
fun login(userParam: UserParam): String {
val gson = Gson()
val json = gson.toJson(userParam)
var resp = ""
val requestBody = json.toString().toRequestBody("application/json".toMediaTypeOrNull())
CoroutineScope(Dispatchers.IO).launch {
val response = createService().login(requestBody)
withContext(Dispatchers.Main){
if (response.isSuccessful){
val gson = GsonBuilder().setPrettyPrinting().create()
val prettyJson = gson.toJson(
JsonParser.parseString(
response.body()
?.string()
)
)
resp = prettyJson
Log.d("Pretty Printed JSON :", prettyJson)
}
else {
Log.e("RETROFIT_ERROR", response.code().toString())
}
}
}
return resp
}
}
LoginActivity.kt
class LoginActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
edtUsername = findViewById(R.id.edtUsername)
edtPassword = findViewById(R.id.edtPassword)
btnLogin = findViewById(R.id.btnLogin)
btnLogin.setOnClickListener {
val api = ApiUtil()
val userParam = UserParam(edtMobileNo.text.toString(), edtPassword.text.toString())
val response = JSONObject(api.login(userParam))
var msg = ""
if (response.getString("message").equals("OK")){
msg = "Login OK"
}
else {
msg = "Login failed"
}
Toast.makeText(applicationContext, msg, Toast.LENGTH_SHORT).show()
}
}
}
When debugging the login activity, the API response is captured properly on prettyJson
The problem is resp is still empty. Guess that's how async process work. What I want is to wait until the API call is completed, then the result can be nicely passed to resp as the return value of login(). How to do that?
Well, you got several things wrong here. We'll try to fix them all.
First, the main problem you described is that you need to acquire resp in login() synchronously. You got this problem only because you first launched an asynchronous operation there. Solution? Don't do that, get the response synchronously by removing launch(). I guess withContext() is also not required as we don't do anything that requires the main thread. After removing them the code becomes much simpler and fully synchronous.
Last thing that we need to do with login() is to make it suspendable. It needs to wait for the request to finish, so it is a suspend function. The resulting login() should be similar to:
suspend fun login(userParam: UserParam): String {
val gson = Gson()
val json = gson.toJson(userParam)
val requestBody = json.toString().toRequestBody("application/json".toMediaTypeOrNull())
val response = createService().login(requestBody)
return if (response.isSuccessful){
val gson = GsonBuilder().setPrettyPrinting().create()
gson.toJson(
JsonParser.parseString(
response.body()
?.string()
)
)
}
else {
Log.e("RETROFIT_ERROR", response.code().toString())
// We need to do something here
}
}
Now, as we converted login() to suspendable, we can't invoke it from the listener directly. Here we really need to launch asynchronous operation, but we won't use CoroutineScope() as you did in your example, because it leaked background tasks and memory. We will use lifecycleScope like this:
btnLogin.setOnClickListener {
val api = ApiUtil()
val userParam = UserParam(edtMobileNo.text.toString(), edtPassword.text.toString())
lifecycleScope.launch {
val response = JSONObject(api.login(userParam))
var msg = ""
if (response.getString("message").equals("OK")){
msg = "Login OK"
}
else {
msg = "Login failed"
}
withContext(Dispatchers.Main) {
Toast.makeText(applicationContext, msg, Toast.LENGTH_SHORT).show()
}
}
}
Above code may not be fully functional. It is hard to provide working examples without all required data structures, etc. But I hope you get the point.
Also, there are several other things in your code that could be improved, but I didn't touch them to not confuse you.

Retrofit with dynamic URL

in my application am using a Retrofit 2.9.0, my issue is the user can change completely the URL from the app menu, in this case is not working when i changed the URL only if i restart the app.
this my instance of Retrofit :
object ApiService {
var token: String = ""
#JvmName("setToken1")
fun setToken(tk: String) {
token = tk
}
private val globalInterceptor = GlobalErrorInterceptor()
private val loginInterceptor = LoginErrorInterceptor()
private val okHttpClient =
OkHttpClient.Builder().addInterceptor(globalInterceptor).build()
private val okHttpClientLogin =
OkHttpClient.Builder().addInterceptor(loginInterceptor).build()
var gson = GsonBuilder()
.setLenient()
.create()
/**This instance for the others requests */
private val retrofit by lazy {
Retrofit.Builder()
.baseUrl(LOGIN_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.client(okHttpClient)
.build()
}
val API: WebServicesApi by lazy {
retrofit.create(WebServicesApi::class.java)
}
/**This instance for the login to get the Token */
private val retrofitLogin by lazy {
Retrofit.Builder()
.baseUrl(LOGIN_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClientLogin)
.build()
}
val APILogin: WebServicesApi by lazy {
retrofitLogin.create(WebServicesApi::class.java)
}
}
You can dynamically change retrofit URL by doing something like this. First change retrofit from val to var.
private fun changeBaseUrl(url: String) {
// change the base url only if new url is different than old url
if (retrofit.baseUrl().toString() != url) {
retrofit = retrofit.newBuilder().baseUrl(url).build()
}
}
Please note you might have to change this method and call it according to your flow. The main point to note here is the use of .newBuilder().baseUrl(url).build().

How do i use rxjava to make a retrofit request in a repository and pass it to a ViewModel using LiveData?

I'm trying to make an Android app that uses Google's architecture components. I'm making a request to the TasteDive API using retrofit and rxjava in a Repository class. The problem that I'm facing is that I can't find a way to pass the outcome of the retrofit request to the ViewModel using LiveData. What I'm currently doing is I have a sealed class called Outcome that keeps track of the state of the request and in this sealed class i have a data class that holds the data of a successful request. However, since rxjava's observable is a callback, I can't figure out a way to assign the outcome of the request to LiveData and pass it to the ViewModel. When I try to assign the outcome to LiveData, it returns null, unsurprisingly, since the observable is a callback. Can any of you help me figure out a way to store the outcome of the retrofit request into LiveData in order to pass it to the ViewModel? I have looked all over the internet for a solution to this and have not found anything helpful. Here's my repository class:
class GetSimilarDataRepository {
private var mAdapter: TasteDiveAdapter? = null
private lateinit var mResultsList: ArrayList<Result>
private var observable: Observable<Response<TasteDive>>? = null
private var liveData = MutableLiveData<Outcome<List<Result>>>()
fun getSimilarData(map: LinkedHashMap<String, String>): LiveData<Outcome<List<Result>>> {
mAdapter?.clear()
val builder = Retrofit.Builder()
.baseUrl("https://www.tastedive.com/api/")
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
val retrofit = builder.build()
val client = retrofit.create(TasteDiveClient::class.java)
observable = client.getSimilarData(map)
observable?.filter { it.code() == 200 }
?.map { Observable.just(it.body()) }
?.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread())
?.doOnNext {
liveData.value = Outcome.loading(true)
}?.doOnError {
liveData.value = Outcome.failure(it)
}?.subscribeBy (
onError = {
liveData.value = Outcome.failure(it)
},
onNext = {
it.subscribe {
var similar = it?.Similar
var results = similar?.Results
if(results!!.isEmpty()) {
liveData.value = Outcome.failure(Throwable("No results for that request"))
} else {
mResultsList = ArrayList(results)
liveData.value = Outcome.success(mResultsList)
}
}
},
onComplete = { Log.v("onComplete", "onComplete")}
)
observable?.filter{ it.code() == 403 }
?.map { Observable.just(it.body()) }
?.subscribeBy(
onNext = {
liveData.value = Outcome.failure(Throwable("403 Response Code"))
},
onError = { Log.v("onError403", "onError403") },
onComplete = { Log.v("onComplete403", "onComplete403") }
)
observable?.filter{ it.code() == 404 }
?.map { Observable.just(it.body()) }
?.subscribeBy(
onNext = {
liveData.value = Outcome.failure(Throwable("404 Response Code"))
},
onError = { Log.v("onError404", "onError404") },
onComplete = { Log.v("onComplete404", "onComplete404") }
)
observable?.filter{ it.code() == 400 }
?.map { Observable.just(it.body()) }
?.subscribeBy(
onNext = {
liveData.value = Outcome.failure(Throwable("400 Response Code"))
},
onError = { Log.v("onError400", "onError400") },
onComplete = { Log.v("onComplete400", "onComplete400") }
)
observable?.filter{ it.code() == 500 }
?.map { Observable.just(it.body()) }
?.subscribeBy(
onNext = {
liveData.value = Outcome.failure(Throwable("500 Response Code"))
},
onError = { Log.v("onError500", "onError500") },
onComplete = { Log.v("onComplete500", "onComplete500") }
)
return liveData
}
}
The request is working, because mResultsList is giving me the correct results, but LiveData is returning null.
Here is the sealed class Outcome:
sealed class Outcome<T> {
data class Progress<T>(var loading: Boolean) : Outcome<T>()
data class Success<T>(var data: T) : Outcome<T>()
data class Failure<T>(val e: Throwable) : Outcome<T>()
companion object {
fun <T> loading(isLoading: Boolean): Outcome<T> = Progress(isLoading)
fun <T> success(data: T): Outcome<T> = Success(data)
fun <T> failure(e: Throwable): Outcome<T> = Failure(e)
}
}
Thanks for your time.
The issue is with the need for a Lifecycle Owner used for observations of LiveData in the Repository.
First, you don't want to do all networking actions inside the ViewModel. I think you have the right idea with the Repository, but you have to remember that the Repository will have to communicate only with the ViewModel. Best case, you would want the function getSimilarData(...) do some something like this:
Repository{
val repositoryItems = BehaviorSubject.create<Outcome<List<Result>>>()
fun observeRepositoryItems(): Observable<Outcome<List<Result>>> {
return repositoryItems
}
fun getSimilarData(map: LinkedHashMap<String, String>){
// Pseudo code for actually items
// Result goes into repositoryItems.onNext(...)
}
}
However, you will have the issue of observing the status from the ViewModel as it itself is not a Lifecycle implementation, so it cannot easily observe the LiveData from the Repository.
My suggestion would be something like this:
Repository{
val repositoryItems = BehaviorSubject.create<Outcome<List<Result>>>()
fun observeRepositoryItems(): Observable<Outcome<List<Result>>> {
return repositoryItems
}
fun getSimilarData(map: LinkedHashMap<String, String>){
// Pseudo code for actually items
// Result goes into repositoryItems.onNext(...)
}
}
ViewModel{
val items: MutableLiveData<Outcome<List<Result>>>
init{
repository.observeRepositoryItems()
.subscribe( items -> items.postValue(items ))
}
fun getData(){
repository.getSimilarData(someMap)
}
}
Fragment{
viewModel.items.observe() // <-- Here you observe items loaded
}
Note that you will have to dispose the subscription in the ViewModel onCleared.
Note that all of this is pseudo code and it should be done a lot cleaner than this.

Change retrofit okhttp client at runtime

fun getClient(token: String, userId: Long, language: String = "en", maxTry: Int = 2): Retrofit {
val okHttpClient = OkHttpClient.Builder()
okHttpClient.readTimeout(30, TimeUnit.SECONDS)
okHttpClient.connectTimeout(30, TimeUnit.SECONDS)
okHttpClient.writeTimeout(90, TimeUnit.SECONDS)
var tryCount = 0
okHttpClient.addInterceptor { chain ->
val original = chain.request()
val request = original.newBuilder()
.addHeader("Content-Type", "application/json")
.addHeader("secToken", token)
.addHeader("userId", userId.toString()).build()
var response = chain.proceed(request)
while (!response.isSuccessful && tryCount < maxTry) {
Log.d("intercept", "Request is not successful - $tryCount")
tryCount++
response = chain.proceed(request)
}
response
}
val builder = GsonBuilder()
builder.registerTypeAdapter(TransModel::class.java, NotificationTypeAdapter(language))
val gson = builder.create()
if (retrofit == null) {
retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.client(okHttpClient.build())
.build()
} else {
}
// .client(getHttpClientForFile())
return retrofit!!
}
Above code is to get singleton retrofit client for every request in App.
What I need to do is what to do in else part of retrofit == null.
Here language is initialized only once. while initializing retrofit, but for second request I don't have idea to change language and maxTry count for request.
I want to change language, and maxTry at runTime. For every request there must different maxTry count and may language also.
Edit:
As per suggestion of #EarlOfEgo my else part is
retrofit!!.newBuilder().baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.client(okHttpClient.build())
.build()
but It is not changing language.
Edit 2:
TransModel
class TransModel {
var en: String = ""
var gu: String = ""
var hi: String = ""
constructor()
fun get(language: String): String? {
return when (language) {
LanguageUtil.languageEn -> {
en
}
LanguageUtil.languageGu -> {
gu
}
LanguageUtil.languageHi -> {
hi
}
else -> {
null
}
}
}
constructor(language: String, value: String) {
when (language) {
LanguageUtil.languageEn -> {
en = value
}
LanguageUtil.languageGu -> {
gu = value
}
LanguageUtil.languageHi -> {
hi = value
}
}
}
fun getValueByLanguage(language: String): String? {
return when (language) {
LanguageUtil.languageEn -> {
en
}
LanguageUtil.languageGu -> {
gu
}
LanguageUtil.languageHi -> {
hi
}
else -> {
null
}
}
}
fun updateIt(title: TransModel, currentLanguage: String) {
when (currentLanguage) {
LanguageUtil.languageEn -> {
gu = title.gu
hi = title.hi
}
LanguageUtil.languageGu -> {
en = title.en
hi = title.hi
}
LanguageUtil.languageHi -> {
gu = title.gu
en = title.en
}
}
}
}
and my NotificationTypeAdapter
class NotificationTypeAdapter(val language: String) : TypeAdapter<TransModel>() {
override fun write(out: JsonWriter?, value: TransModel?) {
if (out == null || value == null) return
out.beginObject()
out.name("title")
out.value(value.getValueByLanguage(language))
out.endObject()
}
override fun read(reader: JsonReader?): TransModel? {
if (reader == null) return null
val jsonParser = JsonParser()
val je = jsonParser.parse(reader)
val trans = TransModel(language, (je.asString))
return trans
}
}
You can use the Retrofit method newBuilder to get a new builder and there set a different OkHttpClient with different attributes. Put something like this into your else case:
retrofit.newBuilder()
.client(anotherOkHttpClientWithOtherAttributes.build())
.build()
You can add Credentials manual as Header To your request.
Example:
#GET("user")
Call<UserDetails> getUserDetails(#Header("Authorization") String credentials)
Credentials.basic("ausername","apassword");
You can change BASE_URL at runtime by passing it as a parameter with the use of #Url annotation.
#FormUrlEncoded
#POST()
fun testService(#Url baseUrl: String,
#Field("exampleParam") exampleParam: String): Observable<String>
Please try this and let me know for any query.
Since I didn't found proper answer I am using new retrofit client for every call.
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.client(okHttpClient.build())
.build()
If anyone have more proper answer, Please suggest me some.

How to call a method multiple times using RXJava - which operator do I use?

I have a function which does a network call to retrieve data, and I need to call it 5 times. My method:
#Throws(IOException::class)
private fun getWeather(future : Int): String {
var twitterURL = Constants.API_URL
if (future > 0) {
twitterURL += "future$future.json"
}
val urlBuilder = HttpUrl.parse(Constants.API_URL).newBuilder()
val url = urlBuilder.build().toString()
val request = Request.Builder()
.url(url)
.build()
val client = OkHttpClient()
val response = client.newCall(request).execute()
val body = response.body()
return if (response.code() == HttpURLConnection.HTTP_OK) {
body.string()
} else {
throw IOException("Bad Request: Server Response" + response.code().toString() + " " + response.message())
}
}
I'd like to use the observable model doing something like:
private val array = DoubleArray(5)
Observable.fromArray(listOf(1, 2, 3, 4, 5))
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread()))
//observation on the main thread
.subscribe(object:Subscriber<Int>(){
override fun onCompleted() {
calculateStandardDeviation(array)
}
override fun onError(e: Throwable?) {
//TODO : Handle error here
}
override fun onNext(t: Int?) {
val string = getWeather(t)
val gson = Gson()
val weather = gson.fromJson(string,TwitterWeather::class.java)
array[t-1] = weather.temperature
}
})
But really, onNext() runs on the main thread. I want to run it all on the Schedulers.io() thread.
.observeOn(AndroidSchedulers.mainThread()) tells on which scheduler will the subscribe callbacks, including onNext run. So just use .observeOn(Schedulers.io())
Check this article for more details:
https://medium.com/upday-devs/rxjava-subscribeon-vs-observeon-9af518ded53a
Conceptually, you need to move your network call from the subscription logic and make it Observable, something like this:
Observable.fromIterable(listOf(1, 2, 3, 4, 5))
.flatMap { t ->
Observable.fromCallable {
val string = getWeather(t)
val gson = Gson()
val weather = gson.fromJson(string, TwitterWeather::class.java)
weather.temperature
}.subscribeOn(Schedulers.io())
}.toList()
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ temperatures ->
calculateStandardDeviation(temperatures)
}, {
//TODO : Handle error here
})

Categories

Resources