I have a problem. I have a json. I'm trying to parse symbols like ' and \u00e7. Symbol \u00e7 successfully parses, but symbol ' remains unchanged. Here's my retrofit builder.
Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.baseUrl(RatersApi.BASE_URL)
.client(get())
.build()
.create(RatersApi::class.java)
and ok http builder which called from get() function
OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor()
.apply { level = HttpLoggingInterceptor.Level.BODY }
)
.addInterceptor(HeaderInterceptor())
.build()
ANSWER
Ok. I didn't find correct solution, so i wrote my own interceptor which transform strings from html. Just inject this into your okhttp builder Here it is:
class HtmlStringInterceptor: Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
val contentType = response.body?.contentType()
val bodyString = if (android.os.Build.VERSION.SDK_INT >= 24) {
Html.fromHtml(response.body?.string(), Html.FROM_HTML_MODE_COMPACT).toString()
} else {
Html.fromHtml(response.body?.string()).toString()
}
val body = bodyString.toResponseBody(contentType)
return response.newBuilder().body(body).build()
}
}
The cause of this issue is already in your title: ' is a HTML code and not Unicode.
So the JSON parsing is correct and you need additional processing to handle HTML content. For example if you want to show it in a TextView you can use the following:
// extension function to handle different api levels
fun TextView.setHtml(htmlContent: String) {
if (android.os.Build.VERSION.SDK_INT >= 24) {
this.text = Html.fromHtml(htmlContent, Html.FROM_HTML_MODE_COMPACT)
} else {
#Suppress("DEPRECATION")
this.text = Html.fromHtml(htmlContent)
}
}
// set view content from JSON
text_view.setHtml("test: '")
Related
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.
in my application am using a Retrofit 2.9.0, my issue is the user can change completely the URL from the app menu, in this case is not working when i changed the URL only if i restart the app.
this my instance of Retrofit :
object ApiService {
var token: String = ""
#JvmName("setToken1")
fun setToken(tk: String) {
token = tk
}
private val globalInterceptor = GlobalErrorInterceptor()
private val loginInterceptor = LoginErrorInterceptor()
private val okHttpClient =
OkHttpClient.Builder().addInterceptor(globalInterceptor).build()
private val okHttpClientLogin =
OkHttpClient.Builder().addInterceptor(loginInterceptor).build()
var gson = GsonBuilder()
.setLenient()
.create()
/**This instance for the others requests */
private val retrofit by lazy {
Retrofit.Builder()
.baseUrl(LOGIN_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.client(okHttpClient)
.build()
}
val API: WebServicesApi by lazy {
retrofit.create(WebServicesApi::class.java)
}
/**This instance for the login to get the Token */
private val retrofitLogin by lazy {
Retrofit.Builder()
.baseUrl(LOGIN_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClientLogin)
.build()
}
val APILogin: WebServicesApi by lazy {
retrofitLogin.create(WebServicesApi::class.java)
}
}
You can dynamically change retrofit URL by doing something like this. First change retrofit from val to var.
private fun changeBaseUrl(url: String) {
// change the base url only if new url is different than old url
if (retrofit.baseUrl().toString() != url) {
retrofit = retrofit.newBuilder().baseUrl(url).build()
}
}
Please note you might have to change this method and call it according to your flow. The main point to note here is the use of .newBuilder().baseUrl(url).build().
I am using Retrofit (2.6) on Android to implement a service which connects to a web server, and which requests that the server undertake some work. The relevant code can be summarized thus:
interface MyService {
#GET(START_WORK)
suspend fun startWork(#Query("uuid") uuid: String,
#Query("mode") mode: Int):
MyStartWorkResponse
}
// Do some other things, include get a reference to a properly configured
// instance of Retrofit.
// Instantiate service
var service: MyService = retrofit.create(MyService::class.java)
I can call service.startWork() with no problem and obtain valid results. However, in some conditions, the web server will return a 400 error code, with a response body which includes specific error information. The request is not malformed, however; it's just that there is another problem which should be brought to the user's attention. The trouble is, I can't tell what the problem is, because I don't get a response; instead, my call throws an exception because of the 400 error code.
I don't understand how to modify my code so that I can catch and handle 400 error responses, and get the information I need from the body of the response. Is this a job for a network interceptor on my okhttp client? Can anyone shed some light?
Use this code (KOTLIN)
class ApiClient {
companion object {
private val BASE_URL = "YOUR_URL_SERVER"
private var retrofit: Retrofit? = null
private val okHttpClientvalor = OkHttpClient.Builder()
.connectTimeout(90, TimeUnit.SECONDS)
.writeTimeout(90, TimeUnit.SECONDS)
.readTimeout(90, TimeUnit.SECONDS)
.build()
fun apiClient(): Retrofit {
if (retrofit == null) {
retrofit = Retrofit.Builder().baseUrl(BASE_URL)
.client(okHttpClientvalor)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
return retrofit!!
}
}
}
object ErrorUtils {
fun parseError(response: Response<*>): ErrorResponce? {
val conversorDeErro = ApiClient.apiClient()
.responseBodyConverter<ErrorResponce>(ErrorResponce::class.java, arrayOfNulls(0))
var errorResponce: ErrorResponce? = null
try {
if (response.errorBody() != null) {
errorResponce = conversorDeErro.convert(response.errorBody()!!)
}
} catch (e: IOException) {
return ErrorResponce()
} finally {
return errorResponce
}
}
}
class ErrorResponce {
/* This name "error" must match the message key returned by the server.
Example: {"error": "Bad Request ....."} */
#SerializedName("error")
#Expose
var error: String? = null
}
if (response.isSuccessful) {
return MyResponse(response.body() // transform
?: // some empty object)
} else {
val errorResponce = ErrorUtils.parseError(response)
errorResponce!!.error?.let { message ->
Toast.makeText(this,message,Toast.LENGTH_SHORT).show()
}
}
Retrofit defines successful response as such:
public boolean isSuccessful() {
return code >= 200 && code < 300; }
which means you should be able to do something like this
class ServiceImpl(private val myService: MyService) {
suspend fun startWork(//query): MyResponse =
myService.startWork(query).await().let {
if (response.isSuccessful) {
return MyResponse(response.body()//transform
?: //some empty object)
} else {
throw HttpException(response)//or handle - whatever
}
}
}
I am using retrofit2 in kotlin, and I need to get the content that is a json and this encrypted, I know that to convert json just use the JacksonConverterFactory (until this part was working well) but an encryption was added before that and I do not know how To handle this, do I need to create a converter of my own? Does anyone have a read to tell me?
My current call for retrofit
val retrofit = Retrofit.Builder()
.baseUrl("http://100.1.1.100/")
.addConverterFactory(JacksonConverterFactory.create())
.client(httpClient.build())
.build()
And i already have my fucntion (working) to decrypt:
CryptAES.decrypt(value))
This can be done by creating an decrypt interceptor:
class DecryptInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response = chain
.run { proceed(request()) }
.let { response ->
return#let if (response.isSuccessful) {
val body = response.body()!!
val contentType = body.contentType()
val charset = contentType?.charset() ?: Charset.defaultCharset()
val buffer = body.source().apply { request(Long.MAX_VALUE) }.buffer()
val bodyContent = buffer.clone().readString(charset)
response.newBuilder()
.body(ResponseBody.create(contentType, bodyContent.let(::decryptBody)))
.build()
} else response
}
private fun decryptBody(content: String): String {
//decryption
return content
}
}
setup:
val httpClient = OkHttpClient().newBuilder()
httpClient.addInterceptor(DecryptInterceptor())
val retrofit = Retrofit.Builder()
.baseUrl("http://100.1.1.100/")
.addConverterFactory(JacksonConverterFactory.create())
.client(httpClient.build())
.build()
I'm making an OkHttp interceptor so it retry to sign in when any end point returns a 401 error, but the interceptor is looping forever.
I've also tried to add a counter, but the counter resets itself every time.
Here's my code:
object Service {
private fun getOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(getLoggingInterceptor())
.addInterceptor(NetworkInterceptor())
.build()
}
private fun getRetrofit(): Retrofit {
return Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(getOkHttpClient())
.baseUrl(getBaseUrl())
.build()
}
}
class NetworkInterceptor: Interceptor {
var counter = 0
override fun intercept(chain: Interceptor.Chain): Response? {
val originalResponse = chain.proceed(chain.request())
if (!originalResponse.isSuccessful && originalResponse.code() == 401) {
Log.e("NetworkInterceptor", "Network error 401. Counter = $counter")
counter++
val refreshedToken = refreshToken()
Log.e("NetworkInterceptor", "refreshedToken = $refreshedToken")
}
return originalResponse
}
private fun refreshToken(): String {
val context = MyApp.appContext
val preferencesUtil = SharePreferencesUtils(context)
val username = preferencesUtil.getUsername()
val password = preferencesUtil.getPassword()
val login = AuthService().loginSync(username, password).execute()
return login.body()?.access_token!!
}
}
I have tried this with an Auth call being an RxJava Single, and a regular synchronous Call<>
In each case, the call happens forever, the 401 gets returned forever, and the counter always stays at 0.
Any ideas on what I'm missing or doing wrong?
Thank you very much!
Add as a NetworkInterceptor using
addNetworkInterceptor(NetworkInterceptor)
refer to this link for further clarification: https://github.com/square/okhttp/wiki/Interceptors