Teknologi yang cukup canggih tidak dapat dibedakan dengan sihir. Kecuali Anda memahaminya. Saya Thomas Steiner, saya bekerja di bidang Hubungan Developer di Google. Dalam presentasi Google I/O ini, saya akan membahas beberapa API Fugu yang baru dan bagaimana API tersebut meningkatkan perjalanan pengguna inti di Excalidraw PWA, sehingga Anda dapat mengambil inspirasi dari ide-ide ini dan menerapkannya pada aplikasi Anda sendiri.
Bagaimana saya bisa menggunakan Excalidraw
Saya ingin mulai dengan sebuah cerita. Pada 1 Januari 2020, Christopher Chedeau, software engineer di Facebook, men-tweet tentang aplikasi menggambar kecil yang sudah dia mulai kerjakan. Dengan alat ini, Anda dapat menggambar kotak dan panah yang terasa seperti kartun dan digambar dengan tangan. Keesokan harinya, Anda juga dapat menggambar elipsis dan teks, serta memilih objek dan memindahkannya. Pada 3 Januari, aplikasi tersebut mendapatkan namanya, Excalidraw, dan, seperti pada setiap project sampingan yang bagus, membeli nama domain adalah salah satu tindakan pertama Christopher. Sekarang Anda bisa menggunakan warna dan mengekspor seluruh gambar sebagai PNG.
Pada 15 Januari, Christopher memposting postingan blog yang menarik banyak perhatian di Twitter, termasuk milik saya. Postingan dimulai dengan beberapa statistik yang mengesankan:
- 12 rb pengguna aktif unik
- 1,5 ribu bintang di GitHub
- 26 kontributor
Untuk proyek yang dimulai hanya dua minggu yang lalu, itu sama sekali tidak buruk. Tetapi hal yang benar-benar meningkatkan minat saya jauh di bawah postingan itu. Christopher menulis bahwa dia mencoba sesuatu yang baru kali ini: memberi semua orang yang mengirim permintaan pull akses commit tanpa syarat. Pada hari yang sama saat membaca postingan blog, saya mendapat permintaan tarik yang menambahkan dukungan File System Access API ke Excalidraw, yang memperbaiki permintaan fitur yang diajukan seseorang.
Permintaan pull saya digabungkan sehari kemudian dan sejak saat itu, saya memiliki akses commit penuh. Bisa dikatakan, saya tidak menyalahgunakan kekuasaan saya. Begitu juga orang lain dari 149 kontributor sejauh ini.
Saat ini, Excalidraw adalah aplikasi web progresif yang dapat diinstal lengkap dengan dukungan offline, mode gelap yang memukau, dan kemampuan untuk membuka dan menyimpan file berkat File System Access API.
Lipis tentang alasan dia mendedikasikan begitu banyak waktunya untuk Excalidraw
Jadi ini adalah akhir dari kisah "cara saya datang ke Excalidraw", tetapi sebelum saya menyelami beberapa fitur Excalidraw yang luar biasa, saya dengan senang hati memperkenalkan Panayiotis. Panayiotis Lipiridis, di internet yang dikenal sebagai lipis, adalah kontributor paling produktif untuk Excalidraw. Saya bertanya kepada lipis apa yang memotivasinya untuk mendedikasikan begitu banyak waktunya untuk Excalidraw:
Seperti semua orang yang saya pelajari tentang proyek ini dari tweet Christopher. Kontribusi pertama saya adalah menambahkan library Open Color, warna yang masih menjadi bagian Excalidraw saat ini. Seiring dengan berkembangnya project dan kami mendapatkan cukup banyak permintaan, kontribusi besar berikutnya adalah membuat backend untuk menyimpan gambar sehingga pengguna dapat membagikannya. Namun, yang sebenarnya mendorong saya untuk berkontribusi adalah bahwa siapa pun yang mencoba Excalidraw ingin menemukan alasan untuk menggunakannya lagi.
Saya sepenuhnya setuju dengan lipis. Siapa pun yang mencoba Excalidraw ingin mencari alasan untuk menggunakannya lagi.
Cara kerja Excalidraw
Saya ingin menunjukkan cara menggunakan Excalidraw dalam latihan. Saya bukan seniman yang hebat, tetapi logo Google I/O cukup sederhana, jadi saya akan mencobanya. Kotak adalah "i", garis bisa berupa garis miring, dan "o" adalah lingkaran. Saya menahan tombol shift, sehingga saya mendapatkan lingkaran yang sempurna. Saya akan memindahkan garis miring sedikit, agar terlihat lebih baik. Sekarang beberapa warna untuk "i" dan "o". Biru itu bagus. Mungkin gaya isian yang berbeda? Semuanya solid atau bisa silang? Tidak, hachure terlihat keren. Memang tidak sempurna, tapi itulah ide Excalidraw, jadi biarkan saya menyimpannya.
Saya klik ikon {i>save<i} dan memasukkan nama {i>file<i} pada dialog {i>file<i} disimpan. Di Chrome, browser yang mendukung File System Access API, ini bukan download, melainkan operasi penyimpanan yang sebenarnya, yang memungkinkan saya memilih lokasi dan nama file, serta di mana, jika saya mengedit, saya bisa menyimpannya ke file yang sama.
Biarkan saya mengubah logo dan membuat "i" menjadi merah. Jika sekarang saya mengklik {i>save<i} lagi, perubahan saya akan disimpan ke {i>file<i} yang sama seperti sebelumnya. Sebagai bukti, izinkan saya membersihkan kanvas dan membuka kembali file tersebut. Seperti yang Anda lihat, logo biru merah yang dimodifikasi ada lagi di sana.
Bekerja dengan file
Pada browser yang saat ini tidak mendukung File System Access API, setiap operasi penyimpanan adalah download. Jadi, saat saya membuat perubahan, akhirnya saya akan menemukan beberapa file dengan angka yang bertambah dalam nama file yang mengisi folder Download saya. Tetapi terlepas dari kekurangan ini, saya masih bisa menyimpan file tersebut.
Membuka file
Jadi apa rahasianya? Bagaimana cara membuka dan menyimpan pekerjaan di browser lain yang mungkin mendukung atau tidak
mendukung File System Access API? Pembukaan file di Excalidraw dilakukan dalam fungsi bernama
loadFromJSON)(
), yang kemudian memanggil fungsi bernama 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 kami gunakan dalam Excalidraw. Library ini memberikan akses sistem file melalui
File System Access API dengan penggantian lama, sehingga dapat digunakan di
browser apa pun.
Pertama-tama, saya akan menunjukkan implementasinya saat API didukung. Setelah menegosiasikan
jenis MIME dan ekstensi file yang diterima, bagian utamanya adalah memanggil fungsi
File System Access API showOpenFilePicker()
. Fungsi ini menampilkan array file atau satu file, bergantung pada apakah beberapa file dipilih atau tidak. Selanjutnya, Anda hanya perlu meletakkan handle file pada objek
file agar dapat diambil kembali.
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 menegosiasikan
jenis dan ekstensi MIME yang akan diterima, langkah selanjutnya adalah mengklik elemen input secara terprogram
sehingga dialog pembukaan file ditampilkan. Saat diubah, yaitu saat pengguna telah memilih satu atau beberapa file, promise akan di-resolve.
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 ke penyimpanan. Di Excalidraw, penyimpanan terjadi dalam fungsi yang disebut saveAsJSON()
. Metode ini terlebih dahulu
melakukan serialisasi array elemen Excalidraw ke JSON, mengonversi JSON menjadi blob, lalu memanggil
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, pertama-tama saya akan melihat implementasi untuk browser dengan dukungan File System Access API. Beberapa baris pertama terlihat sedikit rumit, tetapi yang perlu dilakukan 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 mendapatkan kembali handle file untuk digunakan di lain waktu. Sisanya kemudian hanya menulis ke file, yang terjadi melalui aliran 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 "simpan sebagai" untuk membuat file baru berdasarkan file yang ada. Untuk menampilkannya, saya akan membuka file yang sudah ada, melakukan beberapa modifikasi, lalu tidak menimpa file yang sudah ada, tetapi membuat file baru dengan menggunakan fitur simpan sebagai. Tindakan ini akan membiarkan file asli tetap utuh.
Implementasi untuk browser yang tidak mendukung File System Access API tidak akan terlalu lama, karena yang dilakukan
hanyalah 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 harus dicabut setelah digunakan. Karena ini hanyalah download, tidak akan ada dialog penyimpanan file yang ditampilkan, dan semua
file akan ditempatkan di folder Downloads
default.
Tarik lalu lepas
Salah satu integrasi sistem favorit saya di {i>desktop<i} adalah {i>drag and drop<i}. 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 penyimpanan file karena handle file yang diperlukan telah diperoleh dari operasi
tarik lalu lepas.
Rahasia untuk mewujudkannya 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. Begitu banyak
hal yang dapat Anda lakukan dengan file: membuka, menyimpan, menyimpan secara berlebihan, menyeret, melepas. Rekan saya, Pete, dan saya telah mendokumentasikan semua trik ini dan lainnya di artikel kami sehingga Anda dapat mengetahui jika semua hal ini berjalan 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. Saya berada di aplikasi File di folder Downloads
saya. Saya dapat melihat dua file, salah satunya dengan nama non-deskripsi untitled
dan stempel waktu. Untuk memeriksa isinya, klik tiga titik, lalu bagikan, dan salah satu opsi yang muncul adalah
Excalidraw. Ketika saya mengetuk ikon tersebut, saya dapat melihat bahwa {i>file<i} berisi logo I/O lagi.
Lipis pada versi Electron yang tidak digunakan lagi
Satu hal yang dapat Anda lakukan dengan file yang belum saya bicarakan adalah menggandakan file tersebut. Hal yang biasanya terjadi jika Anda menggandakan file adalah aplikasi yang terkait dengan jenis MIME file tersebut akan terbuka. Misalnya, .docx
adalah Microsoft Word.
Excalidraw sebelumnya memiliki versi Electron untuk aplikasi yang
mendukung pengaitan jenis file tersebut, sehingga saat Anda mengklik dua kali file .excalidraw
, aplikasi
Excalidraw Electron akan terbuka. Lipis, yang sudah Anda temui sebelumnya, adalah
pembuat dan deprecator Excalidraw Electron. Saya bertanya mengapa dia merasa bahwa
penggunaan versi Elektron dapat dihentikan:
Orang-orang telah meminta aplikasi Electron sejak awal, terutama karena ingin membuka file dengan mengklik dua kali. Kami juga bermaksud untuk menempatkan aplikasi di app store. Secara paralel, seseorang menyarankan pembuatan PWA, jadi kami melakukan keduanya. Untungnya kami diperkenalkan dengan Project Fugu API seperti akses sistem file, akses papan klip, penanganan file, dan banyak lagi. Dengan sekali klik, Anda dapat menginstal aplikasi di desktop atau perangkat seluler, tanpa membebani Elektron ekstra. Sangatlah mudah untuk menghentikan penggunaan versi Electron, berkonsentrasi hanya pada aplikasi web, dan menjadikannya PWA terbaik. Selain itu, sekarang kami dapat memublikasikan PWA ke Play Store dan Microsoft Store. Besar sekali!
Dapat dikatakan bahwa Excalidraw untuk Elektron tidak digunakan lagi karena Elektron itu buruk, bukan sama sekali, tetapi karena web telah menjadi cukup baik. Aku suka ini!
Penanganan file
Ketika saya mengatakan "web telah menjadi cukup baik", itu karena fitur seperti Penanganan File yang akan datang.
Ini adalah penginstalan Big Sur macOS biasa. Sekarang perhatikan apa yang terjadi ketika saya mengklik kanan file Excalidraw. Saya dapat memilih untuk membukanya dengan Excalidraw, PWA yang diinstal. Tentu saja mengklik dua kali juga akan berfungsi, tetapi mendemonstrasikannya di screencast menjadi tidak begitu menarik.
Jadi, bagaimana cara kerjanya? Langkah pertama adalah membuat jenis file yang dapat ditangani aplikasi saya diketahui
oleh sistem operasi. Saya melakukan ini di kolom baru bernama file_handlers
dalam manifes aplikasi web. Nilainya adalah array objek dengan tindakan dan properti accept
. Tindakan tersebut akan menentukan jalur URL
tempat sistem operasi meluncurkan aplikasi Anda, dan objek yang menerima merupakan 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 dalam 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 mempedulikan yang pertama. Dari handle file ini, saya mendapatkan blob yang kemudian diteruskan 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 proses ini berjalan terlalu cepat, Anda dapat membaca selengkapnya tentang File Handling API di artikel saya. Anda dapat mengaktifkan penanganan file dengan menyetel tanda fitur platform web eksperimental. Aplikasi tersebut dijadwalkan akan tersedia di Chrome akhir tahun ini.
Integrasi papan klip
Fitur keren lainnya dari Excalidraw adalah integrasi papan klip. Saya dapat menyalin seluruh gambar atau hanya sebagian gambar ke papan klip, mungkin menambahkan watermark, lalu menempelkannya ke aplikasi lain. Ini adalah versi web aplikasi Windows 95 Paint.
Cara kerjanya sangat sederhana. Yang saya butuhkan hanyalah kanvas sebagai blob, yang kemudian saya tulis
ke papan klip dengan meneruskan array satu elemen bersama ClipboardItem
bersama blob ke
fungsi navigator.clipboard.write()
. Untuk informasi selengkapnya tentang apa yang dapat Anda lakukan dengan API
clipboard, Lihat Jason dan artikel 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 langsung, lalu memulai sesi. Saya dapat membagikan URL sesi kepada kolaborator dengan mudah berkat Web Share API yang telah terintegrasi dengan Excalidraw.
Kolaborasi live
Saya telah menyimulasikan sesi kolaborasi secara lokal dengan menggunakan logo Google I/O di Pixelbook, ponsel Pixel 3a, dan iPad Pro saya. Anda dapat melihat bahwa perubahan yang saya buat pada satu perangkat akan diterapkan di semua perangkat lain.
Saya bahkan bisa melihat semua kursor bergerak. Kursor Pixelbook bergerak perlahan karena dikontrol oleh trackpad, tetapi kursor ponsel Pixel 3a dan kursor tablet iPad Pro melompat-lompat, karena saya mengontrol perangkat ini dengan mengetuk menggunakan jari saya.
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 browser atau aplikasi lain. Saat membuka aplikasi Excalidraw, tetapi tidak melakukan apa pun, kursor menunjukkan saya tidak ada aktivitas, yang dilambangkan dengan tiga titik zZZ.
Pembaca aktif publikasi kami mungkin cenderung berpikir bahwa deteksi tidak ada aktivitas direalisasikan melalui Idle Detection API, proposal tahap awal yang telah dikerjakan dalam konteks Project Fugu. Peringatan {i>spoiler<i}: tidak benar. Meskipun kami memiliki implementasi berdasarkan API ini di Excalidraw, pada akhirnya, kami memutuskan untuk menggunakan pendekatan yang lebih tradisional berdasarkan pengukuran gerakan pointer dan visibilitas halaman.
Kami mengajukan masukan tentang alasan Idle Detection API tidak menyelesaikan kasus penggunaan yang kami miliki. Semua Project Fugu API sedang dikembangkan secara terbuka, sehingga semua orang dapat berpartisipasi dan didengar pendapatnya.
Lipis di atas apa yang menahan Excalidraw
Berbicara mengenai hal tersebut, saya mengajukan satu pertanyaan terakhir kepada lipis mengenai apa yang menurutnya hilang dari platform web yang menahan Excalidraw:
File System Access API memang hebat, tetapi tahukah Anda? Sebagian besar file yang penting bagi saya saat ini ada di Dropbox atau Google Drive, bukan di {i>hard disk<i}. Saya berharap File System Access API akan menyertakan lapisan abstraksi untuk penyedia sistem file jarak jauh seperti Dropbox atau Google untuk berintegrasi dan dapat digunakan oleh developer untuk melakukan coding. Pengguna kemudian dapat bersantai dan tahu bahwa file mereka aman dengan penyedia cloud yang mereka percayai.
Saya sepenuhnya setuju dengan lipis, saya juga tinggal di cloud. Semoga hal ini dapat segera diimplementasikan.
Mode aplikasi dengan tab
Hebat! Kami telah melihat banyak integrasi API yang sangat bagus di Excalidraw. Sistem file, penanganan file, papan klip, berbagi web, dan target berbagi web. Tapi, ada satu hal lagi. Sampai sekarang, saya hanya bisa mengedit satu dokumen pada waktu tertentu. Jangan lagi. Untuk pertama kalinya, nikmati versi awal mode aplikasi bertab di Excalidraw. Seperti inilah tampilannya.
Saya memiliki file yang terbuka di PWA Excalidraw yang terinstal yang berjalan dalam mode mandiri. Sekarang saya membuka tab baru di jendela {i>standalone<i}. Ini bukan tab browser biasa, melainkan tab PWA. Di tab baru ini, saya dapat membuka file sekunder, dan mengerjakannya secara terpisah dari jendela aplikasi yang sama.
Mode aplikasi dengan tab masih dalam tahap awal dan tidak semuanya bersifat permanen. Jika Anda tertarik, pastikan untuk membaca status terkini fitur ini di artikel saya.
Penutup
Untuk terus mendapatkan info terbaru tentang fitur ini dan fitur lainnya, pastikan untuk menonton pelacak API Fugu. Kami sangat bersemangat untuk memajukan web dan memungkinkan Anda melakukan lebih banyak hal di platform ini. Mari kita sambut Excalidraw yang terus ditingkatkan, dan inilah untuk semua aplikasi luar biasa yang akan Anda bangun. Mulai buat konten di excalidraw.com.
Saya tidak sabar untuk melihat beberapa API yang saya tampilkan hari ini muncul dalam aplikasi Anda. Nama saya Tom, Anda bisa menemukan saya sebagai @tomayac di Twitter dan internet secara umum. Terima kasih telah menonton dan menikmati Google I/O selanjutnya.