I'm using Retrofit2 to get data in my Android applications. It looks like this:
interface ApiChatService {
#GET("ncs-chat-web/rest/v1/message")
suspend fun getChatMessages(#Header("Authorization") jwtToken: String, #Query("page") page: Long, #Query("count") count: Int): Response<List<ChatMessageApi>>
}
I call this Retrofit function this way:
override suspend fun getChatMessages(
jwtToken: String,
page: Long
): OperationResult<List<ChatMessageApi>> {
return try {
val response: Response<List<ChatMessageApi>> =
apiChatService.getChatMessages(normalizeJwtToken(jwtToken), page = page, count = MESSAGE_PAGE_SIZE)
if (response.isSuccessful) {
OperationResult(operationResult = Result.OK, resultObject = response.body())
} else {
Log.d("ApiDatasourceImpl.getChatMessages", response.errorBody()?.string()?: "Empty error message")
OperationResult(
operationResult = Result.ERROR,
operationInfo = response.errorBody()?.string()
)
}
} catch (e: Exception) {
Log.d("ApiDatasourceImpl.getChatMessages", e.localizedMessage ?: "Empty error message")
OperationResult(operationResult = Result.ERROR, operationInfo = e.localizedMessage)
}
}
In my Android code I got response code 500 with message "Internal server error"
When I call this request in Postman with such URL
https://my-server.com/ncs-chat-web/rest/v1/message?count=10&page=1
I got 200 code and expected payload.
I'm wondering is there any way to get URL which create Retrofit based on my interface function?
Related
Before in other random languages I always returned values from functions and I was so surprised now when I try do like below but got error:
fun getChannels(): List<TblChannel> {
val stringRequest = JsonObjectRequest(
Request.Method.GET, "$baseUrl/api/json/channel_list.json",
null,
{ response ->
try{
val gson = Gson()
val token = TypeToken.getParameterized(ArrayList::class.java,TblChannel::class.java).type
val channels1:JSONArray = response.getJSONArray("groups").getJSONObject(0).getJSONArray("channels")
//got "return isn't allowed here" error
return gson.fromJson(channels1.toString(),token)
} catch (e:Exception){
Log.e(tag,"DkPrintError on getChannels: $e")
}
},
{ error ->
Log.e(tag, "DkPrintError on getChannels: $error")
})
requestQueue.add(stringRequest)
}
How can I convert response body to my class and return them?
This isn't really a kotlin problem, we do have functions that return values, however you cannot return a value from asynch function (which is the case here):
If you perform some calculation asynchronously, you cannot directly return the value, since you don't know if the calculation is finished yet. You could wait it to be finished, but that would make the function synchronous again. Instead, you should work with callbacks
source
what you could do tho (as suggested in the quote), is use callbacks, as shown here
That post will be so helpfull to solve that problem.
In that case I solved the problem with callback method and my code was like below:
fun getChannels(onDataReadyCallback: OnDataReadyCallBack){
val stringRequest = JsonObjectRequest(
Request.Method.GET, "$baseUrl/api/json/channel_list.json",
null,
{ response ->
try{
val gson = Gson()
val token = TypeToken.getParameterized(ArrayList::class.java,TblChannel::class.java).type
val channels1:JSONArray = response.getJSONArray("groups").getJSONObject(0).getJSONArray("channels")
onDataReadyCallback.onDataReady(gson.fromJson(channels1.toString(),token))
} catch (e:Exception){
Log.e(tag,"DkPrintError on getChannels: $e")
}
},
{ error ->
Log.e(tag, "DkPrintError on getChannels: $error")
})
requestQueue.add(stringRequest)
}
and I called that fun like:
private fun getChannels(){
viewModelScope.launch {
channelsLiveData.value=roomRepository.getAllChannels
if (channelsLiveData.value.isNullOrEmpty()){
remoteRepository.getChannels(object :OnDataReadyCallBack{
override fun onDataReady(data: List<TblChannel>) {
viewModelScope.launch {
channelsLiveData.value=data
}
}
})
}
}
}
I am using Spotify API to login user to the app. this is the interface i wrote per documentation:
interface API {
#GET("/authorize")
fun login(#Query("client_id") client_id:String,
#Query("response_type") response_type:String,
#Query("redirect_uri")redirect_uri:String,
#Query("scope") scope:String
):Call<LoginResult>
This is the response result data class:
data class LoginResult(
val code: String
)
And this is the login function:
fun login() {
val BASE_URL = "https://accounts.spotify.com"
val CLIENT_ID = "c6c23e3e2f604f9aa1780fe7504e73c6"
val REDIRECT_URI = "com.example.myapp://callback"
val retrofit: Retrofit = Retrofit.Builder().baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create()).build()
val service: API = retrofit.create(API::class.java)
val listCall: Call<LoginResult> =
service.login(CLIENT_ID, "code", REDIRECT_URI, "user-top-read")
listCall.enqueue(object : Callback<LoginResult> {
override fun onResponse(response: Response<LoginResult>?, retrofit: Retrofit?) {
if (response?.body() != null) {
Log.i("result!", response.body().code)
}
if(response?.body() == null){
Log.i("Code" , response!!.code().toString())
Log.i("Response! ", "null response body")
}
}
override fun onFailure(t: Throwable?) {
Log.e("Here", "it is")
Log.e("Error", t!!.message.toString())
}
})
}
But I am getting this error:
E/Here: it is
E/Error: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 2 column 1 path $
There are a lot of questions here about this particular error and I read all of them and tried to implement the suggested solutions, but none worked.
Any help would be appreciated.
[this is the mentioned documentation link]
(https://developer.spotify.com/documentation/general/guides/authorization/code-flow/)
I'm trying to understand Kotlin couroutine. So here's my code (based on this tutorial). To keep the code relatively simple, I deliberately avoid MVVM, LiveData, etc. Just Kotlin couroutine and Retrofit.
Consider this login process.
ApiInterface.kt
interface ApiInterface {
// Login
#POST("/user/validate")
suspend fun login(#Body requestBody: RequestBody): Response<ResponseBody>
}
ApiUtil.kt
class ApiUtil {
companion object {
var API_BASE_URL = "https://localhost:8100/testApi"
fun getInterceptor() : OkHttpClient {
val logging = HttpLoggingInterceptor()
logging.level = HttpLoggingInterceptor.Level.BODY
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(logging)
.build()
return okHttpClient
}
fun createService() : ApiInterface {
val retrofit = Retrofit.Builder()
.client(getInterceptor())
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(OJIRE_BASE_URL)
.build()
return retrofit.create(ApiInterface::class.java)
}
}
fun login(userParam: UserParam): String {
val gson = Gson()
val json = gson.toJson(userParam)
var resp = ""
val requestBody = json.toString().toRequestBody("application/json".toMediaTypeOrNull())
CoroutineScope(Dispatchers.IO).launch {
val response = createService().login(requestBody)
withContext(Dispatchers.Main){
if (response.isSuccessful){
val gson = GsonBuilder().setPrettyPrinting().create()
val prettyJson = gson.toJson(
JsonParser.parseString(
response.body()
?.string()
)
)
resp = prettyJson
Log.d("Pretty Printed JSON :", prettyJson)
}
else {
Log.e("RETROFIT_ERROR", response.code().toString())
}
}
}
return resp
}
}
LoginActivity.kt
class LoginActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
edtUsername = findViewById(R.id.edtUsername)
edtPassword = findViewById(R.id.edtPassword)
btnLogin = findViewById(R.id.btnLogin)
btnLogin.setOnClickListener {
val api = ApiUtil()
val userParam = UserParam(edtMobileNo.text.toString(), edtPassword.text.toString())
val response = JSONObject(api.login(userParam))
var msg = ""
if (response.getString("message").equals("OK")){
msg = "Login OK"
}
else {
msg = "Login failed"
}
Toast.makeText(applicationContext, msg, Toast.LENGTH_SHORT).show()
}
}
}
When debugging the login activity, the API response is captured properly on prettyJson
The problem is resp is still empty. Guess that's how async process work. What I want is to wait until the API call is completed, then the result can be nicely passed to resp as the return value of login(). How to do that?
Well, you got several things wrong here. We'll try to fix them all.
First, the main problem you described is that you need to acquire resp in login() synchronously. You got this problem only because you first launched an asynchronous operation there. Solution? Don't do that, get the response synchronously by removing launch(). I guess withContext() is also not required as we don't do anything that requires the main thread. After removing them the code becomes much simpler and fully synchronous.
Last thing that we need to do with login() is to make it suspendable. It needs to wait for the request to finish, so it is a suspend function. The resulting login() should be similar to:
suspend fun login(userParam: UserParam): String {
val gson = Gson()
val json = gson.toJson(userParam)
val requestBody = json.toString().toRequestBody("application/json".toMediaTypeOrNull())
val response = createService().login(requestBody)
return if (response.isSuccessful){
val gson = GsonBuilder().setPrettyPrinting().create()
gson.toJson(
JsonParser.parseString(
response.body()
?.string()
)
)
}
else {
Log.e("RETROFIT_ERROR", response.code().toString())
// We need to do something here
}
}
Now, as we converted login() to suspendable, we can't invoke it from the listener directly. Here we really need to launch asynchronous operation, but we won't use CoroutineScope() as you did in your example, because it leaked background tasks and memory. We will use lifecycleScope like this:
btnLogin.setOnClickListener {
val api = ApiUtil()
val userParam = UserParam(edtMobileNo.text.toString(), edtPassword.text.toString())
lifecycleScope.launch {
val response = JSONObject(api.login(userParam))
var msg = ""
if (response.getString("message").equals("OK")){
msg = "Login OK"
}
else {
msg = "Login failed"
}
withContext(Dispatchers.Main) {
Toast.makeText(applicationContext, msg, Toast.LENGTH_SHORT).show()
}
}
}
Above code may not be fully functional. It is hard to provide working examples without all required data structures, etc. But I hope you get the point.
Also, there are several other things in your code that could be improved, but I didn't touch them to not confuse you.
I am making generic classes for hitting otp api.anybody can use otp section just have to pass request ,Response class and url and all will be done by this otp section.
Please note : this response class can be of different type (for eg: MobileOtpResponse,EmailOtpResponse)
below is the generic OtpClient which takes any request type and returns particular passed ResponseType (for example : Request class passed is OtpRequest ,ResponseType class passed is OtpResponse)
interface OtpClient {
#POST
suspend fun <Request : Any, ResponseType> sendOtp(#Url url: String,
#Body request:#JvmSuppressWildcards Any): #JvmSuppressWildcards ResponseType
}
OtpRequest
data class OtpRequest(#SerializedName("mobile_number") val mobileNumber: String,#SerializedName("app_version") val appVersion: String)
OtpResponse
data class OtpResponse(#SerializedName("status") val status: String = "",
#SerializedName("response") val response: OtpData? = null)
data class OtpData(
#SerializedName("otp_status") val otpStatus: Boolean = false,
#SerializedName("message") val message: String = "",
#SerializedName("error") val error: Int? = null,
#SerializedName("otp_length") val otpLength: Int? = null,
#SerializedName("retry_left") val retryLeft: Int? = null,)
Now i create Repo to call this api this simply use flow and when the data fetch it emits the data
class OtpRepoImpl<out Client : OtpClient>(val client: Client) :OtpRepo {
override fun <Request:Any, ResponseType> sentOtpApi(url: String, request: Request): Flow<ResponseType> {
return flow<ResponseType> {
// exectute API call and map to UI object
val otpResponse = client.sendOtp<Request, ResponseType>(url,request)
emit(otpResponse)
}.flowOn(Dispatchers.IO) // Use the IO thread for this Flow
}
}
this repo is used in viewmodel class
#ExperimentalCoroutinesApi
fun <A : Class<ResponseType>, Request : Any, ResponseType : Any> sendOtp(a: Class<ResponseType>, request: Request, response: ResponseType, url: String) {
viewModelScope.launch {
repo.sentOtpApi<Request, ResponseType>(url, request = request)
.onStart { _uiState.value = OtpState.Loading(true) }
.catch { cause ->
_uiState.value = OtpState.Loading(false)
getResponseFromError<Class<ResponseType>,ResponseType>(cause, response) {
// emit(it)
}
}
.collect {
_uiState.value = OtpState.Loading(false)
_uiState.value = OtpState.Success(it)
}
}
}
as you can see above this sendOtp method is called from the view class and inside this method we use repo.sentOtpApi and pass generic request response type.I get data in catch block coz api is send error otp data in 400 HttpException so i created another method getResponseFromError to get error response it should parse the errorBody response and call this lambda block.
private suspend fun <A : Class<*>, ResponseType : Any> getResponseFromError( cause: Throwable, rp: ResponseType, block: suspend (ResponseType) -> Unit) {
if (cause is HttpException) {
val response = cause.response()
if (response?.code() == 400) {
println("fetching error Response")
val errorResponse = response.errorBody()?.charStream()
val turnsType = object : TypeToken<ResponseType>() {}.type
val finalErrorResponse = Gson().fromJson<ResponseType>(errorResponse, turnsType)
block(finalErrorResponse)
} else {
println("someOther exception")
}
} else
_uiState.value = OtpState.Error(cause)
}
so here i am facing the problem inside above method
val turnsType = object : TypeToken<ResponseType>() {}.type
val finalErrorResponse = Gson().fromJson<ResponseType>(errorResponse, turnsType)
block(finalErrorResponse)
This finalErrorResponse is returning LinkedTreeMap instead of ResponseType (in this case its OtpResponse)
i have also tried using Class<*> type like this
val turnsType = object : TypeToken<A>() {}.type
val finalErrorResponse = Gson().fromJson<A>(errorResponse, turnsType)
but its not working.
calling of this sentOtp viewmodel func is like
var classType = OtpResponse::class.java
otpViewModel.sendOtp(a = classType, request = otpRequest, response = OtpResponse() , url =
"http://preprod-api.nykaa.com/user/otp/v2/send-wallet-otp")
[![value in finalErroResponse][1]][1]
[1]: https://i.stack.imgur.com/Holui.png
required: finalErroResponse should be of OtpResponse type because that was passed in sentOtp func
Please help :)
I'm doing self-learning of android (mostly), and I'm can't grasp a concept about Testing. I tried googling and youtube but still don't get it, so I really need a sample to test my code below
Could anyone show me how to create unit test request data to server from this code?
fun fetchJSON() {
val url =baseurl + prevnext + idliga
val request = Request.Builder().url(url).build()
val client = OkHttpClient()
client.newCall(request).enqueue(object: Callback {
override fun onFailure(call: Call, e: IOException) {
}
override fun onResponse(call: Call, response: Response) {
val body = response.body()?.string()
val gson = GsonBuilder().create()
val DataMatch= gson.fromJson(body, DataPertandingan::class.java)
runOnUiThread {
rvPrevMatch.adapter= PrevAdapter(DataMatch)
}
}
})
}
And about the instrumented test, how do I test adding something to SQLite.
here is my activity code of adding data to SQLite.
private fun addToFavorite() {
try {
database.use {
insert(
Favorite.DATA_FAVORITE,
Favorite.ID_EVENT to id_event,
Favorite.DATE to tanggaltandingdet.text,
// home team
Favorite.HOME_ID to idhome,
Favorite.HOME_TEAM to timkandangdet.text,
Favorite.HOME_SCORE to skorkandangdet.text,
Favorite.HOME_GOAL_DETAILS to cetakgolkandang.text,
Favorite.HOME_LINEUP_GOALKEEPER to kiperkandang.text,
Favorite.HOME_LINEUP_DEFENSE to bekkandang.text,
Favorite.HOME_LINEUP_MIDFIELD to midkandang.text,
Favorite.HOME_LINEUP_FORWARD to strikerkandang.text,
Favorite.HOME_LINEUP_SUBSTITUTES to cadangankandang.text,
// Favorite.HOME_TEAM_BADGE to urllogokandang.text,
// away team
Favorite.AWAY_ID to idaway,
Favorite.AWAY_TEAM to timtandangdet.text,
Favorite.AWAY_SCORE to skortandangdet.text,
Favorite.AWAY_GOAL_DETAILS to cetakgoltandang.text,
Favorite.AWAY_LINEUP_GOALKEEPER to kipertandang.text,
Favorite.AWAY_LINEUP_DEFENSE to bektandang.text,
Favorite.AWAY_LINEUP_MIDFIELD to midtandang.text,
Favorite.AWAY_LINEUP_FORWARD to strikertandang.text,
Favorite.AWAY_LINEUP_SUBSTITUTES to cadangantandang.text
// Favorite.AWAY_TEAM_BADGE to urllogotandang.text
)
}
toast ("Data Telah Di Simpan" )
} catch (e: SQLiteConstraintException) {
toast("Error: ${e.message}")
}
}