Lo scenario

Durante lo sviluppo e l’aggiornamento di Casualis si è palesata la necessità di realizzare una versione beta della mia app che mi permettesse di:

  1. installarla sul mio device senza sovrascrivere o rimuovere la versione release che ho installato tramite il PlayStore;
  2. modificare al volo alcune funzionalità rispetto alla versione release (ad esempio, disattivare il tracking di analytics).

La configurazione di default delle app Android ad oggi non permette nulla di tutto ciò ma, con un po’ di ricerca (e sopratutto un paio di dritte da StackOverflow) sono arrivato ad una soluzione.

Obiettivo 1: definire un nuovo build type

La prima parte della soluzione prevede di aggiungere un nuovo build type al file build.gradle dell’app:

     buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        beta {
            applicationIdSuffix '.beta'
            versionNameSuffix '-BETA'
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

Ovviamente il mio nuovo build type si chiama beta, ma il nome è assolutamente ininfluente.

Veniamo al file gradle: grazie agli attributi applicationIdSuffixversionNameSuffix è possibile definire una stringa da appendere all’applicationId e al versionName esclusivamente quando si effettua la build in versione beta. In questo modo la versione beta della nostra app verrà trattata come un’ app diversa (ha un applicationId differente), consentendoci di installarla senza sovrascrivere l’app originale.

Come utilizzare questo nuovo build type? Nella finestra di generazione dell’APK avremo a disposizione una nuova opzione:

screen

 

Obiettivo 1: raggiunto!

Obiettivo 2: definire risorse specifiche per il nuovo build type

A questo punto siamo in grado di eseguire la build dell’app in modalità release o beta, ma a conti fatti non cambia praticamente nulla tra le due versioni: come è possibile personalizzare il codice o le risorse per una delle due? Semplicissimo!

Definire classi Java specifiche per il nuovo build type

In una qualsiasi app Android le risorse e il codice che compongono l’app sono raccolti sotto la cartella app/src/main.

Per definire una classe Java specifica per un build type è necessario rimuovere la classe dalla cartella “main” e replicarla in una cartella allo stesso livello della cartella “main” per ogni build type disponibile. 

Nel mio caso specifico per esempio nella cartella main era presente la classe AnalyticsManager nel path:
app/src/main/java/ph/url/tangodev/randomwallpaper/analytics/AnalyticsManager.class

Per realizzare una versione specifica per la versione beta è stato necessario:

  • Spostare la classe AnalyticsManager nel path:
    app/src/release/java/ph/url/tangodev/randomwallpaper/analytics/AnalyticsManager.class;
  • Copiare e modificare la classe AnalyticsManager nel path:
    app/src/beta/java/ph/url/tangodev/randomwallpaper/analytics/AnalyticsManager.class;

Questo è il risultato:

package ph.url.tangodev.randomwallpaper.analytics;

import android.app.Application;
import android.util.Log;

import com.google.android.gms.analytics.GoogleAnalytics;
import com.google.android.gms.analytics.HitBuilders;
import com.google.android.gms.analytics.Tracker;

import ph.url.tangodev.randomwallpaper.MainActivity;
import ph.url.tangodev.randomwallpaper.R;

public class AnalyticsManager {
    private static Tracker tracker;

    synchronized public static void initTracker(Application application) {
        if (tracker == null) {
            GoogleAnalytics analytics = GoogleAnalytics.getInstance(application);
            // To enable debug logging use: adb shell setprop log.tag.GAv4 DEBUG
            tracker = analytics.newTracker(R.xml.global_tracker);
        }
    }

    public static void setCurrentViewAndSend(String currentView) {
        if(tracker != null) {
            tracker.setScreenName(currentView);
            tracker.send(new HitBuilders.ScreenViewBuilder().build());
        } else {
            Log.e(Utils.TAG, "Tracker non inizializzato");
        }
    }

    public static void sendEvent(String category, String action) {
        if(tracker != null) {
            tracker.send(new HitBuilders.EventBuilder()
                    .setCategory(category)
                    .setAction(action)
                    .build());
        } else {
            Log.e(Utils.TAG, "Tracker non inizializzato");
        }
    }
}

La classe è una semplice utility che utilizzo per inviare gli eventi a google analytics. Nella versione beta di questa classe gli eventi non vengono realmente inviati ma semplicemente mostrati nei log:

package ph.url.tangodev.randomwallpaper.analytics;

import android.app.Application;
import android.util.Log;

import com.google.android.gms.analytics.GoogleAnalytics;
import com.google.android.gms.analytics.HitBuilders;
import com.google.android.gms.analytics.Tracker;

import ph.url.tangodev.randomwallpaper.MainActivity;
import ph.url.tangodev.randomwallpaper.R;

public class AnalyticsManager {

    synchronized public static void initTracker(Application application) {
        Log.i(Utils.TAG, "DEBUG - Init GA tracker");
    }

    public static void setCurrentViewAndSend(String currentView) {
        Log.i(Utils.TAG, "DEBUG - Invio GA view: " + currentView);
    }

    public static void sendEvent(String category, String action) {
        Log.i(Utils.TAG, "DEBUG - Invio GA event: " + category + " - " + action);
    }
}

Definire risorse specifiche per il nuovo build type

In questo caso è più semplice: la risorsa originale può restare sotto la cartella main, basta posizione la risorsa specifica sotto la cartella beta. Nel mio caso ho voluto modificare il nome dell’app in modo da distinguere facilmente la versione beta nell’app launcher del dispositivo. La stringa in questione è presente nel file:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name" translatable="false">Casualis</string>
</resources>

Ho copiato il file sotto la cartella beta e l’ho successivamente modificato:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name" translatable="false">BETA Casualis</string>
</resources>

Questa operazione può essere svolta per qualsiasi tipo di risorsa.

Obiettivo 2: raggiunto!

La ciliegina sulla torta: utilizzare la versione beta anche per la build di debug

Ci siamo quasi: abbiamo due build type distinti con classi e risorse specifiche! A questo punto però sorge un altro problema: quando viene lanciata la build di debug (quella che viene utilizzata solitamente sull’emulatore) la versione beta viene ignorata: come utilizzare il build type beta anche nelle build di debug? Dobbiamo mettere ancora mano al file build.gradle ed aggiungere una nuova sezione:

    sourceSets {
        debug {
            manifest.srcFile 'src/beta/AndroidManifest.xml'
            java.srcDirs = ['src/beta/java']
            res.srcDirs = ['src/beta/res']
            assets.srcDirs = ['src/beta/assets']
        }
    }

Come si può immaginare tramite questa sezione si forza la versione di debug ad utilizzare le risorse specifiche della build beta. Ma, aspetta: il file manifest della versione beta non esiste! Non è un problema: durante la build il manifest viene unito con il manifest principale, funziona!

Conclusioni

Questa soluzione ha decisamente reso più agile il mio processo di test e rilascio dell’app, ma non è tutto! Grazie a questo approccio si possono realizzare build specifiche per qualsiasi scopo: ad esempio si può realizzare una versione “lite” dell’app nella quale vengono rimosse alcune funzionalità riservate alla versione a pagamento.

Alla prossima!