Android Studio app crashes when calling Recyclerview's notifyDataSetChanged() inside CoroutineScope in Kotlin

huangapple go评论59阅读模式
英文:

Android Studio app crashes when calling Recyclerview's notifyDataSetChanged() inside CoroutineScope in Kotlin

问题

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;-&gt;&lt;init&gt;(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(  &lt;-- 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

<details>
<summary>英文:</summary>

I&#39;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&lt;SceneParameters&gt; = ArrayList&lt;SceneParameters&gt;()
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&lt;TextView&gt;(R.id.welcomeUser).text = user?.email
getScenesFromUser(user!!)
findViewById&lt;Button&gt;(R.id.logoutButton).setOnClickListener {
Firebase.auth.signOut()
val myIntent = Intent(this, MainActivity::class.java)
startActivity(myIntent)
}
var recyclerview = findViewById&lt;RecyclerView&gt;(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(
&quot;escena_test&quot;,
&quot;raw&quot;, 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(&quot;https://tfg-backend-gu2x.onrender.com/get/escenas/&quot;)
.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&lt;SceneParameters&gt;
for (scene in sceneList){
listaEscenas.add(scene)
}
adapter.notifyDataSetChanged()
}
}
}

}


The Logcat doesn&#39;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&#39;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&#39;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&#39;t show on screen) it doesn&#39;t crashes until I call it.
I don&#180;t see any other way to invoke the function after I recieve the data. I&#39;m relatively new to Kotlin and I can&#39;t find any other way to do this
</details>
# 答案1
**得分**: 0
1. 将 `ApiService.getScenesFromUser()` 更改为定义为 `suspend` 函数,并从返回类型中删除 `Call&lt;` 和 `&gt;`。这允许您在协程中调用它,无需调用 `enqueue()` 或 `execute()`,也无需担心使用哪个调度程序,因为它是挂起的(不会阻塞)。
2. 使用 `lifecycleScope.launch` 而不是创建一个新的一次性 CoroutineScope 进行启动。这对于避免内存泄漏非常重要。
3. 不要使用 `Dispatchers.IO` 来启动您的协程。这是导致崩溃的原因。除非在主线程上,否则不能触摸UI。由于 `lifecycleScope` 默认使用 `Dispatchers.Main`,如果您在不更改调度程序的情况下启动它,那么它是安全的。由于我们在上面的步骤1中将您的阻塞调用更改为挂起调用,因此对于整个协程使用 `Dispatchers.Main` 是安全的。
<details>
<summary>英文:</summary>
1. Change `ApiService.getScenesFromUser()` to be defined as a `suspend` function and remove `Call&lt;` and `&gt;` 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). 
2. Use `lifecycleScope.launch` instead of creating a new throwaway CoroutineScope to launch from. This is important for avoiding memory leaks. 
3. 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. 
</details>

huangapple
  • 本文由 发表于 2023年6月26日 09:00:25
  • 转载请务必保留本文链接:https://go.coder-hub.com/76552978.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定