kotlin + retrofit + coroutine + androidTest - android

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.

Related

Unable to get data from Retrofit Call to a Direct JSON File

I am working on small project on a Jetpack Compose.
I am trying to data from a static JSON File from this url using Retrofit.
https://firebasestorage.googleapis.com/v0/b/culoader.appspot.com/o/json%2Fcudata.json?alt=media&token=d0679703-2f6c-440f-af03-d4d61305cc84
Network Module
#Module
#InstallIn(SingletonComponent::class)
object NetworkModule {
#Provides
#Singleton
fun proveidesCurrencyService() : CurrencyService{
return Retrofit.Builder()
.baseUrl("https://firebasestorage.googleapis.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(CurrencyService::class.java)
}
}
Service Class
interface CurrencyService {
//#Streaming
#GET
suspend fun getCurrencyFile(#Url fileUrl:String): Response<ResponseBody>
}
data class CurrencyAPIResponse(
#SerializedName("data") var data: MutableState<List<CurrencyRates>>
)
data class CurrencyRates (
#SerializedName("code") var code : String,
#SerializedName("value") var value : String
)
ViewModel
#HiltViewModel
class CurrencyDataViewModel
#Inject
constructor(private val currencyService: CurrencyService
) : ViewModel()
{
init {
getCurrencyFileData()
}
}
fun getCurrencyFileData() = viewModelScope.launch(Dispatchers.IO) {
val url: String =
"https://firebasestorage.googleapis.com/v0/b/culoader.appspot.com/o/json%2Fcudata.json?alt=media&token=d0679703-2f6c-440f-af03-d4d61305cc84"
val responseBody = currencyService.getCurrencyFile(url).body()
if (responseBody != null) {
Log.d("\nresponsebody", responseBody.string())
val gson = GsonBuilder().setPrettyPrinting().create()
val currencyAPIResponse = gson.fromJson(responseBody.string(), CurrencyAPIResponse::class.java)
val data: MutableState<List<CurrencyRates>> = currencyAPIResponse.data
Log.d("Data", data.value[0].code)
}
}
Everytime, I am getting the below error,
Attempt to invoke virtual method 'androidx.compose.runtime.MutableState com.tuts.playlite.network.response.CurrencyAPIResponse.getData()' on a null object reference
Not sure, where I am failing, I have tried to convert this to JSON Object, but still failing. Is it right way to get the data?
Another thing noticed that even though the JSON file is complete in the url, the response body log is showing the JSON with some other content.
{
"code": "IMP",
"value": "0.722603"
},
{
"code": "INR",
[ 1631385414.170 12452:12478 D/
responsebody]
"value": "72.99465"
},
{
"code": "IQD",
"value": "1458.61356"
},
As a result, the GSON might not able to form the json and hence could be getting null exception.
Not sure why random text is getting added!
You are already providing a Gson converter to retrofit, so retrofit should already be able to do the json to object conversion for you. That's the beauty of retrofit!
Try rewriting your CurrencyService like this:
interface CurrencyService {
#GET("v0/b/culoader.appspot.com/o/json%2Fcudata.json?alt=media&token=d0679703-2f6c-440f-af03-d4d61305cc84")
suspend fun getCurrencyFile(): CurrencyAPIResponse
}
Your ViewModel also has some issues. Not sure if you actually meant MutableState but I guess you're looking for MutableLiveData or MutableStateFlow. Below an example using MutableLiveData.
#HiltViewModel
class CurrencyDataViewModel #Injectconstructor(
private val currencyService: CurrencyService
) : ViewModel() {
private val _currencyData = MutableLiveData<List<CurrencyRates>>()
private val currencyData: LiveData = _currencyData
init {
getCurrencyFileData()
}
fun getCurrencyFileData() = viewModelScope.launch(Dispatchers.IO) {
_currencyData.postValue(currencyService.getCurrencyFile().data)
}
}
Use Kotin Coroutines for retorfit, Try something like below
interface CurrencyService {
#GET("v0/b/culoader.appspot.com/o/json%2Fcudata.json?alt=media&token=d0679703-2f6c-440f-af03-d4d61305cc84")
suspend fun getCurrencyFile(): Response<CurrencyAPIResponse>
}
and if you are using MVVM use this is repository class
suspend fun getCurrencyFile:Response<CurrencyAPIResponse>{
return currencyService.getCurrencyFile()
}
then in your view model class
Coroutines.main {
try{
val response = repository.getCurrencyFile()
if(response.isSuccessful){
//response.body is your data
}
}catch(Exception){}
}
if you are not using the repository class you can skip the second step and directly call the service in viewmodel class,
The Coroutines code is
object Coroutines {
fun main(work:suspend (()->Unit)) =
CoroutineScope(Dispatchers.Main).launch {
work()
}
}
Finally, the culprit seems to be responseBody Stream. After I changed the code with below, It seems to be working. I assume, it was unable to get the proper complete JSON earlier and throwing the null object reference error.
val responseBody = currencyService.getCurrencyFile(url).body()
val resData = responseBody?.byteStream()!!.bufferedReader().use {
it.readText()
}
val gson = GsonBuilder().setPrettyPrinting().create()
val currencyAPIResponse = gson.fromJson(resData, CurrencyAPIResponse::class.java)
Thank you for all support!

Android make POST request with retrofit

I'm trying to make my first POST request to make the user login using retrofit library, but it's not working and i don't understand why. If i make a GET request it works, but with POST something gone wrong and i don't understand why. My API run on localhost webserver
My code of the LoginService:
private const val BASE_URL = "http://localhost:10000/api/"
/**
* Build the Moshi object that Retrofit will be using, making sure to add the Kotlin adapter for
* full Kotlin compatibility.
*/
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
/**
* Use the Retrofit builder to build a retrofit object using a Moshi converter with our Moshi
* object.
*/
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(BASE_URL)
.build()
interface LoginApiService {
#Headers("Content-Type: application/json")
#POST("login")
suspend fun makeLogin(#Body usr: User): LoginResponse
}
/**
* A public Api object that exposes the lazy-initialized Retrofit service
*/
object LoginApi {
val retrofitService : LoginApiService by lazy { retrofit.create(LoginApiService::class.java) }
}
code of the LoginResponse class
data class LoginResponse(
val token: String,
val expiration: Date,
val role: Int)
code of the User class:
data class User(
val mail: String,
val pw: String
) : Parcelable
Code of the ViewModel that make the request:
private fun makeLogin(email: String, password: String) {
viewModelScope.launch {
try {
val usr = User(email, password)
val rsp = LoginApi.retrofitService.makeLogin(usr)
_isLogged.value = true
} catch (ex: Exception) {
_status.value = LoginStatus.ERROR
}
}
}
Can someone help me to solve this please? it seems that the request it's not sended.
my retrofit call generate this error in logcat in the try-catch block
java.lang.IllegalArgumentException: Unable to create converter for class com.example.ticketapp.network.LoginResponse
for method LoginApiService.makeLogin
Default Retrofit's timeout is 10sec. You can fix it like this:
val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build()
val retrofit = Retrofit.Builder()
...
.client(client)
.build()
Here I set it to 30secs, but you can use any number and TimeUnit you want.
UPD:
You can store Retorfit builder in a separate file like this:
interface WebService {
companion object {
fun <T> build(clazz: Class<T>): T {
val client = OkHttpClient.Builder()
...
.build()
val retrofit = Retrofit.Builder()
...
.build()
return retrofit.create(clazz)
}
}
}
Then you can have multiple ApiService interfaces. And use them like this:
val myApiService = WebService.build(MyApiServiceInterface::class.java)
myApiService.myRequestFunction()
Try to add
android:usesCleartextTraffic="true"
Into your application tag in manifest

Retrofit returns 200 response code but I'm receiving null when accessing fields

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.

Android Kotlin Retrofit + Rxjava rest call not returning requested data

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)

How to use rxjava2 with retrofit in android

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

Categories

Resources