How to make parallel request in Retrofit? - android

I have a button which sync from my external api some data to my android device database, that data is get from two different APIs.
Which would be the best way to make the call to both of them and wait for the response from both APIs and then insert the data to the database?
My Sercice looks like this:
#GET("api/prodotti/fornitori")
fun getFornitori(): Call<List<Fornitori>>
#GET("api/prodotti/pv")
fun getPuntiVendita(): Call<List<PuntiVendita>
Till now i was doing two calls one after another, but at that point i was showing "Sync" snackbar twice and saying twice that the data has been saved, while i would do it once for both API calls...
The call for one of the APIs was like this:
val urlServer = preferenceScreen.sharedPreferences.getString("server", "http://127.0.0.1/")!!
val snackSincronizzo = Snackbar.make(
requireView(),
"Sincronizzo le impostazioni...",
Snackbar.LENGTH_INDEFINITE
)
snackSincronizzo.show()
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
val retrofit = Retrofit.Builder()
.baseUrl(urlServer)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
val service = retrofit.create(ApiService::class.java)
val callFornitori = service.getFornitori()
callFornitori.enqueue(object : Callback<List<Fornitori>> {
override fun onResponse(
call: Call<List<Fornitori>>,
response: Response<List<Fornitori>>
) {
if (response.isSuccessful) {
val fornitori = response.body()!!
if (fornitori.isNotEmpty()) {
for (fornitore in fornitori) {
corpoViewModel.insertFornitori(fornitore)
}
val callPuntiVendita = service.getPuntiVendita()
callPuntiVendita.enqueue(object : Callback<List<PuntiVendita>> {
override fun onResponse(
call: Call<List<PuntiVendita>>,
response: Response<List<PuntiVendita>>
) {
if (response.isSuccessful) {
val puntiVendita = response.body()!!
if (puntiVendita.isNotEmpty()) {
for (puntoVendita in puntiVendita) {
corpoViewModel.insertPuntiVendita(puntoVendita)
}
}
customSnack(requireView(), "Impostazioni sincronizzati con successo!", false)
snackSincronizzo.dismiss()
}else {
snackSincronizzo.dismiss()
customSnack(requireView(), "Errore durante la sincronizzazione!",true)
}
}
override fun onFailure(
call: Call<List<PuntiVendita>>,
t: Throwable
) {
TODO("Not yet implemented")
}
})
}
snackSincronizzo.dismiss()
}else {
snackSincronizzo.dismiss()
customSnack(requireView(), "Errore durante la sincronizzazione!",true)
}
}
override fun onFailure(call: Call<List<Fornitori>>, t: Throwable) {
snackSincronizzo.dismiss()
customSnack(requireView(), "Errore durante la sincronizzazione!",true)
}
})
So how can i change it to make both getFornitori and getPuntiVendita and get one callback with both data?

In Rx, You can try this.
Change your service class to Single instead of Call like this
#GET("api/prodotti/fornitori")
fun getFornitori(): Single<List<Fornitori>>
#GET("api/prodotti/pv")
fun getPuntiVendita(): Single<List<PuntiVendita>
And call it like this. We can use Zip operator in RxJava to achieve this
private fun getBothData() {
val fornitorList = service.getFornitori()
val puntiVenditaList = service.getPuntiVendita()
val dispose =
Single.zip<List<Fornitori>, List<PuntiVendita>, Pair<List<Fornitori>, List<PuntiVendita>>>(
fornitorList,
puntiVenditaList,
BiFunction { t1, t2 -> Pair(t1, t2) }
).subscribe(
{
val firstApiData = it.first
val secondApiData = it.second
},
{
//Handle Error Part
})
}
Add all RxJava Dependecies & addCallAdapterFactory(RxJava2CallAdapterFactory.create()) in retrofit builder.
Approach 2 in Kotlin using coroutine
Service class
#GET("api/prodotti/fornitori")
suspend fun getFornitori(): List<Fornitori>
#GET("api/prodotti/pv")
suspend fun getPuntiVendita(): List<PuntiVendita>
Change your method
suspend fun getBothData(): Pair<List<Fornitori>, List<PuntiVendita>>> {
var data: Pair<List<Fornitori>, List<PuntiVendita>> = Pair(listOf(), listOf())
coroutineScope {
val firstAPIData = async { service.getFornitori()}.await()
val seconfAPIData = async { service.getPuntiVendita()}.await()
data = Pair(firstAPIData, seconfAPIData)
return#coroutineScope
}
return data
}
Now from calling the place
GlobalScope.launch {
val bothData = getBothData()
val firstApiData = bothData.first
val secondApiData = bothData.second
}

Related

What is the simplest way to make a post request in Kotlin for Android app

The question about post requests in android has been asked before, but all the solutions I've tried have not worked properly. On top of that, a lot of them seem to be overly complicated as well. All I wish to do is make a post to a specific sight with a few body parameters. Is there any simple way to do that?
Let me explain my request calling structure using Retrofit.
build.gradle(app)
// Retrofit + GSON
implementation 'com.squareup.okhttp3:logging-interceptor:4.4.0'
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
ApiClient.kt
object ApiClient {
private const val baseUrl = ApiInterface.BASE_URL
private var retrofit: Retrofit? = null
private val dispatcher = Dispatcher()
fun getClient(): Retrofit? {
val logging = HttpLoggingInterceptor()
if (BuildConfig.DEBUG)
logging.level = HttpLoggingInterceptor.Level.BODY
else
logging.level = HttpLoggingInterceptor.Level.NONE
if (retrofit == null) {
retrofit = Retrofit.Builder()
.client(OkHttpClient().newBuilder().readTimeout(120, TimeUnit.SECONDS)
.connectTimeout(120, TimeUnit.SECONDS).retryOnConnectionFailure(false)
.dispatcher(
dispatcher
).addInterceptor(Interceptor { chain: Interceptor.Chain? ->
val newRequest = chain?.request()!!.newBuilder()
return#Interceptor chain.proceed(newRequest.build())
}).addInterceptor(logging).build()
)
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
return retrofit
}
}
ApiClient will be used to initialize Retrofit singleton object, also initialize logging interceptors so you can keep track of the requests and responses in the logcat by using the keyword 'okhttp'.
SingleEnqueueCall.kt
object SingleEnqueueCall {
var retryCount = 0
lateinit var snackbar: Snackbar
fun <T> callRetrofit(
activity: Activity,
call: Call<T>,
apiName: String,
isLoaderShown: Boolean,
apiListener: IGenericCallBack
) {
snackbar = Snackbar.make(
activity.findViewById(android.R.id.content),
Constants.CONST_NO_INTERNET_CONNECTION, Snackbar.LENGTH_INDEFINITE
)
if (isLoaderShown)
activity.showAppLoader()
snackbar.dismiss()
call.enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
hideAppLoader()
if (response.isSuccessful) {
retryCount = 0
apiListener.success(apiName, response.body())
} else {
when {
response.errorBody() != null -> try {
val json = JSONObject(response.errorBody()!!.string())
Log.e("TEGD", "JSON==> " + response.errorBody())
Log.e("TEGD", "Response Code==> " + response.code())
val error = json.get("message") as String
apiListener.failure(apiName, error)
} catch (e: Exception) {
e.printStackTrace()
Log.e("TGED", "JSON==> " + e.message)
Log.e("TGED", "Response Code==> " + response.code())
apiListener.failure(apiName, Constants.CONST_SERVER_NOT_RESPONDING)
}
else -> {
apiListener.failure(apiName, Constants.CONST_SERVER_NOT_RESPONDING)
return
}
}
}
}
override fun onFailure(call: Call<T>, t: Throwable) {
hideAppLoader()
val callBack = this
if (t.message != "Canceled") {
Log.e("TGED", "Fail==> " + t.localizedMessage)
if (t is UnknownHostException || t is IOException) {
snackbar.setAction("Retry") {
snackbar.dismiss()
enqueueWithRetry(activity, call, callBack, isLoaderShown)
}
snackbar.show()
apiListener.failure(apiName, Constants.CONST_NO_INTERNET_CONNECTION)
} else {
retryCount = 0
apiListener.failure(apiName, t.toString())
}
} else {
retryCount = 0
}
}
})
}
fun <T> enqueueWithRetry(
activity: Activity,
call: Call<T>,
callback: Callback<T>,
isLoaderShown: Boolean
) {
activity.showAppLoader()
call.clone().enqueue(callback)
}
}
SingleEnqueueCall will be used for calling the retrofit, it is quite versatile, written with onFailure() functions and by passing Call to it, we can call an API along with ApiName parameter so this function can be used for any possible calls and by ApiName, we can distinguish in the response that which API the result came from.
Constants.kt
object Constants {
const val CONST_NO_INTERNET_CONNECTION = "Please check your internet
connection"
const val CONST_SERVER_NOT_RESPONDING = "Server not responding!
Please try again later"
const val USER_REGISTER = "/api/User/register"
}
ApiInterface.kt
interface ApiInterface {
companion object {
const val BASE_URL = "URL_LINK"
}
#POST(Constants.USER_REGISTER)
fun userRegister(#Body userRegisterRequest: UserRegisterRequest):
Call<UserRegisterResponse>
}
UserRegisterRequest.kt
data class UserRegisterRequest(
val Email: String,
val Password: String
)
UserRegisterResponse.kt
data class UserRegisterResponse(
val Message: String,
val Code: Int
)
IGenericCallBack.kt
interface IGenericCallBack {
fun success(apiName: String, response: Any?)
fun failure(apiName: String, message: String?)
}
MyApplication.kt
class MyApplication : Application() {
companion object {
lateinit var apiService: ApiInterface
}
override fun onCreate() {
super.onCreate()
apiService = ApiClient.getClient()!!.create(ApiInterface::class.java)
}
}
MyApplication is the application class to initialize Retrofit at the launch of the app.
AndroidManifest.xml
android:name=".MyApplication"
You have to write above tag in AndroidManifest inside Application tag.
MainActivity.kt
class MainActivity : AppCompatActivity(), IGenericCallBack {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val call = MyApplication.apiService.userRegister(UserRegisterRequest(email, password))
SingleEnqueueCall.callRetrofit(this, call, Constants.USER_REGISTER, true, this)
}
override fun success(apiName: String, response: Any?) {
val model = response as UserRegisterResponse
}
override fun failure(apiName: String, message: String?) {
if (message != null) {
showToastMessage(message)
}
}
}
Firstly, we create a call object by using the API defined in ApiInterface and passing the parameters (if any). Then using SingleEnqueueCall, we pass the call to the retrofit along with ApiName and the interface listener IGenericCallBack by using this. Remember to implement it to respective activity or fragment as above.
Secondly, you will have the response of the API whether in success() or failure() function overriden by IGenericCallBack
P.S: You can differentiate which API got the response by using the ApiName parameter inside success() function.
override fun success(apiName: String, response: Any?) {
when(ApiName) {
Constants.USER_REGISTER -> {
val model = response as UserRegisterResponse
}
}
}
The whole concept is to focus on reusability, now every API call has to create a call variable by using the API's inside ApiInterface then call that API by SingleEnqueueCall and get the response inside success() or failure() functions.

How to run retrofit in retrofit?

I'm making cryptocurrency information viewer using Retrofit. Everything is okay, but have a problem.
One exchange demands me execute retrofit twice.
https://api.upbit.com/v1/market/all
https://api.upbit.com/v1/ticker?markets=KRW-BTC,KRW-ETH,KRW-BTG,KRW-BSV,KRW-ETC ..., KRW-(Some cryptocurrency)
Base url is "https://api.upbit.com/v1/". And I have to get 'markets' from first api, and get cryptocurrency's information by using 'markets' as query value.
But there are problem. I thought first and second execution. Get markets from first execution, and get infomations by using markets. But Retrofit basically runs asynchronously, So usually skip first execution and there are no markets in second executions. (markets = "")
How can I run Retrofit in Retrofit? Did I approach wrong? Or there are solution for this problem?
This is Retrofit parts in my code.
var markets = ""
val publishSubject: PublishSubject<String> = PublishSubject.create()
init {
publishSubject.subscribe {
markets = it
}
}
fun getData(exchange: String) {
// var markets = "Basic"
val url = when(exchange) {
coinone -> "https://api.coinone.co.kr/"
bithumb -> "https://api.bithumb.com/"
upbit -> {
getMarketsUpbit()
"https://api.upbit.com/v1/"
}
else -> "https://api-cloud.huobi.co.kr/"
}
val parser = DataParser()
val builder = Retrofit.Builder()
.baseUrl(url)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(RetrofitService::class.java)
val call: retrofit2.Call<Any> =
when (exchange) {
coinone ->
builder.getTickersCoinone("all")
bithumb ->
builder.getTickersBithumb()
upbit ->
builder.getTickersUpbit(markets)
else ->
builder.getTickersHuobi()
}
call.enqueue(object : retrofit2.Callback<Any> {
override fun onResponse(call: retrofit2.Call<Any>, response: retrofit2.Response<Any>) {
coinInfos.value = parser.getParsedData(
if (exchange != "upbit") exchange
else markets
,
response.body().toString()
)
}
override fun onFailure(call: retrofit2.Call<Any>, t: Throwable) {
println("Retrofit process is failed.")
}
})
}
private fun getMarketsUpbit() {
val parser = DataParser()
var markets = ""
val builder = Retrofit.Builder()
.baseUrl("https://api.upbit.com/v1/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(RetrofitService::class.java)
val call: retrofit2.Call<Any> = builder.getMarketsUpbit()
call.enqueue(object : retrofit2.Callback<Any> {
override fun onResponse(call: retrofit2.Call<Any>, response: retrofit2.Response<Any>) {
publishSubject.onNext(parser.parseUpbitMarkets(response.body().toString()))
}
override fun onFailure(call: retrofit2.Call<Any>, t: Throwable) {
println("Retrofit process is failed.")
}
})
}

What is the Correct way to use Live data in MVVM if I need to reload data after some params change?

I have a project that part of it uses MVVM Architecture ,,I use a repo to get the data from backend like this :
private val spotlights: MutableLiveData<List<SpotlightModel>> = MutableLiveData()
fun getSpotLight(segmentId: String, segmentType: String): MutableLiveData<List<SpotlightModel>>? {
mProvider.getSpotLight(
segmentType = segmentType,
adType = TYPE_SPOT_LIGHT,
segmentId = segmentId,
topics = application.profileManger.topicsIds,
publishers = application.profileManger.publisherIds,
debug = application.debugFlag,deviceId = application.idfa)
.enqueue(object : Callback<List<SpotlightModel>> {
override fun onResponse(call: Call<List<SpotlightModel>>, response: Response<List<SpotlightModel>>) {
if (response.isSuccessful) {
spotlights.postValue(response.body())
}
}
override fun onFailure(call: Call<List<SpotlightModel>>, t: Throwable) {
spotlights.postValue(null)
}
})
return spotlights
}
Then in the viewmodel I do this ::
class SpotlightViewModel(val application: NewsApplication) :ViewModel() {
var segmentType: String=""
var segmentId:String=""
val repository=SpotlightRepository(application)
var spotslightsLiveData=repository.getSpotLight(segmentId,segmentType)!!
//if I didn't add this method the data doesn't comeback
fun getSpotLights() {
spotslightsLiveData= repository.getSpotLight(segmentId,segmentType)!!
}}
I observe it like this
fun getSpotlights(s_id:String, s_type:String){
spotlightViewModel?.segmentId=s_id
spotlightViewModel?.segmentType=s_type
spotlightViewModel?.spotslightsLiveData?.removeObservers(viewLifecycleOwner)
spotlightViewModel?.spotslightsLiveData?.observe(viewLifecycleOwner, Observer
{ spotlightList ->
if (spotlightList.isNullOrEmpty()) {
newNewsAdapter?.spotLights = null
newNewsAdapter?.notifyItemChanged(0)
}
else if (spotlightList.isNotEmpty()) {
newNewsAdapter?.spotLights = spotlightList
newNewsAdapter?.notifyItemChanged(0)
}
})
spotlightViewModel?.getSpotLights() }
This code works fine but observer is called many times..also can I use a switch map here if yes how?? Thanks in advance :)
You can use switchMap:
val resultLiveData = paramsLiveData.switchMap { params ->
getLiveDataBy(params)
}

postValue() doesn't call observe on MutableLiveData

I have an API where I get a list of addresses from postcode. Here is where I make the API call, and return a MutableLiveData object that I then try to observe
fun getAddressFromPostCode(postCode: String): MutableLiveData<List<Address>>{
val trimmedPostCode = postCode.replace("\\s".toRegex(),"").trim()
val dataBody = JSONObject("""{"postcode":"$trimmedPostCode"}""").toString()
val hmac = HMAC()
val hmacResult = hmac.sign(RequestConstants.CSSecretKey, dataBody)
val postData = PostCodeBodyData()
postData.postcode = trimmedPostCode
val body = PostCodeRequestData()
body.dataSignature = hmacResult
body.data = postData
//val myBodyModel = MyBodyModel(data = dataBody, data_signature = hmacResult)
val url = RequestConstants.CS_BASE_URL
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(url)
.addConverterFactory(GsonConverterFactory.create())
.build()
val api:GetAddressAPIService = retrofit.create(GetAddressAPIService ::class.java)
val myCall: Call<GetAddressResponse> = api.getPostCodeAddress(body)
myCall.enqueue(object : Callback<GetAddressResponse> {
override fun onFailure(call: Call<GetAddressResponse>?, t: Throwable?) {
Log.d("RegistrationInteractor", "Something went wrong", t)
Log.d("RegistrationInteractor", call.toString())
}
override fun onResponse(call: Call<GetAddressResponse>?, response: Response<GetAddressResponse>?) {
// Success response
if(response?.body()?.success == 1){
addressLiveData.postValue(response!!.body()!!.addresses)
}else{
Log.d("", response!!.body()?.serError?.errorDescription)
addressLiveData.postValue(null)
}
}
})
return addressLiveData
}
I then listen for this in my PostCodeFragment:
btn_find_address.setOnClickListener {
val interactor = RegistrationInteractor()
addresses = interactor.getAddressFromPostCode(et_register_postcode.text.toString())
lifecycle.addObserver(interactor)
addresses.observe( viewLifecycleOwner, Observer {
#Override
fun onChanged(newAddresses: List<Address>) {
//Set UI
addressList.addAll(newAddresses)
adapter.notifyDataSetChanged()
}
})
}
However onChanged never gets called. Why is this happening?
You need to do like below
val interactor = RegistrationInteractor()
interactor.addressLiveData.observe( viewLifecycleOwner, Observer {
#Override
fun onChanged(newAddresses: List<Address>) {
//Set UI
addressList.addAll(newAddresses)
adapter.notifyDataSetChanged()
}
})
Update
Do not return your MutableLiveData<List<Address>> object from your method. Just get a direct instance of your addressLiveData and use it to observe.

How to get result from response inside method?

I am new android developer, how can I get result form this snippet, what way does exist, because it doesn't return anything, because of I'm adding element inside onResponse, but using only kotlin module:
private fun foo(list: ArrayList<CurrencyModel> = ArrayList()): ArrayList<CurrencyModel> {
val request = Request.Builder().url(BASE_URL_YESTERDAY).build()
val client = OkHttpClient()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
}
override fun onResponse(call: Call, response: Response) {
val body = response.body?.string()
val data = Gson().fromJson(body, Currencies::class.java)
list.add(CurrencyModel("USD", data.rates.USD, 0.0))
list.add(CurrencyModel("SEK", data.rates.SEK, 0.0))
list.add(CurrencyModel("EUR", data.rates.EUR, 0.0))
}
})
return list
}
}
You can give your function a callback parameter that's called when the response is receieved. And you shouldn't have an input list in this case, because if you have multiple sources modifying it at unpredictable future moments, it will be difficult to track.
The function can look like this:
private fun getCurrencyModels(callback: (ArrayList<CurrencyModel>) {
val request = Request.Builder().url(BASE_URL_YESTERDAY).build()
val client = OkHttpClient()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
}
override fun onResponse(call: Call, response: Response) {
val body = response.body?.string()
val data = Gson().fromJson(body, Currencies::class.java)
val list = arrayListOf(
CurrencyModel("USD", data.rates.USD, 0.0)),
CurrencyModel("SEK", data.rates.SEK, 0.0)),
CurrencyModel("EUR", data.rates.EUR, 0.0))
)
callback(list)
}
})
}
And then to use it:
getCurrencyModels { modelsList ->
// do something with modelsList when it arrives
}
An alternative is to use coroutines, which allow you to do asynchronous actions without callbacks. Someone has already created a library that lets you use OkHttp requests in coroutines here. You could write your function as a suspend function like this:
private suspend fun getCurrencyModels(): ArrayList<CurrencyModel> {
val request = Request.Builder().url(BASE_URL_YESTERDAY).build()
val client = OkHttpClient()
val response = client.newCall(request).await()
val body = response.body?.string()
val data = Gson().fromJson(body, Currencies::class.java)
return arrayListOf(
CurrencyModel("USD", data.rates.USD, 0.0)),
CurrencyModel("SEK", data.rates.SEK, 0.0)),
CurrencyModel("EUR", data.rates.EUR, 0.0))
)
}
and then use it like this:
lifecycleScope.launch {
try {
val currencyModels = getCurrencyModels()
// do something with currencyModels
} catch (e: IOException) {
// request failed
}
}
Coroutines make it really easy to avoid leaking memory when your asynchronous calls outlive your Activity or Fragment. In this case, if your Activity closes while the request is going, it will be cancelled automatically and references to your Activity will be removed so the garbage collector can release your Activity.
The onResponse() function is only called when the HTTP response is successfully returned by the remote server. Since this response doesn't happen immediately, you can't use the result in your code immediately. What you could do is use a ViewModel and LiveData variable and add the values to that variable in onResponse(). Something like:
private fun foo(list: ArrayList<CurrencyModel> = ArrayList()) {
val request = Request.Builder().url(BASE_URL_YESTERDAY).build()
val client = OkHttpClient()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
}
override fun onResponse(call: Call, response: Response) {
val body = response.body?.string()
val data = Gson().fromJson(body, Currencies::class.java)
val list: ArrayList<CurrencyModel> = arrayListOf()
list.add(CurrencyModel("USD", data.rates.USD, 0.0))
list.add(CurrencyModel("SEK", data.rates.SEK, 0.0))
list.add(CurrencyModel("EUR", data.rates.EUR, 0.0))
viewModel.list.postValue(list)
}
})
}

Categories

Resources