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).
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:
textprojects/ ├── 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
flutter_profile_module/lib/main.dartdartimport '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 — textsettings.gradle
settings.gradleAdd the Flutter module to your native Android app's
settings.gradlegroovy// 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 — textbuild.gradle
build.gradleAdd Flutter dependency in your app-level
build.gradlegroovy// 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
Applicationkotlin// 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
AndroidManifest.xmlxml<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
MainActivitykotlin// 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
textUser 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
| Feature | Cached Engine | New Engine |
|---|---|---|
| Startup time | ~100ms (instant) | ~1-2 seconds (cold start) |
| Memory | Engine always in memory | Engine created on demand |
| Data passing | MethodChannel setup at app start | MethodChannel setup per launch |
| Recommended for | Frequently accessed screens | Rarely used screens |
Common Issues & Solutions
| Issue | Solution |
|---|---|
text | Set text |
| Slow first launch | Use cached engine (pre-warm in text |
| Data not passing to Flutter | Ensure MethodChannel name matches on both sides |
| Flutter screen is blank | Check that text |
| Back button doesn't work | Use text |
| Gradle sync fails | Verify text |
Best Practice: Always use cached Flutter engine for screens accessed frequently. Pre-warm the engine in the
class so the Flutter screen loads instantly when the user taps the icon.textApplication
Learn more at Flutter Add-to-App Documentation and Flutter Platform Channels.