Lắng nghe trực tiếp từ các kỹ sư của WhatsApp về cách họ nâng cấp ứng dụng Wear OS trên quy mô lớn. Phiên này đề cập đến các quyết định về cấu trúc và trải nghiệm người dùng đằng sau việc tích hợp Material 3 và áp dụng Tiện ích Wear.
Tìm hiểu trực tiếp về các bài học, mẫu và công cụ mà nhóm đã sử dụng để cập nhật ứng dụng và mang lại trải nghiệm người dùng nhất quán.
TransformingLazyColumn đúng cách có nghĩa là chỉ áp dụng các phép biến đổi ở một cấp và tránh lưu các vị trí vào bộ nhớ đệm bằng remember của Compose.Bản chép lời
Chào các bạn.
Tôi là Martin.
Còn tôi là Subhag.
Chúng tôi là các kỹ sư tại Meta, thuộc nhóm phát triển WhatsApp trên Wear OS.
Và gần đây, chúng tôi đã hiện đại hoá ứng dụng của mình.
Vì vậy, hôm nay, chúng tôi sẽ chia sẻ những bài học rút ra từ việc di chuyển sang toàn bộ mã nguồn dùng chung, sau đó xây dựng trải nghiệm mới dựa trên toàn bộ mã nguồn đó.
Điều thú vị về WhatsApp trên Wear OS là ứng dụng này hoạt động mà không cần điện thoại.
Sau khi được liên kết, thiết bị này sẽ hoạt động như một thiết bị WhatsApp riêng, được mã hoá hai đầu giống như điện thoại của bạn, có cơ sở dữ liệu và kết nối riêng với máy chủ.
Vì vậy, đồng hồ vẫn hoạt động ngay cả khi dùng Wi-Fi hoặc LTE khi điện thoại của bạn nằm ngoài phạm vi phủ sóng.
Chúng tôi ra mắt WhatsApp trên Wear OS lần đầu vào năm 2023.
Nó được hỗ trợ bởi một ứng dụng nhỏ gọn, di động được viết bằng Rust.
Công cụ này giúp chúng tôi nhanh chóng đưa ứng dụng vào giai đoạn phát hành công khai.
Tuy nhiên, đây là một phương thức triển khai WhatsApp độc lập thứ hai, tức là mọi giao thức, phương thức mã hoá và tính năng đều phải được xây dựng hai lần.
Vì vậy, ứng dụng đồng hồ luôn phải bắt kịp.
Tính di động cũng khiến việc xử lý nền trở nên khó khăn.
Ứng dụng khách xử lý các luồng, bộ hẹn giờ và cổng kết nối của riêng mình, những thứ mà Android muốn quản lý cho bạn.
Trên Android, chế độ nền có các quy tắc.
Khi ở chế độ ngủ, bạn sẽ mất kết nối mạng.
Sau khi quy trình được lưu vào bộ nhớ đệm, bạn sẽ có thời gian thực thi hạn chế hoặc không có thời gian thực thi.
Bạn cần có một dịch vụ, công việc hoặc thông báo đẩy để khắc phục vấn đề đó.
Vì vậy, ứng dụng đồng hồ của chúng tôi phải kết nối các thành phần đó theo cách thủ công.
Nhưng toàn bộ mã nguồn Android của chúng tôi đã làm được điều đó.
Vì vậy, chúng tôi quyết định sử dụng lại và di chuyển sang toàn bộ mã nguồn Android. Toàn bộ mã nguồn này đã được thử nghiệm trên hàng tỷ Thiết bị và tuân thủ các quy tắc của Android.
Nhờ đó, bạn không cần phải xây dựng các tính năng mới hai lần.
Mục tiêu rất đơn giản: giữ lại logic nghiệp vụ và loại bỏ giao diện người dùng.
Phần còn lại là một thư viện không có giao diện người dùng, sử dụng luồng Kotlin làm API.
Tuy nhiên, thách thức là bạn không thể gỡ rối ứng dụng có kích thước này trong một lần.
Một số mô-đun kết hợp giao diện người dùng với cơ sở hạ tầng.
Các phần phụ thuộc khác dựa vào những tính năng mà bạn không cần trong ứng dụng Wear OS.
Vì vậy, chúng tôi đã làm việc theo hai hướng: tiếp tục mô-đun hoá và loại bỏ hoặc ghi đè những gì chưa thể xoá.
Hãy bắt đầu với việc mô-đun hoá.
Để loại bỏ mã khỏi bản dựng, một câu lệnh đơn giản "nếu đây là ứng dụng điện thoại" sẽ không hoạt động vì câu lệnh này không xoá mã.
Vì vậy, chúng tôi phụ thuộc rất nhiều vào DI, đặc biệt là tính năng liên kết đa chiều và không bắt buộc.
Chúng tôi sử dụng DI nội bộ, nhưng DI này liên kết chặt chẽ với những gì Dagger cung cấp.
Vì vậy, các ví dụ sẽ được trình bày trong Dagger.
Với liên kết không bắt buộc, trong Dagger là BindsOptionalOf, bạn có thể khai báo một phần phụ thuộc là không bắt buộc, ví dụ: gọi phần phụ trợ.
@Module
interface CallingModule {
@BindsOptionalOf
fun callingBackend(): CallingBackend
}
class Calls @Inject constructor(
val backend: Optional<CallingBackend>,
) {
fun start(): CallResult =
backend.getOrNull()?.start()
?: CallResult.NotAllowed
}Sau đó, nếu bản dựng đồng hồ bao gồm một mô-đun cung cấp phương thức triển khai, thì phần phụ trợ được chèn sẽ xuất hiện và lệnh gọi sẽ hoạt động.
Nếu bạn bỏ qua mô-đun, mô-đun đó sẽ có giá trị rỗng và API của bạn có thể trả về NotAllowed.
Multibinding cũng hoạt động tương tự, nhưng dành cho các tập hợp.
@Module
interface CoreModule {
@Multibinds
fun handlers(): Set<MessageHandler>
}
@Module
interface ReactionsModule {
@Binds @IntoSet
fun reactions(impl: ReactionHandler):
MessageHandler
}
class MessageDispatcher @Inject constructor(
private val handlers: Set<MessageHandler>,
)Bạn có thể khai báo một tập hợp có multibinds, ví dụ: MessageHandler, sau đó mỗi mô-đun tính năng có thể thêm trình xử lý riêng bằng cách sử dụng multibinding.
Nếu không muốn hỗ trợ thanh toán, bạn có thể bỏ toàn bộ mô-đun thanh toán khỏi bản dựng và tập hợp được chèn sẽ không bao gồm trình xử lý đó.
Bằng cách đó, việc tính năng có được hỗ trợ hay không là câu hỏi cho biểu đồ bản dựng.
Tuy nhiên, việc tách rời này có hai mặt.
Với các tệp tham chiếu trực tiếp, việc xoá một mô-đun sẽ làm hỏng bản dựng của bạn.
Với tính năng liên kết không bắt buộc hoặc liên kết nhiều thành phần, không có thành phần nào tham chiếu đến mô-đun, vì vậy, bản dựng sẽ được thông qua và tính năng này sẽ biến mất mà không có thông báo hoặc tính năng mặc định sẽ được áp dụng.
Và chúng tôi đã đạt được chính xác điều này.
Một lần tái cấu trúc đã chuyển một tính năng vào mô-đun riêng và ứng dụng bản dựng cũng như ứng dụng đồng hồ của chúng tôi không bao gồm tính năng đó.
Lỗi này khiến tin nhắn thoại hiển thị thời lượng là 0 giây.
Vậy làm thế nào để đảm bảo điều này không xảy ra?
Bạn cần có một bài kiểm thử để đưa quy trình kiểm tra trở lại.
Trong trường hợp này, chúng ta sẽ so sánh các tập hợp đa liên kết giữa ứng dụng điện thoại và ứng dụng đồng hồ cho các giao diện quan trọng.
Bằng cách đó, chúng tôi đảm bảo rằng chúng tôi luôn có cách triển khai phù hợp.
Giờ đây, lộ trình thứ hai sẽ giải quyết những nội dung mà chúng tôi chưa thể xoá.
Việc sử dụng lại các mô-đun sẽ giúp bạn tiết kiệm công sức, nhưng mọi nội dung mà các mô-đun đó khai báo sẽ hợp nhất vào tệp kê khai của bạn.
Hoạt động của trình chạy điện thoại sẽ thêm biểu tượng ứng dụng phụ hoặc các nhà cung cấp nội dung không cần thiết sẽ bắt đầu bằng ứng dụng của bạn.
Trình hợp nhất tệp kê khai cung cấp một thuộc tính tools:node="remove" hữu ích cho phép bạn loại bỏ các thành phần.
<!-- remove unused Main activity node -->
<activity android:name="com.whatsapp.Main"
tools:node="remove" />
<!-- redirect any calls to a new Activity -->
<activity-alias
android:name="com.whatsapp.Main"
android:targetActivity=".MyNewActivity" />
<provider
android:name="…MigrationContentProvider"
tools:node="remove" />Và sau khi các lớp này bị loại bỏ, trình rút gọn cũng có thể xoá mã.
Nếu vẫn cần truy cập vào hoạt động, bạn có thể dùng activity-alias để chuyển hướng đến lớp mới.
Tuy nhiên, cần lưu ý rằng các thuộc tính đó chỉ áp dụng cho tệp kê khai có mức độ ưu tiên thấp hơn, vì vậy, thứ tự hợp nhất sẽ quyết định xem có xoá nội dung nào đó hay không.
Tài nguyên cũng hoạt động theo cách tương tự.
Tính năng sử dụng lại giúp bạn có được các chuỗi điện thoại miễn phí.
Tuy nhiên, một số ứng dụng hứa hẹn những điều mà đồng hồ không thể làm được.
Hãy xem một ví dụ ở cuối thông báo về việc mã hoá ở đầu mỗi cuộc trò chuyện.
Trên điện thoại, thông báo này kết thúc bằng đường liên kết "tìm hiểu thêm".
Tuy nhiên, không có gì đằng sau đường liên kết trên đồng hồ.
Vì vậy, chúng ta sẽ khai báo lại cùng một khoá trong mô-đun có mức độ ưu tiên cao hơn trên đồng hồ và thay thế chuỗi bằng "use your phone to learn more" (sử dụng điện thoại để tìm hiểu thêm).
<!-- phone -->
<string name="…_disclosure">… Learn more</string>
<!-- wearable override, same key -->
<string name="…_disclosure">…Use your phone to learn more.</string>Sau đó, trình hợp nhất tài nguyên sẽ chọn tài nguyên của chúng ta.
Bạn cũng cần một số yếu tố để đảm bảo rằng bạn không vô tình ghi đè chuỗi.
Vì vậy, chúng tôi đã xây dựng quy tắc đảm bảo và không thành công nếu cơ chế ghi đè không được đưa vào danh sách cho phép.
Bây giờ, hãy nói về thông báo.
Theo mặc định, Wear OS sẽ bắc cầu hoặc phản chiếu thông báo trên điện thoại.
Tuyệt vời, nhưng tính năng này chỉ hoạt động khi điện thoại của bạn được kết nối.
Và chúng tôi muốn WhatsApp hoạt động độc lập, vì vậy, ứng dụng của chúng tôi sẽ đăng thông báo của riêng mình.
Nhưng nếu tính năng kết nối vẫn bật, thì mỗi tin nhắn sẽ rung hai lần.
Việc tắt tính năng bắc cầu sẽ khắc phục được tình trạng trùng lặp.
Có một cờ Manifest tĩnh, nhưng cờ này áp dụng từ khi cài đặt trước khi người dùng đăng nhập.
Vì vậy, nếu bạn cài đặt và không đăng nhập, bạn sẽ đột nhiên mất tất cả thông báo, điều này không tốt, phải không?
Vì vậy, chúng ta đặt giá trị này trong thời gian chạy bằng cách sử dụng BridgingManager.
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())
}Khi bạn đăng nhập, chúng tôi đã tắt tính năng kết nối.
Khi bạn đăng xuất, chúng tôi sẽ bật lại tính năng này.
Tuy nhiên, xin lưu ý rằng không có API nào để đọc lại các giá trị và chúng tôi nhận thấy đồng hồ bị mất các giá trị này.
Vì vậy, chúng ta sẽ ghi lại dữ liệu này mỗi khi đồng hồ được sạc hoặc nếu ứng dụng cập nhật.
Chủ đề cuối cùng tôi muốn thảo luận là cách đăng xuất.
Khi bạn huỷ liên kết đồng hồ khỏi tài khoản của mình, người dùng sẽ muốn tin nhắn cũng bị xoá theo.
Vì vậy, chúng tôi xoá mọi thứ – tin nhắn, khoá, chế độ cài đặt.
Nhưng làm thế nào để thực hiện một cách an toàn?
Xoá tệp trong khi các luồng nền vẫn ghi vào tệp đó, bạn sẽ gặp phải các ngoại lệ SQLite hoặc tệ hơn là ghi vào cơ sở dữ liệu mới được tạo lại.
Và bạn không thể thực sự xoá trạng thái của một quy trình mà không dừng quy trình đó.
Android cung cấp cho bạn API clearApplicationUserData để xoá dữ liệu và dừng ứng dụng, nhưng API này cũng đặt lại các quyền.
Và trên một số OEM, tính năng kết nối thông báo sẽ liên kết với quyền gửi thông báo, vì vậy, nếu sử dụng tính năng này, bạn cũng sẽ đột ngột mất tính năng phản chiếu điện thoại.
Vì vậy, chúng tôi tự dừng quy trình này.
clearActivityBackStack()
// enumerate our own, kill each
killOtherProcesses()
// no threads alive — now safe
deleteDataDirectories()
// show logged out confirmation
firePostCleanupIntent()Chúng tôi xây dựng một dịch vụ nhỏ trong một quy trình phụ, không có DI, không có trình quản lý công việc, vì vậy, dịch vụ này không khởi động phần còn lại của ứng dụng.
Sau khi bạn đăng xuất, dịch vụ này sẽ tắt mọi quy trình khác trong ứng dụng của chúng tôi, sau đó chỉ xoá tất cả dữ liệu khi không có quy trình nào đang chạy.
Vì chúng ta không sử dụng clearApplicationUserData, nên các quyền vẫn còn và chúng ta có thể bật tính năng kết nối thông báo để thông báo trên điện thoại xuất hiện trở lại.
Đó là phần một, và bây giờ tôi sẽ nhường lại cho Subhag để nói về giao diện người dùng mới của chúng tôi.
Cảm ơn Martin.
Khi đã có cơ sở hạ tầng dùng chung, chúng tôi tập trung vào việc giúp WhatsApp hoạt động mượt mà trên cổ tay bạn.
Phần đầu tiên của hành trình đó là chuyển sang Material 3, nhưng trước tiên, hãy tìm hiểu lý do.
Trên màn hình tròn, nơi mọi pixel đều quan trọng, Material 3 đã cung cấp cho chúng tôi các thành phần phù hợp và nền tảng thiết kế để hiện đại hoá WhatsApp cho cổ tay.
Nếu bạn chú ý đến hình ảnh, thì về mặt hình thức, có vẻ như đây chỉ là một bản cập nhật giao diện người dùng.
Chúng tôi đang chuyển từ Material 2 sang Material 3 và quá trình này không thể diễn ra trong một sớm một chiều.
Mã cũ và mã mới phải tồn tại song song sau một nút chuyển.
Do đó, các màn hình khớp về mặt hình ảnh với Figma vẫn cho thấy một số hành vi bị hỏng.
Ví dụ: thao tác cuộn bằng nút xoay, vuốt sang phải để quay lại, một số cử chỉ cơ bản nhất không hoạt động.
Nói tóm lại, việc di chuyển hệ thống thiết kế không chỉ là di chuyển về mặt hình ảnh mà còn là di chuyển về mặt hành vi.
Nói về hành vi, nếu bạn để ý các hàng gần cạnh, chúng sẽ hẹp hơn và nằm gọn trong đường cong.
Đó là TransformingLazyColumn.
Các hàng sẽ biến đổi để không bị viền tròn cắt mất.
Màn hình động có thể gây khó khăn.
Tin nhắn mới đến, các hàng di chuyển và bong bóng xuất hiện ở mọi hình dạng.
Hãy xem việc làm đúng có ý nghĩa như thế nào.
Nếu bạn để ý bên trái, các bong bóng đang thu nhỏ quá nhanh.
Mỗi tin nhắn là một bong bóng được gói bên trong một thẻ.
Chúng ta đã yêu cầu thẻ mẹ áp dụng một phép biến đổi, nhưng phép biến đổi đó cũng được truyền đến bong bóng con.
Do đó, bong bóng con sẽ nhận được lượng biến đổi gấp đôi so với dự kiến.
Cách khắc phục là đảm bảo rằng chúng tôi áp dụng phép biến đổi ở một cấp và đó là ở thẻ mẹ.
Và đó cũng là một nguyên tắc quan trọng.
Chúng tôi đảm bảo mỗi mục chỉ có một phép biến đổi.
Và đây là một vấn đề khác: bong bóng nhỏ và mờ ngay giữa màn hình, trong khi đáng lẽ chúng phải có kích thước đầy đủ.
Kích thước là một hàm của vị trí của chúng trên màn hình.
Trong phiên bản ban đầu, chúng tôi đã lưu kích thước vừa vặn bằng hàm remember của Compose, nhưng hàm này cũng lưu cả vị trí cũ.
Do đó, khi có tin nhắn mới và tin nhắn này đẩy các bong bóng cũ lên trên, thì phép biến đổi của các bong bóng cũ vẫn dựa trên vị trí cũ.
Và điều đó dẫn đến lỗi này.
Để khắc phục vấn đề này, chúng tôi đã ngừng lưu kích thước bằng lệnh gọi remember và loại bỏ lệnh gọi này.
Và đó cũng là một bài học.
Khi một hàng di chuyển, phép biến đổi của hàng đó cũng phải di chuyển theo.
Tại đây, bạn có thể thấy cả hai bản sửa lỗi trong mã.
// FIX #1: ONE OWNER
CompositionLocalProvider(
LocalSurfaceTransformation provides null
) {
M3Card(transformation = t) {
SharedChatEntry(...)
}
}
// FIX #2: CURRENT ITEM SCOPE
val t = SurfaceTransformation(spec)// BUG #1: DOUBLE MORPH
M3Card(transformation = t) {
CompositionLocalProvider(
LocalSurfaceTransformation provides t
) { SharedChatEntry(...) }
}
// BUG #2: STALE ITEM SCOPE
val t = remember(spec) {
SurfaceTransformation(spec)
}Ở trên cùng bên trái, bạn có thể thấy phép biến đổi đang được áp dụng ở hai cấp và ở trên cùng bên phải, phép biến đổi chỉ được áp dụng ở một cấp, đó là thẻ M3.
Ở dưới cùng bên trái, chúng ta đang sử dụng lệnh gọi remember để lưu phép biến đổi vào bộ nhớ đệm, nhưng lệnh gọi này cũng lưu vị trí vào bộ nhớ đệm.
Và ở dưới cùng bên phải, chúng ta sẽ tính toán phép biến đổi dựa trên vị trí mới nhất của bong bóng mỗi lần.
Như bạn có thể thấy, kết quả là trải nghiệm nhất quán trên tất cả màn hình, trong đó nội dung chính nằm gần giữa và các hàng di chuyển tự nhiên gần cạnh.
Tiếp theo, hãy nói về thông báo.
Bây giờ, hãy tưởng tượng cổ tay bạn rung lên, bạn kiểm tra đồng hồ và thấy một tin nhắn.
Nhưng chỉ vài phút sau, cổ tay bạn lại rung lên, hết lần này đến lần khác.
Bạn nhận được nhiều thông báo và mỗi lần kiểm tra, bạn đều thấy cùng một nội dung.
Thật là phiền phức.
Để khắc phục vấn đề này, chúng tôi đã tự đặt ra hai câu hỏi.
Sự kiện nào sẽ kích hoạt thông báo cho người dùng?
Và sự kiện nào sẽ cập nhật thông báo một cách thầm lặng?
Lỗi này xảy ra vì trong nền, các bản cập nhật ảnh hồ sơ, nội dung tải xuống đa phương tiện và biên nhận (vốn sẽ cập nhật thông báo một cách thầm lặng) lại cảnh báo cho người dùng.
Giờ đây, khi có tin nhắn mới, cổ tay sẽ rung đúng một lần, trong khi các sự kiện không đáng cảnh báo sẽ làm mới thông báo một cách thầm lặng.
Đó cũng là một bài học.
Khi thông báo là một trong những hành trình cốt lõi của người dùng, chúng ta phải đảm bảo rằng mỗi cảnh báo đều thực sự đáng để người dùng chú ý.
Về chủ đề thông báo.
Trước đây, thông báo của WhatsApp trên Wear OS cho phép bạn làm hai việc.
Bạn có thể đọc tin nhắn hoặc mở ứng dụng.
Trước đây, bạn không thể trả lời trực tiếp một thông báo.
Vì vậy, chúng tôi đã thu hẹp khoảng cách đó bằng cách sử dụng hành động thông báo của Android với API RemoteInput.
Giờ đây, người dùng có thể nhập, nói hoặc nhấn vào một câu trả lời nhanh.
Nhưng hãy nhớ rằng việc gửi email không phải là bước cuối cùng.
Nếu tin nhắn được gửi thành công, chúng tôi sẽ đánh dấu cuộc trò chuyện là đã đọc và xoá thông báo.
Nếu tin nhắn không gửi được, bạn có thể nhấn một lần để thử lại.
Và đó cũng là một bài học quan trọng trong quá trình phát triển, đó là hành động thông báo phải được coi là một quy trình hoàn chỉnh từ đầu đến cuối.
Tiếp theo là Thẻ thông tin và Tiện ích.
Như bạn có thể thấy, Thẻ thông tin là một màn hình đầy đủ mà bạn có thể truy cập bằng cách vuốt từ mặt đồng hồ.
Mặt khác, tiện ích là nhiều thẻ nhỏ hơn, trong đó một số thẻ có thể vừa với một màn hình có thể cuộn.
Cả Thẻ thông tin và Tiện ích WhatsApp đều sử dụng cùng một dữ liệu cơ bản.
Và đó cũng là một bước ngoặt.
Bạn có thể nghĩ rằng giao diện người dùng này đang được ứng dụng của chúng tôi vẽ, nhưng thực tế không phải vậy.
Chúng ta chỉ mô tả giao diện người dùng và dữ liệu, còn hệ thống sẽ vẽ giao diện người dùng đó.
Ngoài ra, việc nhấn vào bất kỳ biểu tượng nào cũng không thể chạy mã của ứng dụng.
Bạn chỉ có thể yêu cầu Gemini mở một màn hình.
Bài học của chúng ta đến đây là kết thúc.
Nếu hệ thống đang vẽ giao diện người dùng, thì giao diện người dùng đó là một phần của sản phẩm và chỉ số kiểm thử.
Frank và Yuri sẽ đi sâu hơn vào bài nói chuyện về Tiện ích.
Và điều đó đưa tôi đến trang trình bày cuối cùng, đó là hãy lùi lại một bước và tự hỏi bản thân, cuối cùng, chúng ta muốn ngày càng có nhiều người dùng sử dụng ứng dụng của mình.
Bây giờ, hãy tưởng tượng bạn là một nhà phát triển, một ngày nọ, bạn đăng nhập vào trang tổng quan và thấy số người dùng đang hoạt động giảm mạnh.
Trước tiên, điều này sẽ rất đáng lo ngại.
Sau đó, bạn có thể bắt đầu gỡ lỗi.
Và bạn có thể cảm thấy rằng đã xảy ra sự cố hoặc có một số bản phát hành không tốt đang diễn ra.
Nhưng chúng tôi không gặp phải trường hợp nào như vậy.
Hoá ra mọi phiên bản ứng dụng WhatsApp đều có thời hạn sử dụng tích hợp là 90 ngày.
Vì vậy, hãy tưởng tượng người dùng bị kẹt ở một bản dựng cũ, trong khi các bản dựng mới hơn cần thời gian để đến được đồng hồ của họ.
Do đó, người dùng không rời khỏi ứng dụng nhưng ứng dụng của họ vẫn ngừng hoạt động.
Vì vậy, chúng tôi đã đưa việc cập nhật ứng dụng vào sản phẩm.
Như bạn có thể thấy trên màn hình, 15 ngày trước khi ứng dụng hết hạn, thông báo này sẽ xuất hiện trên màn hình.
Sau đó, thông báo này sẽ xuất hiện lại khi còn 7, 5, 3, 2 và 1 ngày.
Người dùng có thể chọn đóng thông báo hoặc chuyển đến Cửa hàng Play để cập nhật ứng dụng.
Điều này giúp nhắc nhở người dùng nhiều lần rằng ứng dụng của họ sắp hết hạn, đồng thời cho họ nhiều cơ hội để cập nhật ứng dụng.
Đó là bài học cuối cùng của chúng ta.
Công việc của nhà phát triển chưa kết thúc khi chúng ta phát hành bản cập nhật cho 100% người dùng.
Quá trình này chỉ hoàn tất khi người dùng thực sự nhận được bản cập nhật của chúng tôi.
Đến đây là kết thúc.
Nếu bạn muốn tiếp tục trò chuyện, vui lòng ghé qua và chào hỏi trong giờ làm việc.
Tiếp theo, Frank và Yuri sẽ nói về Tiện ích Wear.
Cảm ơn bạn.
@Module
interface CallingModule {
@BindsOptionalOf
fun callingBackend(): CallingBackend
}
class Calls @Inject constructor(
val backend: Optional<CallingBackend>,
) {
fun start(): CallResult =
backend.getOrNull()?.start()
?: CallResult.NotAllowed
}@Module
interface CoreModule {
@Multibinds
fun handlers(): Set<MessageHandler>
}
@Module
interface ReactionsModule {
@Binds @IntoSet
fun reactions(impl: ReactionHandler):
MessageHandler
}
class MessageDispatcher @Inject constructor(
private val handlers: Set<MessageHandler>,
)<!-- remove unused Main activity node -->
<activity android:name="com.whatsapp.Main"
tools:node="remove" />
<!-- redirect any calls to a new Activity -->
<activity-alias
android:name="com.whatsapp.Main"
android:targetActivity=".MyNewActivity" />
<provider
android:name="…MigrationContentProvider"
tools:node="remove" /><!-- phone -->
<string name="…_disclosure">… Learn more</string>
<!-- wearable override, same key -->
<string name="…_disclosure">…Use your phone to learn more.</string>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())
}clearActivityBackStack()
// enumerate our own, kill each
killOtherProcesses()
// no threads alive — now safe
deleteDataDirectories()
// show logged out confirmation
firePostCleanupIntent()// FIX #1: ONE OWNER
CompositionLocalProvider(
LocalSurfaceTransformation provides null
) {
M3Card(transformation = t) {
SharedChatEntry(...)
}
}
// FIX #2: CURRENT ITEM SCOPE
val t = SurfaceTransformation(spec)// BUG #1: DOUBLE MORPH
M3Card(transformation = t) {
CompositionLocalProvider(
LocalSurfaceTransformation provides t
) { SharedChatEntry(...) }
}
// BUG #2: STALE ITEM SCOPE
val t = remember(spec) {
SurfaceTransformation(spec)
}