I'm building an app for a company using MVVM & clean architecture so I've created 3 modules, the app module (presentation layer), the data module (data layer) & the domain module (domain/interactors layer). Now, in my data module, I'm using Retrofit and Gson to automatically convert the JSON I'm receiving from a login POST request to my kotlin data class named NetUserSession that you see below. The problem I'm having is that the logging interceptor prints the response with the data in it normally but the response.body() returns an empty NetUserSession object with null values which makes me think that the automatic conversion isn't happening for some reason. Can somebody please tell me what I'm doing wrong here?
KoinModules:
val domainModule = module {
single<LoginRepository> {LoginRepositoryImpl(get())}
single { LoginUseCase(get()) }
}
val presentationModule = module {
viewModel { LoginViewModel(get(),get()) }
}
val dataModule = module {
single { ApiServiceImpl().getApiService() }
single { LoginRepositoryImpl(get()) }
}
}
Api interface & retrofit:
interface ApiService {
#POST("Login")
fun getLoginResult(#Body netUser: NetUser) : Call<NetUserSession>
#GET("Books")
fun getBooks(#Header("Authorization") token:String) : Call<List<NetBook>>
}
class ApiServiceImpl {
fun getApiService(): ApiService {
val logging = HttpLoggingInterceptor()
logging.setLevel(HttpLoggingInterceptor.Level.BODY)
//TODO:SP Remove the interceptor code when done debugging
val client: OkHttpClient = OkHttpClient.Builder()
.addInterceptor(logging)
.build()
val retrofit = Retrofit.Builder().baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()
// tell retrofit to implement the interface of our api
return retrofit.create(ApiService::class.java)
}
}
NetUserSession:
data class NetUserSession(
#SerializedName("expires_in")
val expires_in: Int,
#SerializedName("token_type")
val token_type: String,
#SerializedName("refresh_token")
val refresh_token: String,
#SerializedName("access_token")
val access_token: String
) {
fun toUserSession(): UserSession = UserSession(
expiresIn = expires_in,
tokenType = token_type,
refreshToken = refresh_token,
accessToken = access_token
)
}
UserSession in domain:
data class UserSession(
val expiresIn:Int,
val tokenType:String,
val refreshToken:String,
val accessToken:String
)
LoginRepositoryImpl where the error occurs:
class LoginRepositoryImpl(private val apiService: ApiService) : LoginRepository {
override suspend fun login(username:String,password:String): UserSession? = withContext(Dispatchers.IO){
val response = apiService.getLoginResult(NetUser(username,password)).awaitResponse()
println("THE RESPONSE WAS : ${response.body()}")
return#withContext if(response.isSuccessful) response.body()?.toUserSession() else null
}
}
LoggingInterceptor result after the 200-OK:
{"expires_in":3600,"token_type":"Bearer","refresh_token":"T1amGR21.IdKM.5ecbf91162691e15913582bf2662e0","access_token":"T1amGT21.Idup.298885bf38e99053dca3434eb59c6aa"}
Response.body() print result:
THE RESPONSE WAS : NetUserSession(expires_in=0, token_type=null, refresh_token=null, access_token=null)
Any ideas what I'm failing to see here?
After busting my head for hours, the solution was to simply change the model class's members from val to var like so :
data class NetUserSession(
#SerializedName("expires_in")
var expires_in: Int = 0,
#SerializedName("token_type")
var token_type: String? = null,
#SerializedName("refresh_token")
var refresh_token: String? = null,
#SerializedName("access_token")
var access_token: String? = null
) {
fun toUserSession(): UserSession = UserSession(
expiresIn = expires_in,
tokenType = token_type!!,
refreshToken = refresh_token!!,
accessToken = access_token!!
)
}
Related
I am posting the user value to the server and through the HttploggingInterciptore I can see that its a success and the server respond with 200 but my data class for a reason I don't know doesn't read at all the response Body
So I have prepared my retrofit and Moshi Builders
#SuppressLint("StaticFieldLeak")
private val context = UADApp.appContext
fun getRetrofitInstance() : Retrofit{
val loggingInterceptor = HttpLoggingInterceptor()
loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(AuthenticationInterceptor(ADVPreference(context = context)))
.addInterceptor(loggingInterceptor)
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient.build())
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
}
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
}
and This is my service Interface
interface ICheckPhoneService {
#POST("checkPhone")
suspend fun checkPhoneService(#Body model: BaseBodyModel) : BaseResponse
}
and the BaseResponse Data Class that my server respond with
#JsonClass(generateAdapter = true)
data class BaseResponse(
#Json(name = "data")
val data: String ,
#Json(name ="message")
val message: String ,
#Json(name ="status")
val status: Boolean
As this is the server response
)
and here is how I call it in my ViewModel
lateinit var loginResponse : BaseResponse
var logText = ""
val userPhone = "01202777373"
fun checkIfUserAlreadyExist(){
val body = BaseBodyModel(userPhone)
viewModelScope.launch {
val service = RetrofitInstance.getRetrofitInstance().create(ICheckPhoneService::class.java)
loginResponse = service.checkPhoneService(body)
logText = "${loginResponse.status} + ${loginResponse.data} + ${loginResponse.message}"
}
}
And I get the respond through the logger that my Post was success and I am supposed to get the response body and parse it successfully through my DataClass, yet I receive no data At all and no Error either
I can't figure out what I am missing here, and why the Service doesn't return the response Body and Parse it.
Can someone point to me where is the issue here, and why it cant read it?
I have post method where I am sending login request to server but first I want to save that response using shared preferences in my repository how can save retrofit response in shared preferences
below my interface class I have implemented login post request logic
interface MeloApi {
#Headers("Content-Type: application/json")
#POST("/login")
suspend fun makeLogin(#Body loginModel: LoginModel) : Response<LoginModel>
}
below my loginModel class
data class LoginModel(
val userName:String,
val password:String)
below LoginResponse.kt
data class LoginResponse(
#SerializedName("accessToken")
val accessToken: String,
#SerializedName("refreshToken")
val refreshToken: String,
#SerializedName("status")
val status: String,
#SerializedName("user")
val user: User
)
below my Interceptor
class HeaderInterceptor(
private val tokenManager: TokenManager
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
request = request.newBuilder()
.header("refreshToken", tokenManager.refreshToken.toString())
.header("accessToken", tokenManager.accessToken.toString())
.build()
return chain.proceed(request)
}
}
below my token manager
class TokenManager {
var accessToken: String? = null
var refreshToken: String? = null
}
below my appModule.kt
val apiModule = module {
single {
TokenManager()
}
single {
HeaderInterceptor(get())
}
single {
val httpInterceptor = HttpLoggingInterceptor()
httpInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
Retrofit.Builder()
.client(
OkHttpClient.Builder()
.addInterceptor(HeaderInterceptor(get()))
.addInterceptor(httpInterceptor).build()
)
// .addConverterFactory(GsonConverterFactory.create())
.baseUrl(BASE_URL)
.build()
.create(MeloApi::class.java)
}
}
below my LoginRepository
class LoginRepository(
private val meloApi: MeloApi
) {
suspend fun login(loginModel: LoginModel) {
GlobalScope.launch {
val response = meloApi.makeLogin(loginModel)
response.isSuccessful
val userName = response.body()?.userName
val password = response.body()?.password
loginRequest(userName, password)
}
}
private suspend fun loginRequest(userName: String?, password: String?){
}
}
If you want to store as object you have to use serialization/deserialization.
I used gson here.
val loginResponse = LoginResponse("AToken", "RToken", "200", User("Edgar"))
val objToString = Gson().toJson(loginResponse)
val sharedPref = context.getSharedPreferences("loginRes", MODE_PRIVATE)
sharedPref.edit().putString("response", objToString).apply()
val response = sharedPref.getString("response", "null")
val stringToObj = Gson().fromJson(response, LoginResponse::class.java)
You can use proto datastore instead of shared preferences.
I am using retrofit2.6.2 for api call. LoggingInterceptor showing full response in logcat but retrofit response body return null. I didn't figure out where is my problem.
My json data schema is
{
"error":false,
"msg":"Banner Found",
"id":"9",
"activity":"VipPremium1",
"imageUrl":"https:\/\/1.bp.blogspot.com\/-Kh3RQlJH7Xw\/X-1mIPi7_HI\/AAAAAAAAFME\/Y2bCnU5odngcdDT83uC9QwUr7IGJdTDfACLcBGAsYHQ\/s2616\/COMPRESSED_IMG_1609393684674.jpg",
"actionUrl":"https:\/\/www.youtube.com\/watch?v=ukJX5ZgJec4",
"actionType":1,
"visible":true
}
Model Class BannerRes
data class BannerRes(
#SerializedName("actionType")
val actionType: Int?,
#SerializedName("actionUrl")
val actionUrl: String?,
#SerializedName("activity")
val activity: String?,
#SerializedName("error")
val error: Boolean?,
#SerializedName("id")
val id: String?,
#SerializedName("imageUrl")
val imageUrl: String?,
#SerializedName("msg")
val msg: String?,
#SerializedName("visible")
val visible: Boolean?
)
Api Interface
#GET("api/helper.getBanner.php")
suspend fun getBanner(
#Query("bannerName") bannerName: String,
): Response<BannerRes>
Api call done here
private fun loadPremiumBanner() {
Coroutines.main {
val res = viewModel.getBanner("VipPremium1")
Log.d("Response", res.body()!!.msg!!)
}
}
When I print response body using
Log.d("Response", Gson().toJson(res.body()))
It shows the the response in logcat,
Logcat
{"error":false,"msg":"Banner Found","id":"9","activity":"VipPremium1","imageUrl":"https://1.bp.blogspot.com/-Kh3RQlJH7Xw/X-1mIPi7_HI/AAAAAAAAFME/Y2bCnU5odngcdDT83uC9QwUr7IGJdTDfACLcBGAsYHQ/s2616/COMPRESSED_IMG_1609393684674.jpg","actionUrl":"https://www.youtube.com/watch?v\u003dukJX5ZgJec4","actionType":1.0,"visible":true}
but when access res.body()!!.msg It shows null.
Retrofit Setup
companion object {
#Volatile
private var myApiInstance: MyApi? = null
private val LOCK = Any()
operator fun invoke() = myApiInstance ?: synchronized(LOCK) {
myApiInstance ?: createClient().also {
myApiInstance = it
}
}
private fun createClient(): MyApi {
val AUTH: String = "Basic ${
Base64.encodeToString(
("${BuildConfig.USER_NAME}:${BuildConfig.USER_PASSWORD}").toByteArray(),
Base64.NO_WRAP
)
}"
val interceptor = run {
val httpLoggingInterceptor = HttpLoggingInterceptor()
httpLoggingInterceptor.apply {
httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
}
}
val okHttpClient: OkHttpClient = OkHttpClient.Builder()
.callTimeout(10,TimeUnit.SECONDS)
.addInterceptor(interceptor)
.addInterceptor { chain ->
val original: Request = chain.request()
val requestBuilder: Request.Builder = original.newBuilder()
.addHeader("Authorization", AUTH)
.method(original.method, original.body)
val request: Request = requestBuilder.build()
chain.proceed(request)
}
.build()
val gsonBuilder = GsonBuilder()
gsonBuilder.setLenient()
val gson = gsonBuilder.create()
return Retrofit.Builder()
.baseUrl(BuildConfig.BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.addConverterFactory(ScalarsConverterFactory.create())
.client(okHttpClient)
.build()
.create(MyApi::class.java)
}
}
I resolved this issue by adding kotlin model data class member filed default value. I don't know what is the reason behind this,
Old data class
data class BannerRes(
#SerializedName("actionType")
val actionType: Int?,
#SerializedName("actionUrl")
val actionUrl: String?,
#SerializedName("activity")
val activity: String?,
#SerializedName("error")
val error: Boolean?,
#SerializedName("id")
val id: String?,
#SerializedName("imageUrl")
val imageUrl: String?,
#SerializedName("msg")
val msg: String?,
#SerializedName("visible")
val visible: Boolean?
)
Modified or data class with member field default value which fix my problem
data class BannerRes(
#SerializedName("error") var error : Boolean = true,
#SerializedName("msg") var msg : String? = null,
#SerializedName("id") var id : String? = null,
#SerializedName("activity") var activity : String? = null,
#SerializedName("imageUrl") var imageUrl : String? = null,
#SerializedName("actionUrl") var actionUrl : String? = null,
#SerializedName("actionType") var actionType : Int = 0,
#SerializedName("visible") var visible : Boolean = false
)
I think you can't use both Gson and Scalars Converter in Retrofit because retrofit confuse to wrap it.
Remove Scaler (I prefer Gson) and try again.
If not work then use GsonConverterFactory.create() this.
With Retrofit you can consume the response body only once. So the first call to body() will return the response but further calls will not. You're consuming your body when you're logging it.
My problem is, when I use Call inside my retrofit api, I get this exception:
Unable to create converter for retrofit2.Call<java.util.List>
for method IntegApi.getAllProductBarcodeAsync
The intersting thing, if I don't use Call then the error message is gone.
I want to use Call, becuse I would like to use a custom response class, because i want the know when the api sends 404 status, and then I want to skip this exception.
I use Moshi to convert Json
Sync function:
private suspend fun syncProductBarcodes() {
try {
val productBarcodes = api.getAllProductBarcodeAsync().await()
if(productBarcodes.isSuccessful) {
productBarcodeRepository.deleteAndInsertAll(productBarcodes.body() ?: emptyList())
addOneToStep()
}
}catch (e: Exception){
Timber.d(e)
throw e
}
}
Api:
#GET("Product/GetAllBarcode")
suspend fun getAllProductBarcodeAsync(): Call<List<ProductBarcode>>
Entity class:
#Entity(
tableName = ProductBarcode.TABLE_NAME
)
#JsonClass(generateAdapter = true)
class ProductBarcode(
#PrimaryKey
#ColumnInfo(name = "id")
#Json(name = "Id")
val id: String = "",
#ColumnInfo(name = "product_id", index = true)
#Json(name = "ProductId")
var ProductId: String = "",
#ColumnInfo(name = "barcode", index = true)
#Json(name = "Barcode")
var barcode: String = ""
) {
companion object {
const val TABLE_NAME = "product_barcode"
}
}
ExtensionFun:
suspend fun <T> Call<T>.await(): Response<T> = suspendCoroutine { continuation ->
val callback = object : Callback<T> {
override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}
override fun onResponse(call: Call<T>, response: Response<T>) =
continuation.resumeNormallyOrWithException {
if (response.isSuccessful || response.code() == 404) {
return#resumeNormallyOrWithException response
} else {
throw IllegalStateException("Http error ${response.code()}, request:${request().url()}")
}
}
}
enqueue(callback)
}
ApiModule:
#Provides
#Singleton
fun provideMoshi(): Moshi {
return Moshi.Builder()
.add(DateConverter())
.add(BigDecimalConverer())
.add(KotlinJsonAdapterFactory())
.build()
}
fun provideIntegApi(
#Named("base_url") url: String, moshi: Moshi,
prefManager: PrefManager
): IntegApi {
var builder = OkHttpClient.Builder()
builder = BuildTypeInitializations.setupInterceptor(builder)
builder.addInterceptor { chain ->
val request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer ${prefManager.token}")
.addHeader("Connection", "close")
.build()
val response = chain.proceed(request)
return#addInterceptor response
}
.readTimeout(5, TimeUnit.MINUTES)
.writeTimeout(5, TimeUnit.MINUTES)
.connectTimeout(5, TimeUnit.MINUTES)
.retryOnConnectionFailure(true)
return Retrofit.Builder()
.baseUrl(url)
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.addConverterFactory(MoshiConverterFactory.create(moshi))
.client(builder.build())
.build()
.create(IntegApi::class.java)
}
I don't know exactly why, but If the Api send 404 response, after than I get the Unable to crate converter exception, but I the server send the response, the error message is gone.
Update:
If is use this :
#get:GET("Product/GetAllBarcode")
val getAllProductBarcodeAsync: Call<List<ProductBarcode>>
instead of this:
#GET("Product/GetAllBarcode")
suspend fun getAllProductBarcodeAsync(): Call<List<ProductBarcode>>
There won't be error, and everything works fine, but I don't understand what's the problem
Update2
I changed Moshi to Jackson, and it doesn't throw converter error like moshi, but throw Http 404 Error which is more friendlier for me, but I' m not completely satisfied. I created await() fun because of Http 404 errors, and I think this bunch of code skipped because of http 404?
Finally I found the issue, I made a mistake, because I tried to use suspend fun and retrofit Call at the same time. I deleted the suspend keyword, and it works perfectly.
I'm getting this error ** Expected BEGIN_OBJECT but was STRING at line 1 column 1 path $**
gradles below which I used for retrofit
implementation 'com.squareup.retrofit2:retrofit:2.6.0'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.squareup.retrofit2:converter-gson:2.5.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.12.0'
Retrofit class
class RetrofitClient private constructor() {
val myApi: Api
companion object {
#get:Synchronized
var instance: RetrofitClient? = null
get() {
if (field == null) {
field = RetrofitClient()
}
return field
}
private set
}
init {
val gson = GsonBuilder()
.setLenient()
.create()
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BODY
val client : OkHttpClient = OkHttpClient.Builder().addInterceptor(interceptor).build()
val retrofit: Retrofit = Retrofit.Builder().baseUrl("http://ctyf.co.in/api/")
.client(client)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
myApi = retrofit.create(Api::class.java)
}
}
Api interface
public interface Api {
#Headers("Content-Type: application/json")
#GET("companyvehiclelatestinfo?token=F934A0C439/")
fun getLatLngs(): Call<ResponseLoc?>?
}
data class
data class ResponseLoc(
val Vehicle: List<Vehicle>
)
data model
data class Vehicle(
val Angle: String,
val Date: String,
val Ignition: String,
val Imei: String,
val Lat: String,
val Location: String,
val Long: String,
val Speed: String,
val Tempr: String,
val VehicleNo: String
)
finally calling here
private fun getLatLngs() {
val call: Call<ResponseLoc?>? = RetrofitClient.instance!!.myApi.getLatLngs()
call!!.enqueue(object : Callback<ResponseLoc?> {
override fun onResponse(call: Call<ResponseLoc?>, response: Response<ResponseLoc?>) {
val responseLoc: List<ResponseLoc> = response.body() as List<ResponseLoc>
//Creating an String array for the ListView
val data = arrayOfNulls<String>(responseLoc.size)
for (i in responseLoc.indices) {
data[i] = responseLoc[i].Vehicle.toString()
Log.d("apiii", data[i].toString())
}
}
override fun onFailure(call: Call<ResponseLoc?>, t: Throwable) {
Log.d("apii", t.message.toString())
}
})
}
JSON values
{"Vehicle":[{"VehicleNo":"Test","Imei":"354019","Location":"Tamil Nadu-India","Date":"2021-03-17 19:27:12.000","Tempr":"0","Ignition":"","Lat":"13.11","Long":"80.282","Speed":"0","Angle":"0"}]}
I have tried many stacks none of them helped
Is there any other ways available except Retrofit ???
anybody please help me to get the api results
I have tried many stacks none of them helped
Is there any other ways available except Retrofit ???
anybody please help me to get the api results