Intégrer un SDK natif avec Flutter - Faire un passe-plat
En ce moment, je dois intégrer des SDKs natifs Android et iOS dans une application développée avec Flutter. L’un des SDK est distribué via un cocoapods privé, l’autre sous la forme d’une librairie java. C’est un exercice intéressant. J’ai envie de partager le cheminement que l’on a fait avec mes pairs, et expliquer pourquoi on a terminé par faire un simple passe-plat.
Une recette native
J’enfonce peut-être une porte ouverte, mais ça me semble important à expliciter. Plutôt que de se lancer directement dans la création des pont tout en intégration les SDKs, on a décidé de commencer par tester simplement les SDKs en créant des applications natives.
Un nouveau projet sur iOS et un nouveau pour Android donc, qui importent chacun le SDK correspondant. Rien de plus. Deux raisons à cela.
Premièrement, on n’était pas sachant sur la création du pont, c’était la première fois qu’on devait le faire. On voulait éviter d’apprendre deux choses en même temps (le fonctionnement d’un pont, le fonctionnement du SDK). Là, on reste focus sur une seule chose et on avance par petites étapes.
Deuxièmement, on voulait restreindre le nombre de problèmes au minimum. Lorsque ça ne marchait pas, ça pouvait venir soit de notre code natif, contenant d’ailleurs peu de lignes, soit du SDK. On s’est enlevé de nombreux doutes. “Est-ce que ça déconne à cause du code Flutter ? De notre façon de faire le pont ? Ou alors de notre code natif ? Ou du SDK ? Ou peut-être bien de notre projet ?". Non, tout ce bruit, on veut l’éviter.
Et on a bien fait. On a pu repérer des problèmes provenant de notre partenaire. Même si on a d’abord douté de notre façon d’utiliser le SDK, on a rapidement éliminé ces pistes au vu du peu de lignes de code que l’on avait dans nos applications créées pour l’occasion.
Un pont Flutter <> Natif
Vient maintenant de la recherche pour créer ce fameux pont entre du code Flutter et du code natif. Toujours dans de nouvelles applications, ET sans utiliser les SDKs pour le moment. Mêmes raisons qu’au-dessus, on voulait se concentrer sur une chose à la fois et diminuer les sources d’erreurs.
On a fouillé les internets, et on a débusqué plusieurs options pour faire discuter ensemble Flutter et le natif :
- Platform channels
- dart:ffi (Android et iOS)
- Pigeon
Notre préférence est allée vers les Platform channels, qui nous semblaient plus adaptés à notre besoin et assez simple d’usage.
Intégration du SDK dans le projet
Le temps d’intégrer les SDKs natifs directement dans notre application Flutter est ensuite arrivé. Nous étions serein avec nos expérimentations et apprentissages.
Pour donner un peu de contexte (simplifié), on devait intégrer un SDK de messagerie. Disons qu’il permet d’identifier un utilisateur via son token, d’ouvrir un salon de discussion, d’envoyer un message et de voir les nouveaux messages arriver.
On a utilisé un MethodChannel
et un EventChannel
. Le premier nous sert de donneur d’ordres, il contrôle le SDK depuis notre code Flutter. Le second nous permet de faire transiter des flux de données du SDK vers Flutter - les messages du chat.
On a un repository qui cache la complexité du SDK. Les deux channels ne se connaissent pas, ils ne parlent qu’à l’instance de ce repository.
Tout fonctionne bien, on avance rapidement. Mais je commence à écrire un morceau de code côté iOS qui ne me plaît pas.
Pour voir les nouveaux messages arriver, il faut ouvrir un salon de discussion. Et pour ouvrir un salon, il faut être connecté. Ce séquencement, je l’écris en swift, donc nativement sur iOS. De base, ce qui m’intéresse fonctionnellement, c’est de voir les messages. Le reste doit être transparent pour l’utilisateur. Je n’expose à Flutter qu’un simple démarrer l'écoute des messages
.
Tous ces appels de fonctions sont bien entendus asynchrones. Je target iOS 11, donc pas de async/await
. Je ne vais pas ajouter RxSwift
, PromiseKit
ou une autre librairie juste pour ça non plus. Je vous laisse imaginer la cascade de closure qui se forme. J’essaie d’éclater en plusieurs fonctions, mais ce n’est pas idéal.
Ah, et chaque appel peut échouer. Donc il faut gérer des erreurs à chaque niveau. Ok mais on passe pars un seul et même callback pour répondre à Flutter, comment je fais pour différencier les erreurs ? Est-ce que même j’ai envie de différencier ?
Vers un passe-plat
C’est à ce moment que je me dis “et si on déportait tout ça vers Flutter ?", “et si on ne faisait qu’un passe-plat ?". Mais oui. Tout simplement.
Notre pont ne sert plus que d’interface pour parler au natif. Il fait passe-plat. Côté natif, on expose les fonctions dont a besoin uniquement et telle quelle, rien d’autres. Côté Flutter, on orchestre, on a de la logique.
Ce que j’aime bien avec cette solution, c’est qu’on tend vers du zéro logique côté natif.
On écrit moins de code en doublon (Android et iOS). Ce qui est quand même une des raisons d’être d’une application en Flutter, autant continuer en ce sens. S’il y a une modification de la logique, on ne le fera qu’à un seul endroit - côté Flutter.
On profite de la modernité de Dart et des packages que l’on a ajouté à notre projet pour développer la logique. J’ai par exemple pu jouer avec le async/await
proposé naturellement par le langage Dart.
On capitalise sur l’expertise Flutter des développeurs. Certain n’ont jamais développé sur Android ou iOS, voire aucun des deux. Pour autant, ils maîtrisent Dart et l’écosystème de Flutter.
Et c’est enfin à ce moment-là que je me dis “et si j’écrivais un article ?". La boucle est bouclée.