Dans la première partie de cet article, je présentais la partie organisationnelle des projets mobiles du Monde, et notamment comment avec une super équipe et un peu d'attention aux utilisateurs, on est parvenu à voir notre application dans le top des applications Françaises sur le store.
Dans cette seconde partie, je m'attache à décrire notre organisation au sein de l'équipe Android (gestion des sources, revues de code, releases, ...), ainsi que des aspects plus techniques (architecture, outillage, ...).
La chose essentielle lorsqu'on parle de développement, c'est le partage des sources. Je ne vais pas revenir sur l'intérêt d'avoir un gestionnaire de source mais sur l'utilisation qu'on en a fait et l'organisation associée.
Au Monde, nous utilisons github (dépôts privés). Basé sur git, maintenant très répandu, github a l'avantage d'être hors du SI et donc de pouvoir travailler hors des locaux si besoin. Bien entendu, github apporte de nombreuses fonctionnalités dont quelques unes nous ont été bien utiles.
Sur notre mode de travail, les points importants sont les suivants :
Remarque : Il y a les défenseurs du trunk based development et ceux qui sont plutôt feature branch. Je vous invite à faire la recherche google "feature branch vs trunk based development" pour vous rendre compte du combat acharné :
Sur ces points, nous avons eu deux phases assez différentes :
Au delà des revues de code, nous nous efforçons de couvrir le maximum de code (métier, utilitaires) par des tests unitaires ou d'intégration. Si c'était moins vrai au début du projet Actu En Continue, ça l'est de plus en plus (notamment sur la Matinale que nous avons démarré en janvier et parue le 11 mai), et c'en est presque devenu un jeu, "celui qui aura la plus grosse... couverture".
Sur l'application Actu En Continu, dont l'architecture initiale n'était pas très structurée, la couverture globale est de 30% de 38000 loc (~720 tests). Sur l'application La Matinale, très orienté MVP, la couverture est pratiquement de 60% de 10500 loc (480 tests). Je reviendrai sur MVP un peu plus loin...
Techniquement, on utilise des frameworks assez standards sur Android, à savoir JUnit et Robolectric. Ces deux outils ont l'avantage de s'exécuter sur une JVM et d'être ainsi beaucoup plus rapides à s'exécuter.
Easymock, bien connu sur java, nous permet de mocker et rendre nos tests plus unitaires et indépendants.
Plus récemment, nous avons décidé de mettre en place des tests d'interface avec espresso 2 pour valider certains parcours nominaux (ouvrir une rubrique, parcourir les articles, en ouvrir un, ...). Le principal intérêt d'espresso par rapport à Robotium ou Selendroid est la synchronisation avec le thread UI. Sur Robotium, il est nécessaire de faire des wait pour attendre que l'interface soit à jour avant de continuer les tests. Parfois (souvent ?), même après avoir attendu, l'interface n'est pas à jour et les tests échouent. C'est quelque peu irritant de ne pas pouvoir faire confiance à ses tests... De son côté, espresso continue l'execution des tests lorsque le thread UI est disponible. De même il gère nativement les AsyncTask. Par contre si vous avez des traitements longs et que vous n'utilisez pas les AsyncTask, vous devrez définir des Idling Resources, typiquement pour attendre le retour d'un web service. Pour l'instant, nous n'avons posé que quelques tests de ce type mais ça reste un objectif à court/moyen terme.
Là encore, deux outils bien connus nous ont été fort utiles. Jenkins d'une part, comme moteur de notre intégration continue, construisant et testant notre application à chaque commit. SonarQube d'autre part, nous fournissant un rapport complet de la qualité du code (problèmes potentiels, couverture des tests, ...).
Dernièrement (mars), nous avons également mis en place des builds Travis qui valident nos pull request avant de les merger. Avec l’intégration à github, on obtient quelque chose de très visuel et d'assez pratique. Ca facilite le travail par feature branch dont je parlais plus haut, en constituant en quelque sorte un unbreakable merge (cf. unbreakable build dont je parlais sur ce blog en 2008...).
À ces outils de plus en plus standards (et je dirai même indispensables), s'ajoute un autre qui nous permet d'aller plus loin et de faire de la livraison continue (continuous delivery). Il s'agit de HockeyApp (équivalent à Appaloosa). Il nous permet de mettre à disposition la fameuse dogfood dont je parlais plus haut. À chaque build Jenkins réussi, la version la plus récente de l'application est poussée à tous les utilisateurs ayant accepté de jouer au cobaye.
Nous aurions pu aller plus loin en faisant de la mise en production en continue, avec le plugin Jenkins Google Play Android Publisher sur le build de notre branche master (production). Nous ne l'avons pas fait pour la simple et bonne raison que nous souhaitons faire une dernière passe de tests manuels et notamment un test de montée de version avant le déploiement.
Sur l'application Actu En Continu, nous n'avions pas d'architecture applicative à proprement parler. Nous utilisions des "managers" qui contiennent l'essentiel du code métier et des frameworks pour nous aider à découpler notre code (dagger et otto notamment; j'y reviendrai), mais il subsiste beaucoup de code dans nos Fragments, ce qui rend difficile les tests unitaires.
Sur l'application La Matinale, nous avons décidé dès le départ d'utiliser le pattern MVP (Model View Presenter). Ce pattern a l'avantage de manipuler la vue au travers d'une interface, rendant plus facile les tests unitaires puisqu'il devient possible de mocker cette vue et d'être agnostique du SDK Android, du Context, ...
Couplé à ce pattern, nous utilisons Dagger pour l'injection des dépendances. Je vous épargne l'explication du concept, prenons un exemple simple :
La fameuse interface pour notre vue :
public interface ArticleView {
void displayArticle(final Article article);
void displayError();
}
Et le fragment (ca pourrait aussi être une activité) qui implémente cette interface :
public class ArticleFragment implements ArticleView {
@Inject
ArticlePresenter mArticlePresenter;
@Override
public void onAttach(final Activity activity) {
super.onAttach(activity);
final ArticlesModule articlesModule = new ArticlesModule();
articlesModule.setArticleView(this);
// https://gist.github.com/doridori/602f00e37fdeeac7756d
DaggerHelper.injectWithScopedModules(this, Arrays.<DaggerModule>asList(articlesModule));
}
@Override
public void displayArticle(final Article article) {
// fill article webview and others fields
}
@Override
public void displayError() {
// display an error message / toast / whatever
}
}
Notre presenter :
public interface ArticlePresenter {
void load(final long articleId);
}
Et son implémentation :
public class ArticlePresenterImpl implements ArticlePresenter {
private ArticleView mArticleView;
private ItemsManager mItemsManager;
public ArticlePresenterImpl(final ArticleView articleView, final ItemsManager itemsManager) {
mArticleView = articleView;
mItemsManager = itemsManager;
}
@Override
public void load(final long articleId) {
// load data from cache, database or anywhere
Article article = mItemsManager.loardArticle(articleId);
if (article == null) {
mArticleView.displayError();
} else {
mArticleView.displayArticle(article);
}
}
}
Et le boilerplate code dagger pour que la magie opère :
@Module(
injects = {
ArticleFragment.class
},
addsTo = AndroidModule.class,
complete = false
)
public class ArticlesModule implements DaggerModule {
private ArticleView mArticleView;
public void setArticleView(final ArticleView articleView) {
mArticleView = articleView;
}
@Provides
public ArticlePresenter provideArticlePresenter(final ItemsManager itemsManager) {
return new ArticlePresenterImpl(mArticleView, itemsManager);
}
}
Si nous avons évolué sur notre manière d'architecturer l'application, nous avons également évolué sur notre usage de Dagger. En effet, sur l'Actu En Continu, nous avions un unique module, contenant tous nos Provides, toutes les classes injectées, bref le mega module. On peut donc injecter tout, partout, tout le temps... Si c'est plus simple à l'usage, c'est par contre très consommateur de mémoire puisque toutes les classes fournies par ce module sont instantiées.
Sur l'application La Matinale, nous sommes partis sur des modules plus fins (comme dans l'exemple ci-dessus), avec des scopes limités. Par exemple un module pour un pan fonctionnel précis, voire pour un écran. Cela permet au GC de faire le ménage lorsqu'on quitte ce pan fonctionnel ou cet écran et réduire un peu l'empreinte mémoire.
Par ailleurs, toujours dans notre objectif de tester plus, Dagger met à disposition une option pour surcharger un objet providé (overrides = true
, malheureusement, cette option n'existe plus dans Dagger 2). Si on reprend notre exemple ci-dessus on peut par exemple surcharger la classe ItemsManager pour tester unitairement le presenter :
public class ArticlePresenterTest {
@Before public void setUp() {
DaggerHelper.initWithTestModules(new TestModule());
}
@Module(
addsTo = AndroidModule.class,
overrides = true
)
static class TestModule {
@Provides @Singleton ItemsManager provideItemsManager() {
return new ItemsManagerMock(); // use easymock, mockito, ... or your own mock
}
}
}
Pour compléter notre boîte à outils, nous utilisons également Otto, fonctionnant sur le principe du publish & subscribe, et qui permet d'envoyer des événements sur un bus. Cela nous évite les nombreux listeners passés un peu partout et permet donc de découpler le code encore un peu plus.
Un bémol tout de même car si cela semble magique, ça peut être source d'arrachage de cheveux. Quelques cas d'usage dont il faut se méfier :
getUserVisibleHint()
.startActivityForResult()
est peut-être plus approprié dans ce cas.Outre Dagger et Otto qui sont très pratique pour découpler le code au maximum, nous avons utilisé d'autres frameworks pour nous faciliter la vie :
@InjectView
) et leur manipulation (@OnClick
).En un an (février 2014 - février 2015), nous sommes passé de 1.2 millions à plus d'1.7 millions d'utilisateurs actifs (environ 250 000 visiteurs uniques et plus de 2 000 000 de pages vues par jour).
Sur la même période, la note a progressé de pratiquement 0.5 point, passant de 3.6 à 4.06 étoiles, pas si simple quand il s’agit de la moyenne de plusieurs dizaines de milliers de notes.
Bien entendu, s'appeler Le Monde aide beaucoup à générer du trafic, et cela permet aussi d'avoir une aide précieuse de la part de Google (préconisations, invitations à des événements, support...). Mais avoir une application de qualité permet de le conserver, d'augmenter sa visibilité et la satisfaction utilisateurs, et pourquoi pas, de faire partie de la sélection Google des meilleures applis de l'année.