0

If I understand correctly: When we run an Android app built with Flutter, it first goes to the AndroidManfiest.xml file, looks for the LAUNCHER activity and launches it. This activity is MainActivity.kt by default that extends FlutterActivity.

But the Flutter part of the app begins when the main() method in main.dart gets called.

My question is, who calls this main() method?

For Android, is it the MainActivity that extends FlutterActivity? Or is there some logic in the FlutterActivity itself? Or is there some other mechanism altogether and I'm completely missing the point?

The same question applies to iOS too with FlutterViewController instead of FlutterActivity.

A link to the source code for this that just clarifies when does main() get called would be great.

Rohan Taneja
  • 9,687
  • 3
  • 36
  • 48
  • The Dart runtime (or VM in debug builds) executes `main`. The Dart runtime/VM is started by the Flutter engine. – jamesdlin Aug 10 '21 at 19:33
  • Okay. Can you point me to the code that triggers the Dart VM in debug builds? When I run the iOS app from xcode, there has to be some configuration in some iOS specific file that does things differently than it would for a Swift/Obj-C iOS app i.e. triggering this Dart VM. – Rohan Taneja Aug 10 '21 at 22:11
  • 1
    I don't remember the exact details, but I think you would have to look at the C++ and Objective-C code in [flutter_engine](https://github.com/flutter/engine), e.g. [`FlutterDartProject.mm`](https://github.com/flutter/engine/blob/master/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm). – jamesdlin Aug 10 '21 at 22:25
  • Perfect, this is exactly what I was curious about. Thanks! Documenting the direct links here for further reading: iOS: https://github.com/flutter/engine/blob/138c91c614d742c52aa5432b4cb921f0ff9fdee2/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h#L22 Android: https://github.com/flutter/engine/blob/f9d717cef57c3527750b5dfd293f8ca70d95d64e/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java#L222 – Rohan Taneja Aug 10 '21 at 23:29

1 Answers1

3

On iOS

A main.m file was autogenerated in your Flutter projects ios/Runner directory, which defines the regular, main function which a C program implements, int main(int argc, char* argv[]);.

One compiled output can only have one main method, which the compiler will run immediately when the program is started. The following code creates a UIApplicationMain, which "Creates the application object and the application delegate and sets up the event cycle":

#import "AppDelegate.h"

int main(int argc, char* argv[]) {
  @autoreleasepool {
    return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
  }
}

It's simpler in Swift, just annotate AppDelegate with @UIApplicationMain.

The AppDelegate is the class which launches Flutter, because it extends FlutterAppDelegate. When FlutterAppDelegate is instantiated, iOS will create the FlutterViewController, which creates a FlutterEngine. It creates FlutterViewController because the FlutterViewController is configured in the Main.storyboard, which has been specified in the applications Info.plist. So technically, Flutter apps are Storyboard apps .

Screenshot of Xcode's Interface Builder with Main.storyboard open, with the identity inspector open showing the FlutterViewController as custom class.

Anyway, when the storyboard is created by iOS, the window property is set on the AppDelegate. You can get the FlutterViewController in the AppDelegate using window.rootViewController. An Objective-C++ file, FlutterViewController.mm's sharedSetupWithProject method creates a FlutterEngine using [[FlutterEngine alloc]initWithName:...:

- (void)sharedSetupWithProject:(nullable FlutterDartProject*)project
                  initialRoute:(nullable NSString*)initialRoute {
  // Need the project to get settings for the view. Initializing it here means
  // the Engine class won't initialize it later.
  if (!project) {
    project = [[[FlutterDartProject alloc] init] autorelease];
  }
  FlutterView.forceSoftwareRendering = project.settings.enable_software_rendering;
  auto engine = fml::scoped_nsobject<FlutterEngine>{[[FlutterEngine alloc]
                initWithName:@"io.flutter"
                     project:project
      allowHeadlessExecution:self.engineAllowHeadlessExecution
          restorationEnabled:[self restorationIdentifier] != nil]};

  if (!engine) {
    return;
  }

  _viewOpaque = YES;
  _weakFactory = std::make_unique<fml::WeakPtrFactory<FlutterViewController>>(self);
  _engine = std::move(engine);
  _flutterView.reset([[FlutterView alloc] initWithDelegate:_engine opaque:self.isViewOpaque]);
  [_engine.get() createShell:nil libraryURI:nil initialRoute:initialRoute];
  _engineNeedsLaunch = YES;
  _ongoingTouches.reset([[NSMutableSet alloc] init]);
  [self loadDefaultSplashScreenView];
  [self performCommonViewControllerInitialization];
}

Eventually, in FlutterEngine.mm, launchEngine is called, with the entrypoint (your dart's main function.)

- (void)launchEngine:(NSString*)entrypoint libraryURI:(NSString*)libraryOrNil {
  // Launch the Dart application with the inferred run configuration.
  self.shell.RunEngine([_dartProject.get() runConfigurationForEntrypoint:entrypoint
                                                            libraryOrNil:libraryOrNil]);
}

Shell::RunEngine is a C++ function implemented in shell.cc. I'm going to stop there for now. This is probably where the event loop starts , using platform_runner->PostTask(...).


On Android

In a generated Flutter project's android directory, MainActivity is declared in the AndroidManifest.xml to be the application which launches from the home screen.

When the activity is launched, Android will call the activity's onCreate method. Because MainActivity extends FlutterActivity, this onCreate method is called. FlutterActivityAndFragmentDelegate is instantiated, and its onAttach method is called. Eventually, a Java representation of FlutterEngine is created using:

    flutterEngine =
        new FlutterEngine(
            host.getContext(),
            host.getFlutterShellArgs().toArray(),
            /*automaticallyRegisterPlugins=*/ false,
            /*willProvideRestorationData=*/ host.shouldRestoreAndSaveState());

This communicates with the C++ FlutterEngine library using FlutterJNI.

/**
 * Interface between Flutter embedding's Java code and Flutter engine's C/C++ code.
 *
 * <p>Flutter's engine is built with C/C++. The Android Flutter embedding is responsible for
 * coordinating Android OS events and app user interactions with the C/C++ engine. Such coordination
 * requires messaging from an Android app in Java code to the C/C++ engine code. This communication
 * requires a JNI (Java Native Interface) API to cross the Java/native boundary.
 *

Later, in FlutterActivityAndFragmentDelegate, doInitialFlutterViewRun is called, which creates a DartEntryPoint, your main method again, based on the FlutterActivity's ActivityInfo. This gets the entrpoint function name using the following, but it will default to "main":

  @NonNull
  public String getDartEntrypointFunctionName() {
    try {
      Bundle metaData = getMetaData();
      String desiredDartEntrypoint =
          metaData != null ? metaData.getString(DART_ENTRYPOINT_META_DATA_KEY) : null;
      return desiredDartEntrypoint != null ? desiredDartEntrypoint : DEFAULT_DART_ENTRYPOINT;
    } catch (PackageManager.NameNotFoundException e) {
      return DEFAULT_DART_ENTRYPOINT;
    }
  }

Then, flutterEngine.getDartExecutor().executeDartEntrypoint(entrypoint); is called. So your main method has "been called". But this uses the FlutterJNI and the FlutterEngine C++ code. First, flutterJNI.runBundleAndSnapshotFromLibrary is called, and finally this JNI native method:

  private native void nativeRunBundleAndSnapshotFromLibrary(
      long nativeShellHolderId,
      @NonNull String bundlePath,
      @Nullable String entrypointFunctionName,
      @Nullable String pathToEntrypointFunction,
      @NonNull AssetManager manager);

This native method is defined in platform_view_adroid_jni_impl.cc:

      {
          .name = "nativeRunBundleAndSnapshotFromLibrary",
          .signature = "(JLjava/lang/String;Ljava/lang/String;"
                       "Ljava/lang/String;Landroid/content/res/AssetManager;)V",
          .fnPtr = reinterpret_cast<void*>(&RunBundleAndSnapshotFromLibrary),
      },

Where RunBundleAndSnapshotFromLibrary is a C++ method:

static void RunBundleAndSnapshotFromLibrary(JNIEnv* env,
                                            jobject jcaller,
                                            jlong shell_holder,
                                            jstring jBundlePath,
                                            jstring jEntrypoint,
                                            jstring jLibraryUrl,
                                            jobject jAssetManager) {
  auto asset_manager = std::make_shared<flutter::AssetManager>();

  asset_manager->PushBack(std::make_unique<flutter::APKAssetProvider>(
      env,                                             // jni environment
      jAssetManager,                                   // asset manager
      fml::jni::JavaStringToString(env, jBundlePath))  // apk asset dir
  );

  auto entrypoint = fml::jni::JavaStringToString(env, jEntrypoint);
  auto libraryUrl = fml::jni::JavaStringToString(env, jLibraryUrl);

  ANDROID_SHELL_HOLDER->Launch(asset_manager, entrypoint, libraryUrl);
}

Where AndroidShellHolder::Launch is:

void AndroidShellHolder::Launch(std::shared_ptr<AssetManager> asset_manager,
                                const std::string& entrypoint,
                                const std::string& libraryUrl) {
  if (!IsValid()) {
    return;
  }

  asset_manager_ = asset_manager;
  auto config = BuildRunConfiguration(asset_manager, entrypoint, libraryUrl);
  if (!config) {
    return;
  }
  shell_->RunEngine(std::move(config.value()));
}

Just like iOS, Shell::RunEngine is called.

Ben Butterworth
  • 22,056
  • 10
  • 114
  • 167