Flutter module into Android/iOS: a complete guide to handle your flutter module

Build, distribute and embed your Flutter module into existing native apps

Bernardo Iribarne
Nerd For Tech
9 min readNov 27, 2023

--

Photo by Glen Carrie on Unsplash

I had the experience to embed a flutter module into native apps, so I would like to talk about it.

When I read Flutter documentation regarding embed your Flutter app into a native app, I said “It is insane, Who wants to do that?, and Why?” Well, I had that case and believe me, I think I’m going to have more than this.

In the real world there are too many apps and there are a group of apps to to manage generic content, for example, you can contract as a service to manage an event. And it could be a great decision to use it, the app is there, my event is coming, so let’s do it. But the problems come when you want to customize the app, when you want to add some particular bit of your business. I raised my hand and said “I can do it”, “I can embed a flutter module there”

How to deal with this

I would like to share with you how I dealed with this situation:

My module Flutter’s process schema

I created a project where I have the Flutter module and then I defined the way to:

  1. Build: XCFrameworks (iOS) and AAR’s files (Android)
  2. Distribute: Swift Package (iOS) and Maven repository (Android)
  3. Embed: into natives apps (iOS and Android)
Involved artifacts on my flutter module

I have created those artifacts that help me to handle the build and distribution.

  1. my_flutter_module repository contains the flutter module source code (my_flutter_module). Additionally, it has an Android host example (MyModuleAppHostAndroid) and an iOS ones (MyModuleAppHostIOS).
  2. MyModuleSP repository is the Swift Package to distribute the iOS XCFrameworks.
  3. MyModuleAndroidRepo is the Maven repository to distribute the AAR’s files for Android.

How Flutter module works

When you write your Flutter module, you are able to run it into an iOS or an Android native app. Your module will run into a Flutter Engine which knows how to run into an Android app and also knows how to run into an iOS one, so you don’t have to worry about it. Have you ever hear about Java Virtual Machine? It’s the same concept, Flutter Engine is like a JVM.

Flutter module into native apps

The Key will be to have a well known process to build and distribute your module.

Create your module

You can find the official Flutter documentation and too many others that cover it. But let’s see the basics:

To embed Flutter into your existing application, you have to create a Flutter module.

From the command line, run:

flutter create --template module my_flutter_module

The module directory structure is similar to a normal Flutter application:

my_flutter_module/
├── .ios/
│ ├── Runner.xcworkspace
│ └── Flutter/podhelper.rb
├── lib/
│ └── main.dart
├── test/
└── pubspec.yaml

At this point, your module looks very similar to a Flutter app. You also can run it and debug it, so it is very easy to developing and testing it.

Build and distribute your module

Once you has wrote your module, you have to create the distribution files.

Build and distribute

As you can imagine, the distribution files are different for Android and iOS, so we have to split our work. First we are going to see how to distribute the module to be used in Android and then for iOS.

Android build and distribution

Flutter can be embedded into your existing Android application as a source code Gradle subproject or as AARs. In this example you will see the AARs option.

You module will be packaged as a Maven repository composed of AARs and POMs artifacts. The great advantage of this option is that you will be able to embed the module into the host app without installing the Flutter SDK. And also you will be able to distribute the Maven repository through a remote repository.

Open a Terminal, go to your Flutter module root folder and run this command:

 flutter build aar --build-number x.y.z

This will generate all the required dependencies (your module + third party libraries) to embed your module into a native Android app. You will see the maven artifacts into the output build folder. Also you will see 3 folders for debug, profile, and release, and each one will have the x.y.z version:

build/host/outputs/repo
└── com
└── example
└── my_flutter_module
├── flutter_release
│ ├── 1.0
│ │ ├── flutter_release-1.0.aar
│ │ ├── flutter_release-1.0.aar.md5
│ │ ├── flutter_release-1.0.aar.sha1
│ │ ├── flutter_release-1.0.pom
│ │ ├── flutter_release-1.0.pom.md5
│ │ └── flutter_release-1.0.pom.sha1
│ ├── maven-metadata.xml
│ ├── maven-metadata.xml.md5
│ └── maven-metadata.xml.sha1
├── flutter_profile
│ ├── ...
└── flutter_debug
└── ...

Once we have to AAR’s files we can share them and distribute to use them into the native Android host app.

I distribute this build through the repository MyModuleAndroidRepo. So, I copy the build folder to that repository and distribute it through Git.

For more details you can go to the official Flutter documentation

iOS build and distribution

Flutter provides 3 methods to add UI components into an existing iOS application as embedded frameworks.

  • Using CocoaPods dependency manager and Flutter SDK.
  • Create frameworks for the Flutter engine, your compiled Dart code, and all Flutter plugins.
  • Create frameworks for your compiled Dart code, and all Flutter plugins. Use CocoaPods for the Flutter engine.

I will use the second option, as it requires no Flutter knowledge.

So, open a new Terminal, go to the module folder and run this command:

flutter build ios-framework -xcframework 
-output=/some/path/to/MyModuleSP/Sources/binaryFrameworks

Flutter generates XC Frameworks with all the required binaries (our module + third party libraries). To distribute those binaries, I have created a Swift Package. As you can see, in the command example, I send the xc frameworks to the Swift Package project (MyModuleSP).

It is extremely critical to have the right MyModuleSP branch in your local repository. There is a strong relationship between the module build files and the MyModuleSP.

IMPORTANT NOTE: Flutter and XCFramework Optimization

When creating XCFrameworks from Flutter, an extra optimization step takes place. The Dart VM architecture changes based on the mode, with the Debug version only running in the Simulator or during a debugging session in Xcode with a device plugged in, and the Release version only working on the device that is not connected to a debugging session.

Regarding this we must distribute the Swift Package in two modes:

1) Debug: to be included when you are developing your App, and you want to run it in a Simulator or a connected device.

2) Release: to be included when you are going to distribute your App.

XCFrameworks Debug and Release distribution

To generate this type of distribution we have 2 branches on the MyModuleSP repository: dev and release.

1) branch dev: to distribute debug versions.

2) branch release: to distribute release versions.

Distribute a Debug version

Follow these steps to distribute the Swift Package for debugging

1) Open a new Terminal and go to the MyModuleSP project folder

2) Change to dev branch:

git checkout dev

3) Go to the flutter module project

4) Generate the XC frameworks (it takes several minutes, go for a caffe or mate)

flutter build ios-framework -xcframework 
–output=/some/path/to/MyModuleSP/Sources/binaryFrameworks
-no-profile –no-release

5) Back to the MyModuleSP project folder

6) Tag the new distribution and push it

git tag dev-vX.Y.Z
git push origin dev-vX.Y.Z

Distribute a Release version

Follow these steps to distribute the Swift Package for release

1) Open a new Terminal and go to the MyModuleSP project folder

2) Change to release branch:

git checkout release

3) Go to the flutter module project

4) Generate the XC frameworks (it takes several minutes, go for a caffe or mate)

flutter build ios-framework - xcframework 
–output=/some/path/to/MyModuleSP/Sources/binaryFrameworks
-no-profile -no-debug

5) Back to the MyModuleSP project folder

6) Tag the new distribution and push it

git tag release-vX.Y.Z
git push origin release-vX.Y.Z

Embed the module into an Android app

Photo by Denny Müller on Unsplash

I distributed the Android files using GitHub, so first we must clone the project with the Maven repository. Then add that repository in your native Android app and include the dependencies.

Clone the GitHub repository:

Open a new Terminal and clone the repository:

git clone https://github.com/Owner/MyModuleAndroidRepo.git

Add the maven repository in your native Android App:

Each project has its configuration. Below is an approach that could work as an example:

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

}
}
String storageUrl = System.env.FLUTTER_STORAGE_BASE_URL ?: "https://storage.googleapis.com"

dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {

maven {
url = "D:/path/to/the/repo/MyModuleAndroidRepo"
}

google()
mavenCentral()
maven {
url "$storageUrl/download.flutter.io"
}

}
}
rootProject.name = "My Application Host"
include ':app'

Include the dependencies in your native Android App

 

dependencies {

. . .

debugImplementation 'com.example.my_flutter_module:flutter_debug:x.y.z'
profileImplementation 'com.example.my_flutter_module:flutter_profile:x.y.z'
releaseImplementation 'com.example.my_flutter_module:flutter_release:x.y.z'

}

Open the Flutter module in a View

package com.example.myapplicationhost;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.FlutterEngineCache;
import io.flutter.embedding.engine.dart.DartExecutor;

public class MainActivity extends AppCompatActivity {
public FlutterEngine flutterEngine;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

// Instantiate a FlutterEngine.
flutterEngine = new FlutterEngine(this);
// Start executing Dart code to pre-warm the FlutterEngine.
flutterEngine.getDartExecutor().executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
);
// Cache the FlutterEngine to be used by FlutterActivity.
FlutterEngineCache.getInstance().put("my_engine_id", flutterEngine);

Button button = (Button) findViewById(R.id.btnOpenFlutter);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(
FlutterActivity.withCachedEngine("my_engine_id").build(MainActivity.this)
);
}
});
}
}

Embed the module into an iOS app

Photo by Christian Allard on Unsplash

I built a Swift application to test the integration and I included it into my flutter module project. It includes a simple view with a button to open the flutter module.

Add the Swift Package dependency

First, you have to add the Swift Package into your iOS app.

Go to “Package Dependencies” and click “+” button

Package Dependencies

Add the package dependency using the tag of the version you have released

Add the released version

Initialize the Flutter module

import SwiftUI 
import Flutter
import FlutterPluginRegistrant

class AppDelegate: FlutterAppDelegate, ObservableObject {
let flutterEngine = FlutterEngine(name: "my flutter engine")
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Runs the default Dart entrypoint with a default Flutter route.
flutterEngine.run();
// Used to connect plugins.
GeneratedPluginRegistrant.register(with: self.flutterEngine);
return true;
}
}

@main
struct MyModuleHostIOSApp: App {
// Use this property wrapper to tell SwiftUI
// it should use the AppDelegate class for the application delegate
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}

}

Open the Flutter module in a View

//  ContentView.swift 
// Created by Bernardo on 11/24/2023.
import SwiftUI
import Flutter

struct ContentView: View {
// Flutter dependencies are passed in an EnvironmentObject.

@EnvironmentObject var appDelegate: AppDelegate ;
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("My Module IOS Host")

Button("Open Flutter Module") {
showFlutter()
}
}
.padding()
}

func showFlutter() {
guard
let windowScene = UIApplication.shared.connectedScenes
.first(where: { $0.activationState == .foregroundActive && $0 is UIWindowScene }) as? UIWindowScene,
let window = windowScene.windows.first(where: \.isKeyWindow),
let rootViewController = window.rootViewController
else { return }

// Create the FlutterViewController.
let flutterViewController = FlutterViewController(
// Access the Flutter Engine via AppDelegate.
engine: appDelegate.flutterEngine,
nibName: nil,
bundle: nil)
flutterViewController.modalPresentationStyle = .overCurrentContext
flutterViewController.isViewOpaque = false
rootViewController.present(flutterViewController, animated: true)
}
}

Conclusion

  1. Create a project for your Flutter module
  2. Create a project for the Android distribution (Maven repository)
  3. Create a project for the iOS distribution (Swift Package)
  4. Create examples of how to embed your module into the native hosts.
  5. There are lot of artifacts working together, so, document all of them with details

Thanks for reading, Clap if you like it!

Photo by Wil Stewart on Unsplash

Let me know your comments below.

--

--

Bernardo Iribarne
Nerd For Tech

Passionate about design patterns, frameworks and well practices. Becoming a Flutter expert