Skip to the content.

Trabajo práctico integrador - Laboratorio 4

Fecha de entrega para el laboratorio 4: 30 de octubre 2020

Objetivos

Intro

Hasta ahora todas nuestras aplicaciones no manejaban datos o solo se almacenaban en memoria, perdiendose al momento de cerrar la app.

Una app suele requerir de algún mecanismo que le permita persistir datos, estos datos deber guardarse en algún lugar que este fuera del ciclo de vida de una app, ya sea dentro del mismo dispositivo (perdiendolos si el usuario decide utilizar otro o borra la app) o en algun servidor en internet a travez de servicios (como una api rest).

En este trabajo utilizaremos ambas opciones, localmente guardaremos los datos en una base de datos SQL utilizando Room y además utilizaremos Firebase, una plataforma de desarrollo de aplicaciones de Google, la cual entre sus servicios nos provee una forma sencilla persistir colleciones de JSONs a travez de una API REST.

1. Clonar el proyecto realizado en el laboratorio anterior

(De no tenerlo ya de manera local)

A continuación abrir el proyecto en Android Studio y hacer un checkout de la branch develop, esta rama debe contener los cambios realizados durante el laboratorio 3.

A partir de develop, crear una nueva branch llamada lab04. Esta es la branch sobre la cual se realizará este laboratorio.

2. Persistir platos creados en una base de dato SQL

En el lab anterior mencionamos que un plato debería contener:

Partiendo de esto deberíamos tener una clase similar a la siguiente, a la que le agregamos el campo id para poder identificar cada plato de forma única:

public class Plato {
    Long id; //Campo que agregamos
    String titulo;
    String descripcion;
    Double precio;
    Integer calorias;
}

2.1. Configurar ROOM

2.2. Anotando nuestras clases para ROOM

Room es una librería que permite el mapeo de objetos a SQLite a través de Java Annotations

Es parte de lo que android denomina Architecture Components que son una colección de librerias disponibilizadas por google para facilitarnos el desarrollo de aplicaciones.

A nuestra clase Plato que deseamos persistir es necesario agregarle al menos estas annotations

@Entity
public class Plato {
    @PrimaryKey(autoGenerate = true)
    Long id;
    String titulo;
    String descripcion;
    Double precio;
    Integer calorias;
}

2.3. Generando nuestros DAOs para persistir los platos

Para acceder a los datos de nuestra app utilizando la librería de persistencia room esta nos propone la utilización del patron de objeto de acceso a datos (data access object o DAO), en el que estos objetos nos proveen los métodos para acceder a los objetos de la base de datos.

El DAO para la entidad Plato, quedaría de la siguiente forma:

@Dao
public interface PlatoDao {
    @Insert
    void insertar(Plato plato);

    @Delete
    void borrar(Plato plato);

    @Update
    void actualizar(Plato plato);

    @Query("SELECT * FROM plato WHERE id = :id LIMIT 1")
    Plato buscar(String id);

    @Query("SELECT * FROM plato")
    List<Plato> buscarTodos();

}

2.4 Creación de un Repository

Una vez que tenemos creados tanto la Entidad, con sus etiquetas, y el DAO; Debemos crear una clase que haga uso de Room y el DAO correspondiente a cada objeto que se desea almacenar.

Como ejemplo vamos a completar el AppRepository con lo necesario para guardar y consultar los platos guardados.

@Database(entities = {Plato.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
    public abstract PlatoDao platoDao();
     /* .... */
    static AppDatabase getInstance(final Context context) {
        /* .... */
        return INSTANCE;
    }
}
public class AppRepository implements OnPlatoResultCallback {
    private PlatoDao platoDao;
    private OnResultCallback callback;

    public AppRepository(Application application, OnResultCallback context){
        AppDatabase db = AppDatabase.getInstance(application);
        platoDao = db.platoDao();
        callback = context;
    }

    public void insertar(final Plato plato){
        AppDatabase.databaseWriteExecutor.execute(new Runnable() {
            @Override
            public void run() {
                platoDao.insertar(plato);
            }
        });
    }

    public void borrar(final Plato plato){
        AppDatabase.databaseWriteExecutor.execute(new Runnable() {
            @Override
            public void run() {
                platoDao.borrar(plato);
            }
        });
    }

    public void actualizar(final Plato plato){
        AppDatabase.databaseWriteExecutor.execute(new Runnable() {
            @Override
            public void run() {
                platoDao.actualizar(plato);
            }
        });
    }

    public void buscar(String id) {
        new BuscarPlatoById(platoDao, this).execute(id);
    }

    public void buscarTodos() {
        new BuscarPlatos(platoDao, this).execute();
    }

    @Override
    public void onResult(List<Plato> platos) {
        Log.d("DEBUG", "Plato found");
        callback.onResult(platos);
    }

    public interface OnResultCallback<T> {
        void onResult(List<T> result);
    }
}

Vale destacar, que cualquier consulta a la base, no debe hacerse sobre el hilo principal de ejecución, por lo tanto y con el objetivo de simplificar la tarea, vamos a hacer uso de AsynkTask y Callbacks. Lo más recomendado es hacer uso de LiveData o RxJava, pero ambos temas quedan fuera del alcance del laboratorio.

void buscar(String id) y void buscarTodos() son dos claros ejemplos de funciones que deben ejecutarse en un hilo distinto del principal, ya que hacen una llamada a la base de datos y ademas queremos conocer su resultado, como logramos ambas cosas?

En este caso, lo que vamos a hacer es crear un AsynkTask que ejecute la búsqueda y nos de el resultado cuando la misma haya terminado. Lo cual quedaría de la siguiente forma

// android.os.AsyncTask<Params, Progress, Result>
class BuscarPlatos extends AsyncTask<String, Void, List<Plato>> {

    private PlatoDao dao;
    private OnPlatoResultCallback callback;

    public BuscarPlatos(PlatoDao dao, OnPlatoResultCallback context) {
        this.dao = dao;
        this.callback = context;
    }

    @Override
    protected List<Plato> doInBackground(String... strings) {
        List<Plato> platos = dao.buscarTodos();
        return platos;
    }

    @Override
    protected void onPostExecute(List<Plato> platos) {
        super.onPostExecute(platos);
        callback.onResult(platos);
    }
}

BuscarPlatos en su constructor recibe dos parámetros muy importantes que debemos entender PlatoDao dao y OnPlatoResultCallback context, el primer parámetro es el DAO que construimos anteriormente, pero el segundo parametro es el contexto desde donde estamos llamando a nuestro Reposity, el cual debe implementar OnPlatoResultCallback para ser valido; Al implementar esta interfaz, nos aseguramos que tendrá declarado el método onResult el cual usaremos para notificarle el resultado de la operación.

La Interfaz de OnPlatoResultCallback seria algo como lo siguiente

interface OnPlatoResultCallback {
    void onResult(List<Plato> plato);
}

El mismo tipo de interfaz, pero de forma más generica para poder ser usada tanto con Platos como con Pedidos, fue declarada en AppRepository

De la siguiente forma quedaría un Activity que quiere hacer uso del Repository

public class ListItemsActivity extends AppCompatActivity implements AppRepository.OnResultCallback {
    /* ..... */
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
       /* ..... */
        repository = new AppRepository(this.getApplication(), this);
        repository.buscarTodos();
    }

    @Override
    public void onResult(List result) {
        // Vamos a obtener una Lista de items como resultado cuando finalize
        Toast.makeText(ListItemsActivity.this, "Exito!", Toast.LENGTH_SHORT).show();
    }

}

De esta forma podemos ejecutar pedidos a la base de datos de forma asíncrona y no consumir los recursos del hilo principal.

Aclaración: Este tipo de AsynkTask podría repetirse para cada operación o hacerse de forma más generica. Queda a elección.

3. Persistir los pedidos creados en la base de dato SQL.

Repetir los pasos anteriores (2.2 - 2.3) pero ahora configurando la entidad Pedido, para que sea guardada en nuestra base de datos, incluyendo sus relaciones.

4. Persistir los platos a través de una API Rest.

Utilizando la misma clase plato que teníamos creada y ampliamos en el paso 2, vamos a guardar los datos de dichos platos usando Retrofit.

4.1 Configurar Retrofit

4.2 Generar la interfaz que va a describir a nuestra API

Retrofit, tambien usa Java Anotation para describir el funcionamiento de la interfaz que vamos a generar.

Al momento de clarar la interfaz de nuestra API cada método debe tener una anotación HTTP que proporcione el método de solicitud y la URL relativa.

Hay ocho anotaciones disponibles: HTTP, GET, POST, PUT, PATCH, DELETE, OPTIONS y HEAD. La URL relativa del recurso se especifica en la anotación.

@GET("platos/lista") 

También puede especificar parámetros de consulta en la URL.

@GET("platos/lista?Sort=desc")

Con estas notaciones vamos a ser capases de generar una Interfaz como la siguiente:

public interface PlatoService {
  @GET("plato/{id}")
  Call<Plato> getPlato(@Path("id") String id);
    
  @GET("plato/list")
  Call<List<Plato>> getPlatoList();

  @POST("plato")
  Call<Plato> createPlato(@Body Plato plato);
  /* 
    Si deciden usar SendMeal-Fake-API deberán usar un body
    del tipo @Body String body. Lo cual les facilitara cumplir el formato esperado
    
    JSONObject bodyExample = new JSONObject();
    paramObject.put("email", "sample@gmail.com");
    paramObject.put("pass", "4384984938943");
    createPlato(bodyExample.toString())
  */
}

Una vez tenemos totalmente declarados todos los metodos de nuestra API Rest, podemos inicializar una instancia de nuestra API como si fuera una Clase Java haciendo uso de Retrofit

Gson gson = new GsonBuilder().setLenient().create();
Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("{urlApiRest}/")
    // En la siguiente linea, le especificamos a Retrofit que tiene que usar Gson para deserializar nuestros objetos
    .addConverterFactory(GsonConverterFactory.create(gson)) 
    .build();

PlatoService platoService = retrofit.create(PlatoService.class);

Cuando tenemos creado nuestra clase, podemos llamar cualquier endpoint de nuestra API Rest, como si fuera un metodo.

Call<List<Plato>> callPlatos = platoService.getPlatoList();

callPlatos.enqueue(
        new Callback<List<Plato>>() {
            @Override
            public void onResponse(Call<List<Plato>> call, Response<List<Plato>> response) {
                if (response.code() == 200) {
                    Log.d("DEBUG", "Returno Exitoso");
                }
            }

            @Override
            public void onFailure(Call<List<Plato>> call, Throwable t) {
                Log.d("DEBUG", "Returno Fallido");
            }
        }
);

5. Persistir los pedidos creados a través de una API Rest.

Repetir los pasos anteriores (4.2 - 4.3) pero ahora configurando la entidad PedidoService, para poder hacer uso de la API Rest, para guardar pedidos.

Recomendación:

En caso de estar usando el JSON-Server de SendMeal-FakeAPI, recuerden que la relación 1 a n de Pedido -> Platos, deben guardarla con un arreglo de IDs Ej:

Pedido = {
    id: "uuidPedido",
    platosId: ["uuidPlato1", "uuidPlato2", "uuidPlato3"],
    ....
}

Al momento de consultar un pedido por ID, o una lista de pedidos; El API automáticamente mapeara los platos relacionados con el pedidos. Ej:

Pedido = {
    id: "uuidPedido",
    platosId: ["uuidPlato1", "uuidPlato2", "uuidPlato3"],
    platos: [ {objPlato1}, {objPlato2}, {objPlato3} ]
    ....
}

Tambien deben recordar, que Android no permite conexiones a ‘localhost’, por lo que deben buscar la ip de su equipo y usar esa dirección. Ej: http://192.168.16.1:3001/ o usando la “IP Magica” http://10.0.2.2/