Android Room – simple requête de sélection – Impossible d’accéder à la firebase database sur le thread principal

J’essaie un exemple avec Room Persistence Library . J’ai créé une entité:

@Entity public class Agent { @PrimaryKey public Ssortingng guid; public Ssortingng name; public Ssortingng email; public Ssortingng password; public Ssortingng phone; public Ssortingng licence; } 

Création d’une classe DAO:

 @Dao public interface AgentDao { @Query("SELECT COUNT(*) FROM Agent where email = :email OR phone = :phone OR licence = :licence") int agentsCount(Ssortingng email, Ssortingng phone, Ssortingng licence); @Insert void insertAgent(Agent agent); } 

Création de la classe Database:

 @Database(entities = {Agent.class}, version = 1) public abstract class AppDatabase extends RoomDatabase { public abstract AgentDao agentDao(); } 

Base de données exposée utilisant la sous-classe ci-dessous dans Kotlin:

 class MyApp : Application() { companion object DatabaseSetup { var database: AppDatabase? = null } override fun onCreate() { super.onCreate() MyApp.database = Room.databaseBuilder(this, AppDatabase::class.java, "MyDatabase").build() } } 

Mis en œuvre ci-dessous la fonction dans mon activité:

 void signUpAction(View view) { Ssortingng email = editTextEmail.getText().toSsortingng(); Ssortingng phone = editTextPhone.getText().toSsortingng(); Ssortingng license = editTextLicence.getText().toSsortingng(); AgentDao agentDao = MyApp.DatabaseSetup.getDatabase().agentDao(); //1: Check if agent already exists int agentsCount = agentDao.agentsCount(email, phone, license); if (agentsCount > 0) { //2: If it already exists then prompt user Toast.makeText(this, "Agent already exists!", Toast.LENGTH_LONG).show(); } else { Toast.makeText(this, "Agent does not exist! Hurray :)", Toast.LENGTH_LONG).show(); onBackPressed(); } } 

Malheureusement, lors de l’exécution de la méthode ci-dessus, il se bloque avec la trace de stack ci-dessous:

  FATAL EXCEPTION: main Process: com.example.me.MyApp, PID: 31592 java.lang.IllegalStateException: Could not execute method for android:onClick at android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:293) at android.view.View.performClick(View.java:5612) at android.view.View$PerformClick.run(View.java:22288) at android.os.Handler.handleCallback(Handler.java:751) at android.os.Handler.dispatchMessage(Handler.java:95) at android.os.Looper.loop(Looper.java:154) at android.app.ActivityThread.main(ActivityThread.java:6123) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:867) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:757) Caused by: java.lang.reflect.InvocationTargetException at java.lang.reflect.Method.invoke(Native Method) at android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:288) at android.view.View.performClick(View.java:5612) at android.view.View$PerformClick.run(View.java:22288) at android.os.Handler.handleCallback(Handler.java:751) at android.os.Handler.dispatchMessage(Handler.java:95) at android.os.Looper.loop(Looper.java:154) at android.app.ActivityThread.main(ActivityThread.java:6123) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:867) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:757) Caused by: java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long periods of time. at android.arch.persistence.room.RoomDatabase.assertNotMainThread(RoomDatabase.java:137) at android.arch.persistence.room.RoomDatabase.query(RoomDatabase.java:165) at com.example.me.MyApp.RoomDb.Dao.AgentDao_Impl.agentsCount(AgentDao_Impl.java:94) at com.example.me.MyApp.View.SignUpActivity.signUpAction(SignUpActivity.java:58) at java.lang.reflect.Method.invoke(Native Method) at android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:288) at android.view.View.performClick(View.java:5612) at android.view.View$PerformClick.run(View.java:22288) at android.os.Handler.handleCallback(Handler.java:751) at android.os.Handler.dispatchMessage(Handler.java:95) at android.os.Looper.loop(Looper.java:154) at android.app.ActivityThread.main(ActivityThread.java:6123) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:867) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:757) 

Il semblerait que ce problème soit lié à l’exécution de l’opération db sur le thread principal. Toutefois, l’exemple de code de test fourni dans le lien ci-dessus ne s’exécute pas sur un thread distinct:

 @Test public void writeUserAndReadInList() throws Exception { User user = TestUtil.createUser(3); user.setName("george"); mUserDao.insert(user); List byName = mUserDao.findUsersByName("george"); assertThat(byName.get(0), equalTo(user)); } 

Est-ce que je manque quelque chose ici? Comment puis-je le faire exécuter sans crash? Veuillez suggérer.

L’access à la firebase database sur le thread principal bloquant l’interface utilisateur est l’erreur, comme l’a dit Dale.

Créez une classe nestede statique (pour éviter les memory leaks) dans votre activité d’extension de AsyncTask.

 private static class AgentAsyncTask extends AsyncTask { //Prevent leak private WeakReference weakActivity; private Ssortingng email; private Ssortingng phone; private Ssortingng license; public AgentAsyncTask(Activity activity, Ssortingng email, Ssortingng phone, Ssortingng license) { weakActivity = new WeakReference<>(activity); this.email = email; this.phone = phone; this.license = license; } @Override protected Integer doInBackground(Void... params) { AgentDao agentDao = MyApp.DatabaseSetup.getDatabase().agentDao(); return agentDao.agentsCount(email, phone, license); } @Override protected void onPostExecute(Integer agentsCount) { Activity activity = weakActivity.get(); if(activity == null) { return; } if (agentsCount > 0) { //2: If it already exists then prompt user Toast.makeText(activity, "Agent already exists!", Toast.LENGTH_LONG).show(); } else { Toast.makeText(activity, "Agent does not exist! Hurray :)", Toast.LENGTH_LONG).show(); activity.onBackPressed(); } } } 

Ou vous pouvez créer une classe finale sur son propre fichier.

Ensuite, exécutez-le dans la méthode signUpAction (View view):

 new AgentAsyncTask(this, email, phone, license).execute(); 

Dans certains cas, vous pouvez également vouloir conserver une référence à AgentAsyncTask dans votre activité afin de pouvoir l’annuler lorsque l’activité est détruite. Mais vous devez interrompre toutes les transactions vous-même.

En outre, votre question sur l’exemple de test de Google … Ils indiquent dans cette page Web:

L’approche recommandée pour tester l’implémentation de votre firebase database consiste à écrire un test JUnit qui s’exécute sur un périphérique Android. Étant donné que ces tests ne nécessitent pas la création d’une activité, ils doivent être plus rapides à exécuter que vos tests d’interface utilisateur.

Aucune activité, aucune interface utilisateur.

–MODIFIER–

Pour les gens qui se demandent … Vous avez d’autres options. Je recommande de consulter les nouveaux composants ViewModel et LiveData. LiveData fonctionne très bien avec Room. https://developer.android.com/topic/libraries/architecture/livedata.html

Une autre option est le RxJava / RxAndroid. Plus puissant mais plus complexe que LiveData. https://github.com/ReactiveX/RxJava

Ce n’est pas recommandé, mais vous pouvez accéder à la firebase database sur le thread principal avec allowMainThreadQueries()

 MyApp.database = Room.databaseBuilder(this, AppDatabase::class.java, "MyDatabase").allowMainThreadQueries().build() 

Pour tous les amateurs de RxJava ou de RxAndroid ou de RxKotlin

 Observable.just(db) .subscribeOn(Schedulers.io()) .subscribe { db -> // database operation } 

Code direct utilisant les Coroutines de Kotlin

AsyncTask est vraiment maladroit. Kotlin Coroutines est une alternative plus propre (essentiellement votre code synchrone avec quelques mots-clés supplémentaires).

gradle.properties:

 # Coroutines opt-in kotlin.coroutines=enable 

Fil de l’interface utilisateur (non bloquant):

 private fun myFun() { launch(UI) { val query = async(CommonPool) { // Async stuff MyApp.DatabaseSetup.database.agentDao().agentsCount(email, phone, license) } val agentsCount = query.await() // do UI stuff } } 

Le mot-clé suspend garantit que les méthodes asynchrones ne sont appelées qu’à partir de blocs asynchrones, cependant (comme indiqué par @Robin), cela ne fonctionne pas très bien avec les méthodes annotées Room.

 // Wrap API to use suspend (probably not worth it) public suspend fun agentsCount(...): Int = agentsCountPrivate(...) @Query("SELECT ...") protected abstract fun agentsCountPrivate(...): Int 

Vous ne pouvez pas l’exécuter sur le thread principal mais utiliser des gestionnaires, des asynchrones ou des threads de travail. Un exemple de code est disponible ici et lisez l’article sur la bibliothèque de pièce ici: Bibliothèque de pièces d’Android

 /** * Insert and get data using Database Async way */ AsyncTask.execute(new Runnable() { @Override public void run() { // Insert Data AppDatabase.getInstance(context).userDao().insert(new User(1,"James","Mathew")); // Get Data AppDatabase.getInstance(context).userDao().getAllUsers(); } }); 

Si vous voulez l’exécuter sur le thread principal, ce n’est pas préférable.

Vous pouvez utiliser cette méthode pour obtenir sur le thread principal Room.inMemoryDatabaseBuilder()

Avec la bibliothèque Jetbrains Anko, vous pouvez utiliser la méthode doAsync {..} pour exécuter automatiquement les appels de firebase database. Cela prend en compte le problème de verbosité que vous sembliez avoir avec la réponse de mcastro.

Exemple d’utilisation:

  doAsync { Application.database.myDAO().insertUser(user) } 

Je l’utilise fréquemment pour les insertions et les mises à jour, mais pour certaines requêtes, je recommande l’utilisation du stream de production RX.

Une solution élégante de RxJava / Kotlin consiste à utiliser Completable.fromCallable , qui vous donnera une Observable qui ne renvoie pas de valeur, mais qui peut être observée et abonnée à un autre thread.

 public Completable insert(Event event) { return Completable.fromCallable(new Callable() { @Override public Void call() throws Exception { return database.eventDao().insert(event) } } } 

Ou à Kotlin:

 fun insert(event: Event) : Completable = Completable.fromCallable { database.eventDao().insert(event) } 

Vous pouvez observer et souscrire comme vous le feriez habituellement:

 dataManager.insert(event) .subscribeOn(scheduler) .observeOn(AndroidSchedulers.mainThread()) .subscribe(...) 

Le message d’erreur,

Impossible d’accéder à la firebase database sur le thread principal car cela peut potentiellement verrouiller l’interface utilisateur pendant une longue période.

Est assez descriptif et précis. La question est de savoir comment éviter d’accéder à la firebase database sur le thread principal. C’est un sujet énorme, mais pour commencer, lisez AsyncTask (cliquez ici)

—–MODIFIER———-

Je vois que vous rencontrez des problèmes lorsque vous exécutez un test unitaire. Vous avez quelques choix pour résoudre ce problème:

  1. Exécutez le test directement sur la machine de développement plutôt que sur un périphérique Android (ou un émulateur). Cela fonctionne pour les tests centrés sur la firebase database et ne se soucie pas vraiment de savoir s’ils s’exécutent sur un périphérique.

  2. Utilisez l’annotation @RunWith(AndroidJUnit4.class) pour exécuter le test sur le périphérique Android, mais pas dans une activité avec une interface utilisateur. Plus de détails à ce sujet peuvent être trouvés dans ce tutoriel

Vous devez exécuter la demande en arrière-plan. Un moyen simple pourrait être d’utiliser un Executors :

 Executors.newSingleThreadExecutor().execute { yourDb.yourDao.yourRequest() //Replace this by your request } 

Si vous êtes plus à l’aise avec la tâche Async :

  new AsyncTask() { @Override protected Integer doInBackground(Void... voids) { return Room.databaseBuilder(getApplicationContext(), AppDatabase.class, DATABASE_NAME) .fallbackToDestructiveMigration() .build() .getRecordingDAO() .getAll() .size(); } @Override protected void onPostExecute(Integer integer) { super.onPostExecute(integer); Toast.makeText(HomeActivity.this, "Found " + integer, Toast.LENGTH_LONG).show(); } }.execute(); 

Pour des requêtes rapides, vous pouvez laisser de la place pour l’exécuter sur le thread d’interface utilisateur.

 AppDatabase db = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME).allowMainThreadQueries().build(); 

Dans mon cas, je devais trouver l’utilisateur cliqué dans la liste existe dans la firebase database ou non. Sinon, créez l’utilisateur et lancez une autre activité

  @Override public void onClick(View view) { int position = getAdapterPosition(); User user = new User(); Ssortingng name = getName(position); user.setName(name); AppDatabase appDatabase = DatabaseCreator.getInstance(mContext).getDatabase(); UserDao userDao = appDatabase.getUserDao(); ArrayList users = new ArrayList(); users.add(user); List ids = userDao.insertAll(users); Long id = ids.get(0); if(id == -1) { user = userDao.getUser(name); user.setId(user.getId()); } else { user.setId(id); } Intent intent = new Intent(mContext, ChatActivity.class); intent.putExtra(ChatActivity.EXTRAS_USER, Parcels.wrap(user)); mContext.startActivity(intent); } } 

Vous pouvez autoriser l’access à la firebase database sur le thread principal, mais uniquement à des fins de débogage, vous ne devriez pas le faire en production.

Voici la raison.

Remarque: Room ne prend pas en charge l’access à la firebase database sur le thread principal, sauf si vous avez appelé allowMainThreadQueries () sur le générateur car il risque de verrouiller l’interface utilisateur sur une longue période. Les requêtes asynchrones (requêtes renvoyant des instances de LiveData ou Flowable) sont exemptées de cette règle car elles exécutent de manière asynchrone la requête sur un thread d’arrière-plan si nécessaire.

Vous ne pouvez pas accéder à la firebase database directement sur le thread principal, par exemple:

  public void add(MyEntity item) { appDatabase.myDao().add(item); } 

Vous devez étendre AsyncTask pour la mise à jour, l’ajout et la suppression dans ViewModel.

Exemple:

 public class MyViewModel extends AndroidViewModel { private LiveData> list; private AppDatabase appDatabase; public MyViewModel(Application application) { super(application); appDatabase = AppDatabase.getDatabase(this.getApplication()); list = appDatabase.myDao().getItems(); } public LiveData> getItems() { return list; } public void delete(Obj item) { new deleteAsyncTask(appDatabase).execute(item); } private static class deleteAsyncTask extends AsyncTask { private AppDatabase db; deleteAsyncTask(AppDatabase appDatabase) { db = appDatabase; } @Override protected Void doInBackground(final MyEntity... params) { db.myDao().delete((params[0])); return null; } } public void add(final MyEntity item) { new addAsyncTask(appDatabase).execute(item); } private static class addAsyncTask extends AsyncTask { private AppDatabase db; addAsyncTask(AppDatabase appDatabase) { db = appDatabase; } @Override protected Void doInBackground(final MyEntity... params) { db.myDao().add((params[0])); return null; } } } 

Vous pouvez utiliser Future et Callable. Donc, vous ne seriez pas obligé d’écrire un long asyntask et pouvez effectuer vos requêtes sans append allowMainThreadQueries ().

Ma requête dao: –

 @Query("SELECT * from user_data_table where SNO = 1") UserData getDefaultData(); 

Ma méthode de repository: –

 public UserData getDefaultData() throws ExecutionException, InterruptedException { Callable callable = new Callable() { @Override public UserData call() throws Exception { return userDao.getDefaultData(); } }; Future future = Executors.newSingleThreadExecutor().submit(callable); return future.get(); }