Flutter Handoff

First what is Handoff? According to the Apple Docs:

Use Handoff to transfer activities the user starts on one iOS, watchOS, or macOS device to a different device.

~ Apple Developer Documentation

This is the source of the Apple magic that makes all the devices feel like a single entity. With this, we could implement the full Handoff with sharing activity between Flutter apps running on iOS and macOS and maybe even web.

But for now I was only interested in sharing a link from my app that can be opened in a browser on a Mac. 1 This was easier than I expected.

One just has to:

  1. Create NSUserActivity with webpageURL set to the URL you want to open.
Code
let activity = NSUserActivity(activityType: activityType)
activity.title = title
activity.webpageURL = url
activity.isEligibleForHandoff = true
activity.isEligibleForSearch = false
activity.isEligibleForPublicIndexing = false
  1. Call activity.becomeCurrent().
Code
activity.becomeCurrent()
  1. Enjoy the magic!

To my surprise, there was no Flutter plugin that implemented this functionality. So I thought fine, I’ll do it myself. However, setting up the whole Flutter plugin boilerplate + CocoaPods boilerplate, I thought it would be a perfect opportunity to test how well Claude Code can perform on such a task.

So I created a prompt summarizing the NSUserActivity API and the Flutter plugin getting started page together with what I wanted to achieve. With this input Claude proceeded to one-shot the task — as it’s called in the lingo. All I had to do was clean up the interface, test the plugin, add a readme, and publish it to pub.dev.

How to use it?

In your Flutter project run:

flutter pub add handoff

Then somewhere in your app, call:

await Handoff.setHandoffUrl(
	"http://mastermakrela.com//flutter-handoff",
	title: "Flutter Handoff",
);

Now if you run this on a real iOS device where you are logged in to your iCloud account, you should see the link appear on all other nearby devices logged in to the same iCloud account.

In some cases it also makes sense to remove the activity, e.g., when the user navigates away from the page. In that case you can call:

Handoff.clearHandoff();

And the link will be gone from the Dock or the App Switcher.

Notes on real world usage

If your Flutter app uses go_router, it makes sense to create a NavigationObserver subclass that will set and clear the handoff URL based on which page of your app is currently active (obviously it only makes sense to link the pages that have counterpart in your web app):

class GoRouterObserver extends NavigatorObserver {
  
  void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
    print('didPush: $route');
	if(route.hasHandoffUrl) {
		Handoff.setHandoffUrl(
			route.handoffUrl,
			title: route.handoffTitle,
		);
	} else {
		Handoff.clearHandoff();
	}
  }
}

final GoRouter router = GoRouter(
	observers: [GoRouterObserver()],
	routes: [ /* your routes */ ]
);,

or something like that.

From my experience, the web pages change more rapidly than the app, additionally, you can’t force app users to update the app, so if you hardcode the links in the app, sooner or later they will break.

In my opinion the best way is to lean into the flexibility of the web and let your web server handle different versions of the links.

In my case I’ve set up a subpath https://example.com/handoff/<some-data-here> that allows the web server to decide what to do with the request. If you’re feeling fancy, you could even add ?version=<version> query parameter to the URL.

But what’s even better, with this approach you can use associated domains to open your app directly on another device with your app installed, without forcing all links from your domain to open in the app. 2

This is especially useful if you have one phone with the development version of the app and one with the current production version, then you can quickly land on the same page on both devices, without having to manually go through all the screens.

Example association JSON file
{
	"applinks": {
		"appIDs": ["ABCDE12345.com.example.app"],
		"details": [
			{
				"appID": "TEAMID.com.example.app",
				"paths": ["/handoff/*"]
			}
		]
	}
}
GitHubhttps://github.com/mastermakrela/flutter-handoff
pub.devhttps://pub.dev/packages/handoff

1 In the project I'm currently working on the App is used to collect data, that is later available in a Web Portal for review. So it would be nice to be able the open the capture directly from the app on the computer, without having to search the directory manually.

2 Yes, it's a niche use case, but it's exactly what we have to support right now, so I'm glad it works like that.


Created on . Last updated on .
  • flutter
  • handoff
  • macos
  • ios
  • swift
  • dart
Content width
© 2023 - 2025 mastermakrela