?

Rebuilding a Native Mobile App in Flutter From the Inside Out: Part III - Entry Points

The third installment on migrating the ATC Connect mobile app to Flutter looks at how Entry Points let us specify which part of a Flutter app to integrate into our otherwise fully-native iOS or Android project. This will allow us to weave seamlessly in and out of different parts of a Flutter application, allowing your team to focus on higher-value areas.

January 7, 2021 6 minute read

In our previous installment, we saw how to integrate a Flutter page into an otherwise fully-native application. But what if we want to display multiple, unique Flutter pages depending on user interaction with the native side of our app? This is the role of “entry points.” Entry points allow us to specify which part of our Flutter app we want to send the user to when they open a FlutterViewController or FlutterActivity/FlutterFragment.  

Before moving forward, be sure to follow along with the example project. If you are continuing from the previous article, no further action is required. If you haven't yet worked with the example project, be sure to clone it and check out the part-iii branch. After cloning, cd into FlutterIntegration_Flutter and run flutter pub get. Then cd into FlutterIntegration_iOS and run pod install. You should now be ready to work with the example project.

Creating an entry point

Let’s go ahead and update our apps to give the user the option to open the same Hello World page, but with all of the text in German! For our Flutter project to know that we want to display the page in German instead of English, we must specify an entry point in our main.dart file. This is done by using the @pragma('vm:entry-point') identifier. First, let’s update our MyApp and HelloWorldPage widgets in main.dart to take an isInGerman Boolean:  

class MyApp extends StatelessWidget {
    final bool isInGerman;
    MyApp({this.isInGerman = false});
    Widget build(BuildContext context) {
        return MaterialApp(
            title: 'Flutter Integration Example',
            theme: ThemeData(
                primarySwatch: Colors.blue,
            ),
            home: HelloWorldPage(
                isInGerman: isInGerman,
            ),
        );
    }
}

class HelloWorldPage extends StatelessWidget {
    final bool isInGerman;
    HelloWorldPage({this.isInGerman = false});
    Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
                title: Text(isInGerman ? 'Hallo Welt!' : 'Hello World!'),
            ),
            body: Center(
                child: Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: Text(
                        isInGerman
                            ? 'Dies ist eine vollständig Flutter Seite, die in Ihre native App integriert ist!'
                            : 'This is a fully Flutter page, integrated into your native app!',
                        textAlign: TextAlign.center,
                    ),
                ),
            ),
        );
    }
} 

Now, we simply use the @pragma('vm:entry-point') identifier to specify an entry point at the top of our main.dart file and tell it that we want to run our Flutter app in German:  

import 'package:flutter/material.dart';

void main() => runApp(MyApp());
@pragma('vm:entry-point')void german() => runApp(MyApp(isInGerman: true));  

Specifying an entry point in iOS

Now, we need to update our iOS and Android Flutter Engines to tell our Flutter project which entry point we want to hit. On iOS, this is done by calling FlutterEngine.run(withEntryPoint:) (instead of the aforementioned FlutterEngine.run()).  

Let’s go ahead and add a helloWorldGermanEngine to our FlutterEngineManager singleton:  

class FlutterEngineManager {
    static var shared: FlutterEngineManager = FlutterEngineManager()
    public var helloWorldEngine: FlutterEngine
    public var helloWorldGermanEngine: FlutterEngine
    init() {
        helloWorldEngine = FlutterEngine(name: "hello_world")
        helloWorldGermanEngine = FlutterEngine(name: "hello_world_german")
    }
}  

Now, let’s head back over to our app delegate and specify that we want to run the helloWorldGermanEngine with the “german” entry point:  

@UIApplicationMainclass AppDelegate: UIResponder, UIApplicationDelegate {

    ...
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        FlutterEngineManager.shared.helloWorldEngine.run()
        FlutterEngineManager.shared.helloWorldGermanEngine.run(withEntrypoint: "german")
        return true
    }
    
    ...
    
} 

Now, all that’s left to do is to present a HelloWorldFlutterViewController that is constructed with the helloWorldGermanEngine:  

class ViewController: UIViewController {

    ...
    
    @IBAction func checkOutGermanButtonPressed() {
        let helloWorldFlutterVC = HelloWorldFlutterViewController(engine: FlutterEngineManager.shared.helloWorldGermanEngine)
        present(helloWorldFlutterVC, animated: true, completion: nil)
    }
}  

Now, clicking the top button in our iOS project will open up our Flutter project through the default entry point, and clicking the bottom button will open up our Flutter project through the “German” entry point! 

NOTE: If you don't see the iOS German button when you run the app, go into the interface builder and set the German button's “hidden” property to false. If this “hidden” property is enabled, we won't see our German button and we won't be able to hit the German entry point in our Flutter project! 

Specifying an entry point in Android

The first step in specifying a custom entry point in Android is to make a new Flutter Engine. Again, we will create our new, German Flutter Engine in our MainActivity.onCreate() method. This time around, we must call FlutterEngine.dartExecutor.executeDartEntrypoint() after we construct the engine to specify which point we want this engine to enter through:  

package com.wwt.flutterintegrationexample

import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.FlutterEngineCache
import io.flutter.embedding.engine.dart.DartExecutor
import io.flutter.view.FlutterMain

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val helloWorldEngine = FlutterEngine(applicationContext)
        FlutterEngineCache.getInstance().put("hello_world", helloWorldEngine)
        val helloWorldGermanEngine = FlutterEngine(applicationContext)
        helloWorldGermanEngine.dartExecutor.executeDartEntrypoint(
            DartExecutor.DartEntrypoint(
                FlutterMain.findAppBundlePath(), "german"))
        FlutterEngineCache.getInstance().put("hello_world_german", helloWorldGermanEngine)
        
        ...
        
    }
    
    ...
    
} 

Now, we want the user to have a button they can click and launch the Flutter project through the “german” entry point. So first, let’s edit our HelloWorldFlutterActivity to accept an isInGerman Boolean in its initializer:  

package com.wwt.flutterintegrationexample

import android.content.Context
import android.content.Intent
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.FlutterEngineCache

class HelloWorldFlutterActivity : FlutterActivity() {
    private val isInGerman: Boolean
        get() = intent.getBooleanExtra(IN_GERMAN_KEY, false)
        
    ...
        
    companion object {
        internal const val IN_GERMAN_KEY = "IN_GERMAN_KEY"
        fun intentFor(context: Context, isInGerman: Boolean = false): Intent {
            return Intent(context, HelloWorldFlutterActivity::class.java).apply {
                putExtra(IN_GERMAN_KEY, isInGerman)
            }
        }
    }
}  

Then, in our HelloWorldFlutterActivity.provideFlutterEngine, we must add a check if isInGerman is true, and change which cached Flutter Engine we grab accordingly:  

override fun provideFlutterEngine(context: Context): FlutterEngine? {
    val engineId: String = if (isInGerman) "hello_world_german" else "hello_world"
    return FlutterEngineCache.getInstance().get(engineId)
} 

The starter project already has a button with the “button_german” identifier. Now we must simply set the onClick listener for our German button so that a new HelloWorldFlutterActivity with isInGerman set to true is started:  

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        ...
        
        val germanButton: Button = findViewById(R.id.button_german)
        germanButton.setOnClickListener { _ ->
            val intent = HelloWorldFlutterActivity.intentFor(this, true)
            startActivity(intent)
        }
    }
}

NOTE: If you don't see the Android German button when you run the app, open up activity_main.xml and set the German button's “visibility” property to “visible”. If this “visibility” property is set to “invisible”, we won't see our German button and we won't be able to hit the German entry point in our Flutter project! 

 

In part four, we're going to take a look at how we can facilitate communication between the Flutter and native sides of our application.

Share this