Add Flutter to existing apps for 0.9.4

Making it easy to add Flutter to an existing app is work in progress, tracked by the Add2App project.

This page documents the current state of that work and will be updated as we build out the necessary tooling.

Last updated October 19, 2018.

The "add2app" support is in preview, and is so far only available on the master channel.

The Flutter module project template

Flutter projects created using flutter create xxx include very simple host apps for your Flutter/Dart code (a single-Activity Android host and a single-ViewController iOS host). You can modify these host apps to suit your needs and build from there.

But if you're starting off with an existing host app for either platform, you'll likely want to include your Flutter project in that app as some form of library instead.

This is what the Flutter module template provides. Executing flutter create -t module xxxproduces a Flutter project containing an Android library and a Cocoapods pod designed for consumption by your existing host app.

Android

Create a Flutter module

Let's assume you have an existing Android app at some/path/MyApp, and that you want your Flutter project as a sibling:

$ cd some/path/
$ flutter create -t module my_flutter

This creates a some/path/my_flutter/ Flutter module project with some Dart code to get you started and a .android/ hidden subfolder that wraps up the module project in an Android library.

(While not required in what follows, if you so desire, you can build that library using Gradle:

$ cd .android/
$ ./gradlew flutter:assembleDebug

This results in a flutter-debug.aar archive file in .android/Flutter/build/outputs/aar/.)

Make the host app depend on the Flutter module

Include the Flutter module as a sub-project in the host app's settings.gradle:

// MyApp/settings.gradle
include ':app'                                     // assumed existing content
setBinding(new Binding([gradle: this]))                                 // new
evaluate(new File(                                                      // new
  settingsDir.parentFile,                                               // new
  'my_flutter/.android/include_flutter.groovy'                          // new
))                                                                      // new

The binding and script evaluation allows the Flutter module to include itself (as :flutter) and any Flutter plugins used by the module (as :package_info, :video_player, etc) in the evaluation context of your settings.gradle.

Introduce an implementation dependency on the Flutter module from your app:

// MyApp/app/build.gradle
:
dependencies {
  implementation project(':flutter')
  :
}

Use the Flutter module from your Java code

Use the Flutter module's Java API to add Flutter views to your host app. This can be done by directly using Flutter.createView:

// MyApp/app/src/main/java/some/package/MainActivity.java
fab.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View view) {
    View flutterView = Flutter.createView(
      MainActivity.this,
      getLifecycle(),
      "route1"
    );
    FrameLayout.LayoutParams layout = new FrameLayout.LayoutParams(600, 800);
    layout.leftMargin = 100;
    layout.topMargin = 200;
    addContentView(flutterView, layout);
  }
});

It is also possible to create a FlutterFragment that takes care of lifecycle by itself:

// MyApp/app/src/main/java/some/package/SomeActivity.java
fab.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View view) {
    FragmentTransaction tx = getSupportFragmentManager().beginTransaction();
    tx.replace(R.id.someContainer, Flutter.createFragment("route1"));
    tx.commit();
  }
});

Above we use the string "route1" to tell the Dart code which widget to display in the Flutter view. The lib/main.dart file of the Flutter module project template should switch on (or otherwise interpret) the provided route string, available as window.defaultRouteName, to determine which widget to create and pass to runApp. Schematically,

import 'dart:ui';
import 'package:flutter/material.dart';

void main() => runApp(_widgetForRoute(window.defaultRouteName));

Widget _widgetForRoute(String route) {
  switch (route) {
    case 'route1':
      return SomeWidget(...);
    case 'route2':
      return SomeOtherWidget(...);
    default:
      return Center(
        child: Text('Unknown route: $route', textDirection: TextDirection.ltr),
      );
  }
}

It is entirely up to you which route strings you want and how to interpret them.

Building and running your app

You build and run MyApp in exactly the same way that you did before you added the Flutter module dependency, typically using Android Studio. The same goes for editing, debugging, and profiling your Android code.

Hot restart/reload and debugging Dart code

Full IDE integration to support working with the Flutter/Dart code of your hybrid app is work in progress. But the fundamentals are already present via the Flutter command line tools and the Dart Observatory web user interface.

Connect a device or launch an emulator. Then make Flutter CLI tooling listen for your app to come up:

$ cd some/path/my_flutter
$ flutter attach
Waiting for a connection from Flutter on Nexus 5X...

Launch MyApp in debug mode from Android Studio (or whichever way you usually do it). Navigate to an area of the app that uses Flutter. Then turn back to the terminal, and you should see output similar to the following:

Done.
Syncing files to device Nexus 5X...                          5.1s

🔥  To hot reload changes while running, press "r". To hot restart (and rebuild state), press "R".
An Observatory debugger and profiler on Nexus 5X is available at: http://127.0.0.1:59556/
For a more detailed help message, press "h". To quit, press "q".

You can now edit the Dart code in my_flutter, and the changes can be hot reloaded by pressing r in the terminal. You can also paste the URL above into your browser to use the Dart Observatory for setting breakpoints, analyzing memory retention and other debugging tasks.

iOS

Create a Flutter module

Let's assume you have an existing iOS app at some/path/MyApp, and that you want your Flutter project as a sibling:

$ cd some/path/
$ flutter create -t module my_flutter

This creates a some/path/my_flutter/ Flutter module project with some Dart code to get you started and a .ios/ hidden subfolder that wraps up the module project that contains some Cocoapods and a helper Ruby script.

Make the host app depend on the Flutter module

The description below assumes that your existing iOS app has a structure similar to what you get by asking Xcode version 10.0 to generate a new "Single View App" project using Objective-C. If your existing app has a different folder structure and/or existing .xcconfig files, you can reuse those, but probably need to adjust some of the relative paths mentioned below accordingly.

The assumed folder structure is as follows:

some/path/
  my_flutter/
    lib/main.dart
    .ios/
  MyApp/
    MyApp/
      AppDelegate.h
      AppDelegate.m (or swift)
      :

Add your Flutter app to your Podfile

Integrating the Flutter framework requires use of the CocoaPods dependency manager. This is because the Flutter framework needs to be available also to any Flutter plugins that you might include in my_flutter.

Please refer to cocoapods.org for how to install CocoaPods on your development machine, if needed.

If your host application (MyApp) is already using Cocoapods, you only have to do the following to integrate with your my_flutter app:

  1. Add the following lines to your Podfile:
  flutter_application_path = 'path/to/flutter_app/'
  eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)
  1. Run pod install.

Whenever you change the Flutter plugin dependencies in some/path/my_flutter/pubspec.yaml, you need to run flutter packages get from some/path/my_flutter to refresh the list of plugins read by the podhelper.rb script. Then run pod install again from some/path/MyApp.

The podhelper.rb script will ensure that your plugins and the Flutter.framework get added to your project, and also ensure that bitcode is disabled for all targets.

Add a build phase for building the Dart code

Select the top-level MyApp project in the Project navigator. Select TARGET MyApp in the left part of the main view, and then select the Build Phases tab. Add a new build phase by clicking the +towards the top left of the main view. Select New Run Script Phase. Expand the new Run Script, just appended to the list of phases.

Paste the following into the text area just below the Shell field:

"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed

Finally, drag the new build phase to just after the Target Dependencies phase.

You should now be able to build the project using ⌘B.

Under the hood

If you have some reason to do this manually or debug why these steps aren't working, here's what's going on under the hood:

  1. Flutter.framework (the Engine library) is getting embedded into your app for you. This has to match up with the release type (debug/profile/release) as well as the architecture for your app (arm*, i386, x86_64, etc.). Cocoapods pulls this in as a vendored framework and makes sure it gets embedded into your native app.
  2. App.framework (your Flutter application binary) is embedded into your app.
  3. flutter_assets folder is getting embedded as a resource - it contains fonts, images, and in certain build modes it also contains binary files required by the engine at runtime. Problems with this folder can lead to runtime errors such as "Could not run engine for configuration" - usually indicating that either the folder is not getting embedded, or you're trying to cross a JIT application with an AOT enabled engine, or vice versa!
  4. Any plugins are getting added as Cocoapods. In theory, it should be possible to manually merge those in as well, but that becomes much more specific to the plugin itself.
  5. Bitcode is disabled for every target in your project. This is a requirement to link with the Flutter Engine.
  6. Generated.xcconfig (containing Flutter-specific environment varaibles) is included in the release and debug .xcconfig files that Cocoapods generates.

The build phase script (xcode_backend.sh) is ensuring that the binaries you build stay up to date with the Dart code that's actually in the folder. It also attempt to respect your build configuration setting once this pull request lands.

Write code to use FlutterViewController from your host app

The proper place to do this will be specific to your host app. Here is an example that makes sense for the blank screen of the host app generated by Xcode 10.0.

First declare your app delegate to be a subclass of FlutterAppDelegate.

In AppDelegate.h:

#import <UIKit/UIKit.h>
#import <Flutter/Flutter.h>

@interface AppDelegate : FlutterAppDelegate
@end

This allows AppDelegate.m to be really simple, unless your host app needs to override other methods here:

#import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h> // Only if you have Flutter Plugins

#include "AppDelegate.h"

@implementation AppDelegate

// This override can be omitted if you do not have any Flutter Plugins.
- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  [GeneratedPluginRegistrant registerWithRegistry:self];
  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

@end

If you are writing in Swift, you can do the following in your AppDelegate.swift:

import UIKit
import Flutter
import FlutterPluginRegistrant // Only if you have Flutter Plugins.

@UIApplicationMain
class AppDelegate: FlutterAppDelegate {

  // Only if you have Flutter plugins.
  override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    GeneratedPluginRegistrant.register(with: self);
    return super.application(application, didFinishLaunchingWithOptions: launchOptions);
  }

}

<details style="box-sizing: border-box; display: block; color: rgb(36, 41, 46); font-family: -apple-system, system-ui, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;"><summary style="box-sizing: border-box; display: list-item; cursor: pointer;">What to do if the app delegate already inherits from somewhere else.</summary></details>

ViewController.m:

#import <Flutter/Flutter.h>
#import "ViewController.h"

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    [button addTarget:self
               action:@selector(handleButtonAction)
     forControlEvents:UIControlEventTouchUpInside];
    [button setTitle:@"Press me" forState:UIControlStateNormal];
    [button setBackgroundColor:[UIColor blueColor]];
    button.frame = CGRectMake(80.0, 210.0, 160.0, 40.0);
    [self.view addSubview:button];
}

- (void)handleButtonAction {
    FlutterViewController* flutterViewController = [[FlutterViewController alloc] init];
    [self presentViewController:flutterViewController animated:false completion:nil];
}
@end

Or, using Swift:

ViewController.swift:

import UIKit
import Flutter

class ViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    let button = UIButton(type:UIButtonType.custom)
    button.addTarget(self, action: #selector(handleButtonAction), for: .touchUpInside)
    button.setTitle("Press me", for: UIControlState.normal)
    button.frame = CGRect(x: 80.0, y: 210.0, width: 160.0, height: 40.0)
    button.backgroundColor = UIColor.blue
    self.view.addSubview(button)
  }

  @objc func handleButtonAction() {
    let flutterViewController = FlutterViewController()
    self.present(flutterViewController, animated: false, completion: nil)
  }
}

You should now be able to build and launch MyApp on the Simulator or on a device. Pressing the button should bring up a full-screen Flutter view with the standard Flutter Demo counting app. You can use routes to show different widgets at different places in your app, as described in the Android section above. To set the route, call

  • Objective-C:
[flutterViewController setInitialRoute:@"route1"];
  • Swift:
flutterViewController.setInitialRoute("route1")

immediately after construction of the FlutterViewController (and before presenting it).

You can have the Flutter app dismiss itself by calling SystemNavigator.pop() in the Dart code.

Building and running your app

You build and run MyApp using Xcode in exactly the same way that you did before you added the Flutter module dependency. The same goes for editing, debugging, and profiling your iOS code.

Hot restart/reload and debugging Dart code

Connect a device or launch a Simulator. Then make Flutter CLI tooling listen for your app to come up:

$ cd some/path/my_flutter
$ flutter attach
Waiting for a connection from Flutter on iPhone X...

Launch MyApp in debug mode from Xcode. Navigate to an area of the app that uses Flutter. Then turn back to the terminal, and you should see output similar to the following:

Done.
Syncing files to device iPhone X...                          4.7s

🔥  To hot reload changes while running, press "r". To hot restart (and rebuild state), press "R".
An Observatory debugger and profiler on iPhone X is available at: http://127.0.0.1:54741/
For a more detailed help message, press "h". To quit, press "q".

You can now edit the Dart code in my_flutter, and the changes can be hot reloaded by pressing r in the terminal. You can also paste the URL above into your browser to use the Dart Observatory for setting breakpoints, analyzing memory retention and other debugging tasks.

Debugging specific instances of Flutter

It's possible to add multiple instances of Flutter (root isolates) to an app. flutter attachconnects to all of the available isolates by default. Any commands sent from the attached CLI are then forwarded to each of the attached isolates.

List all attached isolates by typing l from an attached flutter CLI tool. If unspecified, isolate names are automatically generated from the dart entry point file and function name.

Example l output for an application that's displaying two Flutter isolates simultaneously:

Connected views:
  main.dart$main-517591213 (isolates/517591213)
  main.dart$main-332962855 (isolates/332962855)

Attach to specific isolates instead in two steps:

  1. Name the Flutter root isolate of interest in its Dart source.
// main.dart
import 'dart:ui' as ui;

void main() {
  ui.window.setIsolateDebugName("debug isolate");
  // ...
}
  1. Run flutter attach with the --isolate-filter option.
$ flutter attach --isolate-filter='debug'
Waiting for a connection from Flutter...
Done.
Syncing files to device...      1.1s

🔥  To hot reload changes while running, press "r". To hot restart (and rebuild state), press "R".
An Observatory debugger and profiler is available at: http://127.0.0.1:43343/
For a more detailed help message, press "h". To detach, press "d"; to quit, press "q".

Connected view:
  debug isolate (isolates/642101161)

You can check out 93573de for a more detailed example.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容