import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import java.io.IOException
import java.lang.Exception
...
private val client = OkHttpClient()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val tvDisplay: TextView = findViewById(R.id.displayTV) as TextView
tvDisplay.setOnClickListener {
tvDisplay.text = run("https://jsonplaceholder.typicode.com/todos/1")
}
}
#Throws(IOException::class)
fun run(url: String): String {
val request = Request.Builder()
.url(url)
.build()
try {
client.newCall(request).execute().use { response -> return response.body().toString() }
}
catch (e: Exception) {
return e.message.toString()
}
}
Using android studio and kotlin. Trying to call an API but all I get is NULL instead of the string it should be getting.
Additionally how do I add basic authentication to this (username/password) if the API required it?
Also what does "#Throws" do?
To start off with, I'd suggest looking into retrofit as I personally find it easier to work with (though may be overkill if you're only making one or two REST calls)
I'd also probably do
client.newCall(request).enqueue(object: Callback {
override fun onResult(call: Call, response: Response) {
if (response.isSuccessful()) {
return#run response.body.toString()
}
}
)}
to be asynchronous.
Authentication is a pain to add in OkHttp imo and is best answered from here, and much easier in Retrofit.
Finally, Throws marks the function as having the potential to throw an Exception when called from Java code (as Kotlin and Java can co-exist)
Longer explanation through code
#Throws(IOException::class) // If IOException occur it will throw error to its parent the one that call to this function so you do not need try catch in this function
fun run(url : String) : Response{
val request = Request.Builder()
.url(url)
.get()
.build()
val client = OkHttpClient()
return client.newCall(request).execute()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val tvDisplay: TextView = findViewById(R.id.displayTV) as TextView
val thread = object : Thread() { //Always use another thread from UIthread so UI will not lock while waiting get response from API
override fun run() {
try{
val _response = run("https://jsonplaceholder.typicode.com/todos/1").body()!!.string()
runOnUiThread { //Change to UI thread again if you need change somthing in UI
tvDisplay.setText(_response)
}
}
catch(e: Excpetion){
Log.d("Exception", e.toString()) //if anything error it goes here
}
}
}
thread.start()
}
Related
I'm learning to use the Retrofit library for different tasks, but don't fully understand how it works yet.
The main task is to get the body if the response code is 200, overwise (all other codes) just set flag:
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.ResponseBody
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.http.GET
interface APIService {
#GET("/")
suspend fun getRoot(): Response<ResponseBody>
}
...
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
button.setOnClickListener {
val url = editText.text.toString()
// url = "https://"+ "google.coN"
val retrofit = Retrofit.Builder()
.baseUrl(url)
.build()
val service = retrofit.create(APIService::class.java)
...
CoroutineScope(Dispatchers.IO).launch {
val response = service.getRoot()
withContext(Dispatchers.Main) {
if (response.isSuccessful){
Ok = true // address is ok
} else {
Ok = false // this address dosnt exist
}
....
}
}
}
}
}
Code works well (remastered from some tutor example) with good links but the app crashes whenever the address is wrong or poorly formatted, it requires a well-formatted URL ("https://"+)
How to modify code and add an exception and do pre-format of URL?
PS: Prob it is better to use OkHTTP directly, but I use integration
of GSON lib with this retrofit code, dropped for clarity
Thanx.
First, Create a sealed class to hold the result
sealed class ApiResult<out T : Any?>
data class Success<out T : Any?>(val data: T) : ApiResult<T>()
data class ApiError(val exception: Exception) : ApiResult<Nothing>()
Now write a helper function to map okhttp response to ApiResult
suspend fun <T : Any> handleApi(
call: suspend () -> Response<T>,
errorMessage: String = "Some errors occurred, Please try again later"
): ApiResult<T> {
try {
val response = call()
if (response.isSuccessful) {
isConnectedToNetwork = true
response.body()?.let {
return Success(it)
}
}
response.errorBody()?.let {
try {
val errorString = it.string()
val errorObject = JSONObject(errorString)
return ApiError(
RuntimeException(if(errorObject.has("message")) errorObject.getString("message") else "Error occurred, Try again Later"))
} catch (ignored: JsonSyntaxException) {
return ApiError(RuntimeException(errorMessage))
}
}
return ApiError(RuntimeException(errorMessage))
} catch (e: Exception) {
if (e is IOException) {
isConnectedToNetwork = false
}
return ApiError(RuntimeException(errorMessage))
}
}
Finally, use below to code to access the result
CoroutineScope(Dispatchers.IO).launch {
val result: ApiResult<ResponseBody> = handleApi( { service.getRoot() } )
when(result){
is ApiResult.Success -> // result.data will give you ResponseBody
is ApiResult.ApiError -> // result.exception will provide the error
}
}
There are few things which can help you with this, it will be more efficient:
Create a view model and create an instance of that in your activity.
In the view model, create a method for executing background tasks, like this:
private fun loadNetworkRequest(block: suspend () -> Unit): Job {
return viewModelScope.launch {
try {
block()
}catch (ex: Exception) {
Toast.makeText(appContext, ex.message, Toast.LENGTH_SHORT).show()
}
}
}
Add the suspend keyword for the request in the service file, which you want to execute using this method.
#GET("category/")
suspend fun getCategories(): Response<CategoryResponseModel>
Execute the request in the view model, like this:
fun performRequest(callback: (Boolean) -> Unit) {
loadNetworkRequest {
val response = service.getRoot()
callback.invoke(response.isSuccessful)
}
}
Call the request method in the activity.
button.setOnClickListener {
....
viewModel.performRequest { response ->
// ok = response
}
}
I am new android developer, how can I get result form this snippet, what way does exist, because it doesn't return anything, because of I'm adding element inside onResponse, but using only kotlin module:
private fun foo(list: ArrayList<CurrencyModel> = ArrayList()): ArrayList<CurrencyModel> {
val request = Request.Builder().url(BASE_URL_YESTERDAY).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 data = Gson().fromJson(body, Currencies::class.java)
list.add(CurrencyModel("USD", data.rates.USD, 0.0))
list.add(CurrencyModel("SEK", data.rates.SEK, 0.0))
list.add(CurrencyModel("EUR", data.rates.EUR, 0.0))
}
})
return list
}
}
You can give your function a callback parameter that's called when the response is receieved. And you shouldn't have an input list in this case, because if you have multiple sources modifying it at unpredictable future moments, it will be difficult to track.
The function can look like this:
private fun getCurrencyModels(callback: (ArrayList<CurrencyModel>) {
val request = Request.Builder().url(BASE_URL_YESTERDAY).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 data = Gson().fromJson(body, Currencies::class.java)
val list = arrayListOf(
CurrencyModel("USD", data.rates.USD, 0.0)),
CurrencyModel("SEK", data.rates.SEK, 0.0)),
CurrencyModel("EUR", data.rates.EUR, 0.0))
)
callback(list)
}
})
}
And then to use it:
getCurrencyModels { modelsList ->
// do something with modelsList when it arrives
}
An alternative is to use coroutines, which allow you to do asynchronous actions without callbacks. Someone has already created a library that lets you use OkHttp requests in coroutines here. You could write your function as a suspend function like this:
private suspend fun getCurrencyModels(): ArrayList<CurrencyModel> {
val request = Request.Builder().url(BASE_URL_YESTERDAY).build()
val client = OkHttpClient()
val response = client.newCall(request).await()
val body = response.body?.string()
val data = Gson().fromJson(body, Currencies::class.java)
return arrayListOf(
CurrencyModel("USD", data.rates.USD, 0.0)),
CurrencyModel("SEK", data.rates.SEK, 0.0)),
CurrencyModel("EUR", data.rates.EUR, 0.0))
)
}
and then use it like this:
lifecycleScope.launch {
try {
val currencyModels = getCurrencyModels()
// do something with currencyModels
} catch (e: IOException) {
// request failed
}
}
Coroutines make it really easy to avoid leaking memory when your asynchronous calls outlive your Activity or Fragment. In this case, if your Activity closes while the request is going, it will be cancelled automatically and references to your Activity will be removed so the garbage collector can release your Activity.
The onResponse() function is only called when the HTTP response is successfully returned by the remote server. Since this response doesn't happen immediately, you can't use the result in your code immediately. What you could do is use a ViewModel and LiveData variable and add the values to that variable in onResponse(). Something like:
private fun foo(list: ArrayList<CurrencyModel> = ArrayList()) {
val request = Request.Builder().url(BASE_URL_YESTERDAY).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 data = Gson().fromJson(body, Currencies::class.java)
val list: ArrayList<CurrencyModel> = arrayListOf()
list.add(CurrencyModel("USD", data.rates.USD, 0.0))
list.add(CurrencyModel("SEK", data.rates.SEK, 0.0))
list.add(CurrencyModel("EUR", data.rates.EUR, 0.0))
viewModel.list.postValue(list)
}
})
}
Im struggling here a little bit.
What i am trying to achieve is to navigate from a Httpinterceptor back to the login fragment if a 401 occurs.
Im using Jwt for authorization and if the token is not valid anymore a 401 unauthorized will be returned from the backend.
I've tried googling the issue but ended up with no real answer to my problem.
What i've tried to do so far is getting the navcontroller by NavHostFragment.findNavController and by findNavController(MainActivity.activity, 1).
Both things didnt work since the first one requires you to provide a fragment which i dont have in the Interceptor and the second one failed because i couldnt figure out a way to access the MainActivity from the interceptor.
Dont have much code but this is how the interceptor looks right now (not working for obvious reasons)
package de.wabi.vistascraper.library.http.interceptors
import androidx.navigation.Navigation.findNavController
import androidx.navigation.fragment.NavHostFragment
import de.wabi.vistascraper.MainActivity
import de.wabi.vistascraper.ui.main.MainFragment
import okhttp3.Interceptor
import okhttp3.Response
class ErrorInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var builder = chain.request().newBuilder()
var response = chain.proceed(builder.build())
var test = NavHostFragment.findNavController(this.);
if(response.code == 401){
// var test = findNavController(MainActivity.activity, 1)
}
return response;
}
}
Maybe on of you people around the internet have an idea.
Thanks in advance
David
You could do something like this to tie up you interceptor and activity:
class ErrorInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var builder = chain.request().newBuilder()
var response = chain.proceed(builder.build())
if (response.code == 401) {
GlobalNavigator.logout()
}
return response
}
}
object GlobalNavigator {
private var handler: GlobalNavigationHandler? = null
fun registerHandler(handler: GlobalNavigationHandler) {
this.handler = handler
}
fun unregisterHandler() {
handler = null
}
fun logout() {
handler?.logout()
}
}
interface GlobalNavigationHandler {
fun logout()
}
class YourActivity : GlobalNavigationHandler {
override fun logout() {
findNavController().navigate(R.id.loginFragment)
}
override fun onStart() {
GlobalNavigator.registerHandler(this)
}
override fun onStop() {
GlobalNavigator.unregisterHandler()
}
}
class Service{
interface RedditApi {
#GET("/top.json")
fun getTop(#Query("after") after: String,
#Query("limit") limit: String)
: Deferred<Response<News>>;
}
}
val okHttpClient = OkHttpClient.Builder()
.readTimeout(40, TimeUnit.SECONDS)
.addInterceptor { chain ->
val ongoing = chain.request().newBuilder()
ongoing.addHeader("Cache-Control", "no-cache")
ongoing.addHeader("User-Agent", System.getProperty("http.agent"))
//ongoing.addHeader("Authorization", val.trim());
chain.proceed(ongoing.build())
}
.connectTimeout(40, TimeUnit.SECONDS)
.build()
val retrofit = Retrofit.Builder()
.baseUrl( "/rest/s1/mobile/")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.client(okHttpClient)
.build()
redditApi = retrofit.create(Service.RedditApi::class.java)
Okey I have that, am trying to use retrofit with Coroutine. I go to my activity and implement it like below.I get error dispatchers.main unresolved reference main.I am using kotlin 1.3.21. Also my other question is, what if user clicks back on the activity how can I cancel the coroutine operation?Like In Java I used to do call.cancel() with retrofit.It cancelled the call.
class MainActivity : AppCompatActivity(), Fightable {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
CoroutineScope(Dispatchers.IO).launch {
val request = App.redditApi.getTop("after", "limit")
withContext(Dispatchers.Main) {
try {
val response = request.await()
if (response.isSuccessful) {
val news: News? = response.body()
//Do something with response e.g show to the UI.
} else {
}
} catch (e: HttpException) {
} catch (e: Throwable) {
}
}
}}}
You need to create a single instance of coroutine context and also have a job defined to it.
val job = Job()
val coroutineScope = CoroutineContext(Dispatchers.Main+job)
And start the work using the declared scope and when you want to cancel the work, you can simply call job.cancel() which cancels all current and upcoming works.
I have been having problems with OkHttp when I nest an OkHttp call inside another OkHttp call I am having a problem with the concurrency. I want to wait for my inner call to finish its thread's job before proceeding. Please take a look.
Note: I am a novice with Kotlin and Multi-thread handling.
private fun parseJson(url: String) {
val request = Request.Builder()
.url(url)
.build()
client.newCall(request).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response?) {
var bodyOfProducts = response?.body()?.string()
var collectionJsonObject = jsonParseTool.fromJson(bodyOfProducts, Products::class.java)
val productsWithDetails = ArrayList<ProductDetails>()
for(product in collectionJsonObject.collects){
var concatProductUrl = "https://shopicruit.myshopify.com/admin/products.json?ids=" + product.product_id+ "&page=1&access_token=c32313df0d0ef512ca64d5b336a0d7c6"
val newRequest = Request.Builder()
.url(concatProductUrl)
.build()
val job = thread {
client.newCall(newRequest).enqueue(object : Callback {
override fun onResponse(call: Call, newResponse: Response?) {
var bodyOfProductDetails = newResponse?.body()?.string()
var productJsonObject = jsonParseTool.fromJson(bodyOfProductDetails, ProductDetails::class.java)
productsWithDetails.add(productJsonObject)
}
override fun onFailure(call: Call, e: IOException) {
println("EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE")
}
})
}
job.start()
job.join() // This should force my thread to finish before the rest of the code is executed on the main thread.
}
// println(collectionJsonObject.collects[0].product_id)
/*runOnUiThread {
recyclerViewCustomCollections.adapter = CollectionsAdapter(jsonObject)
}*/
}
override fun onFailure(call: Call, e: IOException) {
println("EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE")
}
})
}
In this case you should be using execute as mentioned and since http calls are handled asynchronously your thread is redundant and should be removed.
If you want to run code after all the requests are finished one way of doing this is by passing in a onComplete callback function and count the number of requests completed, when all of the threads are completed call the callback function containing the code that should be run after all of the requests.