Flutter WatchOS Connectivity
A plugin that provides a wrapper that enables Flutter apps to communicate with apps running on WatchOS.
Note: I'd also written packages to communicate with WearOS devices, you can check it out right here.
Table of contents
- Screenshots
- Supported platforms
- Features
- Getting started
- Configuration
- How to use
Screenshots
Supported platforms
- iOS
Features
Use this plugin in your Flutter app to:
- Communicate with WatchOS application.
- Send message.
- Update application context.
- Send user info data.
- Transfer files.
- Check for wearable device info.
- Detect wearable reachability.
Getting started
For WatchOS companion app, this plugin uses Watch Connectivity framework under the hood to communicate with IOS app.
Configuration
IOS
- Create an WatchOS companion app, you can follow this instruction to create new WatchOS app.
Note: If you've created a WatchOS app with UIKit, the WatchOS companion app must have Bundle ID with the following format in order to communicate with IOS app: YOUR_IOS_BUNDLE_ID.watchkitapp.
That's all, you're ready to communicate with WatchOS app now.
How to use
Get started
Import the library
import 'package:flutter_watch_os_connectivity/flutter_watch_os_connectivity.dart';
Create new instance of FlutterWatchOsConnectivity
FlutterWatchOsConnectivity _flutterWatchOsConnectivity = FlutterWatchOsConnectivity();
Configuring and handling the activation state
ActivationState
tells us about the current FlutterWatchOsConnectivity
session activation state. You can read more detail about it here.
Each IOS device can only pair with one WatchOS device at the same time, so you need to monitor on ActivationState
and have a suitable solution for each case.
There are 3 states of ActivationState
:
- Activated
The session is active and the Watch app and iOS app may communicate with each other freely.
- Not activated
The session is not activated. When in this state, no communication occurs between the Watch app and iOS app. It is a programmer error to try to send data to the counterpart app while in this state.
- Inactive
The session was active but is transitioning to the deactivated state. The session’s delegate object may still receive data while in this state, but it is a programmer error to try to send data to the counterpart app.
Configure and activate FlutterWatchOsConnectivity
session
NOTE: Your cannot send or receive messages until you call
configureAndActivateSession()
method.
_flutterWatchOsConnectivity.configureAndActivateSession();
Get current ActivationState
NOTE: You can only interact with some methods of
FlutterWatchOsConnectivity
plugin if yourActivationState
isactivated
ActivationState _currentState = await _flutterWatchOsConnectivity.getActivateState();
if (_currentState == ActivationState.activated) {
// Continue to use the plugin
}else{
// Do something in this case
}
Listen to ActivationState
changed
NOTE: You can only interact with some methods of
FlutterWatchOsConnectivity
plugin if yourActivationState
isactivated
_flutterWatchOsConnectivity.activationStateChanged.listen((activationState) {
if (activationState == ActivationState.activated) {
// Continue to use the plugin
}else{
// Do something in this case
}});
Getting paired device information and reachability
Each IOS device can only pair with one WatchOS device at the same time, so the WatchOsPairedDeviceInfo
object retrieved from FlutterWatchOsConnectivity
is unique for each device.
Users can disconnect and reconnect various WatchOS devices to their IOS phones, so you should keep track on WatchOsPairedDeviceInfo
.
WatchOSPairedDeviceInfo
has following properties:
isPaired
The value of this property is true when the iPhone is paired to an Apple Watch or false when it is not.
isWatchAppInstalled
The user can choose to install only a subset of available apps on Apple Watch. The value of this property is true when the Watch app associated with the current iOS app is installed on the user’s Apple Watch or false when it is not installed.
isComplicationEnabled
The value of this property is true when the app’s complication is installed on the active clock face. When the value of this property is false, calls to the transferUserInfo(userInfo: userInfo, isComplication: true)
method fail immediately.
watchDirectoryURL
You must activate the current session before accessing this URL. Use this directory to store preferences, files, and other data that is relevant to the specific instance of your Watch app running on the currently paired Apple Watch. If more than one Apple Watch is paired with the same iPhone, the URL in this directory changes when the active Apple Watch changes.
When the value in the activationState property is WCSessionActivationState.notActivated, the URL in this directory is undefined and should not be used. When a session is active or inactive, the URL corresponds to the directory for the most recently paired Apple Watch. Even when the session becomes inactive, the URL remains valid so that you have time to update your data files before the final deactivation occurs.
If the user uninstalls your app or unpairs their Apple Watch, iOS deletes this directory and its contents. If there is no paired watch, the value of this property is nil.
Getting current paired device information
WatchOsPairedDeviceInfo _pairedDeviceInfo = await _flutterWatchOsConnectivity.getPairedDeviceInfo();
Listen to paired device information changed
_flutterWatchOsConnectivity.pairedDeviceInfoChanged.listen((info) {
_pairedDeviceInfo = info;
});
Getting reachability of WatchOsPairedDeviceInfo
bool _isReachable = await _flutterWatchOsConnectivity.getReachability();
This property is true when the WatchKit extension and the iOS app can communicate with each other.
Specifically:
-
WatchKit extension: The iOS device is within range, so communication can occur and the WatchKit extension is running in the foreground, or is running with a high priority in the background (for example, during a workout session or when a complication is loading its initial timeline data).
-
iOS app: A paired and active Apple Watch is in range, the corresponding WatchKit extension is running, and the WatchKit extension’s isReachable property is true.
In all other cases, the value is false.
Listen WatchOsPairedDeviceInfo
reachability state changed
_flutterWatchOsConnectivity.reachabilityChanged.listen((isReachable) {
_isReachable = isReachable;
});
Sending and handling messages
IOS apps can send message data to WatchOS apps.
Note: Messages can only be sent if both apps are reachable, see Getting paired device information and reachability for more details.
Each message received will be constructed using the WatchOsMessage
object.
WatchOsMessage
will have following properties:
data
A Map<String, dynamic>
represented for each message map data.
The following data value types are supported:
null
| bool
| int
| double
| String
| Uint8List
| Int32List
| Int64List
| Float32List
| Float64List
| List
| Map
relyMessage
A optional callback method to indicate whether this message is waiting for reply.
Send message
You can construct a message map by passing a Map<String, dynamic>
into sendMessage
method
await _flutterWatchOsConnectivity.sendMessage({
"message": "This is a message sent from IOS app at ${DateTime.now().millisecondsSinceEpoch}"
});
Send message and wait for reply
You can also wait for a reply from WatchOS apps by specify a replyHandler
_flutterWatchOsConnectivity.sendMessage({
"message": "This is a message sent from IOS app with reply handler at ${DateTime.now().millisecondsSinceEpoch}"
}, replyHandler: ((message) async {
// After watchOS received and replied to your message, this callback will be triggered
_currentRepliedMessage = message;
}));
Receive messages
You can listen to upcoming message sent by WatchOS apps
_flutterWatchOsConnectivity.messageReceived.listen((message) async {
/// New message is received, you can read it data map
_currentMessage = message.data;
});
Reply the message
If the replyMessage
property of received WatchOsMessage
object is not null
. You should reply to that WatchOsMessage
.
_flutterWatchOsConnectivity.messageReceived.listen((message) async {
/// New message is received, you can read it data map
_currentMessage = message.data;
/// Check if this message is needed to reply
if (message.onReply != null) {
/// If so, reply to this message
try {
await message.replyMessage!({
"message":
"Message received on IOS app at ${DateTime.now().millisecondsSinceEpoch}"
});
} catch (e) {
print(e);
}
}
});
Sending and receiving messages are great ways to communicate with the WatchOS app, but they have one limitation:
- They can only work if both IOS and WatchOS device are reachable which mean they can only work if both apps are in foreground only.
So to be able to communicate in the background, we have 2 alternative solutions: Obtaining and syncing ApplicationContext
and Transfering and handling user info with UserInfoTransfer
Obtaining and syncing ApplicationContext
ApplicationContext
can be defined as a shared data between iOS and WatchOS applications and can be obtained and updated on both sides.
The ApplicationContext
's data will be synchronized and retained as long as the connection between the iOS app and the WatchOS app persists.
ApplicationContext
contains following properties:
currentData
A Map<String, dynamic>
represents the current application context on this respective application.
The following data value types are supported:
null
| bool
| int
| double
| String
| Uint8List
| Int32List
| Int64List
| Float32List
| Float64List
| List
| Map
receivedData
A Map<String, dynamic>
represents the received application context on this respective application.
The following data value types are supported:
null
| bool
| int
| double
| String
| Uint8List
| Int32List
| Int64List
| Float32List
| Float64List
| List
| Map
Obtaining an ApplicationContext
ApplicationContext _applicationContext = await _flutterWatchOsConnectivity.getApplicationContext();
Syncing an ApplicationContext
Call updateApplicationContext
method on iOS app to cope new data to currentData
on iOS app and to receivedData
on WatchOS app.
To make it easy to understand:
currentData
on iOS app andreceivedData
on WatchOS are same.receivedData
on iOS app andcurrentData
on WatchOS are same.
So if you call this method to sync ApplicationContext
's currentData
on the iOS application, WatchOS application will received it as receivedData
.
_flutterWatchOsConnectivity.updateApplicationContext({
"message": "Application Context updated by IOS app at ${DateTime.now().millisecondsSinceEpoch}"
});
Listen to ApplicationContext
changed
ApplicationContext
can be observed by listen to applicationContextUpdated
stream.
_flutterWatchOsConnectivity.applicationContextUpdated.listen((context) {
_applicationContext = context;
});
Transfering and handling user info with UserInfoTransfer
As an alternative solution for Send and handling messages, we can transfer a Map<String, dynamic>
to counterpart app with the following advantages:
UserInfoTransfer
is transferred on a transaction-by-transaction basis.- Each
UserInfoTransfer
can be transferred even if the iOS or WatchOS is not in foreground. UserInfoTransfer
can be cancelled.
An UserInfoTransfer
has following properties:
id
A String
used to uniquely identify the UserInfoTransfer
.
isCurrentComplicationInfo
A bool
indicating whether the data is related to the app’s complication, for more info about WatchOS's complications, please check this link.
userInfo
The Map<String, dynamic>
data being transferred.
isTransfering
A bool
indicating whether the data is being tranfered.
cancel
A method used to cancel the UserInfoTransfer
Transfering user info
Call transferUserInfo
method when you want to send a Map<String, dynamic>
to the counterpart and ensure that it’s delivered. Map<String, dynamic>
sent using this method are queued on the other device and delivered in the order in which they were sent. After a transfer begins, the transfer operation continues even if the app is suspended.
UserInfoTransfer? _userInfoTransfer = await _flutterWatchOsConnectivity.transferUserInfo({
"message": "User info sent by IOS app at ${DateTime.now().millisecondsSinceEpoch}"
});
You can also transfer a user info as Complication
Call this method when you have new data to send to your complication. Your WatchKit extension can use the data to replace or extend its current timeline entries.
Note: Make sure you Obtaining the number of complication transfers remaining before calling this method, otherwise,
UserInfoTransfer
will be treated as non-complication. About complication, please check this link.
UserInfoTransfer? _userInfoTransfer = await _flutterWatchOsConnectivity.transferUserInfo({
"message": "User info sent by IOS app at ${DateTime.now().millisecondsSinceEpoch}"
}, isComplication: true);
Canceling an UserInfoTransfer
Call this method on an UserInfoTransfer
with isTransfering
flag is true
to cancel the transfer.
await _userInfoTransfer.cancel()
Obtaining the number of complication transfers remaining
Note: This is the number of remaining times that you can call
transferUserInfo(isComplication: true)
method during the current day. If this property is set to 0, any additional calls totransferUserInfo(isComplication: true)
method usetransferUserInfo(isComplication: false)
instead. About WatchOS complication, please check this link.
int remainingComplicationCount = await _flutterWatchOsConnectivity.getRemainingComplicationUserInfoTransferCount();
Waiting for upcoming UserInfoTransfer
s
You can wait for user info Map
to be sent through this stream.
_flutterWatchOsConnectivity.userInfoReceived.listen((userInfo) {
_receivedUserInfo = userInfo;
print(_receivedUserInfo is Map<String, dynamic>) /// true
});
Obtaning a list of pending UserInfoTransfer
In progress UserInfoTransfer
can be retrieved via this method.
List<UserInfoTransfer> _pendingTransfers = await _flutterWatchOsConnectivity.getInProgressUserInfoTransfers()
Listen to pending UserInfoTransfer
list changed
You can also observe change events in the pending UserInfoTransfer
list.
_flutterWatchOsConnectivity.pendingUserInfoTransferListChanged.listen((transfers) {
_userInfoPendingTransfers = transfers;
});
Listen to the completion event of UserInfoTransfer
This stream will emit corresponding UserInfoTransfer
object when any UserInfoTransfer
is completed.
_flutterWatchOsConnectivity.userInfoTransferDidFinish.listen((transfer) {
inspect(transfer);
});
Transfering and handling File
with FileTransfer
Like Transfering and handling user info with UserInfoTransfer
, you can also transfer a single File
using FileTransfer
with same avantages.
FileTransfer
object contains the following properties:
id
A String
used to uniquely identify the FileTransfer
.
file
The File
being transferred.
metadata
The optionalMap<String, dynamic>
for additional payload data.
isTransfering
A bool
indicating whether the File
is being tranfered.
cancel
A method used to cancel the FileTransfer
Note: The duration of the transfer will depend on the size of the file, the larger the size, the longer the transfer will take.
Transfering file
Pass a File
into transferFile
method, you can either pass an addtional payload data called metadata
or not.
import 'package:image_picker/image_picker.dart';
import 'dart:io';
XFile? _file = await ImagePicker().pickImage(source: ImageSource.gallery);
if (_file != null) {
FileTransfer? _fileTransfer = await _flutterWatchOsConnectivity.transferFile(File(_file.path), metadata: {
"message": "File transfered by IOS app at ${DateTime.now().millisecondsSinceEpoch}"
});
}
Obtaning current on progress UserInfoTransfer
list
You can observe the current FileTransfer
progress by calling a callback method on FileTransfer
instance
if (_fileTransfer?.setOnProgressListener != null)
_fileTransfer?.setOnProgressListener(((progress) {
print("${_fileTransfer.id}: ${progress.currentProgress}");
}));
Canceling a FileTransfer
Call this method on an FileTransfer
with isTransfering
flag is true
to cancel the transfer.
await _userInfoTransfer.cancel()
Obtaning a list of pending FileTransfer
In progress FileTransfer
can be retrieved via this method.
List<FileTransfer> _pendingTransfers = await _flutterWatchOsConnectivity.getInProgressFileTransfers()
Listen to pending FileTransfer
list changed
You can also observe change events in the pending FileTransfer
list.
_flutterWatchOsConnectivity.pendingFileTransferListChanged
.listen((transfers) {
_filePendingTransfers = transfers;
});
Waiting for upcoming FileTransfer
s
You can wait for file data to be sent through this stream.
File data is a Pair<File, Map<String, dynamic>?>
:
- The
File
can be retrieved viapair.left
- The
Metadata
can be retrieved viapair.right
_flutterWatchOsConnectivity.fileReceived.listen((pair) {
_receivedFileDataPair = pair;
});
Listen to the completion event of FileTransfer
This stream will emit corresponding FileTransfer
object when any FileTransfer
is completed.
_flutterWatchOsConnectivity.fileTransferDidFinish.listen((transfer) {
inspect(transfer);
});
For more details, please check out my Flutter example project and WatchOS example project.