Provide a step-by-step guide to implement a Flutter screen when a user presses a button in a native Android app (Add-to-App).

#flutter#android#native#add-to-app#platform-channel#integration

Answer

Overview

Flutter's Add-to-App feature lets you embed Flutter screens into an existing native Android app. This is useful when you want to gradually migrate to Flutter or add Flutter-powered features to a native app.

Scenario: In a native Android app, when the user presses the user icon, load a Flutter profile screen.


Step 1: Create a Flutter Module

Create a Flutter module (not a full app) alongside your native Android project:

bash
# Navigate to the parent directory of your Android project
cd /path/to/projects

# Create Flutter module
flutter create --template module flutter_profile_module

Directory structure:

text
projects/
├── my_native_android_app/    # Existing Android app
└── flutter_profile_module/    # New Flutter module
    ├── lib/
    │   └── main.dart
    ├── .android/               # Auto-generated
    └── pubspec.yaml

Step 2: Write the Flutter Screen

Edit

text
flutter_profile_module/lib/main.dart
:

dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() => runApp(const MyFlutterApp());

class MyFlutterApp extends StatelessWidget {
  const MyFlutterApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const UserProfileScreen(),
    );
  }
}

class UserProfileScreen extends StatefulWidget {
  const UserProfileScreen({super.key});

  
  State<UserProfileScreen> createState() => _UserProfileScreenState();
}

class _UserProfileScreenState extends State<UserProfileScreen> {
  static const platform = MethodChannel('com.example/user');
  String _userName = 'Loading...';
  String _userEmail = '';

  
  void initState() {
    super.initState();
    _getUserData();
  }

  Future<void> _getUserData() async {
    try {
      final Map<dynamic, dynamic> result =
          await platform.invokeMethod('getUserData');
      setState(() {
        _userName = result['name'] ?? 'Unknown';
        _userEmail = result['email'] ?? '';
      });
    } on PlatformException catch (e) {
      setState(() => _userName = 'Error: ${e.message}');
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('User Profile'),
        leading: IconButton(
          icon: const Icon(Icons.arrow_back),
          onPressed: () => SystemNavigator.pop(),
        ),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const CircleAvatar(
              radius: 50,
              child: Icon(Icons.person, size: 50),
            ),
            const SizedBox(height: 16),
            Text(
              _userName,
              style: const TextStyle(
                fontSize: 24,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 8),
            Text(
              _userEmail,
              style: const TextStyle(
                fontSize: 16,
                color: Colors.grey,
              ),
            ),
            const SizedBox(height: 32),
            ElevatedButton(
              onPressed: () => SystemNavigator.pop(),
              child: const Text('Back to Native'),
            ),
          ],
        ),
      ),
    );
  }
}

Step 3: Configure Android —
text
settings.gradle

Add the Flutter module to your native Android app's

text
settings.gradle
:

groovy
// my_native_android_app/settings.gradle

pluginManagement {
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
    repositories {
        google()
        mavenCentral()
    }
}

rootProject.name = "MyNativeAndroidApp"
include ':app'

// Add Flutter module
def flutterProjectRoot = file('../flutter_profile_module')
include ':flutter'
project(':flutter').projectDir = new File(
    flutterProjectRoot, '.android/Flutter'
)

apply from: "$flutterProjectRoot/.android/include_flutter.groovy"

Step 4: Configure Android —
text
build.gradle

Add Flutter dependency in your app-level

text
build.gradle
:

groovy
// my_native_android_app/app/build.gradle

android {
    // ...
    defaultConfig {
        // Minimum SDK must be 21+ for Flutter
        minSdk 21
        // ...
    }

    buildTypes {
        profile {
            initWith debug
        }
    }
}

dependencies {
    // Add Flutter module dependency
    implementation project(':flutter')
    // ... other dependencies
}

Step 5: Initialize Flutter Engine in Application Class

Create or update your

text
Application
class for engine pre-warming (faster Flutter screen launch):

kotlin
// MyApplication.kt
package com.example.mynativeandroidapp

import android.app.Application
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.FlutterEngineCache
import io.flutter.embedding.engine.dart.DartExecutor

class MyApplication : Application() {
    companion object {
        const val FLUTTER_ENGINE_ID = "flutter_profile_engine"
    }

    override fun onCreate() {
        super.onCreate()

        // Pre-warm the Flutter engine for faster startup
        val flutterEngine = FlutterEngine(this)

        // Start executing Dart code
        flutterEngine.dartExecutor.executeDartEntrypoint(
            DartExecutor.DartEntrypoint.createDefault()
        )

        // Cache the engine for reuse
        FlutterEngineCache
            .getInstance()
            .put(FLUTTER_ENGINE_ID, flutterEngine)
    }
}

Register in

text
AndroidManifest.xml
:

xml
<application
    android:name=".MyApplication"
    android:label="My Native App"
    ... >

    <!-- Register FlutterActivity -->
    <activity
        android:name="io.flutter.embedding.android.FlutterActivity"
        android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
        android:hardwareAccelerated="true"
        android:windowSoftInputMode="adjustResize" />

    <!-- ... other activities -->
</application>

Step 6: Launch Flutter Screen on User Icon Press

In your native Android

text
MainActivity
:

kotlin
// MainActivity.kt
package com.example.mynativeandroidapp

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import io.flutter.embedding.android.FlutterActivity
import io.flutter.plugin.common.MethodChannel
import io.flutter.embedding.engine.FlutterEngineCache

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Setup MethodChannel to pass data to Flutter
        setupMethodChannel()

        // User icon button click → open Flutter profile screen
        val userIconButton = findViewById<ImageButton>(R.id.user_icon)
        userIconButton.setOnClickListener {
            openFlutterProfileScreen()
        }
    }

    private fun openFlutterProfileScreen() {
        // Option 1: Using cached engine (FAST — recommended)
        startActivity(
            FlutterActivity
                .withCachedEngine(MyApplication.FLUTTER_ENGINE_ID)
                .build(this)
        )

        // Option 2: Without cached engine (SLOWER — cold start)
        // startActivity(
        //     FlutterActivity
        //         .withNewEngine()
        //         .build(this)
        // )
    }

    private fun setupMethodChannel() {
        val flutterEngine = FlutterEngineCache
            .getInstance()
            .get(MyApplication.FLUTTER_ENGINE_ID)
            ?: return

        MethodChannel(
            flutterEngine.dartExecutor.binaryMessenger,
            "com.example/user"
        ).setMethodCallHandler { call, result ->
            when (call.method) {
                "getUserData" -> {
                    // Pass user data from native to Flutter
                    val userData = mapOf(
                        "name" to "John Doe",
                        "email" to "john@example.com"
                    )
                    result.success(userData)
                }
                else -> result.notImplemented()
            }
        }
    }
}

Step 7: Using FlutterFragment (Embed Inside a View)

If you want to embed Flutter as part of a screen (not full screen):

kotlin
// In your Activity or Fragment
import io.flutter.embedding.android.FlutterFragment

class UserActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_user)

        // Add Flutter as a Fragment inside a FrameLayout
        val flutterFragment = FlutterFragment
            .withCachedEngine(MyApplication.FLUTTER_ENGINE_ID)
            .build<FlutterFragment>()

        supportFragmentManager
            .beginTransaction()
            .replace(R.id.flutter_container, flutterFragment)
            .commit()
    }
}
xml
<!-- activity_user.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <!-- Native header -->
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Native Header"
        android:padding="16dp" />

    <!-- Flutter embedded here -->
    <FrameLayout
        android:id="@+id/flutter_container"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />
</LinearLayout>

Complete Flow Diagram

text
User presses User Icon (Native Android)
MainActivity.openFlutterProfileScreen()
FlutterActivity.withCachedEngine("flutter_profile_engine")
Cached FlutterEngine loads main.dart
UserProfileScreen calls MethodChannel → getUserData
Native Android returns { name, email } via MethodChannel
Flutter renders profile screen with user data
User presses Back → SystemNavigator.pop() → returns to Native

Key Differences: Cached vs New Engine

FeatureCached EngineNew Engine
Startup time~100ms (instant)~1-2 seconds (cold start)
MemoryEngine always in memoryEngine created on demand
Data passingMethodChannel setup at app startMethodChannel setup per launch
Recommended forFrequently accessed screensRarely used screens

Common Issues & Solutions

IssueSolution
text
minSdk
too low
Set
text
minSdk 21
or higher
Slow first launchUse cached engine (pre-warm in
text
Application
)
Data not passing to FlutterEnsure MethodChannel name matches on both sides
Flutter screen is blankCheck that
text
DartEntrypoint.createDefault()
is called
Back button doesn't workUse
text
SystemNavigator.pop()
in Flutter
Gradle sync failsVerify
text
settings.gradle
path to Flutter module

Best Practice: Always use cached Flutter engine for screens accessed frequently. Pre-warm the engine in the

text
Application
class so the Flutter screen loads instantly when the user taps the icon.

Learn more at Flutter Add-to-App Documentation and Flutter Platform Channels.