β€’

tech

How to execute an Android intent in Flutter

Learn how to implement and execute a platform-specific functionality directly inside your Flutter app. Create an Android intent, write some Kotlin code to read the intent, and forward the intent value to Flutter by using a MethodChannel.


Sandro Maglione

Sandro Maglione

Software development

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 ✨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:

AndroidManifest.xml
<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 to ACTION_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 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 actionThe 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:

AndroidManifest.xml
<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):

MainActivity.kt
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:

MainActivity.kt
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 documentationThe 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:

MainActivity.kt
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:

MainActivity.kt
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:

MainActivity.kt
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.

πŸ‘‹γƒ»Interested in learning more, every week?

Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.