Excalidraw et Fugu: améliorer le parcours utilisateur principal

Toute technologie suffisamment avancée est indiscernable de la magie. À moins que vous ne les compreniez. Je m'appelle Thomas Steiner et je travaille dans les relations avec les développeurs chez Google. Dans cet article, je vais vous présenter certaines des nouvelles API Fugu et la façon dont elles améliorent les parcours utilisateur principaux dans la PWA Excalidraw. Vous pourrez ainsi vous inspirer de ces idées et les appliquer à vos propres applications.

Comment j'ai découvert Excalidraw

Je veux commencer par une histoire. Le 1er janvier 2020, Christopher Chedeau, ingénieur logiciel chez Facebook, a tweeté au sujet d'une petite application de dessin sur laquelle il a commencé à travailler. Avec cet outil, vous pouvez dessiner des cases et des flèches qui donnent l'impression qu'elles sont dessinées à la main. Le lendemain, vous pouvez également dessiner des points de suspension et du texte, ainsi que sélectionner des objets et les déplacer. Le 3 janvier, l'application a reçu son nom, Excalidraw. Comme pour tout bon projet annexe, l'achat du nom de domaine a été l'un des premiers actes de Christopher. À présent, vous pouvez utiliser des couleurs et exporter l'ensemble du dessin au format PNG.

Capture d'écran de l'application prototype Excalidraw montrant qu'elle prend en charge les rectangles, les flèches, les ellipses et le texte.

Le 15 janvier, Christopher a publié un article de blog qui a suscité beaucoup l'attention sur Twitter, y compris le mien. Le post commence par des statistiques impressionnantes :

  • 12 000 utilisateurs actifs uniques
  • 1,5 k étoiles sur GitHub
  • 26 contributeurs

Pour un projet qui a commencé il y a à peine deux semaines, ce n'est pas mal du tout. Mais ce qui a vraiment attisé mon intérêt se trouvait plus loin dans le post. Christopher a écrit qu'il avait essayé quelque chose de nouveau cette fois-ci : il a accordé à tous ceux qui ont envoyé une requête de tirage un accès illimité aux commits. Le même jour où j'ai lu l'article de blog, j'ai ajouté une pull request qui a permis d'intégrer l'API File System Access à Excalidraw, corrigeant une demande de fonctionnalité que quelqu'un avait envoyée.

Capture d'écran du tweet dans lequel j'annonce ma communication de presse.

Ma demande d'extraction a été fusionnée un jour plus tard et, à partir de là, j'avais un accès complet au commit. Inutile de dire que je n'ai pas abusé de mon pouvoir. pas plus que personne d'autre parmi les 149 contributeurs.

Aujourd'hui, Excalidraw est une application Web progressive installable complète avec prise en charge hors connexion, un mode sombre époustouflant et, oui, la possibilité d'ouvrir et d'enregistrer des fichiers grâce à l'API File System Access.

Capture d'écran de la PWA Excalidraw dans sa version actuelle.

Lipis explique pourquoi il consacre une grande partie de son temps à Excalidraw

Cela marque donc la fin de mon histoire « Comment je suis arrivé à Excalidraw », mais avant de plonger dans les fonctionnalités étonnantes d'Excalidraw, j'ai le plaisir de vous présenter Panayiotis. Panayiotis Lipiridis, sur Internet simplement appelé lipis, est le contributeur le plus prolifique d'Excalidraw. J'ai demandé à lipis ce qui le motive à consacrer autant de temps à Excalidraw :

Comme tout le monde, j'ai découvert ce projet grâce au tweet de Christopher. Ma première contribution a été d'ajouter la bibliothèque Open Color, les couleurs qui font toujours partie d'Excalidraw aujourd'hui. Au fur et à mesure que le projet s'est développé et que nous avons reçu un grand nombre de demandes, ma prochaine grande contribution a été de créer un backend pour stocker les dessins afin que les utilisateurs puissent les partager. Mais ce qui me motive vraiment, c'est que quiconque a essayé Excalidraw cherche des excuses pour l'utiliser à nouveau.

Je suis entièrement d'accord avec lipis. Quiconque a essayé Excalidraw cherche des excuses pour l'utiliser à nouveau.

Excalidraw en action

Je vais maintenant vous montrer comment utiliser Excalidraw en pratique. Je ne suis pas un grand artiste, mais le logo Google I/O est assez simple, alors laissez-moi essayer. Un cadre correspond à la lettre "i", une ligne peut être la barre oblique, et le "o" est un cercle. Je maintiens la touche Maj enfoncée pour obtenir un cercle parfait. Permettez-moi de déplacer la barre oblique pour améliorer l'affichage. Maintenant, voici quelques couleurs pour le "i" et le "o". Le bleu, c'est bien. Peut-être un style de remplissage différent ? Tous pleins ou hachurés ? Non, les hachures sont parfaites. Ce n'est pas parfait, mais c'est le principe d'Excalidraw, alors laissez-moi l'enregistrer.

Je clique sur l'icône d'enregistrement, puis saisis un nom de fichier dans la boîte de dialogue d'enregistrement. Dans Chrome, un navigateur compatible avec l'API File System Access, il ne s'agit pas d'un téléchargement, mais d'une véritable opération d'enregistrement, où je peux choisir l'emplacement et le nom du fichier et où, si j'apporte des modifications, je peux simplement les enregistrer dans le même fichier.

Permettez-moi de modifier le logo et de rendre le « i » rouge. Si je clique à nouveau sur Enregistrer, ma modification est enregistrée dans le même fichier qu'avant. Pour vous en convaincre, je vais effacer le canevas et rouvrir le fichier. Comme vous pouvez le voir, le logo rouge-bleu modifié est de nouveau présent.

Utiliser des fichiers

Sur les navigateurs qui ne sont pas compatibles avec l'API File System Access, chaque opération d'enregistrement est un téléchargement. Par conséquent, lorsque j'apporte des modifications, je me retrouve avec plusieurs fichiers dont le nom de fichier contient un numéro incrémentiel qui remplit mon dossier de téléchargements. Mais malgré cet inconvénient, je peux toujours enregistrer le fichier.

Ouvrir des fichiers

Alors, quel est le secret ? Comment l'ouverture et l'enregistrement peuvent-ils fonctionner dans différents navigateurs compatibles ou non avec l'API File System Access ? L'ouverture d'un fichier dans Excalidraw s'effectue dans une fonction appelée loadFromJSON)(, qui appelle à son tour une fonction appelée fileOpen().

export const loadFromJSON = async (localAppState: AppState) => {
  const blob = await fileOpen({
    description: 'Excalidraw files',
    extensions: ['.json', '.excalidraw', '.png', '.svg'],
    mimeTypes: ['application/json', 'image/png', 'image/svg+xml'],
  });
  return loadFromBlob(blob, localAppState);
};

La fonction fileOpen(), qui provient d'une petite bibliothèque que j'ai écrite et appelée browser-fs-access, que nous utilisons dans Excalidraw. Cette bibliothèque fournit un accès au système de fichiers via l'API File System Access avec un ancien remplacement, ce qui lui permet d'être utilisée dans n'importe quel navigateur.

Je vais d'abord vous montrer l'implémentation lorsque l'API est prise en charge. Après avoir négocié les types MIME et les extensions de fichier acceptés, l'élément central consiste à appeler la fonction showOpenFilePicker() de l'API File System Access. Cette fonction renvoie un tableau de fichiers ou un seul fichier, selon que plusieurs fichiers sont sélectionnés. Il ne vous reste plus qu'à placer le handle de fichier sur l'objet de fichier afin qu'il puisse être récupéré à nouveau.

export default async (options = {}) => {
  const accept = {};
  // Not shown: deal with extensions and MIME types.
  const handleOrHandles = await window.showOpenFilePicker({
    types: [
      {
        description: options.description || '',
        accept: accept,
      },
    ],
    multiple: options.multiple || false,
  });
  const files = await Promise.all(handleOrHandles.map(getFileWithHandle));
  if (options.multiple) return files;
  return files[0];
  const getFileWithHandle = async (handle) => {
    const file = await handle.getFile();
    file.handle = handle;
    return file;
  };
};

L'implémentation de remplacement repose sur un élément input de type "file". Après la négociation des types et des extensions MIME à accepter, l'étape suivante consiste à cliquer de manière programmatique sur l'élément de saisie pour afficher la boîte de dialogue d'ouverture de fichier. En cas de modification, c'est-à-dire lorsque l'utilisateur a sélectionné un ou plusieurs fichiers, la promesse est résolue.

export default async (options = {}) => {
  return new Promise((resolve) => {
    const input = document.createElement('input');
    input.type = 'file';
    const accept = [
      ...(options.mimeTypes ? options.mimeTypes : []),
      options.extensions ? options.extensions : [],
    ].join();
    input.multiple = options.multiple || false;
    input.accept = accept || '*/*';
    input.addEventListener('change', () => {
      resolve(input.multiple ? Array.from(input.files) : input.files[0]);
    });
    input.click();
  });
};

Enregistrer des fichiers

Enregistrement Dans Excalidraw, l'enregistrement se produit dans une fonction appelée saveAsJSON(). Il sérialise d'abord le tableau d'éléments Excalidraw en JSON, convertit le JSON en blob, puis appelle une fonction appelée fileSave(). Cette fonction est également fournie par la bibliothèque browser-fs-access.

export const saveAsJSON = async (
  elements: readonly ExcalidrawElement[],
  appState: AppState,
) => {
  const serialized = serializeAsJSON(elements, appState);
  const blob = new Blob([serialized], {
    type: 'application/vnd.excalidraw+json',
  });
  const fileHandle = await fileSave(
    blob,
    {
      fileName: appState.name,
      description: 'Excalidraw file',
      extensions: ['.excalidraw'],
    },
    appState.fileHandle,
  );
  return { fileHandle };
};

Comme précédemment, commençons par l'implémentation pour les navigateurs compatibles avec l'API File System Access. Les premières lignes semblent un peu complexes, mais elles permettent uniquement de négocier les types MIME et les extensions de fichier. Lorsque j'ai déjà enregistré et que je dispose déjà d'un gestionnaire de fichiers, aucune boîte de dialogue d'enregistrement ne doit s'afficher. Toutefois, s'il s'agit de la première sauvegarde, une boîte de dialogue de fichier s'affiche et l'application récupère un gestionnaire de fichiers pour une utilisation ultérieure. Le reste consiste simplement à écrire dans le fichier, via un flux en écriture.

export default async (blob, options = {}, handle = null) => {
  options.fileName = options.fileName || 'Untitled';
  const accept = {};
  // Not shown: deal with extensions and MIME types.
  handle =
    handle ||
    (await window.showSaveFilePicker({
      suggestedName: options.fileName,
      types: [
        {
          description: options.description || '',
          accept: accept,
        },
      ],
    }));
  const writable = await handle.createWritable();
  await writable.write(blob);
  await writable.close();
  return handle;
};

Fonctionnalité "Enregistrer sous"

Si je décide d'ignorer un gestionnaire de fichiers existant, je peux implémenter une fonctionnalité d'enregistrement sous pour créer un fichier basé sur un fichier existant. Pour illustrer cela, laissez-moi ouvrir un fichier existant, apporter quelques modifications, puis ne pas écraser le fichier existant, mais créer un nouveau fichier à l'aide de la fonctionnalité "Enregistrer sous". Le fichier d'origine n'est pas altéré.

L'implémentation pour les navigateurs qui ne sont pas compatibles avec l'API File System Access est courte, car elle crée simplement un élément d'ancrage avec un attribut download dont la valeur est le nom de fichier souhaité et une URL blob comme valeur de l'attribut href.

export default async (blob, options = {}) => {
  const a = document.createElement('a');
  a.download = options.fileName || 'Untitled';
  a.href = URL.createObjectURL(blob);
  a.addEventListener('click', () => {
    setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
  });
  a.click();
};

L'élément d'ancrage reçoit alors un clic programmatique. Pour éviter les fuites de mémoire, l'URL du blob doit être révoquée après utilisation. Comme il ne s'agit que d'un téléchargement, aucune boîte de dialogue d'enregistrement de fichier ne s'affiche jamais, et tous les fichiers se retrouvent dans le dossier Downloads par défaut.

Glisser-déposer

L'une de mes intégrations système préférées sur ordinateur est le glisser-déposer. Dans Excalidraw, lorsque je dépose un fichier .excalidraw dans l'application, il s'ouvre immédiatement et je peux commencer à apporter des modifications. Sur les navigateurs compatibles avec l'API File System Access, je peux même enregistrer immédiatement mes modifications. Il n'est pas nécessaire d'utiliser une boîte de dialogue d'enregistrement de fichier, car le gestionnaire de fichiers requis a été obtenu à partir de l'opération de glisser-déposer.

Pour ce faire, le secret consiste à appeler getAsFileSystemHandle() sur l'élément de transfert de données lorsque l'API File System Access est compatible. Je transmets ensuite ce handle de fichier à loadFromBlob(), dont vous vous souvenez peut-être grâce aux paragraphes ci-dessus. Vous pouvez faire beaucoup de choses avec les fichiers : les ouvrir, les enregistrer, les enregistrer de manière excessive, les faire glisser, les déposer, etc. Mon collègue Pete et moi-même avons documenté toutes ces astuces et bien d'autres dans notre article. Vous pourrez ainsi rattraper votre retard au cas où tout se passerait un peu trop vite.

const file = event.dataTransfer?.files[0];
if (file?.type === 'application/json' || file?.name.endsWith('.excalidraw')) {
  this.setState({ isLoading: true });
  // Provided by browser-fs-access.
  if (supported) {
    try {
      const item = event.dataTransfer.items[0];
      file as any.handle = await item as any
        .getAsFileSystemHandle();
    } catch (error) {
      console.warn(error.name, error.message);
    }
  }
  loadFromBlob(file, this.state).then(({ elements, appState }) =>
    // Load from blob
  ).catch((error) => {
    this.setState({ isLoading: false, errorMessage: error.message });
  });
}

Partager des fichiers

Une autre intégration système actuellement sur Android, ChromeOS et Windows s'effectue via l'API Web Share Target. Me voici dans l'application Fichiers de mon dossier Downloads. Je vois deux fichiers, dont l'un porte le nom non descriptif untitled et un code temporel. Pour vérifier son contenu, je clique sur les trois points, puis sur "Partager". L'une des options qui s'affiche est "Excalidraw". Lorsque j'appuie sur l'icône, je peux voir que le fichier contient à nouveau le logo E/S.

Lipis sur la version obsolète de Electron

Une chose que vous pouvez faire avec les fichiers dont je n'ai pas encore parlé, c'est doubleclick. En règle générale, lorsque vous doublez un fichier, l'application associée au type MIME du fichier s'ouvre. Par exemple, il s'agit de Microsoft Word pour .docx.

Excalidraw avait auparavant une version Electron de l'application compatible avec ces associations de types de fichiers. Par conséquent, lorsque vous double-cliquez sur un fichier .excalidraw, l'application Excalidraw Electron s'ouvre. Lipis, que vous avez déjà rencontré, était à la fois le créateur et le développeur de la version Electron d'Excalidraw. Je lui ai demandé pourquoi il pensait qu'il était possible de supprimer la version Electron :

Les utilisateurs demandent une application Electron depuis le début, principalement parce qu'ils souhaitent ouvrir des fichiers en double-cliquant dessus. Nous avions également l'intention de la proposer sur les plates-formes de téléchargement d'applications. En parallèle, quelqu'un a suggéré de créer une PWA à la place. Nous avons donc fait les deux. Heureusement, nous avons découvert les API du projet Fugu, telles que l'accès au système de fichiers, l'accès au presse-papiers, la gestion des fichiers, etc. D'un simple clic, vous pouvez installer l'application sur votre ordinateur de bureau ou votre mobile, sans le poids supplémentaire d'Electron. La décision d'abandonner la version Electron a été simple, et de se concentrer uniquement sur l'application Web et d'en faire la meilleure PWA possible. De plus, nous pouvons désormais publier des PWA sur le Play Store et le Microsoft Store. C'est énorme !

On pourrait dire qu'Excalidraw pour Electron n'a pas été abandonné parce qu'Electron est mauvais, pas du tout, mais parce que le Web est devenu suffisamment bon. J'aime ça !

Gestion des fichiers

Quand je dis que le Web est devenu assez performant, c'est grâce à des fonctionnalités comme la prochaine gestion de fichiers.

Il s'agit d'une installation standard de macOS Big Sur. Voyons maintenant ce qui se passe lorsque j'effectue un clic droit sur un fichier Exacalidraw. Je peux choisir de l'ouvrir avec Excalidraw, la PWA installée. Bien sûr, le double-clic fonctionnerait également, mais il est tout simplement moins théâtral de le démontrer dans un enregistrement d'écran.

Comment cela fonctionne-t-il ? La première étape consiste à informer le système d'exploitation des types de fichiers que mon application peut gérer. Je le fais dans un nouveau champ appelé file_handlers dans le fichier manifeste de l'application Web. Sa valeur est un tableau d'objets avec une action et une propriété accept. L'action détermine le chemin d'URL sur lequel le système d'exploitation lance votre application, et l'objet d'acceptation correspond à des paires clé/valeur de types MIME avec les extensions de fichier associées.

{
  "name": "Excalidraw",
  "description": "Excalidraw is a whiteboard tool...",
  "start_url": "/",
  "display": "standalone",
  "theme_color": "#000000",
  "background_color": "#ffffff",
  "file_handlers": [
    {
      "action": "/",
      "accept": {
        "application/vnd.excalidraw+json": [".excalidraw"]
      }
    }
  ]
}

L'étape suivante consiste à gérer le fichier au lancement de l'application. Cela se produit dans l'interface launchQueue où je dois définir un consommateur en appelant setConsumer(). Le paramètre de cette fonction est une fonction asynchrone qui reçoit launchParams. Cet objet launchParams possède un champ appelé "files" qui me fournit un tableau de poignées de fichiers à utiliser. Je ne m'intéresse qu'au premier et à partir de ce gestionnaire de fichiers, je reçois un blob que je transmets ensuite à notre vieil ami loadFromBlob().

if ('launchQueue' in window && 'LaunchParams' in window) {
  window as any.launchQueue
    .setConsumer(async (launchParams: { files: any[] }) => {
      if (!launchParams.files.length) return;
      const fileHandle = launchParams.files[0];
      const blob: Blob = await fileHandle.getFile();
      blob.handle = fileHandle;
      loadFromBlob(blob, this.state).then(({ elements, appState }) =>
        // Initialize app state.
      ).catch((error) => {
        this.setState({ isLoading: false, errorMessage: error.message });
      });
    });
}

Là encore, si vous êtes allé trop vite, vous pouvez consulter cet article pour en savoir plus sur l'API File Handling. Vous pouvez activer la gestion des fichiers en définissant l'indicateur expérimental des fonctionnalités de la plate-forme Web. Elle devrait être disponible dans Chrome dans le courant de l'année.

Intégration du presse-papiers

Une autre fonctionnalité intéressante d'Excalidraw est l'intégration du presse-papiers. Je peux copier l'intégralité de mon dessin ou seulement une partie de celui-ci dans le presse-papiers, en ajoutant un filigrane si je le souhaite, puis le coller dans une autre application. Il s'agit d'une version Web de l'application Windows 95 Paint.

Son fonctionnement est étonnamment simple. Tout ce dont j'ai besoin, c'est le canevas sous forme de blob, que j'écris ensuite dans le presse-papiers en transmettant un tableau à un élément avec un ClipboardItem avec le blob à la fonction navigator.clipboard.write(). Pour en savoir plus sur ce que vous pouvez faire avec l'API du presse-papiers, consultez l'article de Jason et le mien.

export const copyCanvasToClipboardAsPng = async (canvas: HTMLCanvasElement) => {
  const blob = await canvasToBlob(canvas);
  await navigator.clipboard.write([
    new window.ClipboardItem({
      'image/png': blob,
    }),
  ]);
};

export const canvasToBlob = async (canvas: HTMLCanvasElement): Promise<Blob> => {
  return new Promise((resolve, reject) => {
    try {
      canvas.toBlob((blob) => {
        if (!blob) {
          return reject(new CanvasError(t('canvasError.canvasTooBig'), 'CANVAS_POSSIBLY_TOO_BIG'));
        }
        resolve(blob);
      });
    } catch (error) {
      reject(error);
    }
  });
};

Collaboration avec d'autres personnes

Partager une URL de session

Saviez-vous qu'Excalidraw propose également un mode collaboratif ? Différentes personnes peuvent travailler ensemble sur le même document. Pour démarrer une nouvelle session, je clique sur le bouton "Live collaboration" (Collaboration en direct), puis je lance une session. Je peux partager facilement l'URL de la session avec mes collaborateurs grâce à l'API Web Share intégrée à Excalidraw.

Collaboration en direct

J'ai simulé une session de collaboration en local en travaillant sur le logo Google I/O sur mon Pixelbook, mon téléphone Pixel 3a et mon iPad Pro. Vous pouvez constater que les modifications que j'apporte sur un appareil sont répercutées sur tous les autres.

Je peux même voir tous les curseurs se déplacer. Le curseur du Pixelbook se déplace de façon régulière, puisqu'il est contrôlé par un pavé tactile, mais celui du téléphone Pixel 3a et celui de la tablette de l'iPad Pro sautent, car je contrôle ces appareils en tapotant avec mon doigt.

Afficher les états des collaborateurs

Pour améliorer l'expérience de collaboration en temps réel, un système de détection de l'inactivité est même en cours d'exécution. Le curseur de l'iPad Pro affiche un point vert lorsque je l'utilise. Le point devient noir lorsque je passe à un autre onglet de navigateur ou à une autre appli. Et quand je suis dans l'appli Excalidraw, mais que je ne fais rien, le curseur me montre comme inactif, symbolisé par les trois « zZZ ».

Les lecteurs assidus de nos publications pourraient penser que la détection d'inactivité est réalisée via l'API Idle Detection, une proposition en phase de démarrage sur laquelle nous avons travaillé dans le cadre de Project Fugu. Alerte spoiler: non. Bien que nous ayons une implémentation basée sur cette API dans Excalidraw, nous avons finalement décidé d'adopter une approche plus traditionnelle basée sur la mesure du mouvement du pointeur et de la visibilité de la page.

Capture d&#39;écran des commentaires sur la détection d&#39;inactivité enregistrés dans le dépôt WICG Idle Detection Repo.

Nous avons envoyé des commentaires sur les raisons pour lesquelles l'API de détection d'inactivité ne résolvait pas le cas d'utilisation que nous avions. Toutes les API du projet Fugu sont développées en open source, afin que chacun puisse s'exprimer.

Lipis sur les freins à Excalidraw

À ce propos, j'ai posé une dernière question à Lipis concernant ce qui, selon lui, manque à la plate-forme Web qui retient Excalidraw:

L'API File System Access est géniale, mais saviez-vous que ? La plupart des fichiers qui me tiennent à cœur se trouvent dans Dropbox ou Google Drive, et non sur mon disque dur. J'aimerais que l'API File System Access inclue une couche d'abstraction permettant l'intégration des fournisseurs de systèmes de fichiers distants tels que Dropbox ou Google, et avec laquelle les développeurs peuvent coder. Les utilisateurs peuvent alors se détendre en sachant que leurs fichiers sont en sécurité auprès du fournisseur de services cloud de leur choix.

Je suis tout à fait d'accord avec lipis, je vis aussi dans le cloud. J'espère que cette fonctionnalité sera bientôt implémentée.

Mode d'application à onglets

Impressionnant ! Nous avons vu de nombreuses intégrations d'API très intéressantes dans Excalidraw. Système de fichiers, gestion des fichiers, Bloc-notes, partage Web et cible de partage Web. Mais j'ai encore une chose à vous dire. Jusqu'à présent, je ne pouvais modifier qu'un seul document à la fois. Plus vraiment. Profitez pour la première fois d'une première version du mode application par onglets dans Excalidraw. Voici à quoi cela ressemble.

Un fichier existant est ouvert dans la PWA Excalidraw installée, qui s'exécute en mode autonome. Je vais maintenant ouvrir un nouvel onglet dans la fenêtre autonome. Il ne s'agit pas d'un onglet de navigateur classique, mais d'un onglet PWA. Dans ce nouvel onglet, je peux ensuite ouvrir un fichier secondaire et travailler dessus indépendamment de la même fenêtre d'application.

Le mode application par onglets n'en est qu'à ses débuts, et tout n'est pas figé. Si vous êtes intéressé, consultez cet article pour en savoir plus sur l'état actuel de cette fonctionnalité.

Conclusion

Pour vous tenir informé de cette fonctionnalité et d'autres, consultez notre outil de suivi de l'API Fugu. Nous sommes ravis de faire progresser le Web et de vous permettre d'en faire plus sur la plate-forme. Nous vous saluons à l'amélioration d'Excalidraw en constante évolution. Voici toutes les applications incroyables que vous allez créer. Lancez la création sur excalidraw.com.

J'ai hâte de voir apparaître certaines des API que je vous ai présentées aujourd'hui dans vos applications. Je m'appelle Tom. Vous pouvez me trouver sur Twitter et sur Internet en général sous le nom @tomayac. Merci de votre attention, et bonne continuation de Google I/O.