Excalidraw dan Fugu: Meningkatkan Perjalanan Pengguna Inti

Setiap teknologi yang cukup canggih tidak dapat dibedakan dari keajaiban. Kecuali jika Anda memahaminya. Nama saya Thomas Steiner, saya bekerja di Developer Relations di Google dan dalam laporan presentasi Google I/O saya ini, saya akan melihat beberapa Fugu API baru dan cara API tersebut meningkatkan perjalanan pengguna inti di PWA Excalidraw, sehingga Anda dapat mengambil inspirasi dari ide-ide ini dan menerapkannya ke aplikasi Anda sendiri.

Bagaimana saya menemukan Excalidraw

Saya ingin memulai dengan sebuah cerita. Pada 1 Januari 2020, Christopher Chedeau, seorang engineer software di Facebook, membuat tweet tentang aplikasi gambar kecil yang telah dia mulai kerjakan. Dengan alat ini, Anda dapat menggambar kotak dan panah yang terlihat seperti kartun dan digambar tangan. Keesokan harinya, Anda juga dapat menggambar elipsis dan teks, serta memilih objek dan memindahkannya. Pada 3 Januari, aplikasi tersebut telah mendapatkan namanya, Excalidraw, dan, seperti setiap project sampingan yang baik, membeli nama domain adalah salah satu tindakan pertama Christopher. Sekarang, Anda dapat menggunakan warna dan mengekspor seluruh gambar sebagai PNG.

Screenshot aplikasi prototipe Excalidraw yang menunjukkan bahwa aplikasi tersebut mendukung persegi panjang, panah, elips, dan teks.

Pada 15 Januari, Christopher memposting postingan blog yang menarik banyak perhatian di Twitter, termasuk saya. Postingan dimulai dengan beberapa statistik yang mengesankan:

  • 12 ribu pengguna aktif unik
  • 1,5 ribu bintang di GitHub
  • 26 kontributor

Untuk project yang baru dimulai dua minggu lalu, itu bukan hasil yang buruk. Namun, hal yang benar-benar meningkatkan minat saya ada di bagian bawah postingan. Christopher menulis bahwa ia mencoba sesuatu yang baru kali ini: memberikan akses commit tanpa syarat kepada semua orang yang mengirimkan permintaan pull. Pada hari yang sama dengan membaca postingan blog, saya memiliki permintaan pull yang menambahkan dukungan File System Access API ke Excalidraw, memperbaiki permintaan fitur yang telah diajukan seseorang.

Screenshot tweet tempat saya mengumumkan PR saya.

Pull request saya digabungkan sehari kemudian dan sejak saat itu, saya memiliki akses commit penuh. Tidak perlu dikatakan, saya tidak menyalahgunakan kekuasaan saya. Begitu juga dengan 149 kontributor lainnya sejauh ini.

Saat ini, Excalidraw adalah aplikasi web progresif lengkap yang dapat diinstal dengan dukungan offline, mode gelap yang memukau, dan kemampuan untuk membuka serta menyimpan file berkat File System Access API.

Screenshot PWA Excalidraw dalam status saat ini.

Lipis menjelaskan alasan dia menghabiskan banyak waktu untuk Excalidraw

Jadi, ini menandai akhir dari cerita "bagaimana saya menemukan Excalidraw", tetapi sebelum saya membahas beberapa fitur luar biasa Excalidraw, dengan senang hati saya memperkenalkan Panayiotis. Panayiotis Lipiridis, di Internet dikenal sebagai lipis, adalah kontributor paling produktif untuk Excalidraw. Saya bertanya kepada lipis apa yang memotivasinya untuk mencurahkan begitu banyak waktunya untuk Excalidraw:

Seperti orang lain, saya mengetahui project ini dari tweet Christopher. Kontribusi pertama saya adalah menambahkan library Open Color, warna yang masih menjadi bagian dari Excalidraw saat ini. Seiring berkembangnya project dan kami memiliki cukup banyak permintaan, kontribusi besar saya berikutnya adalah membuat backend untuk menyimpan gambar sehingga pengguna dapat membagikannya. Namun, yang benar-benar mendorong saya untuk berkontribusi adalah siapa pun yang mencoba Excalidraw akan mencari alasan untuk menggunakannya lagi.

Saya sepenuhnya setuju dengan lipis. Siapa pun yang telah mencoba Excalidraw akan mencari alasan untuk menggunakannya lagi.

Cara kerja Excalidraw

Sekarang, saya ingin menunjukkan cara menggunakan Excalidraw dalam praktik. Saya bukan seniman hebat, tetapi logo Google I/O cukup sederhana, jadi saya akan mencobanya. Kotak adalah "i", garis dapat berupa garis miring, dan "o" adalah lingkaran. Saya menahan shift, sehingga saya mendapatkan lingkaran yang sempurna. Izinkan saya memindahkan garis miring sedikit, agar terlihat lebih baik. Sekarang beberapa warna untuk "i" dan "o". Biru bagus. Mungkin gaya isian yang berbeda? Semua solid, atau cross-hatch? Nah, hachure terlihat bagus. Hasilnya tidak sempurna, tetapi itulah ide Excalidraw, jadi izinkan saya menyimpannya.

Saya mengklik ikon simpan dan memasukkan nama file di dialog simpan file. Di Chrome, browser yang mendukung File System Access API, ini bukan download, tetapi operasi simpan yang sebenarnya, tempat saya dapat memilih lokasi dan nama file, dan jika saya melakukan pengeditan, saya dapat menyimpannya ke file yang sama.

Mari kita ubah logo dan buat "i" menjadi merah. Jika saya sekarang mengklik simpan lagi, modifikasi saya akan disimpan ke file yang sama seperti sebelumnya. Sebagai bukti, izinkan saya menghapus kanvas dan membuka kembali file. Seperti yang dapat Anda lihat, logo merah-biru yang diubah sudah ada lagi.

Bekerja dengan file

Di browser yang saat ini tidak mendukung File System Access API, setiap operasi simpan adalah download, jadi saat saya membuat perubahan, saya akan mendapatkan beberapa file dengan nomor yang bertambah di nama file yang mengisi folder Download. Namun, meskipun ada kekurangan ini, saya tetap dapat menyimpan file.

Membuka file

Jadi, apa rahasianya? Bagaimana cara membuka dan menyimpan berfungsi di berbagai browser yang mungkin mendukung atau tidak mendukung File System Access API? Membuka file di Excalidraw terjadi dalam fungsi yang disebut loadFromJSON)(), yang pada gilirannya memanggil fungsi yang disebut 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);
};

Fungsi fileOpen() yang berasal dari library kecil yang saya tulis bernama browser-fs-access yang kita gunakan di Excalidraw. Library ini menyediakan akses sistem file melalui File System Access API dengan penggantian lama, sehingga dapat digunakan di browser mana pun.

Pertama-tama, izinkan saya menunjukkan implementasi saat API didukung. Setelah menegosiasikan jenis MIME dan ekstensi file yang diterima, bagian tengah akan memanggil fungsi showOpenFilePicker() File System Access API. Fungsi ini menampilkan array file atau satu file, bergantung pada apakah beberapa file dipilih. Yang tersisa hanyalah menempatkan handle file pada objek file, sehingga dapat diambil lagi.

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;
  };
};

Implementasi penggantian bergantung pada elemen input dari jenis "file". Setelah negosiasi jenis dan ekstensi MIME yang akan diterima, langkah berikutnya adalah mengklik elemen input secara terprogram sehingga dialog buka file ditampilkan. Saat berubah, yaitu saat pengguna telah memilih satu atau beberapa file, promise akan diselesaikan.

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();
  });
};

Menyimpan file

Sekarang saatnya menyimpan. Di Excalidraw, penyimpanan terjadi dalam fungsi yang disebut saveAsJSON(). Pertama, serialisasi array elemen Excalidraw ke JSON, konversikan JSON ke blob, lalu panggil fungsi yang disebut fileSave(). Fungsi ini juga disediakan oleh library 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 };
};

Sekali lagi, mari kita lihat implementasi untuk browser dengan dukungan File System Access API terlebih dahulu. Beberapa baris pertama terlihat sedikit rumit, tetapi yang dilakukannya hanyalah menegosiasikan jenis MIME dan ekstensi file. Jika saya telah menyimpan sebelumnya dan sudah memiliki handle file, tidak ada dialog simpan yang perlu ditampilkan. Namun, jika ini adalah penyimpanan pertama, dialog file akan ditampilkan dan aplikasi akan mendapatkan handle file kembali untuk digunakan pada masa mendatang. Selanjutnya, hanya menulis ke file, yang terjadi melalui aliran data yang dapat ditulis.

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;
};

Fitur "simpan sebagai"

Jika saya memutuskan untuk mengabaikan handle file yang sudah ada, saya dapat menerapkan fitur "save as" untuk membuat file baru berdasarkan file yang ada. Untuk menunjukkan hal ini, izinkan saya membuka file yang ada, melakukan beberapa perubahan, lalu tidak menimpa file yang ada, tetapi membuat file baru menggunakan fitur simpan sebagai. Tindakan ini akan mempertahankan file asli.

Implementasi untuk browser yang tidak mendukung File System Access API singkat, karena semua yang dilakukannya adalah membuat elemen anchor dengan atribut download yang nilainya adalah nama file yang diinginkan dan URL blob sebagai nilai atribut href-nya.

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();
};

Elemen anchor kemudian diklik secara terprogram. Untuk mencegah kebocoran memori, URL blob perlu dicabut setelah digunakan. Karena ini hanya download, tidak ada dialog simpan file yang ditampilkan, dan semua file akan berada di folder Downloads default.

Tarik lalu lepas

Salah satu integrasi sistem favorit saya di desktop adalah tarik lalu lepas. Di Excalidraw, saat saya meletakkan file .excalidraw ke aplikasi, file tersebut akan langsung terbuka dan saya dapat mulai mengedit. Di browser yang mendukung File System Access API, saya bahkan dapat langsung menyimpan perubahan. Tidak perlu melalui dialog simpan file karena handle file yang diperlukan telah diperoleh dari operasi tarik lalu lepas.

Rahasianya adalah dengan memanggil getAsFileSystemHandle() pada item transfer data saat File System Access API didukung. Kemudian, saya meneruskan handle file ini ke loadFromBlob(), yang mungkin Anda ingat dari beberapa paragraf di atas. Ada begitu banyak hal yang dapat Anda lakukan dengan file: membuka, menyimpan, menyimpan ulang, menarik, melepas. Saya dan rekan kerja saya, Pete, telah mendokumentasikan semua trik ini dan lainnya dalam artikel kami sehingga Anda dapat membacanya jika semua ini terlalu cepat.

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 });
  });
}

Membagikan file

Integrasi sistem lain yang saat ini ada di Android, ChromeOS, dan Windows adalah melalui Web Share Target API. Di sini saya berada di aplikasi File di folder Downloads. Saya dapat melihat dua file, salah satunya dengan nama non-deskriptif untitled dan stempel waktu. Untuk memeriksa isinya, saya mengklik tiga titik, lalu berbagi, dan salah satu opsi yang muncul adalah Excalidraw. Saat mengetuk ikon, saya dapat melihat bahwa file hanya berisi logo I/O lagi.

Lipis pada versi Electron yang tidak digunakan lagi

Satu hal yang dapat Anda lakukan dengan file yang belum saya bahas adalah mengkliknya dua kali. Yang biasanya terjadi saat Anda mengklik dua kali file adalah aplikasi yang terkait dengan jenis MIME file akan terbuka. Misalnya, untuk .docx, ini adalah Microsoft Word.

Excalidraw memiliki versi Electron aplikasi yang mendukung pengaitan jenis file tersebut, sehingga saat Anda mengklik dua kali file .excalidraw, aplikasi Excalidraw Electron akan terbuka. Lipis, yang telah Anda temui sebelumnya, adalah pencipta dan penghenti penggunaan Excalidraw Electron. Saya bertanya kepadanya mengapa ia merasa versi Electron dapat dihentikan:

Orang-orang telah meminta aplikasi Electron sejak awal, terutama karena mereka ingin membuka file dengan mengklik dua kali. Kami juga bermaksud untuk menempatkan aplikasi di app store. Secara paralel, seseorang menyarankan untuk membuat PWA, jadi kami hanya melakukan keduanya. Untungnya, kami diperkenalkan dengan Project Fugu API seperti akses sistem file, akses papan klip, penanganan file, dan lainnya. Dengan sekali klik, Anda dapat menginstal aplikasi di desktop atau perangkat seluler, tanpa beban tambahan Electron. Keputusan untuk menghentikan penggunaan versi Electron, berkonsentrasi hanya pada aplikasi web, dan menjadikannya PWA terbaik adalah keputusan yang mudah. Selain itu, kini kita dapat memublikasikan PWA ke Play Store dan Microsoft Store. Itu adalah jumlah yang sangat besar.

Dapat dikatakan bahwa Excalidraw untuk Electron tidak dihentikan karena Electron buruk, sama sekali tidak, tetapi karena web sudah cukup baik. Saya suka ini.

Penanganan file

Saat saya mengatakan "web sudah cukup baik", hal ini karena fitur seperti Penanganan File mendatang.

Ini adalah penginstalan macOS Big Sur reguler. Sekarang, lihat apa yang terjadi saat saya mengklik kanan file Exaclidraw. Saya dapat memilih untuk membukanya dengan Excalidraw, PWA yang diinstal. Tentu saja, mengklik dua kali juga akan berfungsi, tetapi tidak terlalu dramatis untuk ditunjukkan dalam screencast.

Jadi, bagaimana cara kerjanya? Langkah pertama adalah membuat jenis file yang dapat ditangani aplikasi saya dikenal oleh sistem operasi. Saya melakukannya di kolom baru bernama file_handlers dalam manifes aplikasi web. Nilainya adalah array objek dengan tindakan dan properti accept. Tindakan ini menentukan jalur URL tempat sistem operasi meluncurkan aplikasi Anda dan objek terima adalah key-value pair dari jenis MIME dan ekstensi file terkait.

{
  "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"]
      }
    }
  ]
}

Langkah berikutnya adalah menangani file saat aplikasi diluncurkan. Hal ini terjadi di antarmuka launchQueue tempat saya perlu menetapkan konsumen dengan memanggil, setConsumer(). Parameter untuk fungsi ini adalah fungsi asinkron yang menerima launchParams. Objek launchParams ini memiliki kolom bernama file yang memberi saya array handle file untuk digunakan. Saya hanya peduli dengan yang pertama dan dari handle file ini, saya mendapatkan blob yang kemudian saya teruskan ke teman lama kita 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 });
      });
    });
}

Sekali lagi, jika ini terlalu cepat, Anda dapat membaca lebih lanjut File Handling API di artikel saya. Anda dapat mengaktifkan penanganan file dengan menetapkan flag fitur platform web eksperimental. Fitur ini dijadwalkan akan hadir di Chrome pada tahun ini.

Integrasi papan klip

Fitur keren lainnya dari Excalidraw adalah integrasi papan klip. Saya dapat menyalin seluruh gambar atau hanya sebagian ke papan klip, mungkin menambahkan watermark jika saya mau, lalu menempelkannya ke aplikasi lain. Ini adalah versi web dari aplikasi Paint Windows 95.

Cara kerjanya sangat sederhana. Yang saya perlukan hanyalah kanvas sebagai blob, yang kemudian saya tulis ke papan klip dengan meneruskan array satu elemen dengan ClipboardItem dengan blob ke fungsi navigator.clipboard.write(). Untuk informasi selengkapnya tentang hal yang dapat Anda lakukan dengan API clipboard, lihat artikel Jason dan saya.

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);
    }
  });
};

Berkolaborasi dengan orang lain

Membagikan URL sesi

Tahukah Anda bahwa Excalidraw juga memiliki mode kolaboratif? Orang yang berbeda dapat bekerja sama pada dokumen yang sama. Untuk memulai sesi baru, saya mengklik tombol kolaborasi live, lalu memulai sesi. Saya dapat membagikan URL sesi dengan kolaborator dengan mudah berkat Web Share API yang telah terintegrasi dengan Excalidraw.

Kolaborasi live

Saya telah menyimulasikan sesi kolaborasi secara lokal dengan mengerjakan logo Google I/O di Pixelbook, ponsel Pixel 3a, dan iPad Pro saya. Anda dapat melihat bahwa perubahan yang saya buat di satu perangkat akan tercermin di semua perangkat lainnya.

Saya bahkan dapat melihat semua kursor bergerak. Kursor Pixelbook bergerak dengan stabil, karena dikontrol oleh trackpad, tetapi kursor ponsel Pixel 3a dan kursor tablet iPad Pro berpindah-pindah, karena saya mengontrol perangkat ini dengan mengetuk dengan jari.

Melihat status kolaborator

Untuk meningkatkan pengalaman kolaborasi real-time, bahkan ada sistem deteksi tidak ada aktivitas yang berjalan. Kursor iPad Pro menampilkan titik hijau saat saya menggunakannya. Titik berubah menjadi hitam saat saya beralih ke tab atau aplikasi browser yang berbeda. Dan saat saya berada di aplikasi Excalidraw, tetapi tidak melakukan apa pun, kursor akan menampilkan saya sebagai tidak ada aktivitas, yang dilambangkan dengan tiga zZZ.

Pembaca setia publikasi kami mungkin cenderung berpikir bahwa deteksi tidak ada aktivitas diwujudkan melalui Idle Detection API, proposal tahap awal yang telah dikerjakan dalam konteks Project Fugu. Peringatan spoiler: tidak. Meskipun kami memiliki implementasi berdasarkan API ini di Excalidraw, pada akhirnya, kami memutuskan untuk menggunakan pendekatan yang lebih tradisional berdasarkan pengukuran pergerakan pointer dan visibilitas halaman.

Screenshot masukan Deteksi Tidak Ada Aktivitas yang diajukan di repo Deteksi Tidak Ada Aktivitas WICG.

Kami mengajukan masukan tentang alasan Idle Detection API tidak dapat menyelesaikan kasus penggunaan yang kami miliki. Semua Project Fugu API sedang dikembangkan secara terbuka, sehingga semua orang dapat berpartisipasi dan suara mereka didengar.

Lipis membahas hal yang menghambat Excalidraw

Bicara soal itu, saya mengajukan pertanyaan terakhir kepada lipis tentang apa yang menurutnya tidak ada di platform web yang menghambat Excalidraw:

File System Access API sangat bagus, tetapi tahukah Anda? Sebagian besar file yang saya anggap penting saat ini disimpan di Dropbox atau Google Drive, bukan di hard disk. Saya berharap File System Access API akan menyertakan lapisan abstraksi untuk penyedia sistem file jarak jauh seperti Dropbox atau Google untuk berintegrasi dan yang dapat di-coding oleh developer. Pengguna kemudian dapat bersantai dan mengetahui bahwa file mereka aman dengan penyedia cloud yang mereka percayai.

Saya sepenuhnya setuju dengan lipis, saya juga hidup di cloud. Semoga fitur ini akan segera diterapkan.

Mode aplikasi dengan tab

Wow! Kami telah melihat banyak integrasi API yang sangat bagus di Excalidraw. Sistem file, penanganan file, papan klip, berbagi web, dan target berbagi web. Namun, ada satu hal lagi. Hingga saat ini, saya hanya dapat mengedit satu dokumen dalam satu waktu. Jangan khawatir. Nikmati untuk pertama kalinya versi awal mode aplikasi dengan tab di Excalidraw. Beginilah tampilannya.

Saya memiliki file yang sudah terbuka di PWA Excalidraw yang diinstal dan berjalan dalam mode mandiri. Sekarang, saya membuka tab baru di jendela mandiri. Ini bukan tab browser biasa, tetapi tab PWA. Di tab baru ini, saya dapat membuka file sekunder, dan mengerjakannya secara independen dari jendela aplikasi yang sama.

Mode aplikasi dengan tab masih dalam tahap awal dan tidak semuanya sudah pasti. Jika Anda tertarik, pastikan untuk membaca status fitur ini saat ini di artikel saya.

Penutup

Untuk terus mendapatkan info terbaru tentang fitur ini dan fitur lainnya, pastikan untuk menonton pelacak Fugu API kami. Kami sangat antusias untuk mendorong web maju dan memungkinkan Anda melakukan lebih banyak hal di platform ini. Selamat untuk Excalidraw yang terus berkembang, dan selamat untuk semua aplikasi luar biasa yang akan Anda buat. Mulailah membuat di excalidraw.com.

Saya tidak sabar untuk melihat beberapa API yang telah saya tampilkan hari ini muncul di aplikasi Anda. Nama saya Tom, Anda dapat menemukan saya sebagai @tomayac di Twitter dan internet secara umum. Terima kasih banyak telah menonton, dan nikmati acara Google I/O lainnya.