Receive Share Text intents in Flutter
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.