The Wear OS application itself is not a .NET MAUI application, but a common .NET Android application. It used to be native Xamarin.Android with some .NET packages. You can read about the differences between Xamarin and MAUI in these articles:
https://grialkit.com/blog/learn-the-key-differences-between-net-maui-vs-xamarin-forms-for-cross-platform-mobile-and-desktop-development
https://sdk.docutain.com/blogartikel/xamarin-versus-net-maui
Thankfully, the project structure is the same – there are Activities in which some UI logic is written, while the XML layout files under Resources/Layout folder define the UI.
Don’t forget to develop a simplistic UI and keep in mind that your users will literally be wearing your application on their wrists. While writing watch apps, maintaining a friendly UX is even more important than for mobile applications. A comprehensive guide throughout Wear OS UI development can be found here: https://developer.android.com/design/ui/wear
When communicating with online resources, developers don’t need to write their own gateway via a mobile application. The Wear OS itself takes care of it. It offers network connection via direct WiFi or a Bluetooth connection via the connected smartphone. But since managing that is hidden from the Wear OS app developer, what we only see is whether the network connection is available or not.
The most important feature for non-standalone Wear OS apps is communication with mobile applications over Bluetooth. As mentioned above, the ongoing watch-smartphone communication is hidden from the developer behind the WearableClass static class. Instead, this API offers the DataClient (among others), which gives us a tool to Publish messages and attach Listeners to listen to messages published by other paired devices.
Publishing data from the watch to the smartphones (or vice versa) can be achieved like so:
var dataMap = PutDataMapRequest.Create(“/specific_path”);
dataMap.DataMap.PutString(“MessageKey”, “Message Value”);
var request = dataMap.AsPutDataRequest();
WearableClass.GetDataClient(Application.Context).PutDataItem(request);
On the other side, we attach a listener onto the same path and read a string message once it arrives:
public override void OnDataChanged(DataEventBuffer dataEvents) {
foreach (var data in dataEvents) {
if (data.DataItem.Uri.Path != “/specific_path”) continue;
var item = DataMapItem.FromDataItem(data.DataItem);
var message = data.DataMap.GetString(“MessageKey”);
...
break;
}
}
There are other clients offered by the WearableClass – CapabilityClient, MessageClient, NodeClient, etc. This page on Android Developer Training provides much more information about all of them: https://developer.android.com/training/wearables/data/data-layer
The communication over Bluetooth between mobile and watches is based on messages. You cannot simply “read” data from mobile on watches. You must ask for them and listen for response.
In real scenarios, when Wear OS app starts and needs to synchronize itself with the smartphone:
There is a hidden feature/bug in WearableClass when publishing data. It publishes only data, which has changed since the previous publication. If the data is the same, the WearableClass layer skips the data publication without any warning or response.
So, the best practice to ensure our logic in mobile – watch communication forks fine is to include a timestamp in each data when publishing. In previous example it should looks like:
dataMap.DataMap.PutString(“TimeStamp”, DateTime.UtcNow.Ticks.ToString());
dataMap.DataMap.PutString(“MessageKey”, “Message Value”);
A common task which improves the UX of our Wear OS app significantly is starting the watch app when the user opens the mobile app. This is not automatically done by WearableClass API. We must take care of it ourselves.
On the main activity of our Android project of MAUI application, we must override the OnResume method and publish a message to all connected nodes (= devices):
protected override void OnResume() {
base.OnResume();
var nodes = await WearableClass.GetNodeClient(this).GetConnectedNodesAsync();
foreach (var node in nodes)
await WearableClass.GetMessageClient(this).SendMessage(node.Id, “/app_starts, null);
}
And in our Wear OS application, we create a StartupListenerService:
[Service(Exported = true, Name= "bundleid.StartupListenerService")]
[IntentFilter(new[] { "com.google.android.gms.wearable.BIND_LISTENER" })]
public class StartupListenerService : WearableListenerService {
public override void OnMessageReceived(IMessageEvent p0) {
if (p0.Path.Equals(“/app_starts”)) {
var intent = new Intent(Application.Context, typeof(MainActivity));
intent.AddFlags(ActivityFlags.NewTask);
StartActivity(intent);
} else {
base.OnMessageReceived(p0);
}
}
}
And register it in the AndroidManifest.xml file:
<application … >
<uses-library android:name="com.google.android.wearable" android:required="true" />
<meta-data android:name="com.google.android.wearable.standalone" android:value="false" />
<service android:name="bundleid.StartupListenerService" android:exported="true">
<intent-filter>
<action android:name="com.google.android.gms.wearable.BIND_LISTENER" />
</intent-filter>
</service>
</application>
Please note that in the service, we must specify explicitly the Service name (bundleid.StartupListenerService) in ServiceAttribute above the class definition. And the same name must be specified in AndroidManifest.
We’re building an Android Wear OS supplement for our .NET MAUI application, which is multiplatform and must be working on both platforms – Android and iOS. So, we’ve created platform specific implementations of watch related components defined by its interface.
All watch related components were registered in an IoC container and are injected to the rest of mobile app code by their interfaces.
After implementing the functionalities and alpha testing on our development environment looked fine, we released the app for further testing into production. In that very moment, a very unfortunate issue was raised in our Wear OS application. The communication over WearableClass stopped working and the watch application stopped responding to messages / data distributed over DataClient.
After a careful investigation of the issue, we found the problem in the signing certificate process.
To enable Wearable Data-layer between the smartphone and watch apps, two conditions must be met:
If we don’t sign our app for beta testing distribution with the same certificate as the watch app, the Data-layer API requests are ignored.
The best solution is to enable “.APK Signature” in Android properties of .NET MAUI application for mobile and .NET Android application for watches and fill Keystore certificate details. For development and QA purposes, untrusted certificates created by the developer may be used (for example during MAUI app distribution over Archiving option in Visual Studio). For production, a publicly trusted developer certificate must be used.
In conclusion, this showcase of our .NET MAUI Android Watch Application highlights the versatile and powerful features of the framework. We hope you enjoyed following through, as well as that you learned something useful for your future endeavours.