While our affection for Flutter is unanimous, transitioning the entire app to Flutter all at once might not always be feasible. However, an effective approach involves integrating the Flutter project into our existing native app as a library or module. This can be seamlessly achieved using the add-to-app feature, allowing us to accomplish precisely that and beyond!

how to add screens designed in flutter into native app

This approach is particularly advantageous when we aim to implement identical functionalities within both our Android and iOS applications using Flutter app development. In such scenarios, there’s no necessity to develop distinct code for each platform. Instead, we can create one Flutter design that works for both iOS and Android. We can then use this design on both platforms without needing to make separate versions. This makes the development process smoother and easier.

Let’s start by implementing a simple demo iOS application that will have a few screens designed in Flutter. We’ll embed the Flutter module in our iOS app as a framework.

Step 1: Create a Flutter Module

To create a Flutter module, go to your command line interface and directory where you want to create the Flutter module and run the command mentioned below:

flutter create --template module --org com.mobisoft mi_flutter_moduleCode language: JavaScript (javascript)

This command will create a Flutter module with .ios and .android hidden folders.

Step 2: Write Your Flutter Code

Design your screens and add all your necessary code files under the lib folder in your Flutter module. In our case, for demonstration purposes, we are going to design a single screen with GridView containing a few feature tiles like Transport, Announcements, and Resources.

Step 3: Generate iOS Framework from the Flutter Module

The Flutter documentation explains that there are a couple of methods for making an iOS framework and connecting it to the regular app. However, we’ll be focusing on Option B mentioned there, which is called “Embed frameworks in Xcode

Before generating the iOS framework, we need to make sure that we have defined an entry point function that will be called from the native app. 

In our main.dart file located under mi_flutter_module/lib folder, add the following entry point function. This function will be called from the iOS app.

@pragma("vm:entry-point")
void loadFromNative() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}
Code language: JavaScript (javascript)

To generate the iOS framework, run the following command in your mi_flutter_module directory. This command will generate 3 frameworks for Debug, Profile, and Release. For development purposes, you can use the framework under your Debug Directory.

flutter build ios-framework

Step 4: Integrating and Embedding Framework in iOS App

To ensure runtime loading, it’s essential to embed the generated dynamic frameworks into your app. For embedding just drag and drop the framework into the Frameworks, Libraries, and Embedded content section under your Target -> General.

If all the steps for generating the framework are correct, you should be able to build the project successfully.

Step 5:  Open the Flutter View 

To initiate a Flutter screen from an existing iOS interface, we’ll leverage the FlutterEngine and FlutterViewController classes.

Let’s begin by incorporating a basic button into the ViewController. This button will serve as the trigger to open the Flutter view upon being clicked.

Create a Flutter Engine: Go to your AppDelegate file and add the following code:

import UIKit
import Flutter

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    
    lazy var flutterEngine = FlutterEngine(name: "flutter_engine")
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions:       [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        
        flutterEngine.run(withEntrypoint: "loadFromNative", libraryURI: "package:home_derma/main.dart")
        
         return true
}

We made the following changes to the above code:

  • We created an instance of a Flutter engine class with a name.
  • Since we are only showing one Flutter screen, we prepared only one FlutterEngine. But if you want to show more than one Flutter screen, you’ll need to make separate engines for each of them.
  • We added a flutter run statement, with arguments: 
    • withEntrypoint: Pass entryPoint function name which we had configured in the flutter module’s main.dart file.
    • libraryURI: Pass the module’s dart file path. Otherwise, it’ll simply open an empty blur screen.

Open Flutter View as a UIViewController:  Opening Flutter view on the button, click –

@IBAction func miFlutterModuleButtonTapped(_ sender: Any) {
        
        let flutterEngine = (UIApplication.shared.delegate as!    AppDelegate).flutterEngine
        let flutterViewController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)
        flutterViewController.modalPresentationStyle = .fullScreen
        present(flutterViewController, animated: true, completion: nil)
 }
Code language: JavaScript (javascript)

Quite straightforward, wouldn’t you agree? 

 We’re all set! You can now run the application and Voila!

We have now successfully integrated the Flutter screens into your Native iOS application. But wait, how can you come back to your native application? 

To achieve this, we are going to use the FlutterMethodChannel class provided by the Flutter framework.

FlutterMethodChannel is a class in the Flutter framework, which is used for communication between Dart code (the programming language used in Flutter) and platform-specific code (such as Java, Kotlin for Android, and Swift, Objective-C for iOS) in your Flutter app.

Step 6: Create a Helper Class called CloseFlutterModuleHelper with a Method Channel in Your Flutter Module

class CloseFlutterModuleHelper {
  // Create a static constant MethodChannel instance with a specific channel name.
  static const methodChannel = MethodChannel('com.mobisoft.FlutterNativeModuleApp');


  // Private constructor to prevent direct instantiation of the class.
  CloseFlutterModuleHelper._privateConstructor();


  // A static method to close a Flutter screen using platform-specific code.
  static Future<void> closeFlutterScreen() async {
    try {
      // Invoke a method named 'dismissFlutterModule' on the MethodChannel.
      await methodChannel.invokeListMethod('dismissFlutterModule');
    } on PlatformException catch (e) {
      // If an exception occurs during method invocation, print the error.
      debugPrint(e.toString());
    }
  }
}
Code language: JavaScript (javascript)

Here’s what the above code does:

  • We have created a CloseFlutterModuleHelper class. Its purpose is to close the Flutter screen.
  • MethodChannel instance named methodChannel is created. This channel is used for communication between Dart code and platform-specific code. The channel name ‘com.mobisoft.FlutterNativeModuleApp‘ identifies the channel and should match the channel set up on the platform side.
  • The closeFlutterScreen method is defined as a static method. It is responsible for initiating the process to close a Flutter screen using platform-specific code. Inside the closeFlutterScreen method, there’s a try block. Within this block, the methodChannel is used to invoke a method named ‘dismissFlutterModule’

Step 7: Update Your AppDelegate to Listen to Callbacks from Your Flutter Module

import UIKit
import Flutter
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    
    var methodChannel: FlutterMethodChannel!
    var window: UIWindow?
    lazy var flutterEngine = FlutterEngine(name: "flutter_engine")
    
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        flutterEngine.run(withEntrypoint: "loadFromNative", libraryURI: "package:mi_flutter_module/main.dart")
        
        let controller : FlutterViewController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)
        
        methodChannel = FlutterMethodChannel(name: "com.mobisoft.FlutterNativeModuleApp",
                                             binaryMessenger: controller.binaryMessenger)
        methodChannel.setMethodCallHandler ({ (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
            switch call.method {
            case "dismissFlutterModule":
                
                NotificationCenter.default.post(name: Notification.Name("DismissFlutterModule"),
                                                object: nil,
                                                userInfo: nil)
            default: result(FlutterMethodNotImplemented)
            }
        })
        return true
    }
}

Here’s what the above code does:

  • We need to create a FlutterViewController instance using the initialized FlutterEngine. This controller will host the Flutter UI within the native iOS app.

Setting up Method Channel: We are setting up a methodChannel with a unique name (“com.mobisoft.FlutterNativeModuleApp”) (This should be the same as defined in our Flutter module) for communication between native and Flutter code. The binaryMessenger here refers to the channel of communication.

methodChannel = FlutterMethodChannel(name: "com.mobisoft.FlutterNativeModuleApp", binaryMessenger: controller.binaryMessenger)
Code language: JavaScript (javascript)

Method Call Handler: We are setting a method call handler on the methodChannel. This handler processes incoming method calls from the Flutter side. In this case, it responds to the method called “dismissFlutterModule” by posting a notification named “DismissFlutterModule”.

    methodChannel.setMethodCallHandler({ (call: FlutterMethodCall, result: @escaping     FlutterResult) -> Void in


    // ...
})
Code language: JavaScript (javascript)

Here is how our updated ViewController code looks like –

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        NotificationCenter.default.addObserver(self, selector: #selector(self.dismissView),
                                               name: Notification.Name("DismissFlutterModule"),
                                               object: nil)
    }
    
    @IBAction func miFlutterModuleButtonTapped(_ sender: Any) {
        
        let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).flutterEngine
        let flutterViewController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)
        flutterViewController.modalPresentationStyle = .fullScreen
        present(flutterViewController, animated: true, completion: nil)
    }
    
    @objc func dismissView() {
        self.dismiss(animated: true)
    }
}

Summing It Up

I hope you enjoyed this tutorial on How to add screens designed in Flutter into a Native App. To download the code for this app, please click here.

In the next Part 2, we will cover How to add Flutter screens in Android with a focus on optimizing performance and user experience. Feel free to contact us if you have any questions or feedback. If you’re seeking a dedicated Flutter app development company to guide you through this transformative journey, our experts are just a click away. Together, we can elevate your app’s performance, aesthetics, and user experience to new heights.

Author's Bio

Prashant Telangi
Prashant Telangi

Prashant Telangi brings over 14 years of experience in Mobile Technology, He is currently serving as Technical Architect - Mobility at Mobisoft Infotech. With a proven history in IT and services, he is a skilled, passionate developer specializing in Mobile Applications. His strong engineering background underscores his commitment to crafting innovative solutions in the ever-evolving tech landscape.