You are on page 1of 14

CarCare

Introducción

Este tutorial tiene por objetivos:

• Incorporar el soporte para almacenamiento en bases de datos locales.


• Acceder a datos vía un Object Relational Mapping Framework.
• Implementar consultas personalizadas.

A través de los siguientes pasos, crearemos una aplicación en Android Studio que presente una vista de
tipo Dashboard con indicadores sobre consumo de combustible. Esta aplicación permitirá además
registrar una nueva entrada de consumo de combustible.

¡Vamos a codificar!

Creando el proyecto

Estando ubicados en la vista inicial de Android Studio seleccionamos la opción Start a new Android
Studio project.

Ello nos llevará a la ventana con el título Create New Project.

En dicha ventana colocamos la siguiente información en los campos indicados a continuación.

Application Name: CareCare


Company Domain: upc.edu.pe

Observaremos que de forma automática se establece pe.edu.upc.carcare como nombre de package para
la aplicación.

Hacemos click en Next.

En la siguiente vista de la ventana seleccionamos los dispositivos objetivo para la aplicación. En nuestro
caso marcamos la casilla

Phone and Tablet

Luego indicamos el nivel de API que utilizaremos como base mínima para la aplicación:
Minimum SDK: API 18: Android 4.3 (Jelly Bean)

Si hacemos click en la opción Help me choose, notaremos que se muestra una vista con los niveles de
API y la lista de features de Android que se encuentran disponibles a partir de dicha versión.

Hacemos click en Next

Se presentará en la siguiente vista una galería de Activities bajo el título Add and Activity to Mobile. De
dicha galería seleccionamos Basic Activity.

Hacemos click en Next.

Con ello se presenta la vista con el título Customize the Activity. En los campos completamos los
siguientes valores:
Activity Name: MainActivity
Layout Name: activity_main
Title: Car Care Dashboard
Menu Resource Name: menu_main.

Dejamos los demás campos en sus valores por defecto y presionamos el botón Finish.

Organizando el proyecto

A continuación, vamos a realizar modificaciones sobre la nomenclatura de paquetes de java de tal forma
que se refleje una arquitectura organizada en capas. En el árbol del proyecto, dentro del nodo java y el
paquete pe.edu.upc.carcare, hacemos clic derecho y seleccionamos New > Package.

Realizamos esta acción dos veces para crear los siguientes sub-paquetes dentro de
pe.edu.upc.carcare:

activities
models

A continuación, en el árbol de proyecto y utilizando el mouse, arrastramos el archivo MainActivity desde


su ubicación original hacia el subpaquete activities, confirmamos el diálogo de acción Move. De esta
forma el nombre completo de la clase MainActivity es ahora
pe.edu.upc.carcare.activities.MainActivity.

Diseñando el Layout de Main Activity

Hacemos doble click en el archivo de layout content_main.xml de tal forma que se pueda visualizar su
contenido si éste no se encuentra aún abierto.

Vamos a realizar las modificaciones de tal forma que la interfaz se aprecie de la siguiente manera:
Seleccionamos la pestaña Text en la parte inferior de la ventana del archivo content_main.xml.

Notaremos que la especificación xml se aprecia de esta manera.

<?xml version="1.0" encoding="utf-8"?>


<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="pe.edu.upc.helloandroiddevs.MainActivity"
tools:showIn="@layout/activity_main">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"/>
</RelativeLayout>

Realizamos las modificaciones de tal forma que se aprecie como sigue.


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="pe.edu.upc.carcare.activities.MainActivity"
tools:showIn="@layout/activity_main"
android:orientation="vertical">

<GridLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnCount="2"
android:rowCount="1">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="AVG Km/Gal"
android:id="@+id/textView"
android:gravity="start"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Total Logs"
android:id="@+id/textView2"
android:layout_gravity="right"
android:gravity="end"/>
</GridLayout>
<GridLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:rowCount="1"
android:columnCount="2">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="30"
android:id="@+id/avgKilometersPerGallonTextView"
android:gravity="start"
android:textSize="@dimen/indicator_size"
android:textStyle="bold"
android:textColor="@color/colorAccent"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/totalLogsTextView"
android:gravity="end"
android:layout_gravity="right"
android:textSize="@dimen/indicator_size"
android:text="14"
android:textStyle="bold"
android:textColor="@color/colorAccent"/>
</GridLayout>
<GridLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnCount="2"
android:rowCount="1">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="LAST Km/Gal"
android:id="@+id/textView5"
android:gravity="start"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Total Gallons"
android:id="@+id/textView6"
android:gravity="right"
android:layout_gravity="right"/>
</GridLayout>
<GridLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnCount="2"
android:rowCount="1">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="8.9"
android:id="@+id/lastKilometersPerGallonTextView"
android:textSize="@dimen/indicator_size"
android:textStyle="bold"
android:textColor="@color/colorAccent"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="102"
android:id="@+id/totalGallonsTextView"
android:textSize="@dimen/indicator_size"
android:layout_gravity="right"
android:textStyle="bold"
android:textColor="@color/colorAccent"/>
</GridLayout>
</LinearLayout>

Guardamos los cambios en el archivo content_main.xml.

Agregando el New Fuel Up Activity

Tal como hemos realizado antes, adicionamos al proyecto un Nuevo Activity, haciendo clic derecho
sobre el subpaquete activities y seleccionando desde el menú contextual la opción New > Activity >
Empty Activity. En el diálogo New Android Activity establecemos el valor de NewFuelUpActivity en
Activity Name. Validamos que el Layout Name tenga el valor activity_new_fuel_up y que Package
name tenga el valor de pe.edu.upc.carcare.activities. Hacemos clic en Finish para que se cierre el
diálogo y se cree el Activity.

Diseñando el Layout de NewFuelUpActivity

Abrimos para edición el archivo activity_new_fuel_up.xml. Vamos a realizar los cambios en el archivo
de tal forma que la interfaz se aprecie de la siguiente manera.
Para ello nos ubicamos en la parte inferior y seleccionamos la pestaña Text. Estando activo el editor de
código de layout, realizamos los siguientes cambios de tal forma que el archivo se aprecie de esta
manera.

<?xml version="1.0" encoding="utf-8"?>


<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="pe.edu.upc.carcare.activities.NewFuelUpActivity"
android:orientation="vertical">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:ems="10"
android:id="@+id/lastOdometerEditText"
android:hint="Last Odometer"
android:paddingTop="50dp"/>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:ems="10"
android:id="@+id/costPerGallonEditText"
android:hint="Cost / Gal."
android:paddingTop="50dp"/>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:ems="10"
android:id="@+id/gallonsEditText"
android:hint="Gallons"
android:paddingTop="50dp"/>
<GridLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnCount="2"
android:rowCount="1">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Cancel"
android:id="@+id/cancelButton"
android:layout_marginTop="20dp"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Add"
android:id="@+id/addButton"
android:layout_marginTop="20dp"
android:layout_gravity="right"/>
</GridLayout>
</LinearLayout>

Así hemos diseñado la interfaz de la vista que nos permitirá adicionar una nueva entrada de registro de
combustible.

Nos corresponde en los siguientes pasos trabajar en la capa modelo que concentrará la funcionalidad de
persistencia de información.

Incorporando el soporte de persistencia

Antes de proceder con la implementación de la capa de persistencia, vamos a integrar al proyecto una
biblioteca que contiene un orm framework que nos será de gran utilidad en dicha labor. Dicha biblioteca
es SugarORM y podemos revisar su documentación en http://satyan.github.io/sugar/.

Abrimos para edición el archivo gradle del módulo app, ubicado en el árbol de proyecto dentro de la
sección Gradle Scripts.

Dentro del archivo agregamos la siguiente línea de código al final del bloque dependencies:

compile 'com.github.satyan:sugar:1.5'

Una vez realizado el cambio notamos un mensaje de Android Studio en la parte superior con un enlace
Sync now. Hacemos clic en el enlace para que se descargue e integre la biblioteca en el proyecto.
Agregando el modelo Entry

A continuación, procedemos con la creación de la clase que representa a una entrada de registro de
combustible. Apoyándonos en la funcionalidad que nos ofrece SugarORM, vamos a hacer que esta clase
se vuelva persistente cumpliendo con dos requisitos: será un descendiente de SugarRecord y además
tendrá una versión del método constructor sin parámetros.

Ubicados en el árbol de proyecto, en el subpaquete models previamente creado, hacemos clic derecho
en el subpaquete models y desde el menú contextual seleccionamos New > Java Class. En el diálogo
Create New Class, establecemos el valor de Name: en Entry. Dejamos los demás valores por defecto y
hacemos clic en OK para que se cierre el diálogo y se cree la clase.

Verificamos que el archivo Entry.java esté abierto para edición. Realizamos los siguientes cambios en el
archivo de tal forma que se aprecie de la siguiente manera

package pe.edu.upc.carcare.models;

import com.orm.SugarRecord;

import java.util.Date;

public class Entry extends SugarRecord {


private Date createdAt;
private long lastOdometer;
private float costPerGallon;
private float gallons;

public Entry() {

public Date getCreatedAt() {


return createdAt;
}

public void setCreatedAt(Date createdAt) {


this.createdAt = createdAt;
}

public long getLastOdometer() {


return lastOdometer;
}

public void setLastOdometer(long lastOdometer) {


this.lastOdometer = lastOdometer;
}

public float getCostPerGallon() {


return costPerGallon;
}

public void setCostPerGallon(float costPerGallon) {


this.costPerGallon = costPerGallon;
}

public float getGallons() {


return gallons;
}
public void setGallons(float gallons) {
this.gallons = gallons;
}
}

Con ello, dejamos definida una clase que nos permite registrar la fecha en que se produce la carga de
combustible, el valor en dicho momento del odómetro, el precio por galón y la cantidad de galones
consumidos.

Guardamos el archivo Entry.java.

Agregando el servicio CarCareService

Ahora que tenemos representada como clase una entrada de registro, podemos avanzar un paso más e
implementar una clase que encapsule la funcionalidad de persistencia. Para ello vamos a crear una clase
CarCareService que contenga la siguiente funcionalidad.

addFuelUpEntry() Adiciona la información de un nuevo objeto Entry en la


base de datos. Recibe como parámetros en formato de
String: lastOdometer (valor del odómetro al momento del
registro), costPerGallon (costo por galón), gallons
(cantidad de galones). Retorna un valor boolean indicando si
la operación fue satisfactoria (true).
getAvgKilometersPerGallon() Obtiene el promedio de cantidad de kilómetros por galón.
getLastKilometersPerGallon() Obtiene la última cantidad de galones recorridos por galón.
getTotalGallons() Obtiene la cantidad de total de galones registrados hasta el
momento.
getTotalLogs() Retorna la cantidad de registros ingresados hasta el
momento.
addFuelUpEntry() Segunda versión del método addFuelUpEntry que recibe
como parámetros: lastOdometer (long), costPerGallon
(float), gallons (float). Retorna además un objeto de la
clase Entry con la información del nuevo registro, o null.
getDatabase() Retorna una referencia de tipo SQLiteDatabase a la base
de datos, lo que permite realizar operaciones personalizadas
no contempladas en la funcionalidad ofrecida por
SugarORM.

A fin de proporcionar la funcionalidad que acabamos de describir a la clase CarCareService, realizamos


las siguientes modificaciones y adiciones en el código fuente de la clase de tal forma que se aprecie
como sigue.

public class CarCareService {

public boolean addFuelUpEntry(String lastOdometer, String costPerGallon,


String gallons) {
Entry entry = new Entry();
entry.setLastOdometer(Long.parseLong(lastOdometer));
entry.setCostPerGallon(Float.parseFloat(costPerGallon));
entry.setGallons(Float.parseFloat(gallons));
entry.setCreatedAt(new Date());
return entry.save() > 0;
}
public float getAvgKilometersPerGallon() {
try {
Cursor cursor = getDatabase().rawQuery(
"SELECT AVG(last_odometer/gallons) AS result FROM entry", null);
return (cursor.moveToFirst()) ?
cursor.getInt(cursor.getColumnIndex("result")) : 0;
}
catch (NullPointerException e) {
e.printStackTrace();
}
return 0;
}

public float getLastKilometersPerGallon() {


Entry lastEntry = Entry.last(Entry.class);
if(lastEntry != null) {
return lastEntry.getLastOdometer()/lastEntry.getGallons();
}
return 0;
}

public long getTotalGallons() {


try {
Cursor cursor = getDatabase().rawQuery(
"SELECT SUM(gallons) AS result FROM entry", null);
return (cursor.moveToFirst()) ?
cursor.getInt(cursor.getColumnIndex("result")) : 0;
}
catch (NullPointerException e) {
e.printStackTrace();
}
return 0;
}

public long getTotalLogs() {


return Entry.count(Entry.class);
}

public Entry addFuelUpEntry(long lastOdometer, float costPerGallon, float gallons) {


Entry entry = new Entry();
entry.setLastOdometer(lastOdometer);
entry.setCostPerGallon(costPerGallon);
entry.setGallons(gallons);
entry.setCreatedAt(new Date());
long id = entry.save();
return Entry.findById(Entry.class, id);
}

private SQLiteDatabase getDatabase() {


try {
Field f = SugarContext.getSugarContext().getClass().getDeclaredField("sugarDb");
f.setAccessible(true);
return ((SugarDb) f.get(SugarContext.getSugarContext())).getDB();

} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
}
Guardamos los cambios en CarCareService.java.

Agregando una clase Application

A continuación, vamos a adicionar al proyecto una clase CarCareExpressApplication que sea un


descendiente de SugarApp (un requisito de SugarORM) y además vamos a adicionar un atributo de tipo
CarCareService, lo cual no permitirá contar con la funcionalidad de CarCareService en cualquier activity
de la aplicación.

Tal como hemos realizado anteriormente, vamos a adicionar una clase de Java al proyecto. Para ello nos
ubicamos en el árbol del proyecto, en el paquete raíz pe.edu.upc.carcare, hacemos clic derecho y
del menú contextual seleccionamos New > Java Class. En el diálogo New Android Class,
establecemos como valor para Name: en CarCareExpressApplication. Dejamos los valores por defecto
y seleccionamos OK para cerrar el diálogo y proceder con la creación de la clase.

Verificamos que esté abierto para edición el archivo CarCareExpressApplication.java. Realizamos las
siguientes modificaciones sobre el código de declaración de la clase, de tal forma que se aprecie como
sigue.

public class CarCareExpressApplication extends com.orm.SugarApp {

private CarCareService carCareService = new CarCareService();

public CarCareService getCarCareService() {


return this.carCareService;
}

public void setCarCareService(CarCareService carCareService) {


this.carCareService = carCareService;
}

Guardamos el archivo CarCareExpressApplication.

Ahora que tenemos creada la clase de tipo Application, necesitamos configurar la aplicación para que
Android instancie nuestra clase Application y no la clase por defecto. Para ello, abrimos para edición el
archivo AndroidManifest.xml.

En el archivo AndroidManifest.xml, ubicamos dentro del tag <application> la siguiente propiedad.

android:name="pe.edu.upc.carcare.CarCareExpressApplication"

De esta manera le decimos a Android que, al momento de instanciar la aplicación para ejecutarla, tome
nuestra clase CarCareExpressApplication como la clase Application.

Guardamos el archivo AndroidManifest.xml.

En los siguientes pasos nos enfocaremos en los activities, los cuales van a interactuar con la
funcionalidad que hemos implementado en las clases del modelo.

Implementando el comportamiento de Main Activity

Abrimos para edición el archivo MainActivity.java.


Adicionamos las siguientes líneas de declaración de atributos dentro de la clase MainActivity, antes
del inicio de la declaración del método onCreate().

TextView avgKilometersPerGallonTextView;
TextView totalLogsTextView;
TextView lastKilometersPerGallonTextView;
TextView totalGallonsTextView;

A continuación, adicionamos las siguientes líneas al final del método onCreate().

avgKilometersPerGallonTextView = (TextView)
findViewById(R.id.avgKilometersPerGallonTextView);
totalLogsTextView = (TextView) findViewById(R.id.totalLogsTextView);
lastKilometersPerGallonTextView = (TextView)
findViewById(R.id.lastKilometersPerGallonTextView);
totalGallonsTextView = (TextView) findViewById(R.id.totalGallonsTextView);

Las líneas adicionadas líneas arriba nos permiten vincular los widgets que existen en la interfaz de
usuario con variables que nos permitan establecer los valores para los indicadores en base a la
información en la base de datos.

A continuación, modificamos el bloque de código para el cuerpo del método onClick() de la clase
View.OnClickListener para el Floating Action Button fab, tal como se aprecia a continuación.

fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
startActivity(new Intent(getApplicationContext(), NewFuelUpActivity.class));
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});

Ello nos permite invocar al activity NewFuelUpActivity cuando el usuario toca el Floating Action
Button.

Luego de ello, vamos a adicionar un método refreshDashboard() que nos servirá para presentar los
valores de indicadores en pantalla. Utilizaremos una variable local service de tipo CarCareService y
los métodos que hemos implementado previamente para obtener indicadores.

Adicionamos el siguiente método a la clase MainActivity, antes del cierre de la declaración de la


clase.

private void refreshDashboard() {


CarCareService service = ((CarCareExpressApplication)
getApplication()).getCarCareService();

this.avgKilometersPerGallonTextView.setText(String.valueOf(service.getAvgKilometersPer
Gallon()));

this.lastKilometersPerGallonTextView.setText(String.valueOf(service.getLastKilometersP
erGallon()));
this.totalGallonsTextView.setText(String.valueOf(service.getTotalGallons()));
this.totalLogsTextView.setText(String.valueOf(service.getTotalLogs()));
Log.d("CarCareExpress", "Total Gallons: " +
this.totalGallonsTextView.getText().toString());
Log.d("CarCareExpress", "Avg Km/Gal: " +
this.avgKilometersPerGallonTextView.getText().toString());
Log.d("CarCareExpress", "Last Km/Gal: " +
this.lastKilometersPerGallonTextView.getText().toString());
Log.d("CarCareExpress", "Total Logs: " +
this.totalLogsTextView.getText().toString());

Nos corresponde ahora adicionar un Override del método onResume(), que se encargue de actualizar
el Dashboard de indicadores cada vez que se hace visible el activity. Adicionamos antes del cierre de
declaración de la clase MainActivity el siguiente método.

@Override
protected void onResume() {
super.onResume();
refreshDashboard();

Con estas modificaciones y adiciones, queda establecido el comportamiento para MainActivity.

En los siguientes pasos enfocaremos nuestros esfuerzos en el comportamiento de


NewFuelUpActivity.

Adicionando comportamiento a NewFuelUpActivity

Para implementar el comportamiento de NewFuelUpActivity, vamos primero a adicionar los siguientes


atributos a la clase, que servirán de referencia para los widgets de la interfaz. Adicionamos las siguientes
líneas dentro de la declaración de la clase NewFuelUpActivity, antes de la declaración de métodos.

Button addButton;
EditText lastOdometerEditText;
EditText costPerGallonEditText;
EditText gallonsEditText;

A continuación, ubicamos el método onCreate() y adicionamos al final del método las siguientes líneas.

addButton = (Button) findViewById(R.id.addButton);


lastOdometerEditText = (EditText) findViewById(R.id.lastOdometerEditText);
costPerGallonEditText = (EditText) findViewById(R.id.costPerGallonEditText);
gallonsEditText = (EditText) findViewById(R.id.gallonsEditText);
addButton.setOnClickListener(new View.OnClickListener() {

@Override
public void onClick(View v) {
((CarCareExpressApplication) getApplication()).getCarCareService().addFuelUpEntry(
lastOdometerEditText.getText().toString(),
costPerGallonEditText.getText().toString(),
gallonsEditText.getText().toString());
finish();
}
});

Como podemos apreciar, estas líneas de código realizan lo siguiente. Por un lado se establecen los
vínculos entre las variables previamente declaradas y los widgets de la UI. Por otro lado, se establece el
comportamiento cuando el usuario hace touch en el Button addButton, que incluye el invocar al método
addFuelEntry() para realizar una adición de registro a la base de datos. Finalmente se invoca al método
finish() del activity para cerrarlo y retornar a MainActivity.
Presionamos Ctrl+R (ó Cmd+R en Mac) para iniciar la ejecución de la aplicación. Luego de seleccionar el
AVD (Android Virtual Device) de ejecución, notamos que el comportamiento de la aplicación se
desarrolla como hemos previsto.

Se presenta MainActivity mostrando los valores actuales de indicadores y al hacer touch sobre el
Floating Action Button, se presenta NewFuelUpActivity para permitir al usuario registrar una entrada de
consumo de combustible. Al retornar a MainActivity observamos que los indicadores se actualizan en
base a la nueva información.

En este tutorial hemos aprendido:

• Cómo incorporar el soporte para almacenamiento en bases de datos locales.


• De qué manera acceder a datos vía un Object Relational Mapping Framework.
• Cómo se implementa consultas personalizadas sobre una base de datos SQLite.