# Lessons from WhatsApp's Wear OS journey - Speakers: Martin Pelant (Software Engineer, Meta), Subhag Bhatia (Software Engineer, Meta) - Track: Apps foundation - Video: https://www.youtube.com/watch?v=Mi3zullvIYY - Page: https://developer.android.com/events/dev-summit/talks/lessons-from-whatsapps-wear-os-journey ## About the talk Hear directly from WhatsApp engineers on how they upgraded their Wear OS app at scale. This session covers the architectural and UX decisions behind integrating Material 3 and adopting Wear Widgets. Gain direct insight into the lessons, patterns and tools the team used to update their app and deliver a cohesive user experience. ## Summary - Sharing Android phone code with Wear OS cuts duplicate work but requires dependency injection and manifest overrides. Tests should compare multibinding sets so modularized features do not silently disappear. - Runtime notification bridging management prevents duplicate alerts while letting the watch operate independently. Turning bridging off at login and back on at logout stops phone mirrors from double-buzzing. - Material 3 migrations on round displays require managing behavioral changes alongside visual styling. Using `TransformingLazyColumn` properly means applying transformations at only one level and avoiding caching positions with Compose's `remember`. - App expiry limits can trigger sudden active user drops if watches fail to update promptly. Scheduled update reminders direct users to Google Play before their ninety-day build expires. ## Topics wear-os, material-3, jetpack-compose, modularization, notifications, tiles-and-widgets ## Transcript ### Introduction [00:04](https://www.youtube.com/watch?v=Mi3zullvIYY&t=4s) Hi everyone. [00:05](https://www.youtube.com/watch?v=Mi3zullvIYY&t=5s) My name is Martin. [00:06](https://www.youtube.com/watch?v=Mi3zullvIYY&t=6s) And I'm Subhag. [00:07](https://www.youtube.com/watch?v=Mi3zullvIYY&t=7s) We are engineers at Meta on the team behind WhatsApp on Wear OS. [00:11](https://www.youtube.com/watch?v=Mi3zullvIYY&t=11s) And we recently modernized our app. [00:13](https://www.youtube.com/watch?v=Mi3zullvIYY&t=13s) So today, we are going to share the lessons we learned from migrating to shared codebase and then building new experiences on top of it. [00:20](https://www.youtube.com/watch?v=Mi3zullvIYY&t=20s) What makes WhatsApp on Wear OS interesting is that it works without your phone. [00:24](https://www.youtube.com/watch?v=Mi3zullvIYY&t=24s) Once it's linked, it acts as its own WhatsApp device, end-to-end encrypted just like your phone, with its own database and connection to server. [00:32](https://www.youtube.com/watch?v=Mi3zullvIYY&t=32s) So it keeps working even on Wi-Fi or LTE when your phone is out of range. [00:38](https://www.youtube.com/watch?v=Mi3zullvIYY&t=38s) We first launched WhatsApp on Wear OS in 2023. [00:41](https://www.youtube.com/watch?v=Mi3zullvIYY&t=41s) It was powered by a compact, portable client written in Rust. [00:45](https://www.youtube.com/watch?v=Mi3zullvIYY&t=45s) It got us to production fast. [00:47](https://www.youtube.com/watch?v=Mi3zullvIYY&t=47s) However, it was a second, independent implementation of WhatsApp, which means that every protocol, encryption, and feature had to be built twice. [00:55](https://www.youtube.com/watch?v=Mi3zullvIYY&t=55s) So the watch app was always catching up. [00:58](https://www.youtube.com/watch?v=Mi3zullvIYY&t=58s) Being portable also made handling background tricky. [01:01](https://www.youtube.com/watch?v=Mi3zullvIYY&t=61s) The client handled its own threads, timers, and sockets, things that Android wants to manage for you. [01:07](https://www.youtube.com/watch?v=Mi3zullvIYY&t=67s) On Android, background has rules. [01:10](https://www.youtube.com/watch?v=Mi3zullvIYY&t=70s) When in doze mode, you lose network. [01:12](https://www.youtube.com/watch?v=Mi3zullvIYY&t=72s) Once your process is cached, you have limited or no execution time. [01:16](https://www.youtube.com/watch?v=Mi3zullvIYY&t=76s) You need a service, job, or push to overcome that. [01:20](https://www.youtube.com/watch?v=Mi3zullvIYY&t=80s) So our watch app had to wire those by hand. [01:23](https://www.youtube.com/watch?v=Mi3zullvIYY&t=83s) But our Android codebase already does that. [01:26](https://www.youtube.com/watch?v=Mi3zullvIYY&t=86s) So we decided to reuse and migrate to our Android codebase, which is battle-tested on billions of devices and already follows the Android rules. ### Moving to a shared codebase [01:35](https://www.youtube.com/watch?v=Mi3zullvIYY&t=95s) That way, new features don't have to be built twice. [01:38](https://www.youtube.com/watch?v=Mi3zullvIYY&t=98s) The goal was simple: keep the business logic and remove the UI. [01:41](https://www.youtube.com/watch?v=Mi3zullvIYY&t=101s) What's left is a headless library which which uses Kotlin flows as its API. [01:47](https://www.youtube.com/watch?v=Mi3zullvIYY&t=107s) The challenge, however, is that you cannot untangle app of this size in one go. [01:52](https://www.youtube.com/watch?v=Mi3zullvIYY&t=112s) Some modules mix UI with infra. [01:54](https://www.youtube.com/watch?v=Mi3zullvIYY&t=114s) Other depend on features you don't need in the Wear OS app. [01:57](https://www.youtube.com/watch?v=Mi3zullvIYY&t=117s) So we worked in two tracks: we kept modularizing, and we stripped or over- overwrote what we couldn't remove yet. [02:06](https://www.youtube.com/watch?v=Mi3zullvIYY&t=126s) Let's start with modularization. [02:08](https://www.youtube.com/watch?v=Mi3zullvIYY&t=128s) To keep code out of the build, a simple "if this is phone app" statement won't work because it doesn't remove the code. [02:14](https://www.youtube.com/watch?v=Mi3zullvIYY&t=134s) So we rely heavily on DI, especially optional and multibinding. [02:20](https://www.youtube.com/watch?v=Mi3zullvIYY&t=140s) We use in-house DI, but it maps closely to what Dagger offers. [02:24](https://www.youtube.com/watch?v=Mi3zullvIYY&t=144s) So the examples are in Dagger. [02:27](https://www.youtube.com/watch?v=Mi3zullvIYY&t=147s) With optional binding, in Dagger it's `BindsOptionalOf`, you can declare a dependency as optional, for example calling backend. ```kotlin @Module interface CallingModule { @BindsOptionalOf fun callingBackend(): CallingBackend } class Calls @Inject constructor( val backend: Optional, ) { fun start(): CallResult = backend.getOrNull()?.start() ?: CallResult.NotAllowed } ``` [02:33](https://www.youtube.com/watch?v=Mi3zullvIYY&t=153s) Then if watch build includes a module which provides the implementation, the injected backend will be present, and calling will work. [02:41](https://www.youtube.com/watch?v=Mi3zullvIYY&t=161s) If you omit the module, it will be null, and your API can return `NotAllowed`. [02:47](https://www.youtube.com/watch?v=Mi3zullvIYY&t=167s) Multibinding works for the same, but for sets. ```kotlin @Module interface CoreModule { @Multibinds fun handlers(): Set } @Module interface ReactionsModule { @Binds @IntoSet fun reactions(impl: ReactionHandler): MessageHandler } class MessageDispatcher @Inject constructor( private val handlers: Set, ) ``` [02:51](https://www.youtube.com/watch?v=Mi3zullvIYY&t=171s) You can declare a set with a at multibinds, for example `MessageHandler`, and then each feature module can add their own handler using into set multibinding. [03:02](https://www.youtube.com/watch?v=Mi3zullvIYY&t=182s) If you don't want to support payments, for example you can leave the whole payments module out of the build, and injected set won't include that handler. [03:11](https://www.youtube.com/watch?v=Mi3zullvIYY&t=191s) That way, whether feature is supported is question for the build graph. [03:16](https://www.youtube.com/watch?v=Mi3zullvIYY&t=196s) However, this decoupling works cuts both ways. [03:19](https://www.youtube.com/watch?v=Mi3zullvIYY&t=199s) With direct references, deleting a module would break your build. [03:22](https://www.youtube.com/watch?v=Mi3zullvIYY&t=202s) With optional or multibinding, there's nothing that references the module, so the build passes, and the feature quietly disappears, or the default take takes place. [03:33](https://www.youtube.com/watch?v=Mi3zullvIYY&t=213s) And we hit exactly this. [03:34](https://www.youtube.com/watch?v=Mi3zullvIYY&t=214s) A refactor moved a feature into its own module, and our build app and our watch app didn't include it. [03:41](https://www.youtube.com/watch?v=Mi3zullvIYY&t=221s) It resulted in voice messages showing zero seconds as a duration. [03:45](https://www.youtube.com/watch?v=Mi3zullvIYY&t=225s) So how to ensure this won't happen? [03:47](https://www.youtube.com/watch?v=Mi3zullvIYY&t=227s) Well, you need a test to put the check back in. [03:49](https://www.youtube.com/watch?v=Mi3zullvIYY&t=229s) In our case, what we do is what we compare multibinding sets between the phone app and the watch app for interfaces that matter. [03:56](https://www.youtube.com/watch?v=Mi3zullvIYY&t=236s) That way we ensure that we have the right implementation always bound. [04:02](https://www.youtube.com/watch?v=Mi3zullvIYY&t=242s) Now track two covers what we couldn't remove yet. [04:05](https://www.youtube.com/watch?v=Mi3zullvIYY&t=245s) Reusing modules will save you work, but everything they declare merges into your manifest. [04:08](https://www.youtube.com/watch?v=Mi3zullvIYY&t=248s) Phone launcher activity would add a secondary app icon, or unneeded content providers would start with your app. [04:16](https://www.youtube.com/watch?v=Mi3zullvIYY&t=256s) Manifest merger offers a useful `tools:node="remove"` attribute that allows you to take the components out. ```xml ``` [04:23](https://www.youtube.com/watch?v=Mi3zullvIYY&t=263s) And once they are out, the shrinker can remove the code as well. [04:27](https://www.youtube.com/watch?v=Mi3zullvIYY&t=267s) If something still needs to reach the activity, you can use `activity-alias` to redirect to new class. [04:34](https://www.youtube.com/watch?v=Mi3zullvIYY&t=274s) Things to watch, however, is those attributes only apply to lower-priority manifests, so the merge order decides whether something does gets removed or not. [04:45](https://www.youtube.com/watch?v=Mi3zullvIYY&t=285s) Resources work the same way. [04:47](https://www.youtube.com/watch?v=Mi3zullvIYY&t=287s) Reuse gives you phone strings for free. [04:50](https://www.youtube.com/watch?v=Mi3zullvIYY&t=290s) However, some promise things a watch can't do. [04:52](https://www.youtube.com/watch?v=Mi3zullvIYY&t=292s) Let's take an example at the end on the encryption notice that is at the beginning of every chat. [04:58](https://www.youtube.com/watch?v=Mi3zullvIYY&t=298s) On phone, it ends with a "learn more" link. [05:01](https://www.youtube.com/watch?v=Mi3zullvIYY&t=301s) However, there is nothing behind the link on the watch. [05:03](https://www.youtube.com/watch?v=Mi3zullvIYY&t=303s) So what we do is we redeclare the same key in higher-priority module on the watch and replace the string with "use your phone to learn more." ```xml … Learn more …Use your phone to learn more. ``` [05:13](https://www.youtube.com/watch?v=Mi3zullvIYY&t=313s) And then resource merger picks ours. [05:16](https://www.youtube.com/watch?v=Mi3zullvIYY&t=316s) You also need something to ensure that you don't override string by accident. [05:19](https://www.youtube.com/watch?v=Mi3zullvIYY&t=319s) So we have built build rule that ensures and fails if override is not allow listed. ### Managing notification bridging [05:27](https://www.youtube.com/watch?v=Mi3zullvIYY&t=327s) Now let's talk about notifications. [05:29](https://www.youtube.com/watch?v=Mi3zullvIYY&t=329s) By default, Wear OS bridges or mirrors your phone notifications. [05:32](https://www.youtube.com/watch?v=Mi3zullvIYY&t=332s) That's great, but it only works if your phone is connected. [05:35](https://www.youtube.com/watch?v=Mi3zullvIYY&t=335s) And we want WhatsApp to work independently, so our app posts our own notifications. [05:40](https://www.youtube.com/watch?v=Mi3zullvIYY&t=340s) But if bridging stays on, every message buzzes twice. [05:44](https://www.youtube.com/watch?v=Mi3zullvIYY&t=344s) Turning bridging off fixes duplicates. [05:47](https://www.youtube.com/watch?v=Mi3zullvIYY&t=347s) There is a static Manifest flag, but it applies from install before user logs in. [05:53](https://www.youtube.com/watch?v=Mi3zullvIYY&t=353s) So if you install and don't log in, you suddenly lose all notifications, which is not great, is it? [05:59](https://www.youtube.com/watch?v=Mi3zullvIYY&t=359s) So we set it at runtime using a `BridgingManager`. ```kotlin isRegistered.collect { when (it) { // watch posts its own true -> bridging.setEnabled(false) // phone bridges again false -> bridging.setEnabled(true) } } fun setEnabled(bridgingEnabled){ bridgingManager.setConfig( BridgingConfig.Builder(context, bridgingEnabled) .build()) } ``` [06:02](https://www.youtube.com/watch?v=Mi3zullvIYY&t=362s) When you log in, we turned the bridging off. [06:04](https://www.youtube.com/watch?v=Mi3zullvIYY&t=364s) When you log out, we turn it back on. [06:07](https://www.youtube.com/watch?v=Mi3zullvIYY&t=367s) However, keep in mind there is no API to read the values back, and we observed watch to lose it. [06:12](https://www.youtube.com/watch?v=Mi3zullvIYY&t=372s) So what we do is we write it again whenever watch comes of charger or if the app updates. [06:19](https://www.youtube.com/watch?v=Mi3zullvIYY&t=379s) Last topic I wanted to discuss is, how do you log out? [06:23](https://www.youtube.com/watch?v=Mi3zullvIYY&t=383s) When you unlink a watch from the from your account, people expect the messages to go with it. [06:28](https://www.youtube.com/watch?v=Mi3zullvIYY&t=388s) So we delete everything-- messages, keys, settings. [06:31](https://www.youtube.com/watch?v=Mi3zullvIYY&t=391s) But how to do it safely? [06:32](https://www.youtube.com/watch?v=Mi3zullvIYY&t=392s) Delete files while background threads still write to it, you get SQLite exceptions, or even worse, writes to newly recreated databases. [06:44](https://www.youtube.com/watch?v=Mi3zullvIYY&t=404s) And you can't really clear state of a process without stopping it. [06:48](https://www.youtube.com/watch?v=Mi3zullvIYY&t=408s) Android offers you `clearApplicationUserData` API, which clears the data and stops the app, but it also resets permissions. [06:56](https://www.youtube.com/watch?v=Mi3zullvIYY&t=416s) And on some OEMs, notification bridging ties to the notification permissions, so if you use this, you would suddenly lose phone mirroring as well. [07:06](https://www.youtube.com/watch?v=Mi3zullvIYY&t=426s) So we stop the process ourselves. ```kotlin clearActivityBackStack() // enumerate our own, kill each killOtherProcesses() // no threads alive — now safe deleteDataDirectories() // show logged out confirmation firePostCleanupIntent() ``` [07:07](https://www.youtube.com/watch?v=Mi3zullvIYY&t=427s) We build a tiny service in a secondary process, which has no DI, no work manager, so it doesn't start rest of the app. [07:15](https://www.youtube.com/watch?v=Mi3zullvIYY&t=435s) After you log out, this service kills every other process in our app, and only then deletes all the data when nothing is racing it. [07:22](https://www.youtube.com/watch?v=Mi3zullvIYY&t=442s) Since we don't use the `clearApplicationUserData`, the permissions survive and we can turn notification bridging on, so the phone notifications come back. [07:30](https://www.youtube.com/watch?v=Mi3zullvIYY&t=450s) That's for part one, and now I will hand it over to Subhag to talk about our UI refresh. ### Modernizing the Wear OS UI [07:40](https://www.youtube.com/watch?v=Mi3zullvIYY&t=460s) Thanks, Martin. [07:41](https://www.youtube.com/watch?v=Mi3zullvIYY&t=461s) With the shared infrastructure in place, our focus was to make WhatsApp feel truly at home on your wrist. [07:49](https://www.youtube.com/watch?v=Mi3zullvIYY&t=469s) The first part of that journey was a move to Material 3, but first, let's ask why. [07:55](https://www.youtube.com/watch?v=Mi3zullvIYY&t=475s) Well, on a round display where every pixel matters, Material 3 gave us the right components and the design foundation to modernize WhatsApp for the wrist. [08:07](https://www.youtube.com/watch?v=Mi3zullvIYY&t=487s) If you notice the visuals, on the surface, it might look just like a UI update. [08:11](https://www.youtube.com/watch?v=Mi3zullvIYY&t=491s) Well, we were moving from Material 2 to 3, and it couldn't happen overnight. [08:17](https://www.youtube.com/watch?v=Mi3zullvIYY&t=497s) The old and new code had to live side by side behind a switch. [08:21](https://www.youtube.com/watch?v=Mi3zullvIYY&t=501s) As a result, the screens which visually matched Figma, still showed some broken behaviors. [08:26](https://www.youtube.com/watch?v=Mi3zullvIYY&t=506s) To give a few examples, crown scrolling, swipe right to go back, some of the most basic gestures didn't work. [08:34](https://www.youtube.com/watch?v=Mi3zullvIYY&t=514s) All this to say, a design system migration is also a visual is not only a visual migration, but it's also a behavior migration. [08:45](https://www.youtube.com/watch?v=Mi3zullvIYY&t=525s) Speaking of behaviors, if you notice the rows near the edges, they are narrower and tucked into the curve. [08:52](https://www.youtube.com/watch?v=Mi3zullvIYY&t=532s) That's TransformingLazyColumn. [08:54](https://www.youtube.com/watch?v=Mi3zullvIYY&t=534s) Rows transform so the circular bezel doesn't cut them off. [08:58](https://www.youtube.com/watch?v=Mi3zullvIYY&t=538s) A dynamic screen can make things tricky. [09:00](https://www.youtube.com/watch?v=Mi3zullvIYY&t=540s) New messages arrive, rows move, and bubbles come in every shape. [09:06](https://www.youtube.com/watch?v=Mi3zullvIYY&t=546s) Let's see what it meant to get it right. [09:10](https://www.youtube.com/watch?v=Mi3zullvIYY&t=550s) If you notice on the left, bubbles are shrinking way too aggressively. [09:15](https://www.youtube.com/watch?v=Mi3zullvIYY&t=555s) Well, each message is internally a bubble wrapped inside a card. [09:19](https://www.youtube.com/watch?v=Mi3zullvIYY&t=559s) We told the parent card to apply a transform, but the same transform was being passed on to the child bubble as well. [09:26](https://www.youtube.com/watch?v=Mi3zullvIYY&t=566s) As a result, the child bubble is getting twice the amount of intended transform. [09:32](https://www.youtube.com/watch?v=Mi3zullvIYY&t=572s) The fix was to ensure that we apply a transform at one level, and that was at the parent card. [09:37](https://www.youtube.com/watch?v=Mi3zullvIYY&t=577s) And that's also a key principle. [09:39](https://www.youtube.com/watch?v=Mi3zullvIYY&t=579s) We ensured trans- one transform for one item. [09:46](https://www.youtube.com/watch?v=Mi3zullvIYY&t=586s) And here's another problem: bubbles that are small and faded right in the middle of the screen where they should be full-sized. [09:53](https://www.youtube.com/watch?v=Mi3zullvIYY&t=593s) Well, size is a function of their position on the screen. [09:57](https://www.youtube.com/watch?v=Mi3zullvIYY&t=597s) In our initial version, we saved the size fitting using the Compose's remember function, but that also ended up saving its older position. [10:06](https://www.youtube.com/watch?v=Mi3zullvIYY&t=606s) Now as a result, when a new message arrives and it shifts the older bubbles upwards, their transform is still based on their older position. [10:14](https://www.youtube.com/watch?v=Mi3zullvIYY&t=614s) And that results in that bug. [10:17](https://www.youtube.com/watch?v=Mi3zullvIYY&t=617s) To fix this, we stopped saving the size using the remember call and got rid of the call. [10:22](https://www.youtube.com/watch?v=Mi3zullvIYY&t=622s) And that's also a learning. [10:24](https://www.youtube.com/watch?v=Mi3zullvIYY&t=624s) When a row moves, its transformation must move with it. [10:30](https://www.youtube.com/watch?v=Mi3zullvIYY&t=630s) Here you can see both the fixes in the code. ```kotlin // FIX #1: ONE OWNER CompositionLocalProvider( LocalSurfaceTransformation provides null ) { M3Card(transformation = t) { SharedChatEntry(...) } } // FIX #2: CURRENT ITEM SCOPE val t = SurfaceTransformation(spec) ``` ```kotlin // BUG #1: DOUBLE MORPH M3Card(transformation = t) { CompositionLocalProvider( LocalSurfaceTransformation provides t ) { SharedChatEntry(...) } } // BUG #2: STALE ITEM SCOPE val t = remember(spec) { SurfaceTransformation(spec) } ``` [10:32](https://www.youtube.com/watch?v=Mi3zullvIYY&t=632s) On the top left, you can see the transform is being applied at two levels, and on the top right, only it's being applied at one level, which is the M3 card. [10:42](https://www.youtube.com/watch?v=Mi3zullvIYY&t=642s) On the bottom left, we are using the remember call to cache the transform, but it also caches the position. [10:49](https://www.youtube.com/watch?v=Mi3zullvIYY&t=649s) And on the bottom right, we are computing the transform considering the bubble's latest position every time. [10:57](https://www.youtube.com/watch?v=Mi3zullvIYY&t=657s) As you can see, the result is a consistent experience across all the screens, where key content stays near the center, and rows move naturally near the edge. ### Handling wrist notification alerts [11:09](https://www.youtube.com/watch?v=Mi3zullvIYY&t=669s) Next, let's talk about notifications. [11:12](https://www.youtube.com/watch?v=Mi3zullvIYY&t=672s) Now imagine your wrist buzzes, you check your watch, and you see a message. [11:16](https://www.youtube.com/watch?v=Mi3zullvIYY&t=676s) But moments later, your wrist buzzes again, and again. [11:21](https://www.youtube.com/watch?v=Mi3zullvIYY&t=681s) You get multiple notifications, and when you check each time, it's the same thing that you see. [11:28](https://www.youtube.com/watch?v=Mi3zullvIYY&t=688s) Well, that's really annoying. [11:29](https://www.youtube.com/watch?v=Mi3zullvIYY&t=689s) So to fix this, we asked ourselves two questions. [11:33](https://www.youtube.com/watch?v=Mi3zullvIYY&t=693s) What event should alert the notification should alert the user? [11:37](https://www.youtube.com/watch?v=Mi3zullvIYY&t=697s) And which event should update the notifications silently? [11:41](https://www.youtube.com/watch?v=Mi3zullvIYY&t=701s) This bug was happening because behind the scene, profile photo updates, media downloads, and receipts, which should silently update the notification, were alerting the user instead. [11:52](https://www.youtube.com/watch?v=Mi3zullvIYY&t=712s) Now, when a new message arrives, the wrist buzzes exactly once, while non-alertworthy events refresh the notification silently. [12:02](https://www.youtube.com/watch?v=Mi3zullvIYY&t=722s) And that's also a lesson. [12:03](https://www.youtube.com/watch?v=Mi3zullvIYY&t=723s) When notification is one of your core user journeys, we must ensure that each alert truly deserves the user's attention. [12:12](https://www.youtube.com/watch?v=Mi3zullvIYY&t=732s) On the topic of notifications. [12:14](https://www.youtube.com/watch?v=Mi3zullvIYY&t=734s) Earlier, WhatsApp Wear OS notifications allowed you to do two things. [12:18](https://www.youtube.com/watch?v=Mi3zullvIYY&t=738s) You could read the message, or you could open the app. [12:22](https://www.youtube.com/watch?v=Mi3zullvIYY&t=742s) If you wanted to reply to a notification directly, you couldn't do that earlier. [12:26](https://www.youtube.com/watch?v=Mi3zullvIYY&t=746s) So we bridged that gap using Android's notification action with RemoteInput API. [12:33](https://www.youtube.com/watch?v=Mi3zullvIYY&t=753s) The user can now type it, say it, or tap a quick reply. [12:37](https://www.youtube.com/watch?v=Mi3zullvIYY&t=757s) But remember, hitting send isn't the end. [12:40](https://www.youtube.com/watch?v=Mi3zullvIYY&t=760s) If a message is sent successfully, we mark the chat as read and clear the notification. [12:46](https://www.youtube.com/watch?v=Mi3zullvIYY&t=766s) If a message fails to deliver, we offer a single-tap retry. [12:50](https://www.youtube.com/watch?v=Mi3zullvIYY&t=770s) And that's also a key learning while developing this, is that a notification action must be treated as a complete end-to-end workflow. ### Migrating from tiles to widgets [13:00](https://www.youtube.com/watch?v=Mi3zullvIYY&t=780s) Next, Tiles and Widgets. [13:02](https://www.youtube.com/watch?v=Mi3zullvIYY&t=782s) A Tile, as you can see, is one full screen, where you can reach by swiping from your watch face. [13:09](https://www.youtube.com/watch?v=Mi3zullvIYY&t=789s) Widgets on the other hand, are multiple smaller cards, where several can fit in one scrollable screen. [13:15](https://www.youtube.com/watch?v=Mi3zullvIYY&t=795s) Both WhatsApp Tiles and Widgets are powered by the same underlying data. [13:21](https://www.youtube.com/watch?v=Mi3zullvIYY&t=801s) And that's also a twist. [13:22](https://www.youtube.com/watch?v=Mi3zullvIYY&t=802s) You might think that this UI is being drawn by our app, but that's actually not the case. [13:27](https://www.youtube.com/watch?v=Mi3zullvIYY&t=807s) We only describe the UI and the data, and the system draws it. [13:34](https://www.youtube.com/watch?v=Mi3zullvIYY&t=814s) And also, tapping on any of the icon can't run your app's code. [13:38](https://www.youtube.com/watch?v=Mi3zullvIYY&t=818s) You can only ask it to open a screen. [13:41](https://www.youtube.com/watch?v=Mi3zullvIYY&t=821s) And that's our lesson. [13:42](https://www.youtube.com/watch?v=Mi3zullvIYY&t=822s) If the system is drawing your UI, it's part of your product and testing metrics. [13:47](https://www.youtube.com/watch?v=Mi3zullvIYY&t=827s) Frank and Yuri will go deeper in their talk on Widgets. ### Preventing the active user cliff with update reminders [13:52](https://www.youtube.com/watch?v=Mi3zullvIYY&t=832s) And that brings me to the last slide, which is let's take a step back and ask ourselves, at the end of the day, we want more and more users using our apps. [14:04](https://www.youtube.com/watch?v=Mi3zullvIYY&t=844s) Now imagine as a developer, you log in to your dashboards one day, and you see a significant active user cliff. [14:11](https://www.youtube.com/watch?v=Mi3zullvIYY&t=851s) Well, at first first of all, it would be very concerning. [14:14](https://www.youtube.com/watch?v=Mi3zullvIYY&t=854s) And then you might start debugging it. [14:16](https://www.youtube.com/watch?v=Mi3zullvIYY&t=856s) And you might feel that there was a crash, or there's some bad release going on. [14:21](https://www.youtube.com/watch?v=Mi3zullvIYY&t=861s) But for us, none of those were the case. [14:22](https://www.youtube.com/watch?v=Mi3zullvIYY&t=862s) It turns out that every WhatsApp app version has a 90-day built-in expiry. [14:29](https://www.youtube.com/watch?v=Mi3zullvIYY&t=869s) So imagine users are stuck on a stale build, while the newer builds are taking time to reach their watches. [14:36](https://www.youtube.com/watch?v=Mi3zullvIYY&t=876s) So as a result, the users are not leaving the app, but still their app stopped working. [14:42](https://www.youtube.com/watch?v=Mi3zullvIYY&t=882s) So we made updating the app part of the product. [14:46](https://www.youtube.com/watch?v=Mi3zullvIYY&t=886s) As you can see on the screen, 15 days before the app expiry, this appears on the screen. [14:51](https://www.youtube.com/watch?v=Mi3zullvIYY&t=891s) Then it appears again at 7, 5, 3, 2, and 1 day remaining. [14:58](https://www.youtube.com/watch?v=Mi3zullvIYY&t=898s) User can choose to dismiss it, or land in the Play Store to update their app. [15:03](https://www.youtube.com/watch?v=Mi3zullvIYY&t=903s) This helps by giving user multiple reminders that their app expiry is approaching, and also giving them multiple chances to update their app. [15:11](https://www.youtube.com/watch?v=Mi3zullvIYY&t=911s) And that's our final learning. [15:13](https://www.youtube.com/watch?v=Mi3zullvIYY&t=913s) Our jobs as developers isn't complete when we ship an update to 100%. [15:17](https://www.youtube.com/watch?v=Mi3zullvIYY&t=917s) It's only complete when users actually receive our updates. [15:23](https://www.youtube.com/watch?v=Mi3zullvIYY&t=923s) That brings us to the end. [15:24](https://www.youtube.com/watch?v=Mi3zullvIYY&t=924s) If you'd like to continue the conversation, please feel free to come say hello during the office hours. [15:30](https://www.youtube.com/watch?v=Mi3zullvIYY&t=930s) And next up is going to be Frank and Yuri to talk about Wear Widgets. [15:33](https://www.youtube.com/watch?v=Mi3zullvIYY&t=933s) Thank you.