Othello mobile en React Native

Julien Demangeon
Julien DemangeonNovember 30, 2016
#react#mobile#integration

J'entame déjà mon 2ème mois au sein des équipes Marmelab et les défis d'intégration sont toujours aussi passionnants. Cette semaine, il s'agissait de développer une application mobile du jeu d'Othello (optimisée pour Android) avec le framework React Native.

react native

A travers ce retour d'expérience, je vais présenter les tenants et les aboutissants d'un projet aussi passionnant qu'enrichissant.

Vous avez dit natif ?

La principale "contrainte" du projet était de développer l'application à l'aide du Framework React Native. En réalité, il ne s'agissait pas du tout d'une "contrainte" mais d'une expérience que je ne tarderais pas de reproduire dans mes futurs projets tant j'ai été surpris par cette technologie.

React Native est un framework assez récent (2015) développé par Facebook sur les bases de sa célèbre librairie Javascript React. Il permet de développer des applications pouvant être executées aussi bien sous Android que sous iOS avec des performances "natives".

Comme la plupart de ses concurrents directs (NativeScript ou Xamarin) sur le créneau du natif multi-plateforme, React Native propose:

  • Une expérience de dév. riche (ES6 comme pour NativeScript, C# pour Xamarin)
  • Une interface déclarative et native performante
  • Des possibilités de personnalisation des composants (proche du CSS)
  • Un code 100% open-source, avec une forte communauté

Tout comme NativeScript (dont la différence réside essentiellement dans l'utilisation d'Angular 2), React Native utilise un système de bridge entre un Thread Javascript (dans un moteur V8 pour Android et JavascriptCore pour iOS) et l'UI en asynchrone. Cela permet au code Javascript de l'application de s'exécuter sans aucune latence visuelle sur l'interface.

Ci-après le rendu d'une partie avec à droite, le défilement des différentes actions et des changements d'états associés.

othello native

L'API de ReactNative est très fournie, elle permet d'accéder indirectement à des fonctionnalités natives (communes ou propres à certaines plateformes) par la simple utilisation de modules.

Parmi les modules d'API (et composants) les plus intéressants, on retrouve par exemple:

En plus de l'API, React Native fournit une pléthore de composants intégrés ou déjà développés et partagés par la communauté. Très utile lorsque l'on ne souhaite pas réinventer la roue.

Cependant, React Native est en constante évolution, il est donc nécessaire de vérifier régulièrement si des composants ne sont pas déjà intégrés au Framework. Pour l'exemple, le composant Button n'était pas intégré au Framework lors du développement de l'application de mon collègue il y a un mois (je vous recommande d'ailleurs de lire son article) alors qu'il l'est aujourd'hui.

Offrir un framework c'est une chose, offrir une architecture, c'en est une autre. C'est pourquoi, comme vous pourrez le voir au chapitre suivant, j'ai fais le choix d'utiliser redux afin d'organiser ma logique de jeu.

Redux, pourquoi et comment ?

Plusieurs facteurs sont à l'origine de ma décision d'utiliser Redux afin d'organiser ma logique de jeu (déjà utilisé, forte communauté, ...). Le principal facteur reste cependant ma volonté de séparer au maximum les responsabilités au sein du code afin de le rendre le plus limpide et compréhensible possible.

redux

En effet, il aurait été assez malvenu que le placement des pions ou que le changement de joueur se fasse au sein de mon composant de jeu. Le rôle d'un composant est avant de "présenter" et "d'interfacer", il doit donc essentiellement pouvoir rendre un état de jeu visible (ex: afficher le board à un instant T) et fournir des possibilités d'intéragir avec cet état de jeu (ex: lancer une action de placement de pion au "clic" sur une proposition). Et ça, c'est tout à fait ce que permet de faire redux.

Redux se définit lui même comme un container d'état "prédictible" pour application javascript (tous environnements confondus). Par "prédictible", il entend fournir un état immutable ne pouvant être altéré que par l'intervention "d'actions" (ou "commandes"). Ce mode de fonctionnement permet de suivre l'ensemble des changements, de pouvoir les annuler mais également de rendre le code beaucoup plus "testable".

Voici par exemple un résumé de l'ensemble du code qui permet d'altérer l'état du jeu via le placement d'un pion.


    // Dans le composant

    handleCellClick = (cell) => {
        if (getCurrentPlayer(this.props.game).isHuman) {
            try {
                this.props.placeCellChange(cell);
            } catch (e) {
                alert(e);
            }
            this.props.checkComputerTurn();
        }
    }

    <Board onCellClick={this.handleCellClick} board={game.board} cellProposals={cellProposals} />

    // Dans "l'action creator"

    export function placeCellChange(cellChange) {
        return {
            type: PLACE_CELL_CHANGE,
            cellChange,
        };
    }

    // Dans le reducer

    export default (game, action = {}) => {
        switch (action.type) {
            ...
            case PLACE_CELL_CHANGE: {
                return tryPlayerSwitch(playCellChange(action.cellChange, game));
            }
            ...
        }
    };

Bien que suffisant pour la plupart des cas, il est parfois nécessaire de mettre en place un système permettant de gérer des "sides effects" sur des actions (ex: appel d'API au clic sur un bouton...). Pour cela, j'ai utilisé une librairie nommée "redux-saga".

À la rencontre de Saga

Le terme "Saga" provient initialement d'un article écrit par deux universitaires américains (Hector Garcia-Molina et Kenneth Salem) sur le thème de la gestion de process métier. Ce terme est habituellement utilisé pour décrire une portion de code chargée de coordonner des instructions (actions) entre différents contextes.

Pour en savoir plus à ce sujet et avoir des exemples, je vous invite à lire l'excellent article sur msdn d'où est tiré la phrase suivante:

The term saga is commonly used in discussions of CQRS to refer to a piece of code that coordinates and routes messages between bounded contexts and aggregates.

Dans le cadre de cette application, j'ai été amené à utiliser redux-saga. Il s'agit d'une librairie qui permet à l'aide d'un middleware redux de mettre en place des sortes de "listeners" sur des actions redux. Lorsque l'action écoutée est déclenchée, il est possible de lancer d'autres événements et/ou de coordonner d'autres actions en asynchrone.

Pour fonctionner, il est nécessaire de "démarrer" les listeners lors de l'initialisation de l'application puis de mettre en place des "sagas". C'est à dire des actions asynchrones attachées à d'autres actions.

Ci-dessous, la saga utilisée pour appeler l'intelligence artificielle du jeu afin qu'elle puisse jouer à son tour après l'action du joueur.

function* checkComputerTurn(action) {
  const game = yield select(state => state.Game);
  const currentPlayer = getCurrentPlayer(game);

  if (
    game.isFinished ||
    currentPlayer.isHuman ||
    !playerCanPlay(currentPlayer, game)
  ) {
    return;
  }

  const retrieveCellChange = (cellType, cells) => {
    return fetch(`http://192.168.0.31:8080/?type=${cellType}`, {
      method: "POST",
      body: JSON.stringify(cells),
    }).then(response => response.json());
  };

  try {
    const cell = yield call(
      retrieveCellChange,
      currentPlayer.cellType,
      game.board.cells
    );
    yield put(placeCellChange(createCell(cell.X, cell.Y, cell.CellType)));
  } catch (e) {
    alert(e);
  }
}

function* checkComputerTurnAsync() {
  yield takeEvery(CHECK_COMPUTER_TURN, checkComputerTurn);
}

Comme vous pouvez le voir, nous mettons en place un "listener" sur l'action "CHECK_COMPUTER_TURN" avec l'effet "takeEvery" qui permet de lancer la fonction checkComputerTurn pour chaque action reçue (d'autres effets existent, vous les trouverez ici).

Lors du déclenchement de cette action, les étapes suivantes s’enchaînent:

  • La méthode "checkComputerTurn" est déclenchée
  • Le "Game" courant est extrait à l'aide de l'instruction "select" fournie par redux-saga
  • On vérifie si l'AI doit et peut jouer
  • Un appel est effectué vers l'AI afin de récupérer le meilleur coup à jouer
  • L'action "placeCellChange" est de nouveau dispatchée avec le placement de l'IA

Comme vous l'aurez peut-être remarqué par le "yield", redux-saga est en grande partie basé sur les générateurs fournis par ES6.

Ce qui est magique avec les générateurs, c'est qu'ils offrent un contrôle total sur leur exécution tout en étant capables de retourner des résultats de façon continue. Ce qui résout pas mal de problèmes quand on sait que Javascript est entièrement monothreadé et que l'execution d'une fonction classique ne prend fin qu'une fois "return" ou "throw" atteint. De nombreux frameworks et librairies profitent de cet atout (ex: KoaJS (Framework Web), Co, etc).

Pour en apprendre plus sur redux-saga, je vous invite à lire cette article.

Méthodologie projet et organisation

Lors de ce projet, j'ai eu l'occasion de travailler avec un nouvel outil, "Trello". Pour ceux qui ne connaissent pas encore, il s'agit d'un outil inspiré par la méthode Kanban et qui permet d'organiser un projet sous forme de petites tâches représentées par des cartes. Les cartes peuvent être glissées d'une "planche" à l'autre en fonction de l'avancée du projet. Une "planche" est généralement un "état" dans lequel se situe la carte (exemple le plus courant: "A faire", "En cours", "Fait" ou "Validé").

trello

Chez Marmelab, il est courant de décrire chaque carte sous la forme d'une User Story. Par exemple dans le cadre de ce projet, voici les différentes "User Story" que j'ai été amené à traiter.

  • As Marcel, I want to start a new game
  • As Marcel, I want to play a game
  • As Marcel, I want to play against another player
  • ...

Et figurez vous que j'ai réussi à faire faire à Marcel tout ce qui était prévu ! Une nouvelle manière de travailler pour moi, et qui me convient plutôt bien.

J'ai également énormement appris de mon mentor concernant l'organisation des fichiers et du code. Il peut-être utile, par exemple:

  • De regrouper les tests avec les modules testés (suffixer avec un ".spec")
  • Ne pas créer d'actions inutiles (le "CHECK_COMPUTER_TURN" n'est pas nécessaire, une simple saga sur "PLACE_CELL_CHANGE" suffit et rend le code plus limpide)
  • Il est important de bien linter le code à l'aide de la même configuration que l'équipe afin d'homogénéiser le code (airbnb lint dans mon cas)
  • Éviter la duplication en créant des modules de ce qui peut l'être (ex: PropTypes de mes différents "objets".. Game, Cell, ... dans un même fichier)

Et surtout, j'ai découvert des facettes d'ES6 que je ne connaissais pas encore. Par exemple, il n'est pas possible de "yielder" lorsque nous ne sommes plus dans le contexte d'un générateur, ...

Conclusion

Cette semaine fut forte en découvertes, en plus d'avoir l'impression de découvrir ES6 de nouveau, j'ai eu l'occasion de tester React Native. Il est sûr qu'après cette expérience, je ne risque pas de développer des applications hybrides de nouveau tant le rapport "expérience de développement" / "résultat" est bon.

Concernant redux et saga, je ne regrette pas mes choix. La courbe d'apprentissage est plutôt longue, mais le résultat est réellement au rendez-vous, ce qui est plutôt encourageant.

Concernant mon intégration, je me sens de plus en plus à l'aise sur la méthodologie de projet utilisée, même s'il me reste encore des efforts à faire sur la taille de mes Pull Request pour réellement parvenir à des échanges plus rapides avec mes pairs.

En somme, une très bonne semaine ! Rendez-vous la semaine prochaine pour retrouver ce même jeu d'Othello, mais cette fois-ci en React et en Isomorphique (rendu à la fois du coté serveur et du coté client) !

Note: Le projet en disponible en open-source sur github: reversi-android

Did you like this article? Share it!