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.
Use this plugin in your Flutter app to:
For WatchOS companion app, this plugin uses Watch Connectivity framework under the hood to communicate with IOS 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.
import 'package:flutter_watch_os_connectivity/flutter_watch_os_connectivity.dart';
FlutterWatchOsConnectivity
FlutterWatchOsConnectivity _flutterWatchOsConnectivity = FlutterWatchOsConnectivity();
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
:
The session is active and the Watch app and iOS app may communicate with each other freely.
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.
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.
FlutterWatchOsConnectivity
session NOTE: Your cannot send or receive messages until you call
configureAndActivateSession()
method.
_flutterWatchOsConnectivity.configureAndActivateSession();
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
}
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
}});
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.
WatchOsPairedDeviceInfo _pairedDeviceInfo = await _flutterWatchOsConnectivity.getPairedDeviceInfo();
_flutterWatchOsConnectivity.pairedDeviceInfoChanged.listen((info) {
_pairedDeviceInfo = info;
});
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.
WatchOsPairedDeviceInfo
reachability state changed _flutterWatchOsConnectivity.reachabilityChanged.listen((isReachable) {
_isReachable = isReachable;
});
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.
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}"
});
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;
}));
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;
});
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:
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
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
ApplicationContext
ApplicationContext _applicationContext = await _flutterWatchOsConnectivity.getApplicationContext();
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 and receivedData
on WatchOS are same. receivedData
on iOS app and currentData
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}"
});
ApplicationContext
changed ApplicationContext
can be observed by listen to applicationContextUpdated
stream.
_flutterWatchOsConnectivity.applicationContextUpdated.listen((context) {
_applicationContext = context;
});
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.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
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);
UserInfoTransfer
Call this method on an UserInfoTransfer
with isTransfering
flag is true
to cancel the transfer.
await _userInfoTransfer.cancel()
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();
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
});
UserInfoTransfer
In progress UserInfoTransfer
can be retrieved via this method.
List<UserInfoTransfer> _pendingTransfers = await _flutterWatchOsConnectivity.getInProgressUserInfoTransfers()
UserInfoTransfer
list changed You can also observe change events in the pending UserInfoTransfer
list.
_flutterWatchOsConnectivity.pendingUserInfoTransferListChanged.listen((transfers) {
_userInfoPendingTransfers = transfers;
});
UserInfoTransfer
This stream will emit corresponding UserInfoTransfer
object when any UserInfoTransfer
is completed.
_flutterWatchOsConnectivity.userInfoTransferDidFinish.listen((transfer) {
inspect(transfer);
});
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.
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}"
});
}
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}");
}));
FileTransfer
Call this method on an FileTransfer
with isTransfering
flag is true
to cancel the transfer.
await _userInfoTransfer.cancel()
FileTransfer
In progress FileTransfer
can be retrieved via this method.
List<FileTransfer> _pendingTransfers = await _flutterWatchOsConnectivity.getInProgressFileTransfers()
FileTransfer
list changed You can also observe change events in the pending FileTransfer
list.
_flutterWatchOsConnectivity.pendingFileTransferListChanged
.listen((transfers) {
_filePendingTransfers = transfers;
});
FileTransfer
s You can wait for file data to be sent through this stream.
File data is a Pair<File, Map<String, dynamic>?>
:
File
can be retrieved via pair.left
Metadata
can be retrieved via pair.right
_flutterWatchOsConnectivity.fileReceived.listen((pair) {
_receivedFileDataPair = pair;
});
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.