lunes, 9 de marzo de 2015

Dos versiones de la misma aplicación con Android Studio Flavors





Logo Android Studio
Tras un largo tiempo sin publicar nada por falta de tiempo, vengo a hablar de cómo crear dos versiones de la misma aplicación con Android Studio, es algo común a la hora de desarrollar una aplicación el hacer dos versiones de la misma, una gratuita con publicidad y una de pago por ejemplo. Lógicamente no vamos a hacer dos aplicaciones diferentes ya que a la larga el mantenimiento se haría muy complicado.
Para cumplir nuestro cometido utilizaremos la herramienta "Flavors" de Android Studio que está pensada y diseñada para ello, utilizando esta forma de trabajar será el propio Gradle el que se encargue de crear "las dos versiones" de la aplicación, pero nosotros sólo tendremos que mantener una, lo que sea diferente en alguna de las dos versiones se realiza en esa versión y listo.
Antes de Android Studio si utilizabas Eclipse probablemente hayas hecho esto mismo utilizando librerías, es decir, creabas una librería que contenía el código común a las dos versiones y lo que fuese independiente de cada una lo hacías en la versión correspondiente, esto es algo parecido, sólo que simplificado e ideado para hacer el trabajo mucho más fácil.
Aquí veremos una sencilla aplicación de ejemplo que hará la función de calculadora de la cuál crearemos dos versiones diferentes, una que sólo puede sumar y restar (la gratuita) y otra que puede sumar, restar, multiplicar y dividir (la de pago), he de decir que no voy a comentar el código en sí del ejemplo que vemos a ver, comentaré lo que sea relevante para el tema en cuestión, el código propio de la aplicación es tan simple que no requiere comentarios.

Creamos un nuevo proyecto

Empecemos creando un nuevo proyecto al que llamaremos "TestingFlavorsCalc", en el nombre del paquete podemos poner lo que querramos (yo he puesto "com.dolphinziyo.android.testingflavorscalc") y lo demás lo dejamos por defecto.

Creamos los distintos Flavors

Una vez el proyecto ha sido creado y podemos empezar a trabajar lo que haremos será crear los distintos flavors, que llevarán el nombre de "free" y "pro", para ello entramos en las "Preferencias del módulo" presionando F4 o con el botón derecho encima de la zona blanca de la izquierda "Open Módule Settings".
En cuánto se abra, arriba a la izquierda veremos varias pestañas, entre ellas la que dice "Flavors", la abrimos y debajo presionamos en el "+" de color verde. Al hacerlo, el formulario de la derecha cambiará para que introduzcamos los datos específicos de éste, lo que haremos será cambiar el nombre por "free" y lo demás lo copiamos del otro flavor (defaultConfig), el nombre del paquete ("Application Id") debemos cambiarlo para que sea distinto al de la otra versión, si no al subirlo a la Play Store se pensaría que es la misma aplicación, recordemos que Google identifica la aplicación por el nombre del paquete. Yo lo que he hecho ha sido añadir "free" al final del nombre del paquete (quedando así: com.dolphin.android.testingflavorscalcfree).
Y repetimos el proceso anterior para la versión "pro", llamándola "pro" en vez de "free" y añadiéndole la coletilla al nombre del paquete. Le damos a "Apply" para que se apliquen los cambios y luego cerramos el diálogo pulsando en "Ok".

Cambiando entre una versión (Build Variants)

Para poder trabajar en una aplicación u otra es necesario cambiar el Build Variant ya que Android Studio sólo nos permite estar trabajando en único flavor, para ello a la izquierda de Android Studio abajo, vemos una pestaña en vertical que dice "Build Variants", le damos y se nos abre una pequeña ventanita.

Ahí podemos ver dos columnas "Module" y "Build Variant", pues pinchando en la primera línea, columna "Build Variant" (en "freeDebug" en la imagen anterior) se nos abre un menú desplegable que nos permite seleccionar cualquiera de las cuatro (por defecto) versiones de la aplicación, las dos de la gratuita (debug y release) y las dos de la pro (debug y release también).
Como dijimos antes Android Studio sólo permite trabajar con una por tanto si tenemos ahí seleccionado freeDebug sólo podremos trabajar en la versión "free", para hacer cambios en la "pro" tendremos que cambiarlo ahí.

Las carpetas necesarias para los flavors

Android Studio no crea las carpetas que necesitamos para las diferentes versiones ya que él no sabe cuál es el planteamiento que utilizaremos, por tanto tenemos que crearlas a mano, para ello, dentro de la carpeta "src" (dentro de "app"->"java") crearemos dos carpetas que tienen que llamarse igual que el nombre de los flavor para que Gradle puede identificarlos a la hora de compilar, así que le damos botón derecho a "src"->"New"->"Directory" y le llamamos "free" y lo repetimos para la versión "pro".

Como vemos hay cuatro carpetas:
  • androidTest: No la utilizaremos en este ejemplo, almacenaría los tests de la aplicación
  • free: Contendrá las ventanas y recursos independientes de la versión gratuita
  • main: Contiene y contendrá las ventanas y recursos comunes a las dos versiones de la aplicación
  • pro: Contendrá las ventanas y recursos independientes de la versión de pago

El código común a ambas aplicaciones

Para que se haga notorio que las dos aplicaciones son la misma pero con pequeños cambios antes de acceder a la calculadora habrá una ventana con dos botones que nos permitirá acceder a la calculadora o cerrar la aplicación, así que vamos a crearla.
Por comodidad voy a cambiar la forma en que se muestra la estructura del proyecto ya que para trabajar con flavors la vista por defecto no me parece cómoda, así que si tú también quieres hacerlo , arriba a la izquierda dónde pone "Android" pinchamos para abrir el desplegable y seleccionamos "Project".





Primero creamos el "Layout" que contendrá los botones editando el "activity_main" dentro de "main"->"res"->"layout" cuyo código quedaría:
  <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:orientation="vertical"
  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=".MainActivity">
 
  <Button
  android:id="@+id/btn_entrar_calculadora"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:text="@string/abrir_calculadora"/>
 
  <Button
  android:id="@+id/btn_salir"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:text="@string/salir"/>
</LinearLayout>
Nos mostrará dos errores y es porque no tenemos los strings necesarios, así que vamos a copiar los strings para evitar estos y futuros errores, para ello editamos el fichero "strings.xml" dentro de "main"->"res"->"values" dejándolo así:
  <resources>
    <string name="app_name">TestingFlavorsCalc</string>
    <string name="action_settings">Settings</string>
 
    <string name="num_1">Número 1</string>
    <string name="num_2">Número 2</string>
    <string name="sumar">+</string>
    <string name="restar">-</string>
    <string name="multiplicar">*</string>
    <string name="dividir">/</string>
    <string name="igual">=</string>
    <string name="abrir_calculadora">Calculadora</string>
    <string name="salir">Cerrar aplicacion</string>
  </resources>
Con los errores en el "activity_main.xml" ya resueltos vamos a editar el "MainActivity.java" que contendrá el comportamiento de los botones:
  package com.dolphinziyo.android.testingflavorscalc;
 
  import android.content.Intent;
  import android.support.v7.app.ActionBarActivity;
  import android.os.Bundle;
  import android.view.Menu;
  import android.view.MenuItem;
  import android.view.View;
  import android.widget.Button;
 
 
  public class MainActivity extends ActionBarActivity implements View.OnClickListener {
    Button btnEntrar, btnSalir;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
 
      btnEntrar = (Button) findViewById(R.id.btn_entrar_calculadora);
      btnSalir = (Button) findViewById(R.id.btn_salir);
 
      btnEntrar.setOnClickListener(this);
      btnSalir.setOnClickListener(this);
    }
 
 
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
      // Inflate the menu; this adds items to the action bar if it is present.
      getMenuInflater().inflate(R.menu.menu_main, menu);
      return true;
    }
 
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
      // Handle action bar item clicks here. The action bar will
      // automatically handle clicks on the Home/Up button, so long
      // as you specify a parent activity in AndroidManifest.xml.
      int id = item.getItemId();
 
      //noinspection SimplifiableIfStatement
      if (id == R.id.action_settings) {
        return true;
      }
 
      return super.onOptionsItemSelected(item);
    }
 
    @Override
    public void onClick(View v) {
      if (v == btnEntrar) {
        startActivity(new Intent(this, VentanaCalculadora.class));
      } else if (v == btnSalir) {
        finish();
      }
    }
  }
Ya tenemos la ventana principal creada, vemos que da un error al final, pero no tenemos que preocuparnos, es porque falta una clase que no hemos creado aun. Antes de ello vamos a crear un objeto que será el que haga la función de la calculadora, y como es común  a las dos versiones a pesar de que una no utilizará todos los métodos, lo crearemos también en la parte general (main), para ello pinchamos con el botón derecho del ratón en el paquete dentro de "main"->"java" y seleccionamos "New"->"Java Class", nos pedirá el nombre, así que ponemos "Calculadora".
Ahora vamos a implementar toda la logística de este objeto que consta de un constructor y cuatro métodos (sumar, restar, dividir y multiplicar):
  package com.dolphinziyo.android.testingflavorscalc;
 
  /**
  * Creado por dolphinziyo el 19/02/2015.
  * http://www.tecnogame.org
  * http://twitter.com/dolphinziyo
  */
 
  public class Calculadora {
    double num1, num2;
 
    Calculadora(double num1, double num2){
      this.num1 = num1;
      this.num2 = num2;
    }
 
    public double sumar(){
      return num1 + num2;
    }
 
    public double restar(){
      return num1 - num2;
    }
 
    public double multiplicar(){
      return num1 * num2;
    }
 
    public double dividir(){
      return num1 / num2;
    }
  }
Ya tenemos creado todo lo común a ambas versiones, así que vamos con el funcionamiento de los flavors pero antes aunque no esté creada la ventana de la calculadora (VentanaCalculadora) vamos a añadir al Manifest el "Activity" de esa ventana, para luego no tener errores por culpa de que no es capaz de encontrarla:
Así quedaría nuestro AndroidManifest:

Códificando los Flavors

  • Versión "Free"

Primero y antes que nada seleccionamos "freeDebug" en el "Build Variants" si no lo tenemos seleccionado ya.
Para esta versión vamos a necesitar un "Layout", un fichero "strings" y un código específico por lo que dentro de la carpeta "free" vamos a crear dos carpetas más, una "java" que contendrá el código y otra "res" (y dentro de esta una llamada "layout") que contendrá los recursos, para ello pulsamos botón derecho en "free"->"New"->"Directory" al que llamamos "java" y de nuevo botón derecho en "free"->"New"->"Android resource directory", en la ventana que se nos abre nos pide varias cosas, de "Directory Name" ponemos "layout" en "Resource Type" seleccionamos también "layout" y en "Source Set" seleccionamos "free", le damos a "Ok" y ya nos crea la carpeta "res" que contiene dentro la de los "layout".

Del modo que hicimos con la carpeta "layout" hacemos lo mismo para crear la de "values" ya que llevará algunos textos específicos, para ello botón derecho encima de "res"->"New"->"Android resource directory", y ponemos "values" en "Directory Name", seleccionamos "values" en el desplegable de "Resource Type" y "free" en el de "Source Set".
Ahora creamos el layout principal que llevará nuestra ventana en la carpeta "layout" creada antes en este mismo paso, al layout le llamaremos "calc" para ello botón derecho en la carpeta y "New"->"XML"->"Layout XML File":
  <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:orientation="vertical"
  android:paddingBottom="@dimen/activity_vertical_margin"
  android:paddingLeft="@dimen/activity_horizontal_margin"
  android:paddingRight="@dimen/activity_horizontal_margin"
  android:paddingTop="@dimen/activity_vertical_margin">
 
  <TextView
  android:text="@string/gratuita"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:layout_marginTop="10dp"
  android:layout_marginBottom="10dp"
  android:textSize="20sp"
  android:gravity="center"/>
 
  <LinearLayout
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:orientation="horizontal">
 
  <EditText
  android:id="@+id/et_num1"
  android:layout_width="0dip"
  android:layout_height="wrap_content"
  android:layout_weight="1"
  android:hint="@string/num_1"
  android:inputType="numberDecimal"
  android:gravity="center"/>
 
  <TextView
  android:id="@+id/tv_operacion"
  android:layout_width="0dip"
  android:layout_height="wrap_content"
  android:layout_weight="0.10"
  android:gravity="center"/>
 
  <EditText
  android:id="@+id/et_num2"
  android:layout_width="0dip"
  android:layout_height="wrap_content"
  android:layout_weight="1"
  android:hint="@string/num_2"
  android:inputType="numberDecimal"
  android:gravity="center"/>
 
  <TextView
  android:layout_width="0dip"
  android:layout_height="wrap_content"
  android:layout_weight="0.10"
  android:text="@string/igual"
  android:gravity="center"/>
 
  <TextView
  android:id="@+id/tv_resultado"
  android:layout_width="0dip"
  android:layout_weight="1"
  android:layout_height="wrap_content"
  android:gravity="center" />
</LinearLayout>
 
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
 
<Button
android:id="@+id/btn_sumar"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/sumar" />
 
<Button
android:id="@+id/btn_restar"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/restar" />
</LinearLayout>
</LinearLayout>
Si por casualidad muestra error en alguna de las siguientes líneas, cambiar momentáneamente al "Build Variant" "proDebug" u otro cualquiera y volver al "freeDebug":
xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
Vamos a crear también el fichero strings específico de la versión gratuita, para ello hacemos igual que con el layout "calc" sólo que esta vez pulsando encima de "values" y seleccionando "Values XML File", como nombre ponemos "strings", dentro contiene:

  <?xml version="1.0" encoding="utf-8"?>
  <resources>
    <string name="app_name">TestingFlavorsCalc Free</string>
    <string name="gratuita">Versión gratuita</string>
  </resources>
Sólo nos falta añadir el comportamiento de la ventana de la calculadora gratuita, así que dentro de la carpeta "free"->"java" creamos el paquete antes que nada, botón derecho "New"->"Package" y lo llamamos igual que el nombre del paquete principal (en este ejemplo "com.dolphinziyo.android.testingflavorscalc") y dentro del paquete creamos un nuevo "Java Class" que llamaremos "VentanaCalculadora", una vez creado sustituimos el código por el siguiente:

  package com.dolphinziyo.android.testingflavorscalc;
 
  import android.app.Activity;
  import android.os.Bundle;
  import android.view.View;
  import android.widget.Button;
  import android.widget.EditText;
  import android.widget.TextView;
 
  public class VentanaCalculadora extends Activity implements View.OnClickListener{
    Button btnSumar, btnRestar;
    EditText etNum1, etNum2;
    TextView tvResultado, tvOperacion;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.calc);
 
      btnSumar = (Button)findViewById(R.id.btn_sumar);
      btnRestar = (Button)findViewById(R.id.btn_restar);
 
      btnSumar.setOnClickListener(this);
      btnRestar.setOnClickListener(this);
 
      etNum1 = (EditText)findViewById(R.id.et_num1);
      etNum2 = (EditText)findViewById(R.id.et_num2);
      tvResultado = (TextView)findViewById(R.id.tv_resultado);
      tvOperacion = (TextView)findViewById(R.id.tv_operacion);
    }
 
    @Override
    public void onClick(View v) {
      double num1, num2;
      if(etNum1 != null && etNum2 != null){
        if(!etNum1.getText().toString().equals("")){
          num1 = Float.parseFloat(etNum1.getText().toString());
        }else{
          num1 = 0;
        }
        if(!etNum2.getText().toString().equals("")){
          num2 = Float.parseFloat(etNum2.getText().toString());
        }else{
          num2 = 0;
        }
      }else{
        num1 = 0;
        num2 = 0;
      }
      Calculadora calc = new Calculadora(num1, num2);
      if(v == btnSumar){
        tvResultado.setText("" + calc.sumar());
        tvOperacion.setText(getResources().getString(R.string.sumar));
      }else if(v == btnRestar){
        tvResultado.setText("" + calc.restar());
        tvOperacion.setText(getResources().getString(R.string.restar));
      }
    }
  }
Llegados a este punto ya podemos ejecutar la aplicación, la versión gratuita ya está hecha y completamente funcional, pero nos falta la versión "pro", así que…
  • Versión "pro"

Para no alargar más el artículo simplemente dejaré aquí los códigos de la versión "pro", ya que lo demás es igual que la versión "free", no olvides cambiar el "Build Variant" al "proDebug".
Tras crear las carpetas necesarias y el paquete para el código, les asignamos su comportamiento:
  • strings.xml:
  <?xml version="1.0" encoding="utf-8"?>
  <resources>
    <string name="de_pago">Versión de pago</string>
  </resources>
  • calc.xml:
  <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:orientation="vertical"
  android:paddingBottom="@dimen/activity_vertical_margin"
  android:paddingLeft="@dimen/activity_horizontal_margin"
  android:paddingRight="@dimen/activity_horizontal_margin"
  android:paddingTop="@dimen/activity_vertical_margin">
 
  <TextView
  android:text="@string/de_pago"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:layout_marginTop="10dp"
  android:layout_marginBottom="10dp"
  android:textSize="20sp"
  android:gravity="center"/>
 
  <LinearLayout
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:orientation="horizontal">
 
  <EditText
  android:id="@+id/et_num1"
  android:layout_width="0dip"
  android:layout_height="wrap_content"
  android:layout_weight="1"
  android:hint="@string/num_1"
  android:inputType="numberDecimal"
  android:gravity="center"/>
 
  <TextView
  android:id="@+id/tv_operacion"
  android:layout_width="0dip"
  android:layout_height="wrap_content"
  android:layout_weight="0.10"
  android:gravity="center"/>
 
  <EditText
  android:id="@+id/et_num2"
  android:layout_width="0dip"
  android:layout_height="wrap_content"
  android:layout_weight="1"
  android:hint="@string/num_2"
  android:inputType="numberDecimal"
  android:gravity="center"/>
 
  <TextView
  android:layout_width="0dip"
  android:layout_height="wrap_content"
  android:layout_weight="0.10"
  android:gravity="center"
  android:text="@string/igual" />
 
  <TextView
  android:id="@+id/tv_resultado"
  android:layout_width="0dip"
  android:layout_weight="1"
  android:layout_height="wrap_content"
  android:gravity="center" />
</LinearLayout>
 
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
 
<Button
android:id="@+id/btn_sumar"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/sumar" />
 
<Button
android:id="@+id/btn_restar"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/restar" />
 
<Button
android:id="@+id/btn_multiplicar"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/multiplicar" />
 
<Button
android:id="@+id/btn_dividir"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/dividir" />
</LinearLayout>
</LinearLayout>
  • VentanaCalculadora.java:
  package com.dolphinziyo.android.testingflavorscalc;
 
  import android.app.Activity;
  import android.os.Bundle;
  import android.view.View;
  import android.widget.Button;
  import android.widget.EditText;
  import android.widget.TextView;
 
  public class VentanaCalculadora extends Activity implements View.OnClickListener{
    Button btnSumar, btnRestar, btnMultiplicar, btnDividir;
    EditText etNum1, etNum2;
    TextView tvResultado, tvOperacion;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.calc);
 
      btnSumar = (Button)findViewById(R.id.btn_sumar);
      btnRestar = (Button)findViewById(R.id.btn_restar);
 
      if(BuildConfig.FLAVOR.equals("pro")){
        btnMultiplicar = (Button)findViewById(R.id.btn_multiplicar);
        btnDividir = (Button)findViewById(R.id.btn_dividir);
 
        btnMultiplicar.setOnClickListener(this);
        btnDividir.setOnClickListener(this);
      }
 
      btnSumar.setOnClickListener(this);
      btnRestar.setOnClickListener(this);
 
      etNum1 = (EditText)findViewById(R.id.et_num1);
      etNum2 = (EditText)findViewById(R.id.et_num2);
      tvResultado = (TextView)findViewById(R.id.tv_resultado);
      tvOperacion = (TextView)findViewById(R.id.tv_operacion);
    }
 
    @Override
    public void onClick(View v) {
      double num1, num2;
      if(etNum1 != null && etNum2 != null){
        if(!etNum1.getText().toString().equals("")){
          num1 = Float.parseFloat(etNum1.getText().toString());
        }else{
          num1 = 0;
        }
        if(!etNum2.getText().toString().equals("")){
          num2 = Float.parseFloat(etNum2.getText().toString());
        }else{
          num2 = 0;
        }
      }else{
        num1 = 0;
        num2 = 0;
      }
      Calculadora calc = new Calculadora(num1, num2);
      if(v == btnSumar){
        tvResultado.setText("" + calc.sumar());
        tvOperacion.setText(getResources().getString(R.string.sumar));
      }else if(v == btnRestar){
        tvResultado.setText("" + calc.restar());
        tvOperacion.setText(getResources().getString(R.string.restar));
      }else if(v == btnMultiplicar){
        tvResultado.setText("" + calc.multiplicar());
        tvOperacion.setText(getResources().getString(R.string.multiplicar));
      }else if(v == btnDividir){
        tvResultado.setText("" + calc.dividir());
        tvOperacion.setText(getResources().getString(R.string.dividir));
      }
    }
  }
Este es el resultado:
VERSIÓN FREEVERSIÓN PRO


Puedes descargar el proyecto con todo preparado y listo para ejecutar de mi Github
Y eso es todo, cambiando el "Build Variant" puedes ejecutar una versión de la aplicación u otra. Al generar la aplicación firmada se exportarán las dos versiones listas para publicar en Play Store, espero que este artículo haya sido útil y como siempre cualquier duda, sugerencia o comentario puedes dejarlo abajo.
Relacionado:

No hay comentarios:

Publicar un comentario

Jesús Moreno - Ingeniero Ténico Informático - consultor Informático

Hola, soy Jesús Moreno Ingeniero Técnico Informático en sistemas por la US y propietario de éste blog. Mi trabajo en los ultimos años se ...