How to parse response with retrofit2 Kotlin - android

I'm stuck with parsing the response. In Swift I can make a codable to help parsing the json response. I'm new to Kotlin and I'm working on someone else existing project. I made a data class for string and boolean but I don't know the syntax to parse it. Please help and thank you.
The responseBody json
{
"bearerToken": "########",
"staySignIn": false
}
//Interface
interface PostInterface {
class User(
val email: String,
val password: String
)
#POST("signIn")
fun signIn(#Body user: User): Call<ResponseBody>
//Network handler
fun signIn(email: String, password: String): MutableLiveData<Resource> {
val status: MutableLiveData<Resource> = MutableLiveData()
status.value = Resource.loading(null)
val retrofit = ServiceBuilder.buildService(PostInterface::class.java)
retrofit.signIn(PostInterface.User(email, password)).enqueue(object : Callback<ResponseBody> {
override fun onFailure(call: Call<ResponseBody>, t: Throwable) {
errorMessage(status)
}
override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
if (response.code() == 200) {
try {
status.value = //how to parse using the model??
} catch (ex: Exception) {
parseError(400, response.body().toString(), status)
}
} else {
//do something...
}
}
})
return status
}
//Model
data class SignInModel(
#field:SerializedName("bearerToken")
val bearerToken: String? = null,
#field:SerializedName("staySignIn")
val staySignIn: Boolean? = null
)
//Storing value class
class RrefManager constructor(var applicationContext: Context) {
private fun getSharedPrefEditor(): sharedPrefEditor.Editor {
return applicationContext.getSharedPrefEditor(prefStorageName, Context.MODE_PRIVATE).edit()
}
public fun setBearerToken(token: String) {
getSharedPrefEditor().putString("bearerToken", token).apply()
}
public fun setStaySignIn(enabled: Boolean) {
getSharedPrefEditor().putBoolean("staySignIn", enabled).apply()
}
}
//SignIn Button
viewModel.signIn().observe(viewLifecycleOwner, androidx.lifecycle.Observer { v ->
if (v.status == Resource.Status.SUCCESS) {
val model = v.data as SignInModel
pref.setToken(model.token as String) //storing value
pref.setTwoFactorEnabled(model.twoFactorEnabled as Boolean) //storing value
} else if (v.status == Resource.Status.ERROR) {
//do something...
}
})

I think your best option to achieve something like the codable in swift is to use Gson library for parsing api responses.
When you create the retrofit instance you pass the gson converter to the builder like:
val retrofit = Retrofit.Builder()
.baseUrl(BaseUrl)
.addConverterFactory(GsonConverterFactory.create())
.build()
After you have done that you can make the api return the response you have as the data class, like:
//Interface
interface PostInterface {
#POST("signIn")
fun signIn(#Body user: User): Call<SignInModel>
}
To read the answer from the callback on your class, the response inside the network call is already parsed into your model in the callback. All the retrofit callback should be changed to receive Callback and then you can access directly like status.value = response.body()
For more info you can consult the retrofit library page where it gives all the details and explanations on how to use it correctly.
https://square.github.io/retrofit/

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 Parse Json in Kotlin Using Retrofit?

i am new to kotlin and i am in learning phase. I have followed many links but didn't able to understand completely.
I want Json response to show in my textview.
Problem: 1
I have tried this code but was unable to get data, but i want to get the items inside data object. Quote and author are coming null.
{
"status": 200,
"message": "Success",
"data": {
"Quote": "The pain you feel today will be the strength you feel tomorrow.",
"Author": ""
},
"time": "0.14 s"
}
Problem: 2
I dont know how to parse this response in textview
object ServiceBuilder {
private val client = OkHttpClient.Builder().build()
private val retrofit = Retrofit.Builder()
.baseUrl("https://url.com.pk/") // change this IP for testing by your actual machine IP
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()
fun<T> buildService(service: Class<T>): T{
return retrofit.create(service)
}}
RestApi
interface RestApi{
#Headers("Content-Type: application/json")
#POST("api/getquotes")
abstract fun addUser(#Body userData: UserInfo): Call<UserInfo>}
RestAPiService
class RestApiService
{
fun addUser(userData: UserInfo, onResult: (UserInfo?) -> Unit)
{
val retrofit = ServiceBuilder.buildService(RestApi::class.java)
retrofit.addUser(userData).enqueue(
object : Callback<UserInfo>
{
override fun onFailure(call: Call<UserInfo>, t: Throwable)
{
onResult(null)
}
override fun onResponse( call: Call<UserInfo>, response: Response<UserInfo>)
{
val addedUser = response.body()
Log.d("responsee",""+addedUser)
onResult(addedUser)
}
}
)
}
}
UserInfo
data class UserInfo (
#SerializedName("Quote")
val quote : String,
#SerializedName("Author")
val author : String
)
MainActivity
fun getQuotes() {
val apiService = RestApiService()
val userInfo = UserInfo("","")
apiService.addUser(userInfo) {
Log.d("Error registering user","errter")
/*if ( != null)
{
// it = newly added user parsed as response
// it?.id = newly added user ID
} else {
Log.d("Error registering user","errter")
}*/
}
}
Any help would be appreciated :)
Status, message and data are all part of the response so you need to take care of that. For example this
data class AddUserResponse(
val `data`: UserInfo, //like you defined it
val message: String,
val status: Int,
val time: String
)
This means parameter and response are different so the RestApi needs to be changed to this
abstract fun addUser(#Body userData: UserInfo): Call<AddUserResponse>}
This in turn also change the types in the service like
class RestApiService
{
fun addUser(userData: UserInfo, onResult: (UserInfo?) -> Unit)
{
val retrofit = ServiceBuilder.buildService(RestApi::class.java)
retrofit.addUser(userData).enqueue(
object : Callback<AddUserResponse>
{
override fun onFailure(call: Call<AddUserResponse>, t: Throwable)
{
onResult(null)
}
override fun onResponse( call: Call<AddUserResponse>, response: Response<AddUserResponse>)
{
val addedUser = response.body()
Log.d("responsee",""+addedUser)
onResult(addedUser.data)
}
}
)
}
}
now in getQuotes you will have that it is a UserInfo object
apiService.addUser(userInfo) {
val returnedUserInfo = it
}
just follow my steps :
File->settings->Plugins
search for JSON To Kotlin class and install it
again click on File->New->Kotlin Data class from JSON
paste your json code here and click on generate. It will generate POJO classes and you will good to go.
The first thing I noticed, is that the data in your json is:
"Quote": "The pain you feel today will be the strength you feel tomorrow.",
"Author": ""
While your UserInfo defined #SerializedName("message") for Quote.

Android Retrofit2: Serialize null without GSON or JSONObject

The result of API call in my Android app can be a JSON with configuration which is mapped to SupportConfigurationJson class, or just pure null. When I get a JSON, the app works properly, but when I get null, I get this exception:
kotlinx.serialization.json.internal.JsonDecodingException: Expected start of the object '{', but had 'EOF' instead
JSON input: null
I should avoid using GSON in this project. I also found a solution, where API interface will return Response<JSONObject>, and after that my repository should check if this JSONObject is null and map it to SupportConfigurationJson if not. But in the project we always used responses with custom classes so I wonder, is there any other solution to get response with null or custom data class?
GettSupportConfiguration usecase class:
class GetSupportConfiguration #Inject constructor(
private val supportConfigurationRepository: SupportConfigurationRepository
) {
suspend operator fun invoke(): Result<SupportConfiguration?> {
return try {
success(supportConfigurationRepository.getSupportConfiguration())
} catch (e: Exception) {
/*
THIS SOLUTION WORKED, BUT I DON'T THINK IT IS THE BEST WAY TO SOLVE THE PROBLEM
if (e.message?.contains("JSON input: null") == true) {
success(null)
} else {
failure(e)
}
*/
//I WAS USING THROW HERE TO SEE WHY THE APP ISN'T WORKING PROPERLY
//throw(e)
failure(e)
}
}
}
SupportConfigurationJson class:
#Serializable
data class SupportConfigurationJson(
#SerialName("image_url")
val imageUrl: String,
#SerialName("description")
val description: String,
#SerialName("phone_number")
val phoneNumber: String?,
#SerialName("email")
val email: String?
)
SupportConfigurationRepository class:
#Singleton
class SupportConfigurationRepository #Inject constructor(
private val api: SupportConfigurationApi,
private val jsonMapper: SupportConfigurationJsonMapper
) {
suspend fun getSupportConfiguration(): SupportConfiguration? =
mapJsonToSupportConfiguration(api.getSupportConfiguration().extractOrThrow())
private suspend fun mapJsonToSupportConfiguration(
supportConfiguration: SupportConfigurationJson?
) = withContext(Dispatchers.Default) {
jsonMapper.mapToSupportSettings(supportConfiguration)
}
}
fun <T> Response<T?>.extractOrThrow(): T? {
val body = body()
return if (isSuccessful) body else throw error()
}
fun <T> Response<T>.error(): Throwable {
val statusCode = HttpStatusCode.from(code())
val errorBody = errorBody()?.string()
val cause = RuntimeException(errorBody ?: "Unknown error.")
return when {
statusCode.isClientError -> ClientError(statusCode, errorBody, cause)
statusCode.isServerError -> ServerError(statusCode, errorBody, cause)
else -> ResponseError(statusCode, errorBody, cause)
}
}
SupportConfigurationApi class:
interface SupportConfigurationApi {
#GET("/mobile_api/v1/support/configuration")
suspend fun getSupportConfiguration(): Response<SupportConfigurationJson?>
}
SupportConfigurationJsonMapper class:
class SupportConfigurationJsonMapper #Inject constructor() {
fun mapToSupportSettings(json: SupportConfigurationJson?): SupportConfiguration? {
return if (json != null) {
SupportConfiguration(
email = json.email,
phoneNumber = json.phoneNumber,
description = json.description,
imageUrl = Uri.parse(json.imageUrl)
)
} else null
}
}
I create Retrofit like this:
#Provides
#AuthorizedRetrofit
fun provideAuthorizedRetrofit(
#AuthorizedClient client: OkHttpClient,
#BaseUrl baseUrl: String,
converterFactory: Converter.Factory
): Retrofit {
return Retrofit.Builder()
.client(client)
.baseUrl(baseUrl)
.addConverterFactory(converterFactory)
.build()
}
#Provides
#ExperimentalSerializationApi
fun provideConverterFactory(json: Json): Converter.Factory {
val mediaType = "application/json".toMediaType()
return json.asConverterFactory(mediaType)
}
Everything is explained here (1min read)
Api is supposed to return "{}" for null, If you can't change API add this converter to Retrofit
You are interacting with your repository directly, i will suggest to use
usecases
to interact with data layer.
Because you are not catching this exception over here, your app is crashing
suspend fun getSupportConfiguration(): SupportConfiguration? =
mapJsonToSupportConfiguration(api.getSupportConfiguration().extractOrThrow())
Usecase usually catch these errors and show useful error msg at the ui.

Android: Parse inner unstructured Json with Moshi adapters

I have trouble to parse some inner part of JSON (with Moshi) that can vary from a lot and is highly unstructured. Overall it looks like:
response: {
items: [{
type: "typeA",
data: {
"1563050214700-001": {
foo: 123 ....
}
}
}, {
type: "typeB",
data: {
"1563050214700-002": {[
// differs a lot from previous one
{bar: 123 .... }
]}
}
}]
}
And data class structure looks like:
data class Response(
val items: Map<String,List<Item?>>?
) {
data class Item(
val type: String?,
val data: Map<String,List<DataItem?>>?
) {
data class DataItem(
// members highly unstructured
)
}
}
Schema of "DataItem" varies a lot. Looks like Moshi codegen supports adapters that can potentially allow manual parsing of these inner data classes but I'm not able to find the right tutorial or example. Ideally, I want entire Response parsed just as if it were a well-defined JSON.
Here is how I use retrofit/moshi
#Provides
#Singleton
#MyApp
fun provideMyAppRetrofit(context: Context, #MyApp okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.baseUrl(context.getString(R.string.APP_BASE_URL))
.build()
}
#Provides
#Singleton
fun provideMyAppApiService(#MyApp retrofit: Retrofit): MyAppApiService {
return retrofit.create(MyAppApiService::class.java)
}
How do I achieve this? Any sample or reference implementation will be helpful.
welcome to the polymorphic JSON parsing problems word!
We were writing own JSON adapters that are looking like:
internal class CardJsonAdapter(
moshi: Moshi
) : JsonAdapter<Card>() {
private val cardTypeAdapter = moshi.adapter(Card.Type::class.java)
private val amountsWithActionAdapter = moshi.adapter(AmountsWithActionCard::class.java)
private val backgroundImageCardAdapter = moshi.adapter(BackgroundImageCard::class.java)
#Suppress("TooGenericExceptionCaught")
override fun fromJson(reader: JsonReader): Card = try {
#Suppress("UNCHECKED_CAST")
val jsonMap = reader.readJsonValue() as Map<String, Any?>
val type = cardTypeAdapter.fromJsonValue(jsonMap["type"])
createCardWithType(type, jsonMap)
} catch (error: Exception) {
if (BuildConfig.DEBUG) {
Log.w("CardJsonAdapter", "Failed to parse card", error)
}
// Try not to break the app if we get unexpected data: ignore errors and return a placeholder card instead.
UnknownCard
}
override fun toJson(writer: JsonWriter, value: Card?) {
throw NotImplementedError("This adapter cannot write cards to JSON")
}
private fun createCardWithType(type: Type?, jsonMap: Map<String, Any?>) = when (type) {
null -> UnknownCard
Type.AMOUNTS_WITH_ACTION -> amountsWithActionAdapter.fromJsonValue(jsonMap)!!
Type.BACKGROUND_IMAGE_WITH_TITLE_AND_MESSAGE -> backgroundImageCardAdapter.fromJsonValue(jsonMap)!!
}
}
However, it is not anymore required. Moshi supports polymorphic JSON parsing now - https://proandroiddev.com/moshi-polymorphic-adapter-is-d25deebbd7c5
Make a custom adapter
class YourAdapter {
#FromJson
fun fromJson(reader: JsonReader, itemsAdapter: JsonAdapter<ItemsResponse>): List<ItemsResponse>? {
val list = ArrayList<ItemsResponse>()
if (reader.hasNext()) {
val token = reader.peek()
if (token == JsonReader.Token.BEGIN_ARRAY) {
reader.beginArray()
while (reader.hasNext()) {
val itemResponse = itemsAdapter.fromJsonValue(reader.readJsonValue())
itemsResponse?.let {
list.add(itemResponse)
}
}
reader.endArray()
}
}
return list
}
}

Empty response body on retrofit call

I have a app whos call a service POST (postman test) and get some information from that call.
My retrofit initializer:
class RetrofitInitializer {
private val retrofit = Retrofit.Builder()
.baseUrl("https://bank-app-test.herokuapp.com/api/")
.addConverterFactory(GsonConverterFactory.create())
.build()
fun loginApiContract() : LoginApiContract{
return retrofit.create(LoginApiContract::class.java)
}
}
the interface:
interface LoginApiContract {
#POST("login")
fun login() : Call<UserAccount>
#GET("statements")
fun getStatements()
}
and finally the call:
val call = RetrofitInitializer().loginApiContract().login()
call.enqueue(object: Callback<UserAccount> {
override fun onResponse(call: Call<UserAccount?>?,
response: Response<UserAccount?>?) {
response?.body()?.let {
val myUserAccount: UserAccount = it
loginView.doLogin(myUserAccount)
}
}
override fun onFailure(call: Call<UserAccount?>?,
t: Throwable?) {
Log.e("onFailure error", t?.message)
}
})
I got response code 200, but response body is empty.
This is my postman response:
{
"userAccount": {
"userId": 1,
"name": "Jose da Silva Teste",
"bankAccount": "2050",
"agency": "012314564",
"balance": 3.3445
},
"error": {}
}
and this is my model:
class UserAccount constructor(var userId: Int, var name: String, var bankAccount: String, var agency: String, var balance: Double){
init{
this.userId = userId
this.name = name
this.bankAccount = bankAccount
this.agency = agency
this.balance = balance
}
}
I found!
Its a problem with my postman. For any reason my AVD cant access the mock server. now i solve my problem with a simple restful api node.js.
Thx for the help guys.
Your response contains elements named "userAccount" and "error". Your UserAccount class has neither which is causing the issue. Therefore, use Retrofit with a class like this:
data class UserResponse(val userAccount: UserAccount, val error: BackendError)

Categories

Resources