Volley StringRequest handled by RequestQueue crashes without error - android

This is the code that is crashing:
val url = "https://chotawhatsapp.firebaseio.com/users.json"
val res = object : Response.Listener<String> {
//Toast.makeText(this#Main2Activity,"show",Toast.LENGTH_LONG).show()
#Override
override fun onResponse(s: String) {
Toast.makeText(this#Main2Activity,"show",Toast.LENGTH_LONG).show()
doOnSuccess(s)
//the function OnResponse is shown never used in the program while toast is properly executable
}
}
val eros = object :Response.ErrorListener {
#Override
override fun onErrorResponse(volleyError: VolleyError) {
Toast.makeText(this#Main2Activity,"volley error - $volleyError",Toast.LENGTH_LONG).show()
}
}
// request = StringRequest(Request.Method.GET,url,)
val request = StringRequest(Request.Method.GET, url, res,eros)
val rQueue = Volley.newRequestQueue(this#Main2Activity)
rQueue.add(request)
There is nothing in logcat for the crash.
The toast messages in the code are not shown.

This code is working fine for me. Only minor differences:
fun f() {
val url = "https://www.google.co.uk/"
val res = object :Response.Listener<String>{
override fun onResponse(response: String?) {
Toast.makeText(this#MainActivity,"no err: $response", Toast.LENGTH_LONG).show()
}
}
val eros = object :Response.ErrorListener {
#Override
override fun onErrorResponse(volleyError: VolleyError){
Toast.makeText(this#MainActivity,"volley error - $volleyError", Toast.LENGTH_LONG).show()
}
}
// request = StringRequest(Request.Method.GET,url,)
val request = StringRequest(Request.Method.GET, url, res,eros)
val rQueue = Volley.newRequestQueue(this#MainActivity)
rQueue.add(request)
}
Gradle:
implementation 'com.android.volley:volley:1.1.1'
Manifest:
<uses-permission android:name="android.permission.INTERNET" />
Project creation procedure:
File -> New Project -> [Tick box: Include Kotlin Support]
I mention this procedure because I've often been less than lucky when converting existing Java apps to Kotlin.
I could not remember if Volley posted its results back to the main thread or a worker thread. But both onResponse and onErrorResponse are called on the main thread so Toast.makeText is safe.

Related

return value from volley response

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
}
}
})
}
}
}

How to store the return value of suspended function to a variable?

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.

How can I save JsonObjectRequest elements in variables using Kotlin?

I'm trying to take data from a mySQL database and my code take it correctly.
The problem is that I have the information in a JsonObjectRequest and out of it, I can't use it. My idea was to use variables to save some of the information I need.
Something like this:
val queue=Volley.newRequestQueue(this)
val jsonObjectRequest = JsonObjectRequest(
Request.Method.GET,url,null,
{ response ->
emailGet = response.getString("email")
usernameGet = response.getString("name")
}, { error ->
Toast.makeText(this, error.toString(), Toast.LENGTH_LONG).show()
}
)
queue.add(jsonObjectRequest)
As I said the problem here is that emailGet and usernameGet (variables declared before this code bloc) store the values only inside the JsonObjectRequest, out of it the variables are empty.
Example:
val queue=Volley.newRequestQueue(this)
val jsonObjectRequest = JsonObjectRequest(
Request.Method.GET,url,null,
{ response ->
emailGet = response.getString("email")
usernameGet = response.getString("name")
Toast.makeText(this, emailGet, Toast.LENGTH_LONG).show()
}, { error ->
Toast.makeText(this, error.toString(), Toast.LENGTH_LONG).show()
}
)
queue.add(jsonObjectRequest)
Toast.makeText(this, usernameGet, Toast.LENGTH_LONG).show()
Here the Toast will print on the screen only the emailGet content because it's inside the JsonObjectRequest, the Toast that have to print the usernameGet value will not do it.
Looking for information I have found that this problem could be because this function is asynchronous and I found a possible solution in Java that I tried to translate to Kotlin.
val queue=Volley.newRequestQueue(this)
val future : RequestFuture<JSONObject> = RequestFuture.newFuture()
val request = JsonObjectRequest(
Request.Method.GET, url, null, future, future)
queue.add(request)
try{
var response = future.get()
} catch (e : InterruptedException){
} catch (e : ExecutionException){
}
I do not really understand this second code, but it still doesn't working, the response variable is always empty and the program stays in an infinite loop inside that try.
If you want to use the emailGet and usernameGet variables, you should do it within the callback:
val queue = Volley.newRequestQueue(this)
val jsonObjectRequest = JsonObjectRequest(
Request.Method.GET, url, null,
{ response ->
emailGet = response.getString("email")
usernameGet = response.getString("name")
// TODO do something with the variables here
}, { error ->
Toast.makeText(this, error.toString(), Toast.LENGTH_LONG).show()
}
)
queue.add(jsonObjectRequest)
If instead, you have a method method doSomething() that runs immediately after the response is received:
fun doSomething(email:String, username:String){
// TODO do something with the variables here
}
You can replace the TODO comment in the first code snippet with doSomething(emailGet, usernameGet).
The 4th and 5th parameters for JsonObjectRequest are in fact objects of types Response.Listener and Response.ErrorListener. These two listeners are Single Abstract Method interfaces. If expanded it would look like this:
val jsonObjectRequest = JsonObjectRequest(
Request.Method.GET, url, null,
object : Response.Listener {
override fun onResponse(response: JSONObject) {
emailGet = response.getString("email")
usernameGet = response.getString("name")
doSomething(emailGet, usernameGet)
}
},
object : Response.ErrorListener {
override fun onErrorResponse(error: VolleyError) {
Toast.makeText(this, error.toString(), Toast.LENGTH_LONG).show()
}
}
)
The lambda syntax you were using was a short hand of SAM interfaces.
The simplest way is using #ttyip's answer but you could also use live data and observing it! You're calling an asynchronous method and there's going to be some delay(network API calling and this delay depends on user's internet connection etc) So First you'll need to add jetPack's lifeCycle components inside your project:
dependencies {
def lifecycle_version = "2.4.0-alpha02"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// Annotation processor
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
// alternately - if using Java8, use the following instead of lifecycle-compiler
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
}
After syncing your project, define global inside your activity/fragment:
val email: MutableLiveData<String> = MutableLiveData<String>()
val username: MutableLiveData<String> = MutableLiveData<String>()
And inside your response:
val jsonObjectRequest = JsonObjectRequest(
Request.Method.GET,url,null,
{ response ->
val emailGet = response.getString("email")
val usernameGet = response.getString("name")
email.postValue(emailGet)
username.postValue(usernameGet)
}, { error ->
Toast.makeText(this, error.toString(), Toast.LENGTH_LONG).show()
}
)
And somewhere inside your activity just observe your livedata:
email.observe(this, Observer { string ->
// Do some work
})
username.observe(this, Observer { string ->
// Do some work
})

Passing android.content.Context to a suspended function causes android.os.NetworkOnMainThreadException

I have a suspended function that makes a network call to get some data and returns it. I am using Volley.
class PokeClient {
suspend fun getPokemonData(pokemonName: String, context: Context) = suspendCoroutine<Pokemon> { cont ->
val queue = Volley.newRequestQueue(context)
val request = url.plus(pokemonName)
// some skipped code here
}
I am calling it in my activity like this:
GlobalScope.launch {
val pokeResult = pokeClient.getPokemonData(nameToSearch, applicationContext)
pokemonList.add(pokeResult)
pokemonAdapter.notifyItemInserted(pokemonList.size - 1)
}
And I get an error like: android.os.NetworkOnMainThreadException
I suspect it may be because I am passing the context from the MainActivity and that is not correct. What is the alternative here?
Thanks,
Android Newbie
UPDATE:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val pokeClient = PokeClient()
var pokemonList = mutableListOf<Pokemon>()
val pokemonAdapter = PokemonRvAdapter(pokemonList)
val pokemonRv = findViewById<RecyclerView>(R.id.rv_pokemon)
pokemonRv.adapter = pokemonAdapter
pokemonRv.layoutManager = LinearLayoutManager(this)
val searchButton = findViewById<Button>(R.id.search_btn)
searchButton.setOnClickListener {
val toast = Toast.makeText(applicationContext, "Loading poke data", Toast.LENGTH_SHORT)
toast.show()
val nameToSearch = findViewById<EditText>(R.id.search_pokemon_txt).text.toString().lowercase()
GlobalScope.launch(Dispatchers.IO) {
val pokeResult = pokeClient.getPokemonData(nameToSearch, applicationContext)
pokemonList.add(pokeResult)
pokemonAdapter.notifyItemInserted(pokemonList.size - 1)
}
}
}
}
PokeClient:
const val url = "https://pokeapi.co/api/v2/pokemon/"
class PokeClient {
suspend fun getPokemonData(pokemonName: String, context: Context) = suspendCoroutine<Pokemon> { cont ->
val queue = Volley.newRequestQueue(context)
val request = url.plus(pokemonName)
val stringRequest = StringRequest(Request.Method.GET, request, Response.Listener<String> { response ->
val jObj = JSONObject(response)
val imgUrl = jObj
.getJSONObject("sprites")
.getJSONObject("other")
.getJSONObject("official-artwork")
.getString("front_default")
val inputStream = URL(imgUrl).openStream()
/* call continuation.resume and pass your object */
cont.resume(Pokemon(name = jObj.getString("name"), image = BitmapFactory.decodeStream(inputStream)))
}, Response.ErrorListener { err ->
cont.resumeWithException(err)
})
queue.add(stringRequest)
}
}
as per CommonsWare comment it seems Dispatchers.IO is what you are missing here. To be precise, it should look like this:
GlobalScope.launch(Dispatchers.IO) {
val pokeResult = pokeClient.getPokemonData(nameToSearch, applicationContext)
pokemonList.add(pokeResult)
pokemonAdapter.notifyItemInserted(pokemonList.size - 1)
}
or
GlobalScope.launch {
withContext(Dispatchers.IO) {
val pokeResult = pokeClient.getPokemonData(nameToSearch, applicationContext)
pokemonList.add(pokeResult)
pokemonAdapter.notifyItemInserted(pokemonList.size - 1)
}
}
Through I am completely null about kotlin but as far as I am concerned this error happens when you perform internet related action (Ex, Load Data from a site) on main thread or you haven't declared internet permission in manifest file. Instead you should create new thread and perform the action there. If you don't mind adding java code here is a snippet
class getData implements Runnable{
#Override
public void run() {
//Do work over here such as loading date from url
runOnUiThread(new Runnable() {
#Override
public void run() {
//To update ui put code inside here
}
});
}
}
And in the main method use this:
Thread thread = new Thread(new getData());//use class name
thread.start();//To start execution

Kotlin Volley - App crashes with error while accessing volley from another class

I have only two files in project LoginActivity.kt & ApiActivity.kt. The Loin Activity will call the function called as get() which is inside the ApiActivity, the get() function will just do a simple GET call and returns the value.
This is what i have in LoginActivity.kt
val api = com.sa.sa.ApiActivity();
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
this.getToken();
}
private fun getToken() {
var response = this.api.get ();
Toast.makeText(this#LoginActivity, response, Toast.LENGTH_SHORT).show();
}
And this is what i have in ApiActivity.kt
fun get () : String {
var result : String = "Test";
val queue = Volley.newRequestQueue(this);
var url = getString(R.string.api_url)+getString(R.string.api_getToken);
val stringRequest = StringRequest(Request.Method.GET, url,
Response.Listener<String> { response ->
result = response;
},
Response.ErrorListener {
result = "Error";
})
queue.add(stringRequest);
return result;
}
The IDE doesn't show any error, but when i try to run the application it shows the following error and crashes the App
Attempt to invoke virtual method 'java.io.File android.content.Context.getCacheDir()' on a null object reference
Note : I don't have any layout for ApiActivity.kt and i don't have onCreate too. I have posted all the codes that i have.
What is the reason for App Crash or What i am doing wrong.
You shouldn't make ApiActivity an activity at all.
class LoginActivity {
fun getData(context : Context) : String {
var result : String = "Test";
val queue = Volley.newRequestQueue(context);
var url = context.getString(R.string.api_url) + context.getString(R.string.api_getToken);
val stringRequest = StringRequest(Request.Method.GET, url,
Response.Listener<String> { response ->
result = response;
},
Response.ErrorListener {
result = "Error";
})
queue.add(stringRequest);
return result;
}
}
In LoginActivity call it as
var response = this.api.getData(this);
Toast.makeText(this#LoginActivity, response, Toast.LENGTH_SHORT).show();
Also isn't StringRequest method asynchronous ? In that case you'll always get "Test" back.

Categories

Resources