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:
-
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.
-
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);
}
}
Para todos los https://github.com/ReactiveX/RxJava o RxAndroid o RxKotlin
Observable.just(db)
.subscribeOn(Schedulers.io())
.subscribe { db -> // database operation }
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.
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(...)