Hi I am trying to learn rxjava2. I am trying to call API's using rxjava2 and using retrofit for building URL and converting JSON into Moshi.
I want to use Observable pattern with retrofit. Does anyone know whats way to do it ? Any standard and best approach like wrapper for error handling and all ?
AppModule.kt
#Provides
#Singleton
fun provideRetrofit(moshi: Moshi, okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(BuildConfig.BASE_URL)
.client(okHttpClient)
.build()
}
ApiHelperImpl.kt
#Inject
lateinit var retrofit: Retrofit
override fun doServerLoginApiCall(email: String, password: String): Observable<LoginResponse> {
retrofit.create(RestApi::class.java).login(email, password)
}
I am calling doServerLoginApiCall from the LoginViewModel like below
LoginViewModel.kt
fun login(view: View) {
if (isEmailAndPasswordValid(email, password)) {
ApiHelperImpl().doServerLoginApiCall(email, password)
}
}
RestApi.kt
interface RestApi {
#FormUrlEncoded
#POST("/partner_login")
fun login(#Field("email") email: String, #Field("password") password: String): Call<LoginResponse>
}
LoginResponse.kt
data class LoginResponse(
#Json(name = "code")
val code: Int? = null,
#Json(name = "otp_verify")
val otpVerify: Int? = null,
#Json(name = "data")
val userDetails: UserDetails? = null,
#Json(name = "message")
val message: String? = null,
#Json(name = "status")
val status: String? = null
)
This is the rough idea to show you how to use Retrofit2 with RxJava2. you can find a lot of tutorial in google.
Step 1:
Add the following dependencies to your gradle file
// Rx stuff
compile "io.reactivex.rxjava2:rxjava:$rxJavaVersion"
compile "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion"
// retrofit
compile "com.squareup.retrofit2:retrofit:$retrofitVersion"
compile "com.squareup.retrofit2:adapter-rxjava2:$retrofitVersion"
compile "com.squareup.retrofit2:converter-moshi:$retrofitVersion"
Step 2: Create you Retrofit API interface like you do but it has a bit difference that is the return type should be Observable<LoginResponse> not a Call<LoginResponse>
interface RestApi {
#FormUrlEncoded
#POST("/partner_login")
fun login(#Field("email") email: String, #Field("password") password: String): Observable<LoginResponse>
}
Step 3:
build you retrofit API object:
retrofit.create(RestApi::class.java).login(email, password)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe{ loginResponse ->
// TODO deal with your response here
}
Do not you just know how to return results?
The way to return results with rx is as follows.
ApiHelperImpl().doServerLoginApiCall(email, password)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { result ->
// doSomething
}
subscribeOn calls api in a other thread.
observeOn is a process for processing subscribe in the main thread.
subscribe has multiple overloading methods. Please check the document.
You can refer to the following article: [link]:https://medium.com/3xplore/handling-api-calls-using-retrofit-2-and-rxjava-2-1871c891b6ae
Related
I am learning to make api calls using retrofit, But whenever i am trying to make a get request from he api My app crashes and gives the following errors..
java.lang.IllegalArgumentException: Unable to create call adapter for class java.lang.Object
for method WeatherApi.getweatherbycity
Caused by: java.lang.IllegalArgumentException: Could not locate call adapter for class java.lang.Object.
Tried:
* retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
* retrofit2.ExecutorCallAdapterFactory
at retrofit2.Retrofit.nextCallAdapter(Retrofit.java:237)
at retrofit2.Retrofit.callAdapter(Retrofit.java:201)
at retrofit2.ServiceMethod$Builder.createCallAdapter(ServiceMethod.java:232)
... 29 more
I tried many solutions from stackoverflow but nothing is working i am also new to mvvm and viewmodels stuffs.
class RetrofitInstance {
companion object{
private val retrofit by lazy{
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build()
}
val api by lazy{
retrofit.create(WeatherApi::class.java)
}
}
}
interface WeatherApi {
#GET("data/2.5/weather")
suspend fun getweatherbycity(
#Query("q")
cityname: String="London",
#Query("appid")
api_key: String = API_KEY
): Response<WeatherResponse>
}
i have created the Weather Response class using Json to kotlin converter plugin in android studio
data class WeatherResponse(
val base: String,
val clouds: Clouds,
val cod: Int,
val coord: Coord,
val dt: Int,
val id: Int,
val main: Main,
val name: String,
val sys: Sys,
val timezone: Int,
val visibility: Int,
val weather: List<Weather>,
val wind: Wind
)
class WeatherRepository{
suspend fun getweatherbycityname(cityname:String)=RetrofitInstance.api.getweatherbycity(cityname)
}
class WeatherViewModel(
val weather_rep: WeatherRepository
):ViewModel() {
val current_weather:MutableLiveData<Response<WeatherResponse>> = MutableLiveData()
fun getweatherbycity(city:String="London")= viewModelScope.launch {
val weather=weather_rep.getweatherbycityname(city)
//current_weather.postValue(weather)
}
}
dependencies-
implementation 'com.squareup.retrofit2:retrofit:2.1.0'
implementation 'com.squareup.retrofit2:converter-gson:2.1.0'
implementation 'com.squareup.retrofit2:adapter-rxjava:2.1.0'
In my case, I was using suspend in my API interface method, however the problem was that I was using Retrofit2 2.3.0 when I should have been using Retrofit2 2.6 or higher, as according to this, in that version they added official support for Coroutines.
My API:
interface CoolApi {
#POST("/api/v1/path")
#Headers("#: Noauth")
suspend fun coolApiCall(#Body request: coolRequest): Response<CoolResponse>?
}
The Retrofit builder:
val api = Retrofit.Builder()
.baseUrl(BuildConfig.BASE_URL)
.client(createOkHttpClient())
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(CoolApi::class.java)
And then I launch the Coroutine like this:
lifecycleScope.launch(Dispatchers.IO) {
try {
val response = apiMethod(api)
if (response!!.isSuccessful) {
callback.onSuccess(response.code(), response.body())
} else {
// Error handling
}
} catch (e: Exception) {
when (e) {
// Exception handling
}
}
}
Beware that in order to use lifecycleScope you need to add some dependencies. In my gradel.build (:app) I have:
// For lifecycle-aware coroutines
def lifecycle_version = "2.3.1"
// Lifecycles only (without ViewModel or LiveData)
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:1.5.31"
And then you can use lifecycleScope from an Activity or Fragment. If you're launching the Coroutine in another file (which was my case), you can pass it as an argument.
You are incorrectly to get the response here:
#GET("data/2.5/weather")
suspend fun getweatherbycity(
#Query("q")
cityname: String="London",
#Query("appid")
api_key: String = API_KEY
): Response<WeatherResponse>
Because you're using RxJavaCallAdapterFactory please return observeable for the response
#GET("data/2.5/weather")
suspend fun getweatherbycity(
#Query("q")
cityname: String="London",
#Query("appid")
api_key: String = API_KEY
): Single<Response<WeatherResponse>>
interface WeatherAp make it public
it s happened same things to me, i just updated retrofit version from build.gradle and works. try to update retrofit version.
I'm using retrofit to make a network request to an API. The response code returns 200 but I am receiving null when trying to access the fields. I have checked out other solutions but can't seem to solve my problem. I am using hilt
Here is my API class
interface BlockIOApi{
#GET("/api/v2/get_balance/")
suspend fun getBalance(
#Query("api_key")
apiKey: String = BuildConfig.API_KEY
): Response<BalanceResponse>
}
and here is my app module object
AppModule
#Module
#InstallIn(ApplicationComponent::class)
object AppModule{
#Singleton
#Provides
fun provideOkHttpClient() = if (BuildConfig.DEBUG) {
val loggingInterceptor = HttpLoggingInterceptor()
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.build()
} else OkHttpClient
.Builder()
.build()
#Provides
#Singleton
fun providesRetrofit(okHttpClient: OkHttpClient): Retrofit =
Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(BASE_URL)
.client(okHttpClient)
.build()
#Provides
#Singleton
fun providesApiService(retrofit: Retrofit): BlockIOApi = retrofit.create(BlockIOApi::class.java)
}
And finally here is my repositories, DefaultRepository.kt
class DefaultRepository #Inject constructor(
private val blockIOApi: BlockIOApi,
private val balanceDao: BalanceDao
):BlockIORepository {
override suspend fun getBalance(): Resource<BalanceResponse> {
return try {
val response = blockIOApi.getBalance()
Log.d("TAG", "getBalance>>Response:${response.body()?.balance} ")
if (response.isSuccessful){
response.body().let {
return#let Resource.success(it)
}
}else{
Log.d("TAG", "getBalance: Error Response >>> ${response.message()}")
Resource.error("An unknown error occured",null)
}
}catch (ex :Exception){
Resource.error("Could not reach the server.Check your internet connection",null)
}
}
and this interface,BlockIORepository.kt
interface BlockIORepository {
suspend fun getBalance(): Resource<BalanceResponse>
suspend fun insertBalance(balance: Balance)
suspend fun getCachedBalance(): Balance
suspend fun getAddresses(): Resource<DataX>
}
Here are my data classes
data class BalanceResponse(
val balance: Balance,
val status: String
)
#Entity
data class Balance(
val available_balance: String,
val network: String,
val pending_received_balance: String,
#PrimaryKey(autoGenerate = false)
var id: Int? = null
)
The problem comes when I try to access the data object. I am not getting null for the status object
I have been stuck on this for two days now. Any help will be highly appreciated. Thanks in advance.
The problem is occured here:
data class BalanceResponse(
val balance: Balance, <-- in postman it is "data"
val status: String
)
You should consider putting #SerializedName(xxx) for your class.
data class BalanceResponse(
#SerializedName("data") val balance: Balance,
val status: String
)
Your class should name filed as per the json or it should provide #SerializedName
So your BalanceResponse class should be
data class BalanceResponse(
#SerializedName("data")
val balance: Balance,
#SerializedName("status")
val status: String
)
Since you are trying to hold data in balance, you must provide SerializedName, but if they have the same name and with exact-case then the parser will automatically recognize them.
I keep getting error IllegalArgumentException when making minimal changes to transition from AsyncTask (before) to Kotlin Coroutines (after). Note that code is working as expected with AsyncTask.
Note: Retrofit is calling my own .php script that returns some object SimpleResultObject encoded in json String.
Before the change:
Retrofit:
#FormUrlEncoded
#POST("activity_signup.php")
fun activitySignUp(
#Field("activity_id") activityId: Int,
#Field("user_id") userId: Int) : Call<SimpleResultObject>
Activity (inside of AsyncTask):
#Override
protected doInBackground(...) {
val gson = GsonBuilder().setLenient().create()
val retrofit = Retrofit.Builder()
.baseUrl(LOCALHOST_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
val service = retrofit.create(RetrofitAPI::class.java)
val call = service.activitySignUp(activity_id, userId)
call.enqueue(Callback<SimpleResultObject>() {}
Receive object in #onResponse method and normally proceed futher.
After the change:
Retrofit:
#FormUrlEncoded
#POST("activity_signup.php")
suspend fun activitySignUp(
#Field("activity_id") activityId: Int,
#Field("user_id") userId: Int): SimpleResultObject
Activity:
fun signUp() {
myActivityScope.launch(Dispatchers.Main) {
val gson = GsonBuilder().setLenient().create()
val retrofit = Retrofit.Builder()
.baseUrl(LOCALHOST_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
val service = retrofit.create(RetrofitAPI::class.java)
try {
val result = service.activitySignUp(specificResultObject.activityId, userId)
} catch (t:Throwable)
Throws java.lang.IllegalArgumentException: No Retrofit annotation found. (parameter #3) for method RetrofitAPI.activitySignUpon service.activitySignUp line call
Note: myActivityScope is costum CoroutineScope that finished when hosting Activity finishes.
I have tried everything I could remember: adding OkHttpClient, changing to MoshiConverterFactory, trying other CoroutineScopes and Dispatchers, ...
EDIT: the problem might be on my .php side due to Exeption being above my argument number (maybe null result?), but don't know why something that worked before wouldn't work now.
Based on responses to the question I made a few modifications to the code and managed to fix the issue. The most important, as #Mohammad Sianaki pointed out, was rising Retrofit version from 25.0.0 to 26.0.0 that solved the problem.
So for everyone else that might get the IllegalArgumentException for the argument above the parameter number - consider checking Retrofit versions.
Special thanks to everyone that helped, especially to #CommonsWare!
The provided code in question has some structural issues.
First of all, it seems that a retrofit object is being created for each API call. So, it should be one for all API calls of the application.
Second, network operations should be executed in non-main threads. In the case of coroutines, they should be called in non-main contexts, like Dispatchers.IO.
Third, I think you should return a Response<SimpleResultObject> instead of SimpleResultObject in API functions.
Supposing above, I wrote some codes hoping to solve the problem. Because I think there are some hidden factors in question information.
build.gradle
dependencies {
implementation 'com.squareup.retrofit2:retrofit:2.6.1'
implementation 'com.squareup.retrofit2:converter-gson:2.6.1'
implementation 'com.squareup.okhttp3:logging-interceptor:3.14.1'
implementation 'com.squareup.okhttp3:okhttp-urlconnection:3.14.1'
implementation 'com.squareup.okhttp3:okhttp:3.14.1'
}
RetrofitAPI.kt
import retrofit2.Response
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST
interface RetrofitAPI {
#FormUrlEncoded
#POST("activity_signup.php")
suspend fun activitySignUp(
#Field("activity_id") activityId: Int,
#Field("user_id") userId: Int
): Response<SimpleResultObject>
// Other api declarations ...
}
BaseApiManager.kt
import okhttp3.JavaNetCookieJar
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.net.CookieManager
import java.util.concurrent.TimeUnit
abstract class BaseApiManager(endPoint: String) {
protected val retrofitAPI: RetrofitAPI =
createAdapter(endPoint)
.create(RetrofitAPI::class.java)
private fun createAdapter(choice: String): Retrofit {
return Retrofit.Builder()
.baseUrl(choice)
.client(createHttpClient())
.addConverterFactory(GsonConverterFactory.create()).build()
}
companion object {
private fun createHttpClient(): OkHttpClient {
val httpClient: OkHttpClient.Builder = OkHttpClient.Builder()
val cookieHandler = CookieManager()
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BODY
httpClient.interceptors().add(interceptor)
httpClient.cookieJar(JavaNetCookieJar(cookieHandler))
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
return httpClient.build()
}
}
}
ApiManager.kt
private const val API_END_POINT = "https://your.webservice.endpoint/"
object ApiManager : BaseApiManager(API_END_POINT) {
suspend fun activitySignUp(
activityId: Int,
userId: Int
) = retrofitAPI.activitySignUp(activityId, userId)
// Other api implementations ...
}
Usage:
fun signUp() {
myActivityScope.launch(Dispatchers.IO) {
ApiManager.activitySignUp(activityId, userId).also { response ->
when {
response.isSuccessful -> {
val result = response.body()
result?.apply {
// do something with the result
}
}
else -> {
val code = response.code()
val message = response.message()
// do something with the error parameters...
}
}
}
}
}
How can create androidTest for sample retrofit request?
Sample
data class TestDataClass(
val id: String,
val employee_name: String,
val employee_salary: String,
val employee_age: String,
val profile_image: String)
enum class NetworkState { LOADING, ERROR, DONE }
private const val BASE_URL = "http://dummy.restapiexample.com/api/v1/"
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.baseUrl(BASE_URL)
.build()
interface TestApiService {
#GET("employees")
fun getPropertiesAsync():
Deferred<List<TestDataClass>>
}
object TestApi {
val retrofitTest : TestApiService by lazy { retrofit.create(TestApiService::class.java) }
}
You can use the MockWebServer library by Square.
Create a resources in your tests source set (src/test/resources), and put in it a JSON file containing a sample response from your API. Let's say it looks like this:
src/test/resources/sample_response.json
[
{
"id": "1",
"employee_name": "John Doe",
"employee_salary": "60000",
"employee_age": 37,
"profile_image": "https://dummy.sample-image.com/johndoe"
}
]
You may then write your tests as:
class ApiTest {
private lateinit var server: MockWebServer
private lateinit var retrofit: Retrofit
private lateinit var service: TestApiService
#Before
fun setup() {
server = MockWebServer()
retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(get<Moshi>()))
.addCallAdapterFactory(get<CoroutinesNetworkResponseAdapterFactory>())
.baseUrl(mockWebServer.url("/"))
.build()
service = retrofit.create(TestApi::class.java)
}
#After
fun teardown() {
server.close()
}
#Test
fun simpleTest() = runBlocking<Unit> {
val sampleResponse = this::class.java.getResource("/sample_response.json").readText()
server.enqueue(
MockResponse()
.setBody(sampleResponse)
)
val response = service.getPropertiesAsync().await()
assertTrue(1, response.size)
assertTrue(response[0].employee_name = "John Doe"
// Make as many assertions as you like
}
}
You have to ask yourself though, what exactly is it that you're trying to test? There's no need to test Retrofit's functionality. Nor should you test functionality of other well known libraries like Moshi.
These tests best serve the purpose of validating that the data models you have created for API responses are indeed correct, and that your parser (in this case, Moshi) can correctly handle unexpected values (such as null) gracefully. It is therefore important that the sample responses that you pick are actual responses from your API, so that your data models can be validated against real data in tests before being used in the app.
I have an Android application written in Kotlin, that gets data from an API, for now it's just a local hosted JSON file. When I'm trying to get the data, I receive the error that my list, persons, is not initialized thus persons == null and didn't receive the data. I'm not sure what I did wrong and how to fix this.
The model
data class Person (
#Json(name = "personId")
val personId: Int,
#Json(name = "personName")
val name: String,
#Json(name = "personAge")
val age: Int,
#Json(name = "isFemale")
val isFemale: Boolean,
)
The JSON response
{
"persons": [{
"personId": 1,
"personName": "Bert",
"personAge": 19,
"isFemale": "false",
}
]
}
The ApiClient
class ApiClient {
companion object {
private const val BASE_URL = "http://10.0.2.2:3000/"
fun getClient(): Retrofit {
val moshi = Moshi.Builder()
.add(customDateAdapter)
.build()
return Builder()
.baseUrl(BASE_URL)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
}
}
}
The Retrofit http methods
interface ApiInterface {
#GET("persons")
fun getPersons(): Observable<List<Person>>
}
and finally the call
class PersonActivity: AppCompatActivity(){
private lateinit var jsonAPI: ApiInterface
private lateinit var persons: List<Person>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_person)
val retrofit = ApiClient.getClient()
jsonAPI = retrofit.create(ApiInterface::class.java)
jsonAPI.getPersons()
.subscribeOn(Schedulers.io())
.unsubscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ persons = it })
}
}
Expected: Data from the JSON file into the persons list instead of NULL.
Your moshi adapter is expecting the person objects directly but they are nested inside the "persons" Json array.
You should add another model as follow
data class Persons (
#Json(name="persons")
val persons : List<Person>
)
Also change the interface to return the new object.
interface ApiInterface {
#GET("persons")
fun getPersons(): Observable<Persons>
}
Will also need to change the subscription to
.subscribe({ persons = it.persons })
I think there could be one of two issues.
If you are using moshi reflection you will have something like these dependencies in gradle:
//Moshi Core
implementation "com.squareup.moshi:moshi:1.8.0"
//Moshi Reflection
implementation "com.squareup.moshi:moshi-kotlin:1.8.0"
In that case you will need to use the KotlinJsonAdapterFactory:
val moshi = Moshi.Builder()
.add(customDateAdapter)
.add(KotlinJsonAdapterFactory())
.build()
If you are using codegen you'll need this:
apply plugin: 'kotlin-kapt'
...
//Moshi Core Artifact
implementation "com.squareup.moshi:moshi:1.8.0"
//Moshi Codegen
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.8.0"
Additionally, every class you want to deserialise to should be annotated with
#JsonClass(generateAdapter = true) (so the Person and Persons class)