In this article I'll cover how to receive share intents in Flutter. This is so that users can share an article from Chrome to your app as a link, or share a photo from their favorite photos app directly into your app.

The Flutter documentation already shows a full sample on how to do this for text at https://flutter.dev/docs/get-started/flutter-for/android-devs#how-do-i-handle-incoming-intents-from-external-applications-in-flutter. However, it's written in Java. Here's how it looks like in Kotlin:

Android side

  private var sharedText: String? = null

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    GeneratedPluginRegistrant.registerWith(this)

    handleShareIntent()

    MethodChannel(flutterView, "app.channel.shared.data")
            .setMethodCallHandler { call, result ->
              if (call.method.contentEquals("getSharedText")) {
                result.success(sharedText)
                sharedText = null
              }
            }
  }

  fun handleShareIntent() {
    val action = intent.action
    val type = intent.type

    if (Intent.ACTION_SEND.equals(action) && type != null) {
      if ("text/plain".equals(type)) {
        handleSendText(intent)
      }
    }
  }

  fun handleSendText(intent : Intent) {
    sharedText = intent.getStringExtra(Intent.EXTRA_TEXT)
  }

Flutter side

On the Flutter side, it's as documented in the tutorial:

const platform = const MethodChannel('app.channel.shared.data');

Future<String> getSharedText() async {
  final sharedData = await platform.invokeMethod("getSharedText");
  return sharedData;
}

App Manifest

Don't forget to add the share intent filter in the manifest:

<intent-filter>
    <action android:name="android.intent.action.SEND" />
    <category android:name="android.intent.category.DEFAULT" />
    <data android:mimeType="text/plain" />
</intent-filter>

Unit tests

To unit test it, use MethodChannel.setMockMethodCallHandler:

const platform = const MethodChannel('app.channel.shared.data');

void main() {
  testWidgets('sends shared text', (WidgetTester tester) async {
    platform.setMockMethodCallHandler((_) {
      return Future.value('http://www.google.com');
    });

    final chatService = MockChatService();
    await tester.pumpWidget(MaterialApp(
        home: Scaffold(body: ChatView.withParameters(chatService))));

    await tester.pump();

    verify(chatService.sendMessage('http://www.google.com'));
  });
}

Handle shared data when resuming & quick share (Nov 2019 update)

TLDR

I had a few issues though. If I shared to my app, it might switch to it, but not receive the intent. The quick share UI in Chrome also didn't seem to work. The fix is to remove launchMode from my manifest:

<application
    ...>
    <activity
        android:name=".MainActivity"
        <!-- Remove the launchMode or set to standard! -->
        android:launchMode="singleTop"
        android:theme="@style/LaunchTheme"
        ...

And add a listener for the resume event in Flutter (but not in Android):

class _HomePageState extends State<MyHomePage> with WidgetsBindingObserver {
  ...

  @override
  void initState() {
    super.initState();

    handleShareIntent();

    // Listen to lifecycle events.
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    super.dispose();
    WidgetsBinding.instance.removeObserver(this);
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed) {
      handleShareIntent();
    }
  }

The details

Back to pure Android

In the screenshot below, if I tap "Share...", I get to pick which app I send the URL to. If I select my app, it'll correctly run it with the right share intent. However, if I tap my app's icon on the right side, it'll run my app but not update the intent.

It took a bit of trial and error because it involves both Android and Flutter. In order to test better, I decided to make a tiny standalone Android app that receives a share text intent.

One post about receiving intents in Android I found said I should add a listener on onResume. This is wrong because depending on the app manifest, onResume might run with the same intent as what started the Activity in the first place. In this case the app would process the same intent upon returning. The post that put me on the right track is this one. They say you should listen to onNewIntent instead of onResume. It turns out it also didn't work, but they referred to some launchMode in the manifest. Setting the right value for it fixed it.

android:launchMode has 4 values:

  • standard
  • singleTop
  • singleTask
  • singleInstance

Each one behaves slightly differently. My Flutter app was set to singleTop by default. What this means is that if the activity is at the top of the app stack, it'll return to that. If it's below, it'll start a new activity. That's why I sometimes ended up with several instances of my app...

I've tried the other modes and I seem to get the same issue. It always worked when first running the app. However, it would resume either by calling onNewIntent with the same intent that initially launched the activity (not the one we're sharing the second time), or simply not calling onNewIntent at all.

standard is the one that behaves as I'd expect. Every time you share, via quick share or share..., it'll run onCreate with the new intent. It'll also re-use the currently running instance and never seem to create a new instance. Isn't that the same as singleInstance? The thing is that I don't understand the concepts of instance, root task, activity in Android yet.

Trying it in Flutter

With standard mode, the Android piece starts with an onCreate and the right intent but the Flutter app itself actually just resumes. So I did need to add a listener for resume events in Flutter. To listen to resume events, you have to make your Widget use the WidgetsBindingObserver mixin. Refer to the TLDR above for the code.

Read more on mixins at https://dart.dev/guides/language/language-tour#adding-features-to-a-class-mixins.