I'm trying to learn to code android apps with kotlin. I want to display some data that i get from the api(https://api.kuroganehammer.com/api/characters). I generated the data classes using the JSON to kotlin plugin in IntelliJ.
Logcat shows that i'm getting the Data however it doesn't trigger the onResponse method.
I tried debugging but can't really find anything other than for some reason the onFailure and onResponse methods are ignored.
ApiRequest:
fun fetchAllCharacters(): Call<CharacterApiRequest>
CharacterApiRequest:
#SerializedName("character")
val characters: List<Character>
)
Characters(Fragment, gets called in onViewCreated()):
.enqueue(object : Callback<CharacterApiRequest> {
override fun onFailure(call: Call<CharacterApiRequest>, t: Throwable) {
//Display an error to the user, because there was a io exception
}
override fun onResponse(call: Call<CharacterApiRequest>, response: Response<CharacterApiRequest>) {
//We got a response
if (response.isSuccessful) {
//Bind the data only when we have it
response.body()?.apply {
adapter.setData(this.characters as MutableList<Character>)
}
} else {
//Display an error
}
}
})
Changing the callback's generic parameter from CharacterApiRequest to List worked, as stated in the comment from #MateuszHerych
Related
I've spent hours trying to figure this thing out, and I still can figure it out.
I'm trying to retrieve data from a website using JSON.
If the website is live and everything, it works, but if the website returns something else than the data, like a 403 error, or any other error, then it crashes. I tried to debug it, but I still don't understand what is going on here.
Here is my code:
I have a NetworkModule with an interceptor that is supposed to check is the response is valid or not, and from what I can tell it works, because my variable isDataRetrievable is false (the value by default):
val networkModule = module {
single {
val customGson =
GsonBuilder().registerTypeAdapter(Lesson::class.java, LessonDeserializer())
.create()
Retrofit.Builder()
.client(get())
.addConverterFactory(
GsonConverterFactory.create(customGson)
)
.baseUrl(BuildConfig.URL)
.build()
}
factory {
OkHttpClient.Builder()
.addInterceptor(Interceptor { chain ->
chain.withConnectTimeout(1,TimeUnit.SECONDS)
val request: Request = chain.request()
val response = chain.proceed(request)
if (response.isSuccessful){
networkStatus.isDataRetrievable = true
}
response
}).build()
}
factory {
get<Retrofit>().create(LessonApi::class.java)
}
}
Next, I have my API to get the data:
interface LessonApi {
#GET("/JSON/json_get_data.php")
suspend fun getLessons(): Call<Lesson>
}
Then, for some reason, I have a repository (I'm not the only one working on this code, I didn't do this part):
class LessonRepository(private val service: LessonApi) {
suspend fun getLessons() = service.getLessons()
}
Then, I have my splash screen view model, that is supposed to retrieve the data if possible:
if (networkStatus.isNetworkConnected && networkStatus.isWebsiteReachable) {
var tmp = repository.getLessons()
tmp.enqueue(object : Callback<Lesson> {
override fun onFailure(call: Call<Lesson>, t: Throwable) {
Log.d("DataFailure",t.message.toString())
nextScreenLiveData.postValue(false)
}
override fun onResponse(call: Call<Lesson>, response: Response<Lesson>) {
Log.d("DataFailure","Test")
}
})
}else{
nextScreenLiveData.postValue(false)
}
The problem is that when the program get to the line repository.getLessons(), it crashes with the error:
retrofit2.HttpException: HTTP 403
at retrofit2.KotlinExtensions$await$2$2.onResponse(KotlinExtensions.kt:49)
at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:129)
at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:519)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:919)
So onFailure or onResponse are never called. I tried to run the debugger, to step in, but I cannot figure it out when it fails.
I thought it was because it was trying to deserialize invalid data, but I put breakpoints everywhere in my deserializer, and it never hits the breakpoints.
I'm not a professional android developer, but I'm very confused here.
What I'd like to do is that if the request is unsuccessful, just discard the response (do not deserialize it), and display a message or exit.
Please help, it's so frustrating. I'm not sure how to intercept errors or what to do if Interceptors get an unsuccessful request (for now I just set a variable but it's unused).
Thanks.
Edit: What I'm trying to do, is to retrieve data from a webserver. If it cannot (for any reason), I don't want the gson to parse data (because it will probably be garbage and will not correspond to my deserializer). However, I feel like this okhttp / retrofit is a pipeline, where okhttp get the response from the webserver and pass it to a gson converter. What I want to do is intercept this response, and if it's not successful, to NOT pass it to gson, set a variable, so that the rest of my application knows what to do. But the thing is, for now, it just crash even before it gets to the callback in enqueue. The interceptor works just fine, except I'd like him to drop the response if it's not successful. Is it possible?
I tried something like that, and it worked to handle bad codes (>400), but I also wanted to handle malformed JSON data, so I added the onResponse and onFailure callbacks, but it never worked, because when I receive a malformed JSON, it also trigger an exception, and then go in the catch before it can go on the 'enqueue', so I'm not sure what this is used for.
try {
val lessons = repository.getLessons().enqueue(object : Callback<List<Lesson>> {
override fun onResponse(call: Call<List<Lesson>>, response: Response<List<Lesson>>) {
networkStatus.isDataRetrievable = response.isSuccessful
Log.d("Retrofit", "Successful response")
nextScreenLiveData.postValue(response.isSuccessful)
}
override fun onFailure(call: Call<List<Lesson>>, t: Throwable) {
Log.d("Retrofit", "Failure response")
nextScreenLiveData.postValue(false)
}
})
nextScreenLiveData.postValue(true)
} catch (e: Exception) {
nextScreenLiveData.postValue(false)
}
Anyway, just this code works for everything in the end:
try {
val lessons = repository.getLessons().filter {
it.lesson.contains("video")
}.filter {
DataUtils.isANumber(it.id)
}
lessonDao.insertLessons(lessons)
networkStatus.isDataRetrievable = true
} catch (e: Exception) {
networkStatus.isDataRetrievable = false
}
But in my API, I don't return callbacks, I directly return the objects, as such:
#GET("/JSON/json_get_dat.php")
suspend fun getLessons(): List<Lesson>
I don't know if this is the right way to do it, but it works. I hope this might help others.
I'm trying to use the Android MVVM pattern with a repository class and Retrofit for network calls. I have the common problem that I can't get the coroutine to wait for the network response to return.
This method is in my ViewModel class:
private fun loadConfigModel() {
val model = runBlocking {
withContext(Dispatchers.IO) {
configModelRepository.getConfigFile()
}
}
configModel.value = model
}
In ConfigModelRepository, I have this:
suspend fun getConfigFile(): ConfigModel {
val configString = prefs.getString(
ConfigViewModel.CONFIG_SHARED_PREF_KEY, "") ?: ""
return if (configString.isEmpty() || isCacheExpired()) {
runBlocking { fetchConfig() }
} else {
postFromLocalCache(configString)
}
}
private suspend fun fetchConfig(): ConfigModel {
return suspendCoroutine { cont ->
dataService
.config() // <-- LAST LINE CALLED
.enqueue(object : Callback<ConfigModel> {
override fun onResponse(call: Call<ConfigModel>, response: Response<ConfigModel>) {
if (response.isSuccessful) {
response.body()?.let {
saveConfigResponseInSharedPreferences(it)
cont.resume(it)
}
} else {
cont.resume(ConfigModel(listOf(), listOf()))
}
}
override fun onFailure(call: Call<ConfigModel>, t: Throwable) {
Timber.e(t, "config fetch failed")
cont.resume(ConfigModel(listOf(), listOf()))
}
})
}
}
My code runs as far as dataService.config(). It never enters onResponse or onFailure. The network call goes and and returns properly (I can see this using Charles), but the coroutine doesn't seem to be listening for the callback.
So, my question is the usual one. How can I get the coroutines to block such that they wait for this callback from Retrofit? Thanks.
The problem must be that response.body() returns null since that is the only case that is missing a call to cont.resume(). Make sure to call cont.resume() also in that case and your code should at least not get stuck.
But like CommonsWare points out, even better would be to upgrade to Retrofit 2.6.0 or later and use native suspend support instead of rolling your own suspendCoroutine logic.
You should also stop using runBlocking completely. In the first case, launch(Dispatchers.Main) a coroutine instead and move configModel.value = model inside of it. In the second case you can just remove runBlocking and call fetchConfig() directly.
Trying to get a deeper into coroutines. I have a suspendCancellableCoroutine that is supposed to fetch a network response. I can see in Charles that the network call is dispatched and returns successfully. However, my app just hangs on the network request line.
private suspend fun fetchVisualElementsFromServer(clubId: String): VisualElements {
return suspendCancellableCoroutine { cont ->
visualElementsService.fetchVisualElementsForClub(clubId)
.enqueue(object : Callback<ResultVisualElements> {
override fun onResponse(
call: Call<ResultVisualElements>,
response: Response<ResultVisualElements>
) {
if (response.isSuccessful) {
response.body()?.let {
if (it.result == RESULT_SUCCESS) {
saveVisualElementsResponseInSharedPreferences(it.visual_elements)
cont.resume (it.visual_elements)
} else {
cont.cancel() //edit
}
} ?: cont.cancel() //edit
} else {
cont.cancel(IOException("${response.code()}: ${response.errorBody()}"))
}
}
override fun onFailure(call: Call<ResultVisualElements>, t: Throwable) {
Timber.e(t, "visual elements fetch failed")
cont.cancel() // edit
}
})
}
}
This where it hangs:
VisualElementsService.kt
fun fetchVisualElementsForClub(clubId: String): Call<ResultVisualElements> {
return dataFetcherService.getVisualElementsForClub(clubId)
}
What am I missing here? I tried to make the fetchVisualElementsForClub() a suspend function, but that just makes the suspendCancellableCoroutine throw a Suspension functions can only be called within coroutine body error. But I thought that his was within a coroutine body?
Any help appreciated. Thanks.
EDIT
I response to Rene's answer below, I want to add a few things.
You are right, I am missing three cont.cancel() calls. I've modified the OP. Good points.
I have breakpoints all over the suspendCancellableCoroutine such that any possible scenario (success, failure, etc.) will be hit. But that callback never registers.
Wondering if there is something missing in fetchVisualElementsForClub() that is needed to pass the callback up to the suspendCancellableCoroutine. That seems to be where this is hanging.
You must call cont.resume() or cont.cancel() on every branch in your callback handling.
But in your example at least three cases are missing.
If the response is successful but no body is provided, you call nothing.
If the response is successful, the body is not null, but the it.result is not RESULT_SUCCESS you call nothing.
If something goes wrong in onFailure, you call nothing.
As long as neither resume or cancel is invoked, the coroutine will stay suspended, means hangs.
when you use suspend keyword your are telling that function shoud be called inside a coroutine bode, for example:
suspend fun abc(){
return
}
when you want to call above function you have to call it inside coroutines such as below:
GlobalScope.launch {
abc()
}
I'm trying to releaze function for authorization my android app. I know there are many similar questions, but I'm interesting exectly to get function witch return "success" or "failure".
override fun authorization(login: String, password: String): String {
var result: String = "none"
val call: Call<UserAuthorizationDataModelRetrofit> =
dataBase.getWordApiRetrofit().userAuthorization(login, password)
call.enqueue(object : Callback<UserAuthorizationDataModelRetrofit> {
override fun onResponse(
call: Call<UserAuthorizationDataModelRetrofit>?,
response: Response<UserAuthorizationDataModelRetrofit>?
) {
val dataModel = response?.body()
result = dataModel?.result ?: "failure"
}
override fun onFailure(call: Call<UserAuthorizationDataModelRetrofit>?, t: Throwable?) {
result = "failure"
}
})
return result
}
I'm expecting function return "success" or "failure" acording to response of http request. But http request is asynchronous and every time I get "none". Can anybody help me?
The function .enqueue is asynchronous. You will always get a "none" because enqueue is always not finished when authorization returns.
You can do two things.
One is use enqueue and do all your work inside onResponse, open new Activity for the user, set your View or show a Toast, etc.
Another is use .execute. But you will have to call authorization in another Thread and do your work in there. Note that this is asynchronous and you should not do UI stuff here.
i got solution of this question: Wildcards generic in Kotlin for parameter, but now i have other question which still related to kotlin generic
I have an abstract class using for listen api callback like below. ApiRs is a parent object that every API response object inherit from it
abstract class ApiCallback<in T : ApiRs> {
open fun onSucceed(apiRsModel: T) {}
open fun onFailed(code: Int,
message: String) {
}
}
this time i write a function to handle api succeed with Retrofit2, than check something and callback to UI, here is my function:
fun <T : ApiRs> callbackWithSucceed(apiCallback: ApiCallback<T>?,
context: Context,
response: Response<out ApiRs>?) {
// unexpected error
if (response == null) {
encounterUnexpectedError(apiCallback, context, null)
return
}
// check http
val httpSucceed = response.code() == CODE_HTTP_SUCCEED
&& response.isSuccessful
&& response.body() != null
if (!httpSucceed) {
// HTTP response with error
callbackWithFailed(
apiCallback,
response.code(),
response.message())
return
}
apiCallback?.onSucceed(response.body()!!)
}
}
response is Retrofit2 class, which contains my API response model (body) and every response model are inherit ApiRs, my goal is pass the model to the abstract class using this way apiCallback?.onSucceed(response.body()!!) but it will show an error
Type mismatch, required T? but Found ApiRs?
Sorry about i have bad concept of generic. I think function open fun onSucceed(apiRsModel: T) {} T should inherit ApiRs because i defined in class, so i cannot understand why error message shown?
Your response: Response<out ApiRs>? has to be response: Response<out T>?, then the error should be gone.
The type of T which will be passed to the method onSuccess must match the generic type of the apiCallback parameter, which isn't <ApiRs> but <T>.