Flutter WatchOS Connectivity

Version Publisher Points LINCENSE

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

  • 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

  1. 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.

MarineGEO circle logo

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 your ActivationState is activated

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 your ActivationState is activated

_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 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}"
});

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 to transferUserInfo(isComplication: true) method use transferUserInfo(isComplication: false) instead. About WatchOS complication, please check this link.

int remainingComplicationCount = await _flutterWatchOsConnectivity.getRemainingComplicationUserInfoTransferCount();

Waiting for upcoming UserInfoTransfers

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 FileTransfers

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 via pair.left
  • The Metadata can be retrieved via pair.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.