studio programacion para móviles libro edición desarrollo desarrollar curso aprende aplicaciones android crash kotlin android-studio-3.0 android-room

programacion - Sala de Android-consulta de selección simple-No se puede acceder a la base de datos en el hilo principal



manual de programacion android pdf (17)

Estoy probando una muestra con Room Persistence Library . Creé una entidad:

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

Creó una clase DAO:

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

Creó la clase de base de datos:

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

Base de datos expuesta usando la siguiente subclase en 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() } }

Implementado a continuación la función en mi actividad:

void signUpAction(View view) { String email = editTextEmail.getText().toString(); String phone = editTextPhone.getText().toString(); String license = editTextLicence.getText().toString(); 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(); } }

Desafortunadamente, en la ejecución del método anterior se bloquea con el siguiente seguimiento de la pila:

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) 

Parece que ese problema está relacionado con la ejecución de la operación db en el hilo principal. Sin embargo, el código de prueba de muestra proporcionado en el enlace anterior no se ejecuta en un hilo separado:

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

¿Me estoy perdiendo algo por aquí? ¿Cómo puedo hacer que se ejecute sin bloqueo? Por favor recomiende.


Kotlin Coroutines (Claro y Conciso)

AsyncTask es realmente torpe. Las rutinas de Kotlin son una alternativa más limpia (esencialmente solo código síncrono más un par de palabras clave).

private fun myFun() { launch { // coroutine on Main val query = async(Dispatchers.IO) { // coroutine on IO MyApp.DatabaseSetup.database.agentDao().agentsCount(email, phone, license) } val agentsCount = query.await() // do UI stuff } }

¡¡Y eso es!!

Bonus: Actividad como CoroutineScope

Para usar asíncrono de una Actividad, necesita un CoroutineScope. Puedes usar tu Actividad de esta manera:

class LoadDataActivity : AppCompatActivity(), CoroutineScope { private val job by lazy { Job() } override val coroutineContext: CoroutineContext get() = Dispatchers.Main + job override fun onDestroy() { super.onDestroy() job.cancel() // cancels all coroutines under this scope } // ...rest of class }

Bonus: sala de Android y suspensión

08-mayo-2019: la sala 2.1 ahora admite la suspend ( https://youtu.be/Qxj2eBmXLHg?t=1662 )

La palabra clave suspend garantiza que los métodos asincrónicos solo se invocan desde bloques asincrónicos, sin embargo (como lo señaló @Robin) esto no funciona bien con los métodos anotados Room (<2.1).

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


Actualización: también recibí este mensaje cuando intentaba crear una consulta usando @RawQuery y SupportSQLiteQuery dentro del DAO.

@Transaction public LiveData<List<MyEntity>> getList(MySettings mySettings) { //return getMyList(); -->this is ok return getMyList(new SimpleSQLiteQuery("select * from mytable")); --> this is an error

Solución: cree la consulta dentro del ViewModel y páselo al DAO.

public MyViewModel(Application application) { ... list = Transformations.switchMap(searchParams, params -> { StringBuilder sql; sql = new StringBuilder("select ... "); return appDatabase.rawDao().getList(new SimpleSQLiteQuery(sql.toString())); }); }

O...

No debe acceder a la base de datos directamente en el hilo principal, por ejemplo:

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

Debe usar AsyncTask para actualizar, agregar y eliminar operaciones.

Ejemplo:

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

Si usa LiveData para operaciones seleccionadas, no necesita AsyncTask.


Con la biblioteca Jetbrains Anko, puede usar el método doAsync {..} para ejecutar automáticamente llamadas a la base de datos. Esto resuelve el problema de verbosidad que parecía haber tenido con la respuesta de mcastro.

Ejemplo de uso:

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

Utilizo esto con frecuencia para inserciones y actualizaciones, sin embargo, para consultas seleccionadas, recomiendo usar el flujo de trabajo RX.


Con lambda es fácil de ejecutar con AsyncTask

AsyncTask.execute(() -> //run your query here );


El acceso a la base de datos en el hilo principal que bloquea la interfaz de usuario es el error, como dijo Dale.

Cree una clase anidada estática (para evitar pérdidas de memoria) en su Actividad que extiende AsyncTask.

private static class AgentAsyncTask extends AsyncTask<Void, Void, Integer> { //Prevent leak private WeakReference<Activity> weakActivity; private String email; private String phone; private String license; public AgentAsyncTask(Activity activity, String email, String phone, String 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(); } } }

O puede crear una clase final en su propio archivo.

Luego ejecútelo en el método signUpAction (Vista de vista):

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

En algunos casos, es posible que también desee mantener una referencia a AgentAsyncTask en su actividad para que pueda cancelarla cuando se destruya la Actividad. Pero tendría que interrumpir cualquier transacción usted mismo.

Además, su pregunta sobre el ejemplo de prueba de Google ... Ellos dicen en esa página web:

El enfoque recomendado para probar la implementación de su base de datos es escribir una prueba JUnit que se ejecute en un dispositivo Android. Debido a que estas pruebas no requieren la creación de una actividad, deberían ser más rápidas de ejecutar que sus pruebas de IU.

Sin actividad, sin interfaz de usuario.

--EDITAR--

Para las personas que se preguntan ... Tienes otras opciones. Recomiendo echar un vistazo a los nuevos componentes ViewModel y LiveData. LiveData funciona muy bien con Room. https://developer.android.com/topic/libraries/architecture/livedata.html

Otra opción es el RxJava / RxAndroid. Más potente pero más complejo que LiveData. https://github.com/ReactiveX/RxJava

--EDIT 2--

Dado que muchas personas pueden encontrar esta respuesta ... La mejor opción hoy en día, en general, es Kotlin Coroutines. Room ahora lo admite directamente (actualmente en versión beta). https://kotlinlang.org/docs/reference/coroutines-overview.html https://developer.android.com/jetpack/androidx/releases/room#2.1.0-beta01


El mensaje de error,

No se puede acceder a la base de datos en el subproceso principal, ya que puede bloquear la interfaz de usuario durante largos períodos de tiempo.

Es bastante descriptivo y preciso. La pregunta es cómo debe evitar acceder a la base de datos en el hilo principal. Ese es un gran tema, pero para comenzar, lea sobre AsyncTask (haga clic aquí)

-----EDITAR----------

Veo que tiene problemas cuando ejecuta una prueba unitaria. Tienes un par de opciones para arreglar esto:

  1. Ejecute la prueba directamente en la máquina de desarrollo en lugar de en un dispositivo Android (o emulador). Esto funciona para pruebas que se centran en la base de datos y realmente no les importa si se ejecutan en un dispositivo.

  2. Use la anotación @RunWith(AndroidJUnit4.class) para ejecutar la prueba en el dispositivo Android, pero no en una actividad con una interfaz de usuario. Se pueden encontrar más detalles sobre esto en este tutorial


No puede ejecutarlo en el subproceso principal; en su lugar, use controladores, async o subprocesos de trabajo. Un código de muestra está disponible aquí y lea el artículo sobre la biblioteca de la sala aquí: Biblioteca de la sala de 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 desea ejecutarlo en el hilo principal, que no es la forma preferida.

Puede usar este método para lograr en el hilo principal Room.inMemoryDatabaseBuilder()


No se recomienda, pero puede acceder a la base de datos en el hilo principal con allowMainThreadQueries()

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


Para consultas rápidas, puede dejar espacio para ejecutarlo en el hilo de la interfaz de usuario.

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

En mi caso, tuve que descubrir que el usuario en el que se hizo clic en la lista existe o no en la base de datos. Si no es así, cree el usuario y comience otra actividad

@Override public void onClick(View view) { int position = getAdapterPosition(); User user = new User(); String name = getName(position); user.setName(name); AppDatabase appDatabase = DatabaseCreator.getInstance(mContext).getDatabase(); UserDao userDao = appDatabase.getUserDao(); ArrayList<User> users = new ArrayList<User>(); users.add(user); List<Long> 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); } }



Puede permitir el acceso a la base de datos en el hilo principal, pero solo con fines de depuración, no debe hacerlo en producción.

Aquí está la razón.

Nota: Room no admite el acceso a la base de datos en el subproceso principal a menos que haya llamado allowMainThreadQueries () en el constructor porque podría bloquear la interfaz de usuario durante un largo período de tiempo. Las consultas asincrónicas (consultas que devuelven instancias de LiveData o Flowable) están exentas de esta regla porque ejecutan asincrónicamente la consulta en un subproceso de fondo cuando es necesario.


Puedes usar Future y Callable. Por lo tanto, no se le solicitará que escriba un asynctask largo y pueda realizar sus consultas sin agregar allowMainThreadQueries ().

Mi consulta dao: -

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

Mi método de repositorio: -

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


Si te sientes más cómodo con la tarea Async :

new AsyncTask<Void, Void, Integer>() { @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();


Simplemente haga las operaciones de la base de datos en un hilo separado. Así (Kotlin):

Thread { //Do your database´s operations here }.start()


Simplemente puede usar este código para resolverlo:

Executors.newSingleThreadExecutor().execute(new Runnable() { @Override public void run() { appDb.daoAccess().someJobes();//replace with your code } });

O en lambda puedes usar este código:

Executors.newSingleThreadExecutor().execute(() -> appDb.daoAccess().someJobes());

Puede reemplazar appDb.daoAccess().someJobes() con su propio código;


Tienes que ejecutar la solicitud en segundo plano. Una forma simple podría ser usar un Executors :

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


Una solución elegante de RxJava / Kotlin es usar Completable.fromCallable , que le dará un Observable que no devuelve un valor, pero puede observarse y suscribirse en un hilo diferente.

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

O en Kotlin:

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

Puede observar y suscribirse como lo haría normalmente:

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