I'm a newbie about Kotlin. My first project is to consume a rest api. I already made that using retrofit. But I have a problem when I'm logging the response, my data class is null. I don't know where is the error.
My Rerofit Client
object RetrofitClient {
var retrofit: Retrofit? = null
fun getClient(baseUrl: String): Retrofit? {
if (retrofit == null) {
//TODO While release in Google Play Change the Level to NONE
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BODY
val client = OkHttpClient.Builder()
.addInterceptor(interceptor)
.connectTimeout(100, TimeUnit.SECONDS)
.readTimeout(100, TimeUnit.SECONDS)
.build()
retrofit = Retrofit.Builder()
.client(client)
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
return retrofit
}
}
My Interface
public interface ApiLoginService {
#POST("UserManagementCoreAPI/api/v1/users")
fun loginService(#Body request: RequestBody): Call<DataLogin>
}
object ApiUtils {
val BASE_URL = "------"
val apiLoginService: ApiLoginService
get() = RetrofitClient.getClient(BASE_URL)!!.create(ApiLoginService::class.java)
}
My class data
data class DataLogin (
#SerializedName("employeeId") val employeeId : String,
#SerializedName("fullName") val fullName : String,
#SerializedName("loginConfins") val loginConfins : String,
#SerializedName("branchId") val branchId : String,
#SerializedName("isActive") val isActive : String
)
Main Activity
mApiLoginService!!.loginService(requestBody).enqueue(object : Callback<DataLogin>{
override fun onResponse(call: Call<DataLogin>, response: Response<DataLogin>) {
if(response.isSuccessful()){
if(response.body().toString() == null){
Log.d(tag,"Null")
}else{
Log.d(tag,"Logging In " + response.body()!!)
progressBar.visibility = View.GONE
btn_submit.visibility = View.VISIBLE
startActivity(Intent(this#MainActivity, HomeActivity::class.java))
}
}else{
Toast.makeText(applicationContext, "Invalid username or password", Toast.LENGTH_LONG).show()
Log.d(tag,"Error " + response.errorBody().toString())
progressBar.visibility = View.GONE
btn_submit.visibility = View.VISIBLE
}
}
override fun onFailure(call: Call<DataLogin>, t: Throwable) {
progressBar.visibility = View.GONE
btn_submit.visibility = View.VISIBLE
}
})
My Respone Log
message: Logging In DataLogin(employeeId=null, fullName=null, loginConfins=null, branchId=null, isActive=null)
I don't know where is the error and why my data is null. If the response succeeds is still gives me null.
This is a postman example
You have an issue with your schema , Your DataLogin class is different of your postman schema , Retrofit is waiting for : fullName, isActive ...., and the response is : header , data .. , you have to create class that contains header as variable of type Header(errors:List<AnotherClass>), data as variable of type Data(data(List<DataLogin>),totalRecord:Int), i would suggest if you use helper website like JSON to Class , parse your postman response there , and he will give you your correct response class but it's will be in java , you have to rewrite the code yourself of just copy paste in android studio and he will convert the code to Kotlin for you. (in the web site , check Source type: JSON)
You have to match the json structure with your data classes if you do not provide a custom adapter to Gson. So if you want to have a result, maybe something like this will work:
data class Response(
#SerializedName("headers") val headers: List<Any>,
#SerializedName("data") val data: Data
)
data class Data(
#SerializedName("data") val data: List<DataLogin>,
#SerializedName("totalRecord") val totalRecord: Int
)
data class DataLogin (
#SerializedName("employeeId") val employeeId : String,
#SerializedName("fullName") val fullName : String,
#SerializedName("loginConfins") val loginConfins : String,
#SerializedName("branchId") val branchId : String,
#SerializedName("isActive") val isActive : String
)
You need to return a Response object from your retrofit call.
Also a few tips about kotlin, Gson works well for Java, but it has some issues with kotlin (about null safety). I use Moshi when the project language is kotlin and it works well with it.
Try to avoid using !! in kotlin because it will cause RuntimeException. There are other ways of checking null and to protect your code from RuntimeExceptions.
Related
I have API method mapping like this
#POST("api/updateStarted/{id}/{started}")
suspend fun updateStarted(
#Path("id") id: Int,
#Path("started") started: Date
) : Response <Int>
I want to use yyyy-MM-dd'T'HH:mm:ss format everywhere. My API adapter looks like this:
val gson = GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss")
val apiClient: ApiClient = Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create(gson.create()))
.baseUrl(API_BASE_URL)
.client(getHttpClient(API_USERNAME, API_PASSWORD))
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiClient::class.java)
However GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss") cannot affect date format when I pass it thru URL (because that's not JSON) so Retrofit builds URL like this:
http://myserver.com/api/updateFinished/2/Fri%20Jan%2027%2013:48:42%20GMT+01:00%202023
instead of something like this:
http://myserver.com/api/updateFinished/2/2023-01-28T02:03:04.000
How can I fix that? I'm new in Retrofit and I don't fully understand date/time libraries in Java.
You can switch the data type from java.util.Date to java.time.LocalDateTime if you want your desired format using the toString() method of that data type.
Currently, you have Date.toString() producing an undesired result.
If you don't have to use a Date, import java.time.LocalDateTime and just change your fun a little to this:
#POST("api/updateStarted/{id}/{started}")
suspend fun updateStarted(
#Path("id") id: Int,
#Path("started") started: LocalDateTime
) : Response <Int>
GsonConverterFactory supports responseBodyConverter and requestBodyConverter which aren't used to convert URL params. For that, you need a stringConverter which, fortunately is trivial to implement:
class MyToStringConverter : Converter<SomeClass, String> {
override fun convert(value: SomeClass): String {
return formatToMyDesiredUrlParamFormat(value)
}
companion object {
val INSTANCE = MyToStringConverter()
}
}
class MyConverterFactory : Converter.Factory() {
override fun stringConverter(type: Type, annotations: Array<out Annotation>, retrofit: Retrofit): Converter<*, String>? {
return if (type == SomeClass::class.java) {
//extra check to make sure the circumstances are correct:
if (annotations.any { it is retrofit2.http.Query }) {
MyToStringConverter.INSTANCE
} else {
null
}
} else {
null
}
}
}
and then
val apiClient: ApiClient = Retrofit.Builder()
.baseUrl(API_BASE_URL)
.client(getHttpClient(API_USERNAME, API_PASSWORD))
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(MyConverterFactory())
//(...)
I've added checking for annotations as example if one would want tighter control on when the converter is used.
I am trying to write unit test for a dynamodb get call , the get call gets the records of type SdkIterable<Page> , below is the get method:
fun getByStatus(status: Status): List<MODEL> {
val attributeValue: AttributeValue = AttributeValue.builder().s(status.toString())
.build()
val queryConditional: QueryConditional = QueryConditional.keyEqualTo(
Key.builder()
.partitionValue(attributeValue).build()
)
val results: SdkIterable<Page<MODEL>> = secondaryIndex?.query(
QueryEnhancedRequest.builder()
.queryConditional(queryConditional)
.build()
) ?: throw IllegalArgumentException(
"No Global Secondary Index found with name : $GLOBAL_SECONDARY_INDEX for " +
" table : $DYNAMODB_TABLE_NAME "
)
var items = mutableListOf<MODEL>()
results.forEach { page ->
items.addAll(page.items())
}
return items
}
I am trying to write the unit test of this as below:
#Test
fun `get items by status should return items`() {
val entity = getTestEntity()
val attributeValue: AttributeValue = AttributeValue.builder().s(Status.NEW.toString)
.build()
val queryConditional: QueryConditional = QueryConditional.keyEqualTo(
Key.builder()
.partitionValue(attributeValue).build()
)
val request = QueryEnhancedRequest.builder()
.queryConditional(queryConditional)
.build()
every { unrecoverableEventEntityIndex.query(request) } returns iterable.iterator() as SdkIterable<Page<Model>>//want to return the SdkIterable with some records
verify { repository.getByStatus().size>0 }
}
I am not able to mock the secondaryIndex?.query call, i.e. can't find any documentation around initialising the
SdkIterable<Page>
or its implementation with some data so that I can return it while mocking the
secondaryIndex?.query
.
I am using Kotlin and enhanced dynamodb client for dynamodb operations.
Any help would be really appreciated.
Thanks in advance.
I used the data type java.util.UUID in my data models and I have used Moshi for serialization.
But I encountered an error saying that "Platform class java.util.UUID requires explicit JsonAdapter to be registered"
I have gone through the documentation of Moshi for writing custom adapters and I tried to replicate it accordingly.
I wrote an adapter and added it to a moshi instance. But still I encounter the same error .
Adapter
class UUIDAdapter {
#ToJson
fun toJson(value:java.util.UUID):java.util.UUID{
return value
}
#FromJson
fun fromJson(value: String):java.util.UUID{
return java.util.UUID.fromString(value)
}
}
Model
#JsonClass(generateAdapter = true)
data class AddWorkspace(
#Json(name = "user_id")
val user_id: UUID,
#Json(name = "name")
val name:String,
#Json(name = "descp")
val descp:String,
#Json(name = "created_at")
val created_at:String
)
Moshi
private val moshi = Moshi.Builder()
.add(UUIDAdapter())
.build()
private val retrofitBuilder = Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(MoshiConverterFactory.create(moshi))
What else am I missing so that I can use the adapter correctly ?
Edit : Well, the methods toJson and fromJson are not being called in the first place. I tried to implement the JsonAdapter class and override the methods toJson and fromJson, but the issue I face here is that in case of the method toJson, I need to send a java.util.UUID value, but the JsonWriter cannot write a value of such data type.
Please suggest me a way to work my way through this. Thanks :)
UUID adapter
class UUIDAdapter:JsonAdapter<UUID>(){
#FromJson
override fun fromJson(reader: JsonReader): UUID? {
return UUID.fromString(reader.readJsonValue().toString())
}
#ToJson
override fun toJson(writer: JsonWriter, value: UUID?) {
writer.jsonValue(value)
}
}
You're so close. Change the #ToJson to this:
#ToJson
fun toJson(value:java.util.UUID): String {
return value.toString()
}
as Jesse described just use:
class UuidAdapter {
#FromJson
fun fromJson(uuid: String): UUID = UUID.fromString(uuid)
#ToJson
fun toJson(value: UUID): String = value.toString()
}
Getting Response{protocol=http/1.1, code=404, message=Not Found, url=https://test.test.com/service/one}
The url is correct as postman works fine.
I have tried looking into this error but most things come back with URL was in correct. and the error itself is vague.
code that starts it. the builder is a json string that is valid. I have tested it in postman.
CoroutineScope(Dispatchers.Default).launch {
val call = submitService.submitCarton(builder.toString())
Log.d("submit", "begining")
withContext(Dispatchers.Main) {
if (call.isSuccessful) {
Log.d("submit",call.body() as String)
} else {
Log.d("submit", "else....")
}
}
}
service factory:
fun makeSubmitService() : SubmitService{
val url = "https://test.test.com/service/"
return Retrofit.Builder().baseUrl(url)
.client(okHttpClient).addConverterFactory(GsonConverterFactory.create())
.build().create(SubmitService::class.java)
}
interface:
interface SubmitService {
#POST("one")
suspend fun submitCarton(#Body json: String): Response<myModel>
}
Expected results are a json response however I am not getting that far.
edit: I created a okhttpclient and did a request manual and I get a message 200 ok.
code for my test
val JSON = MediaType.parse("application/json; charset=utf-8")
val client = OkHttpClient()
val body = "some json"
val requestBody = RequestBody.create(JSON, body)
val request = Request.Builder()
.url("https://test.test.com/service/one")
.post(requestBody)
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(request: Request, e: IOException) {
Log.e("test", e.toString())
}
#Throws(IOException::class)
override fun onResponse(response: Response) {
Log.d("test", response.toString())
}
})
Solved it myself.
Issue was dumb, retrofit2 was giving 404 even though the web service was returning a error message.
added
implementation 'com.squareup.okhttp3:logging-interceptor:3.12.1'
private val interceptor = HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)
private val okHttpClient = OkHttpClient().newBuilder()
.connectTimeout(1, TimeUnit.MINUTES)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.addInterceptor(interceptor)
.build()
found out retrofit was sending a very unformatted string
"{ \"all my json filled with \" }"
instead of
{ json }
fixed it by adding
.addConverterFactory(ScalarsConverterFactory.create())
to my service factory
for anyone wondering why I am basically creating the json as a string instead of using a JSON object is because the service I talk to really really wants it to be in a very specific order which JSON just don't care about it however it wants it to look like JSON as well...
networkCall = NetworkCall(context)
val responceCall = networkCall!!.getRetrofit(true).callReadMeService()
responceCall.clone().enqueue(object : Callback<BaseResponse<*>?> {
override fun onResponse(call: Call<BaseResponse<*>?>, response: Response<BaseResponse<*>?>) {
networkCall!!.dismissLoading()
web_view!!.loadData((response.body()?.data as LinkedTreeMap<*, *>)["description"] as String, "text/html", "UTF-8")
}
override fun onFailure(call: Call<BaseResponse<*>?>, t: Throwable) {
networkCall!!.dismissLoading()
}
})
here is api method
#POST("stories/readme")
fun callReadMeService(): Call<BaseResponse<*>?>
now i am getting this exception
here is my BaseResponse class
class BaseResponse<T> {
var message: String? = null
var status: Boolean = false
var errors: Array<String>? = null
var code: String? = null
var data: T? = null
}
Method return type must not include a type variable or wildcard: retrofit2.Call>
for method IApi.callReadMeService
now i can't remove generic data variable from BaseResponse class, cause i am using this class as a common Api parser
any solution for this
You cannot do this, because the type info needs to be fully specified, otherwise retrofit cannot correctly generate the Service. See this discussion.
You need to create a different API method for each type.