En busca de la arquitectura Android perfecta I: Despicable Content Providers

Desde hace tiempo vengo buscando cual puede ser la mejor arquitectura de una aplicación Android. Ojo, no tiene por que ser la mejor en cuanto a rendimiento o velocidad, pero tiene que dar un buen rendimiento, ser lo más Android-compliant y sobre todo, fácil de replicar. Mi objetivo no es más que sentar las bases para nuevos proyectos (tanto profesionales como personales) y decir “pues yo trabajo así”. Tengo muchas ideas en la cabeza, pero sin compartirlas y discutirlas, pues no hay manera, así que voy a ir compartiendo algunas de estas ideas, y a ver que sucede.

Hoy empezaré por los ContentProviders. Voy a asumir que ya sabes que es y como funciona un ContentProvider y el acceso a SQLite. Si no lo sabes, mírate el tutorial del amigo Vogella en http://www.vogella.com/articles/AndroidSQLite/article.html. Si os fijas en el punto 9.4 del tutorial, vereis que el estructura el ContentProvider de la siguiente manera por cada una de las operaciones (query, insert, delete y update):

Odio el switch. Me parece la estructura más fea y poco mantenible que existe. Además, en el ejemplo, sólo utiliza dos URLs, pero imaginad una aplicación donde tengo 20 entidades. Vamos, lo que viene siendo una clase Dios.

Mi propuesta para esto es lo que denomino el DespicableContentProvider. ¿Y por qué lo llamo así? Por que tiene MinionContentProvider (lo se, don’t use cute names, pero no pude resistirme, y la analogía se entiendo muy bien). Si no sabes de que va lo de Despicable y Minions, mira esto: http://www.youtube.com/watch?v=jzpOLs1t8Hg

Todo el código base, tanto del DespicableContentProvider como del MinionContentProvider están en https://gist.github.com/sergiandreplace/8165986 y son bonicos, libres y distribuibles.

Antes de todo, establezcamos como uso los ContentProvider y cual es mi approach:

  • El ContentProvider (CP a partir de ahora) lo veo parecido a una api web.
  • Cada url representa una entidad.
  • Las url estan formadas por content://<nombre de la autoridad>/entidad[/#id].
  • Las url acabadas en id estan pidiendo un elemento específico, y las conocemos como tipo Item. Las otras son tipo Dir (por coherencia con los tipos MIME)
  • El tipo mime será vnd.android.cursor.{dir|item}/vnd.{AUTORIDAD}.{ENTIDAD}. Mis CP suelen ser internos no publicados, así que el tipo MIME me da un poco igual.
  • El consumidor del CP no sabe NADA de la base de datos. En realidad, lo monto para que el único que tenga acceso a la bd sea el CP. Incluso los contratos son diferentes, uno para la bd y otro para el CP (a menos que sean algo muy pequeño).
  • Cuando una petición es de tipo Item, es decir, pido uno con un id espécifico, los parametros de filtrado son ignorados (si ya pido uno ¿para que quiero filtrar?)

La idea básica de esta estructura es que cada Minion sabe hacer Insert, Update, Delete y Query de una entidad para Item o para Dir (no ambos). Veamos un ejemplo:

Como veis, he hecho tres cosas, por un lado getBasePath que devuelve la url específica de la entidad, getType, que devuelve el nombre básico para construir el tipo MIME si queremos, y hemos implementado cuatro funciones muy parecidas a las de un ContentProvider normal. Las diferencias son, que por un lado recibo el objeto db para poder lanzar las aplicaciones y no notifico el cambio al acabar.

Uno de tipo Item no tiene mucha diferencia:

Diferencias:

  1. La clase en singular (soy así…)
  2. El path incluye el /# según la sintaxis del UriMatcher
  3. En cada operación, decidimos ignorar los parámetros que recibimos y filtramos por el id de la ruta a piñón.
  4. Decido que el insert de Item no se puede hacer. No hay razón para esto, era por poner un ejemplo. La otra opción sería cambiarlo en el contentValues por el de la url antes del insert. Ahí ya depende de vuestro modelo.

¿A que es facil? Pues creariamos uno de estos por cada entidad Dir o Item  y ale.

Una vez hechos, hay que implementar el Despicable. Que ya vereis que facil:

El método GetAuthority sólo devuelve el nombre de la autoridad que vamos a utilizar. Es usado por la clase padre. El método getDB() debe devolver un objeto Database con el que trabajar. Y el método recruitMinion se encarga de añadir cada uno de los Minions a la colección instanciando las clases que heredan de MinionContentProvider.

Y ya. Mirad que bonico todo.

Ahora nuestro CP ofrece todas las operaciones en cada minion, incluso, de regalo, un bulkInsert que ejecuta un loop de inserts dentro de una transacción.

Ah, suelo añadir estos dos métodos de regalo en el ContentProvider:

Son muy útiles en los contratos y los CursorHelper, que es algo de lo que hablaré el próximo día.

Me encantaría recibir comentarios al respecto y ver como vosotros abordáis el problema. Y cualquier sugerencia, ya sabes, será bienvenida.

11 Responses to “En busca de la arquitectura Android perfecta I: Despicable Content Providers”

  1. Rul says:

    Bon dia Sergi,

    La verdad es que el post me ha resultado interesante ya que este tema hace unos días lo discutíamos unos compañeros del curro a partir del curso que hicimos de Android, ya que llego el momento de hacer nuestra apk y claro cuando el modelo de datos empezó a ganar tamaño el CP cada vez era más demoniaco de seguir y por no hablador de los mega switch que empezaron a engendrarse.
    Desde nuestra experiencia desarrollando aplicaciones en J2EE siempre hemos intentado hacer que nuestra arquitectura fuera lo más modular posible, por su comodidad a la hora de seguir el código, de mantener y sobretodo para el trabajo en equipo.
    Es cierto que el concepto este que vimos en muchos sitios todo en un CP nos dejo un poco como diría yo WTF, y la verdad es que la solución que planteas es de lo más interesante porque de este modo quedaría más claro para desarrollos con un modelo de datos muy grande y si la apk tiene un par de entidades solo pues igualmente no esta de más aplicar esta arquitectura para ser un poco ordenado.
    Muchas gracias por compartir estas líneas y estaré deseando ver la parte dos de este tema y futuras partes, nos vemos máster.

    • sergi says:

      Gracias Rul

      Yo empecé un poco por encontrarme en el mismo caso, no quería crear clases gigantescas. Ahora uso esto para todo, por supuesto que no siempre es aplicable, esto es muy para “ataque a bbdd con entidades planas”. Si necesitase algo más complejo, pues modificaría esto o atacaría por otro lado.

      Próximamente os pondré una vuelta de tuerca de esto donde ya ni hace falta implementar los minions para entidades simples.

  2. César says:

    Enhorabuena! Es muy buen artículo.

    Como usuario habitual de esta arquitectura, me gustaría destacar a continuación, la problemática que conlleva el uso de Content Providers en una aplicación medianamente compleja; SQLite, Content Providers y Thread Safety

    Como comenta Alex Lockwood en uno de sus geniales posts *1, en Android hay que gestionar de forma manual el acceso simultáneo a la base de datos desde diferentes threads, aunque en algunos casos, como por ejemplo en la escritura en BD desde dos threads diferentes, SQLiteDatabase ya se encargaría de bloquear un segundo thread mientras el primero estuviera escribiendo.

    A lo que me refiero con este ejemplo, es que en una aplicación compleja en la que hagamos un uso intensivo de la base de datos, es muy probable que nos encontremos casos en que se escriba y se lea de la base de datos, o viceversa, y eso conlleve a excepciones del tipo “Database is locked” u otros, algo que está directamente relacionado con el tipo de base de datos que abrimos al usar el ContentProvider, concretamente en el método getDb(). Puede ser de dos tipos: *2

    - Writable Database
    - Readable Database

    En situaciones normales, getReadableDatabase() retorna la misma base de datos escribible que getWritableDatabase(), pero ésta última podría lanzar excepciones por falta de espacio en disco o problemas de permisos, por lo que se recomienda usar por defecto getReadableDatabase(). Sin embargo, en ciertas ocasiones, como por ejemplo al hacer un “create” o “upgrade” de la base de datos o en problemas como el descrito anteriormente, tiene que ser abierta en modo “writable”. Por ello, Reto Meier en el libro de “Professional Android 4 Application Development” recomienda intentar utilizar getWritableDatabase() y si éste fallara, usar la getReadableDatabase, por lo que el método getDb() quedaría de la siguiente manera:

    @Override
    public SQLiteDatabase getDb() {
    if (db == null) {
    try {
    db = new DatabaseHelper(getContext()).getWritableDatabase();
    } catch (SQLiteException ex) {
    db = new DatabaseHelper(getContext()).getReadableDatabase();
    }
    }
    return db;
    }

    De esta forma, nos ahorramos algunos problemillas…

    Cabe destacar que la razón exacta de porqué usar un tipo de base de datos u otro, no la he encontrado en ningún sitio, así como es MUY DIFÍCIL debugar ContentProviders, aunque si queréis seguir mi recomendación, puede que os ahorréis algunos quebraderos de cabeza, de esos que pierdes algún que otro día intentando averiguar que co** esta pasando en tu código.

    Suerte!

    *1 : http://www.androiddesignpatterns.com/2012/10/sqlite-contentprovider-thread-safety.html
    *2 : http://developer.android.com/reference/android/database/sqlite/SQLiteOpenHelper.html

  3. sergi says:

    El tema threadsafe da para otro artículo, y después de tanto tiempo oyendo opiniones contradictorias, necesito hacer una prueba de campo. Quizás en un futuro me ponga, ya que hay casos que tampoco tengo claros. Además, debemos diferenciar entre lo que es threadsafe y lo que es concurrencia de acceso a la base de datos, que son dos cosas diferentes. Puedo tener métodos en threads diferentes, todo seguro, y seguir incurriendo en problemas de concurrencia de acceso a datos, y aquí es donde entramos con bloqueos y transacciones que son otro tema. Vamos, un pollo a mirar con mucho cuidado :)

    Según la docu sobre getReadableDatabase:

    Create and/or open a database. This will be the same object returned by getWritableDatabase() unless some problem, such as a full disk, requires the database to be opened read-only. In that case, a read-only database object will be returned. If the problem is fixed, a future call to getWritableDatabase() may succeed, in which case the read-only database object will be closed and the read/write object will be returned in the future.

    O sea, que al final el código de Reto Meier hace exactamente lo mismo que la llamada al propio getReadableDatabase. Si miras el código fuente de SQLiteOpenHelper (https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/java/android/database/sqlite/SQLiteOpenHelper.java) verás que ambos llaman al método getDatabaseLocked pasando true o false al parametro writable, y acaban devolviendo el mismo objeto con flags diferentes (que creo que ya se pierden en las miasmas del código nativo, en la clase https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/java/android/database/sqlite/SQLiteConnection.java invocando a nativeOpen…).

    Según comentas, los problemas son con los comandos DDL, que necesitan exclusividad a la bbdd y escritura segura (bastante lógico por otra parte), mientras que los DML tiran con cualquier cosa. Ya que los comandos DDL suelen estar muy focalizados ¿no es quizás mejor utilizar la implementación writable específicamente en comandos DDL? A mi lo de no saber que tipo de bd tengo abierta, no me acaba de convencer.

    ¿Muy dificil depurar content providers? no se, no me parece más difícil que otras cosas de Java ¿ejemplos?

    • César says:

      Sip, lo de Reto Meier es cierto, pero no acabo de entender esto

      “¿no es quizás mejor utilizar la implementación writable específicamente en comandos DDL?”
      Con eso quieres decir que tendríamos que controlar de alguna forma que tipo de comandos nos llegan, y cambiar de bd en función de si el comando es una query, insert, delete o update?

      En cuanto al tema de depurar content providers, puede ser más o menos igual de sencillo que llegar a algún método del core de un framework, pero en este caso, al hacer una operación se disparan ciertos “triggers” (creo), para controlar que uri hay que actualizar, pero bueno, eso ya se desvía del tema principal ^^.

      Merci!

      • sergi says:

        Todos esos son DML, los DDL son los tipo create table, alter table, etc

        Mira aquí: http://www.tomjewett.com/dbdesign/dbdesign.php?page=ddldml.php

        Normalmente los DDL están localizados en el SQLiteHelper o similar, ya que no los vas haciendo más que en ocasiones especiales, y no se hacen a través de un ContentProvider, si no que los haces directamente en la bbdd.

        La idea sería: control de datos readable, estructura de datos writable.

        • César says:

          Gracias! Había buscado información sobre DDL y DML, pero yo pensaba que uno tenía que ver con bases de datos y el otro no. En el link que has pasado lo explica muy bien. Así lo haré!

        • César says:

          Mira Sergi, acabo de utilizar el getReadableDatabase() por defecto, y me salta el siguiente error:

          Caused by: android.database.sqlite.SQLiteReadOnlyDatabaseException: attempt to write a readonly database (code 8)

          para una operación update del SimpleMinionContentProvider:

          @Override
          public int update(SQLiteDatabase db, Uri uri, ContentValues values, String where, String[] selectionArgs) {
          return db.update(tableName, values, where, selectionArgs);
          }

          de una op cualquiera…

          context.getContentResolver().update(uri, values, selection, selectionArgs);

          Creía importante comentarte porque en un proyecto tan grande como Sochi, si no nos encontramos problemas de este tipo, nos encontramos problemas de database locked. Una locura, vamos.

          • César says:

            PD: Otra cosa que he leído en no me acuerdo donde (y no se si lo acabé de entender) es que era bueno crear una instancia por cada uso de la db, aunque a bote pronto podemos ver que será más costoso…

            Vamos cambiar el EventsContentProvider de :
            try {
            if (db == null) {
            db = new DatabaseHelper(getContext()).getWritableDatabase();
            }
            } catch (SQLiteException ex) {
            db = new DatabaseHelper(getContext()).getReadableDatabase();
            }
            return db;

            a

            try {
            db = new DatabaseHelper(getContext()).getWritableDatabase();
            } catch (SQLiteException ex) {
            db = new DatabaseHelper(getContext()).getReadableDatabase();
            }
            return db;

            Es una suposición, pero vistos los problemas que tenemos :S

          • sergi says:

            Eeeeh, claro, si intentas hacer un update en una base de datos de sólo lectura te da error, tanto si la abres writable como readable. No veo cual es el problema aquí.

            Lo que tienes que averiguar es por que te da el error de que la base de datos es read-only.

  4. sergi says:

    try {
    if (db == null) {
    db = new DatabaseHelper(getContext()).getWritableDatabase();
    }
    } catch (SQLiteException ex) {
    db = new DatabaseHelper(getContext()).getReadableDatabase();
    }
    return db;

    Esto se asegura de tener sólo una conexión a base de datos abierta. Si intentas abrir más de una concurrente, sqlite te manda a freir esparragos.

Leave a Reply