In Flutter is possible to execute native Android or IOS code by passing messages in a specialized channel.
This is useful when you need to implement a platform-specific functionality directly inside your Flutter app.
In this post we learn how to respond to an Android Intent from a Flutter app.
We are going to implement an ACTION_PROCESS_TEXT
intent. This allows to forward some selected text from another app directly inside our Flutter application:
When a user selects some text in any other app inside her device a "Copy in app" action allows to forward the selected text to our app β¨
We are going to learn:
- How to define a new intent inside
AndroidManifest.xml
- How to read the intent value and create a channel using Kotlin (Android)
- How to execute a custom function using a
MethodChannel
and get the value from Flutter
Define intent in AndroidManifest.xml
We are going to implement a new implicit intent:
An implicit intent declares a general action to perform, which allows a component from another app to handle it
Inside AndroidManifest.xml
we define the list of intents that our app can handle.
This is achieved by adding a new <intent-filter>
that specifies which actions the app can perform.
You can view a full list of all available intents in the Intent Android documentation
In this example, we add a new <intent-filter>
for an ACTION_PROCESS_TEXT
:
<intent-filter android:label="Copy in app">
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent-filter>
- Every
<intent-filter>
requires an<action>
that defines which intents the app can perform. In our example, this corresponds toACTION_PROCESS_TEXT
(action.PROCESS_TEXT
) <data>
defines the type of input our app expects to receive, in this case a simple string of text- The
android_label
string defines the text that the user will see when executing the intent from another app
The action name is defined inside the documentation as "Constant Value"
The value assigned to "android_label" will be shown to the user when performing the action
This is the final AndroidManifest.xml
in this example project:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="action_process_text_intent"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter android:label="Copy in app">
<action android:name="android.intent.action.PROCESS_TEXT" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>
Process the intent from Android
The intent request is received by Android. We therefore need to write some Kotlin code to forward the intent to Flutter.
Inside android > app > src > main > kotlin
you should find a MainActivity.kt
file. This is the entry point of the app for Android.
When a user clicks on "Copy in app" Android will open our app and signal that an intent has been sent.
Inside onCreate
we capture the intent and check that we got the correct ACTION_PROCESS_TEXT
action and that the data received is indeed "text/plain"
(as defined previously inside AndroidManifest.xml
):
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val intent = intent
val action = intent.action
val type = intent.type
if (Intent.ACTION_PROCESS_TEXT == action && type != null) {
if ("text/plain" == type) {
handleSendText(intent)
}
}
}
If the intent is correct we execute the handleSendText
function:
class MainActivity : FlutterActivity() {
private var sharedText: String? = null
private fun handleSendText(intent: Intent) {
sharedText = intent.getStringExtra(Intent.EXTRA_PROCESS_TEXT)
}
// ...
}
handleSendText
gets the intent and extracts the text value (EXTRA_PROCESS_TEXT
, as defined inside the documentation). The value is stored inside a local variable sharedText
.
The name of the value provided by the intent action can be found inside the documentation
Connect Android with Flutter: MethodChannel
The last step is creating a MethodChannel
that allows to request the execution of a function from Flutter.
MethodChannel
: A named channel for communicating with the Flutter application using asynchronous method calls.
We define a new configureFlutterEngine
function that handles the communication between Android and Flutter:
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
GeneratedPluginRegistrant.registerWith(flutterEngine);
// Create MethodChannel here
}
GeneratedPluginRegistrant
is used to register an Android plugin
We then define a new MethodChannel
. The MethodChannel
listens to a custom defined channel (using a constant CHANNEL
string in the example) and performs the code specified in its body:
private val CHANNEL = "app.channel.process.data"
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
GeneratedPluginRegistrant.registerWith(flutterEngine);
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
.setMethodCallHandler { call, result ->
if (call.method.contentEquals("getSharedText")) {
result.success(sharedText);
sharedText = null;
} else {
result.notImplemented();
}
}
}
The channel checks that the method requested is called "getSharedText"
and in that case it returns the sharedText
value that we previously collected from the intent.
getSharedText
is the name of the method that we are going to execute from Flutter, keep reading for more details π
The final MainActivity.kt
file is the following:
package com.sandromaglione.actionprocesstextintent.action_process_text_intent
import android.content.Intent
import android.os.Bundle
import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugins.GeneratedPluginRegistrant
class MainActivity : FlutterActivity() {
private var sharedText: String? = null
private val CHANNEL = "app.channel.process.data"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val intent = intent
val action = intent.action
val type = intent.type
if (Intent.ACTION_PROCESS_TEXT == action && type != null) {
if ("text/plain" == type) {
handleSendText(intent)
}
}
}
private fun handleSendText(intent: Intent) {
sharedText = intent.getStringExtra(Intent.EXTRA_PROCESS_TEXT)
}
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
GeneratedPluginRegistrant.registerWith(flutterEngine);
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
.setMethodCallHandler { call, result ->
if (call.method.contentEquals("getSharedText")) {
result.success(sharedText);
sharedText = null;
} else {
result.notImplemented();
}
}
}
}
There is more π€©
Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.
Execute request from Flutter to Android
We are done on the Android side (no more Kotlin ππΌββοΈ), back to writing dart code now.
Inside our Flutter app, we need to create a MethodChannel
and invoke the method to get the intent value.
The MethodChannel
must contain the same value as CHANNEL
that we used previously in Kotlin:
import 'package:flutter/services.dart';
const _methodChannel = MethodChannel('app.channel.process.data'); // <- Same as CHANNEL from Kotlin
From _methodChannel
we call invokeMethod
, specifying the same name used inside Kotlin (call.method.contentEquals("getSharedText")
):
import 'package:flutter/services.dart';
const _methodChannel = MethodChannel('app.channel.process.data');
Future<String?> getSharedData() async {
final sharedData = await _methodChannel.invokeMethod('getSharedText'); // <- Name of the method from Kotlin
if (sharedData is String) {
return sharedData;
}
return null;
}
If the value returned is of type String
(selected text from the intent), then we return it, otherwise we return null
.
Get intent value using StatefulWidget
When the user executes the action Android will create the intent and open our app.
Inside Flutter we use a StatefulWidget
to execute the getSharedData
function we defined above:
class AndroidIntentText extends StatefulWidget {
const AndroidIntentText({super.key});
@override
State<AndroidIntentText> createState() => _AndroidIntentTextState();
}
class _AndroidIntentTextState extends State<AndroidIntentText> {
String dataShared = 'No data';
@override
void initState() {
super.initState();
getSharedData().then((value) {
setState(() {
dataShared = value ?? "Empty data";
});
});
}
@override
Widget build(BuildContext context) {
return Text(dataShared);
}
}
We execute the function inside initState
. If the returned value is not null
, we update the dataShared
string displayed inside the widget.
That's it!
Now every time a user selects any text from any other app in her device a new "Copy in app" action allows to forward the selected text to our Flutter app.
Using a MethodChannel
allows to execute any Android function from Flutter. This makes it super convenient when we want to integrate any platform-specific functionality directly from Flutter using dart code.
You can subscribe to the newsletter here below for more tutorials, tips, and articles on Flutter and dart π
Thanks for reading.