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
}
}
Related
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.
I am developing android app and I have implemented success and failure cases in viemodel class but I am getting following mismatch Type mismatch.
Required:
Result!
Found:
Result<Response>
below my NewsViewModel where I have implemented success and failure cases when I am getting data
class NewsViewModel(private val repository: NewsRepository) : ViewModel() {
private var _newsResponse= MutableLiveData<Result<NewsResponse>>()
// Expose to the outside world
val news: LiveData<Result<NewsResponse>> = _newsResponse
#UiThread
fun getNews() {
viewModelScope.launch(Dispatchers.IO) {
try {
val response = repository.getNews()
_newsResponse.postValue(Result.success(response))
} catch (ioe: IOException) {
_newsResponse.postValue(Result.Failure("[IO] error please retry", ioe))
} catch (he: HttpException) {
_newsResponse.postValue(Result.Failure("[HTTP] error please retry", he))
}
}
}
}
below my NewsRepository.ktclass
NewsRepository(
private val apiInterface:NewsInterface
){
suspend fun getNews() = apiInterface.getNews()
}
below my Result class
sealed class Result<out T> {
data class Success<out R>(val value: R): Result<R>()
data class Failure(
val message: String?,
val throwable: Throwable?
): Result<Nothing>()
}
I want to know where I exactly I am making mistake what I have to do in order to fix that problem
below my news Interface
import com.example.newsworldwide.model.NewsResponse
import retrofit2.Response
import retrofit2.http.GET
interface NewsInterface {
#GET("ApiKey")
suspend fun getNews(): Response<NewsResponse>
}
Your NewsInterface is returning Response<NewsResponse> & in your NewsViewModel you're passing it directly to response so it becomes Result.Success<Response<NewsResponse>> at the time of posting. That's why this error.
Solution:
Get value from body() of retrofit response class.
Make it Non-nullable using(!!) as your _newsResponse live-data is accepting NewsResponse which is non-nullable. You might want to handle null case here.
So your final code would look something like this.
class NewsViewModel(private val repository: NewsRepository) : ViewModel() {
private var _newsResponse= MutableLiveData<Result<NewsResponse>>()
// Expose to the outside world
val news: LiveData<Result<NewsResponse>> = _newsResponse
#UiThread
fun getNews() {
viewModelScope.launch(Dispatchers.IO) {
try {
val response = repository.getNews().body()!! //change this line
_newsResponse.postValue(Result.Success(response))
} catch (ioe: IOException) {
_newsResponse.postValue(Result.Failure("[IO] error please retry", ioe))
} catch (he: HttpException) {
_newsResponse.postValue(Result.Failure("[HTTP] error please retry", he))
}
}
}
}
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/
Android Studio 3.6
My custom callback interface:
interface RecoveryPasswordConfirmCodeCallback {
fun onSuccess()
fun onError(ex: Throwable?)
}
Use:
val result = TransportService.recoverPasswordConfirmCode(
confirmCode,
ex,
object : RecoveryPasswordConfirmCodeCallback {
override fun onSuccess() {
}
override fun onError(ex: Throwable?) {
if (ex is InvalidOtpException) {
toastMessage.value = SingleEvent(
getApplication<Application>().applicationContext.getString(
R.string.incorrect_confirm_code
)
)
} else {
toastMessage.value = SingleEvent(
getApplication<Application>().applicationContext.getString(
R.string.default_error_message
))
}
}
})
fun recoverPasswordConfirmCode(
confirmCode: String,
ex: NeedTfaException,
callBack: RecoveryPasswordConfirmCodeCallback
) {
//some code here
}
Nice. It's work fine. But... is it possible to replace my custom callback interface by Kotlin's coroutine. I don't want to create custom interface only for execute method recoverPasswordConfirmCode
You can convert recoverPasswordConfirmCode() to a suspend function and return the result in the form of a sealed class to indicate if it's an error or the valid response. Something like this:
// Generic response class
sealed class Response<out T>{
data class Error(val ex: Throwable) : Response<Nothing>()
data class Data<T>(val data: T) : Response<T>()
}
// in your TransportService class
suspend fun recoverPasswordConfirmCode(confirmCode, ex): Response<RecoverPasswordResponse>{
// Do your stuff here
// return Response.Data<RecoverPasswordResponse>(/* your data object here */)
}
Then call it like this and check the response type:
val result = TransportService.recoverPasswordConfirmCode(confirmCode, ex)
when(result){
is Response.Error -> // Do something
is Response.Data -> // Do something
}
Note that you will have to call the suspend function inside a coroutine context.
You don't need to create a custom interface. Consume your API like this:
suspend fun recoverPasswordConfirmCode(confirmCode: String): YourReturnType = suspendCancellableCoroutine { cont ->
try {
val result = //Do your blocking API calls here
if(result.code == confirmCode) //Check confirm code is correct
cont.resume(YourResult) //Return your result here
else
cont.resumeWithException(YourException) //Throw an exception otherwise
} catch (e: Exception) {
cont.resumeWithException(e)
}
}
Call recoverPasswordConfirmCode method inside a Coroutine Scope.
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()
}