I'm trying to use Retrofit to display in a recyclerview a list of data fetched from my backend. The data (scenes) come in form of a simple json. I tried do add them locally and it works just fine. But, when I try to fetch them and update the recyclervies, it crashes. My code is the following:
package com.example.arview
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.arview.databinding.ActivitySceneSelectBinding
import com.google.firebase.auth.FirebaseUser
import com.google.firebase.auth.ktx.auth
import com.google.firebase.ktx.Firebase
import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import okhttp3.OkHttpClient
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
class SceneSelect : AppCompatActivity() {
private lateinit var binding:ActivitySceneSelectBinding
private lateinit var adapter:SceneRecyclerViewAdapter
private var listaEscenas:ArrayList<SceneParameters> = ArrayList<SceneParameters>()
private var auth = Firebase.auth
private var token: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySceneSelectBinding.inflate(layoutInflater)
setContentView(binding.root)
val user = Firebase.auth.currentUser
findViewById<TextView>(R.id.welcomeUser).text = user?.email
getScenesFromUser(user!!)
findViewById<Button>(R.id.logoutButton).setOnClickListener {
Firebase.auth.signOut()
val myIntent = Intent(this, MainActivity::class.java)
startActivity(myIntent)
}
var recyclerview = findViewById<RecyclerView>(R.id.recyclerViewEscenas)
adapter = SceneRecyclerViewAdapter(this, listaEscenas)
recyclerview.layoutManager = LinearLayoutManager(this)
recyclerview.adapter = adapter
// Local json for testing purposes
var gson = Gson()
var jsonData = applicationContext.resources.openRawResource(
applicationContext.resources.getIdentifier(
"escena_test",
"raw", applicationContext.packageName
)
).bufferedReader().use{it.readText()}
var parameters = gson.fromJson(jsonData, SceneParameters::class.java )
listaEscenas.add(parameters)
}
private suspend fun getRetrofit(token: String): Retrofit {
return Retrofit.Builder()
.baseUrl("https://tfg-backend-gu2x.onrender.com/get/escenas/")
.addConverterFactory(GsonConverterFactory.create())
.client(getClient(token))
.build()
}
private suspend fun getClient(token: String): OkHttpClient =
OkHttpClient.Builder()
.addInterceptor(HeaderInterceptor(token))
.build()
private fun getScenesFromUser(user: FirebaseUser) {
auth.currentUser?.getIdToken(true)?.addOnSuccessListener {
CoroutineScope(Dispatchers.IO).launch {
val call = getRetrofit(it.token!!).create(ApiService::class.java).getScenesFromUser(user.uid).execute()
val sceneList = call.body() as List<SceneParameters>
for (scene in sceneList){
listaEscenas.add(scene)
}
adapter.notifyDataSetChanged()
}
}
}
}
The Logcat doesn't display any particular error when it crashes
2023-06-26 02:58:55.982 20639-20683 FA com.example.arview D Connected to remote service
2023-06-26 02:58:55.982 20639-20683 FA com.example.arview V Processing queued up service tasks: 8
2023-06-26 02:58:56.238 20639-20702 FirebaseAuth com.example.arview D Notifying id token listeners about user ( CADi0ELu6DczWNFyurmhqldijMZ2 ).
2023-06-26 02:58:56.266 20639-20709 .example.arvie com.example.arview W Accessing hidden method Ljava/lang/invoke/MethodHandles$Lookup;-><init>(Ljava/lang/Class;I)V (greylist, reflection, allowed)
2023-06-26 02:58:56.338 20639-20709 get com.example.arview D inside inerceptor
2023-06-26 02:58:56.675 20639-20683 FA com.example.arview V Screen exposed for less than 1000 ms. Event not sent. time: 844
2023-06-26 02:58:56.684 20639-20683 FA com.example.arview V Activity paused, time: 3123679469
2023-06-26 02:58:56.685 20639-20683 FA com.example.arview V Activity resumed, time: 3123679475
2023-06-26 02:59:00.873 20639-20732 ProfileInstaller com.example.arview D Installing profile for com.example.arview
2023-06-26 02:59:01.771 20639-20683 FA com.example.arview V Inactivity, disconnecting from the service
2023-06-26 02:59:03.303 20639-20672 .example.arvie com.example.arview I ProcessProfilingInfo new_methods=4726 is saved saved_to_disk=1 resolve_classes_delay=8000
---------------------------- PROCESS ENDED (20639) for package com.example.arview ----------------------------
I'm 100% sure that the fetched data is correct. I printed the content when completed and the objects where the same, as you can see here:
[SceneParameters( <-- the one added locally
uid=,
coordinates=[37.19684078, -3.62350248, 680.0],
image_url=images/kitten.jpg,
loop=true,
model_url=models/objeto(9).glb,
animations=[Death],
name=Escena del gatito,
audio=,
scene_type=null),
SceneParameters(
uid=CADi0ELu6DczWNFyurmhqldijMZ2,
coordinates=[],
image_url=images/FSg1JYnv6AIipgLDcJqe.jpg,
loop=true,
model_url=models/FSg1JYnv6AIipgLDcJqe.glb,
animations=[animation_0],
name=Caja,
audio=,
scene_type=augmented_images),
SceneParameters(
uid=CADi0ELu6DczWNFyurmhqldijMZ2,
coordinates=[],
image_url=images/kJyjMzzzY3asX8HC9ffK.jpg,
loop=true,
model_url=models/kJyjMzzzY3asX8HC9ffK.glb,
animations=[],
name=Robot,
audio=,
scene_type=augmented_images)]
As I said I tryed adding local jsons in the onCreate and it works fine, also called notifyDataSetChanged() from here and doesnt crash. I tryed delaying the recycler's view set up to AFTER the data is fetched and it crashes. I tryed adding local data in the coroutine scope and it crashes only when executing notifyDataSetChanged(). It seems that the problem is when interacting with the recycler view from that scope, because if I dont call notifyDataSetChanged(), even if my ArrayList has all the new data (that doens't show on screen) it doesn't crashes until I call it.
I don´t see any other way to invoke the function after I recieve the data. I'm relatively new to Kotlin and I can't find any other way to do this
Change ApiService.getScenesFromUser()
to be defined as a suspend
function and remove Call<
and >
from the return type. This allows you to call it in a coroutine without having to call enqueue()
or execute()
on it, and without having to worry about which dispatcher you call it with, since it is suspending (not blocking).
Use lifecycleScope.launch
instead of creating a new throwaway CoroutineScope to launch from. This is important for avoiding memory leaks.
Don’t use Dispatchers.IO
to launch your coroutine. This is what is causing your crash. You cannot touch UI unless you are on the main thread. Since lifecycleScope
uses Dispatchers.Main
by default, it is safe to use for UI if you launch it without changing the dispatcher. Since we changed your blocking call to a suspending one in step 1 above, Dispatchers.Main
is fine to use for your whole coroutine.