Android e Docker, la strana coppia

Affrontare l’emulazione Android e uscirne vivi

(English version)

Se sei un Guru con poca voglia di leggere vai direttamente al repository: https://github.com/ccarcaci/andocker

Quante volte ci siamo trovati nella condizione in cui prima di iniziare a sviluppare anche solo una riga di codice o un semplice esempio era necessario seguire infiniti tutorial per configurare l’ambiente di sviluppo e debug?

Spesso questi tutorial, presi singolarmente, consistevano in pochi “semplici” e “veloci” passi. Da buon neofita ho iniziato ad essere un assiduo frequentatore di questi “semplici” e “veloci” tutorial. Salvo poi scoprire che messi uno di seguito all’altro diventavano un fardello poco manutenibile.

Un esempio molto significativo è rappresentato dallo sviluppo in React Native per Android. Per poter testare il codice scritto in un ambiente emulato gli attori coinvolti sono:

          +---------------+         +---------------+
| Code +---------+ Expo |
| | +----+ |
+-------+-------+ | +-------+-------+
| | |
| | |
+-------+-------+ | +-------+-------+
| js libraries | | | ADB |
| | | | |
+-------+-------+ | +-------+-------+
| | |
| | |
+-------+-------+ | +-------+-------+
| $ npm +----+ | Emulator |
| | | |
+-------+-------+ +-------+-------+
| |
| |
+-------+-------+ +-------+-------+
| npm | | Android SDK |
| registry | | |
+---------------+ +-------+-------+
|
|
+-------+-------+
| Android |
| Device |
+---------------+

(Mi perdoneranno i guru dello sviluppo mobile per eventuali imprecisioni)

Riassumendo, con qualche link utile.

  • Codice: IDE + git
  • npm: librerie js e npm registry
  • Expo: packager e debug
  • adb: ponte verso l’emulatore
  • IntelliJ + Android Plugin: usando già la CE di Intellij era insensato usare Android Studio per il solo emulatore
  • Android SDK Tools: fondo pagina

Ognuno di questi strumenti ha il suo tutorial o manuale per l’installazione e configurazione:

Inoltre la configurazione di un ambiente del genere necessita di numerosi passaggi e spesso i pacchetti da installare e le loro dipendenze creano conflitti con quanto già presente sul sistema di sviluppo. Ad esempio ho provato Genymotion ma dava un errore di emulazione di cui ancora non sono riuscito a capire le cause nonostante interminabili ore passate su StackOverflow.

Per evitare in parte il problema si può usare un dispositivo fisico, in questo caso si possono saltare i tutorial sull’utilizzo dell’emulatore Android e usare direttamente l’app di Expo. Questo però comporta un continuo “switch” tra lo sviluppo e il dispositivo mobile che a lungo andare può diventare fastidioso (e poco “Lean”) e quindi è preferibile avere un emulatore.


Tutto questo è molto distante dal desiderata che idealmente dovrebbe essere:

  • Codice (TDD)
  • IDE
  • *Magia* che esegue il codice

Purtroppo se per sistemi Windows o Mac non è così difficile installare delle soluzioni di emulazione Android, su sistemi Linux (Ubuntu) la situazione si fa più complessa. Senza considerare che per qualsiasi sistema operativo è sempre necessario spendere del tempo per installare e configurare l’intero ecosistema. E ogni aggiornamento dei pacchetti potrebbe comportare problemi.

Quindi, per soddisfare i desiderata esposti sopra quello che serviva era poter sviluppare serenamente il codice e poi, tramite un comando, lanciare l’esecuzione del codice scritto. O, ancora meglio, avere un meccanismo che mostrasse le modifiche fatte durante lo sviluppo.


Quello che ho provato a fare

Ovviamente prima di arrivare ad una soluzione ho percorso una strada per nulla lineare.

  • Shell script: ho preso i vari tutorial e li ho tradotti in script. Ok, il processo è automatizzato ma il problema delle dipendenze col sistema operativo host non era risolto. Inoltre lo shell scripting non è il mio forte e non è certo la prima scelta che farei per qualunque sviluppo che superi le 20 righe. Ho comunque creato uno script di poco più di 20 righe, “shame on me” (cit.), che installa l’intera suite Android e avvia l’emulatore. Ci sto ancora lavorando, ma è disponibile qui: https://github.com/ccarcaci/android-emu
  • Dockerfile singolo: sicuramente come soluzione è minimale ma purtroppo l’estensione dell’immagine dell’emulatore a cui ho fatto riferimento (https://github.com/budtmo/docker-android) aggiungendo il resto dell’ecosistema non è di fatto stata possibile. Mi piacerebbe approfondire la questione con il creatore dell’immagine perché non ho capito il meccanismo interno di esecuzione

Quello che effettivamente ha portato qualche risultato

Wise man says: “Microservices lesson can help us”
Una (bruttissima) applicazione in esecuzione

Tipicamente, specie in un ecosistema a microservizi, per limitare e semplificare la configurazione e l’esecuzione dei servizi viene in aiuto Docker. In particolare tramite docker-compose è possibile specificare l’intero ecosistema che eroga il servizio.

Perché non usare la stessa tecnica per migliorare lo sviluppo su React Native?

Non rimane quindi che provare ad usare docker-compose e collegare, tramite ADB, il container con l’emulatore.

Ovviamente ho dovuto usare qualche script shell, come tutte le soluzioni Docker che si rispettino

Come è fatto

Scaricando il codice dal repository:

$ git clone git@github.com:ccarcaci/andocker.git

Si noteranno due cartelle: /code e /ecosystem.

Nella prima cartella c’è il codice (React Native) che verrà poi eseguito nell’emulatore. Nella seconda (/ecosystem) c’è il codice che costituirà l’intero ecosistema Docker.

L’ecosistema Docker è avviato dallo script compose.sh. Questo script imposta inizialmente la versione dell’emulatore Docker

export butomoVersion=8.1

In seguito ferma eventuali istanze in esecuzione della composizione Docker e ripulisce eventuali immagini Docker non utilizzate. Quindi costruisce la composizione Docker e la avvia.

docker-compose build --build-arg butomoVersion=$butomoVersion emulator expo
docker-compose up -d

L’avvio della composizione Docker impiegherà qualche minuto al primo avvio (o in seguito ad aggiornamenti) per poter scaricare ed installare le immagini Docker. Dopo il primo avvio ci vorrà comunque circa un minuto per avviare l’immagine Docker con l’emulatore e quella con il codice dell’applicazione e il relativo packager Expo.

Terminato l’avvio della composizione Docker viene aperta una finestra di Chrome che si collega al server NoVNC (http://localhost:6080) esposto dall’emulatore Android nel container Docker.

Aprendo il file docker-compose.yml si nota come la composizione racchiude l’emulatore Android (https://github.com/butomo1989/docker-android) e l’immagine Docker con tutte le dipendenze per lo sviluppo React Native.

Il file Dockerfile-expo contiene la specifica dell’immagine Docker per l’ecosistema React Native. Questa è basata sull’immagine di node a cui viene aggiunto platform-tools di Android necessario all’esecuzione di ADB (Android Debug Bridge) che consente di avviare tramite Expo l’applicazione React Native collegandosi all’emulatore.

L’avvio dell’applicazione è eseguito dallo script run-expo-app.sh che installa le dipendenze tramite npm, attende 20 secondi per l’avvio dell’emulatore, crea il ponte (ADB) e avvia l’applicazione tramite Expo.

Qualche problemino

In fase di risoluzione

Purtroppo l’emulatore Android non è così stabile, ogni tanto si riavvia, sto indagando le motivazioni.

Per avviare l’applicativo npm installa il packager Expo sul dispositivo emulato, all’installazione viene mostrato un fastidioso messaggio relativo ai permessi.

Non è nulla di insormontabile, bisogna premere “OK” nel popup, abilitare la funzione “Allow display over other apps” e premere “back” nell’angolo in basso a destra del telefono.

[FIXED! v0.0.2] Il codice dell’applicazione React Native viene copiato in fase di creazione della composizione Docker, sto lavorando per poter condividere un volume tra la cartella /code e l’immagine Docker in modo da abilitare and l’hot-reload.

Infine l’attesa di 20 secondi nello script di avvio dell’applicazione React Native va eliminata, l’ideale sarebbe avere un sistema di notifica di avvio dell’emulatore, ma è un obiettivo veramente complesso.

Vantaggi

Il primo chiaro vantaggio è che eseguendo uno script viene avviata l’applicazione su un emulatore ed è possibile vederne i cambiamenti istantaneamente. Parallelamente a quanto avviene in ambienti CI/CD, avendo un processo automatizzato non bisogna perdere tempo nell’eseguire comandi con la possibilità di sbagliare.

Aderendo alla filosofia Infrastructure as Code un secondo vantaggio è che qualora ci fossero dei cambiamenti o degli aggiornamenti all’ecosistema di emulazione è sufficiente mettere mano al codice Docker o agli script e salvare i cambiamenti su git.

Infine questa soluzione è portabile. Usando Docker non si hanno più dipendenze col sistema host e non bisogna perdere tempo per riconfigurare ogni macchina di sviluppo. Tutto l’ecosistema è inoltre descritto tramite codice ospitato su Github e quindi è ampiamente trasferibile, al netto delle immagini Docker di cui non dovremo comunque preoccuparci del caricamento, ma sarà la rete a farlo per noi.

Questo progetto è in continuo sviluppo e miglioramento, aspetto da tutti voi fork del mio progetto su Github e contributi!