目前主流的混合开发方案有两种集成方式:
- 源码集成:也就是谷歌官方提供的方案[https://github.com/flutter/flutter/wiki/Add-Flutter-to-existing-apps]
- 产物集成:Flutter 项目单独开发,开发完成后发布成 aar 包或者 iOS 的 framework 形式,原生项目依赖 Flutter 输出的制品即可。
两种方式对比:
源码集成 | 产物集成 | |
---|---|---|
优点 | 1. 简单快捷、Google 原生支持 (beta 版) 2. 和原生交互较多或需要依赖原生的数据环境使用源码集成开发调试更方便 | 1. 不影响原生项目 2. 不参与 Flutter 开发的人员处于无感状态 3. 无需对现有的持续集成环境和编译发布流程进行修改 |
缺点 | 1. 团队所有人都需要安装 Flutter 环境 2. 需要对现有编译发布体系做修改 | 1. 复杂度相对较高,如果是小团队成本较高 2. 需要和原生交互的时候调试不方便 |
两种方式各有优劣,其实产物集成更好一些,不过即使是进行产物集成,也需要弄懂源码集成的方式,因为当有很多和原生交互的功能进行开发的时候,源码集成的方式可以直接调试会方便很多。整个的集成方案是参考谷歌方法:[https://github.com/flutter/flutter/wiki/Add-Flutter-to-existing-apps],但是有一些不一样,我是创建了一个 flutter 项目后,在原生的项目中使用 git submodule
的形式进行管理的。
1. 创建 flutter module project
我们假定已经有了原生的项目 Native-iOS
和 Native-Android
;现在我们需要创建我们的 flutter 项目。
- 1.1 把我们的 flutter 的 channel 切换到 master (master 分支下是 flutter 的 preview 版本)
MacBook-Pro-3:project kuangzhongwen$ flutter channel master
Switching to flutter channel 'master'...
git: From https://github.com/flutter/flutter
git: * [new branch] Hixie-patch-2 -> origin/Hixie-patch-2
git: * [new branch] compress_pngs -> origin/compress_pngs
git: 3b3f6c7a0..fbefd6b81 dev -> origin/dev
git: de6995cfa..88d50f78f master -> origin/master
git: * [new branch] v1.4.5-hotfixes -> origin/v1.4.5-hotfixes
git: * [new branch] v1.4.6-hotfixes -> origin/v1.4.6-hotfixes
git: * [new tag] v1.4.5 -> v1.4.5
git: * [new tag] v1.4.6-hotfix.1 -> v1.4.6-hotfix.1
git: * [new tag] v1.4.8 -> v1.4.8
git: * [new tag] v1.4.6 -> v1.4.6
git: * [new tag] v1.4.7 -> v1.4.7
git: Switched to a new branch 'master'
git: Branch 'master' set up to track remote branch 'master' from 'origin'.
- 1.2 创建 flutter 模块的项目
我这里创建一个 flutter 的模块项目叫 flutter_module:
MacBook-Pro-3:project kuangzhongwen$ flutter create -t module flutter_module
Downloading Dart SDK from Flutter engine 309d078b597d3a38fe9bcef8d8bae7b56487e8eb...
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 118M 100 118M 0 0 9638k 0 0:00:12 0:00:12 --:--:-- 10.3M
Building flutter tool...
Downloading android-arm-profile/darwin-x64 tools... 1.0s
Downloading android-arm-release/darwin-x64 tools... 0.9s
Downloading android-arm64-profile/darwin-x64 tools... 0.9s
Downloading android-arm64-release/darwin-x64 tools... 0.9s
Downloading android-arm-dynamic-profile/darwin-x64 tools... 0.9s
Downloading android-arm-dynamic-release/darwin-x64 tools... 0.9s
Downloading android-arm64-dynamic-profile/darwin-x64 tools... 0.8s
Downloading android-arm64-dynamic-release/darwin-x64 tools... 0.8s
Downloading android-x86 tools... 2.9s
Downloading android-x64 tools... 2.8s
Downloading android-arm tools... 1.3s
Downloading android-arm-profile tools... 1.1s
Downloading android-arm-release tools... 1.0s
Downloading android-arm64 tools... 1.4s
Downloading android-arm64-profile tools... 1.2s
Downloading android-arm64-release tools... 1.0s
Downloading android-arm-dynamic-profile tools... 1.1s
Downloading android-arm-dynamic-release tools... 1.1s
Downloading android-arm64-dynamic-profile tools... 1.2s
Downloading android-arm64-dynamic-release tools... 1.0s
Downloading ios tools... 4.7s
Downloading ios-profile tools... 3.7s
Downloading ios-release tools... 2.9s
Downloading package sky_engine... 0.8s
Downloading common tools... 1.6s
Downloading darwin-x64 tools... 4.3s
Creating project flutter_module...
flutter_module/test/widget_test.dart (created)
flutter_module/flutter_module.iml (created)
flutter_module/.gitignore (created)
flutter_module/.metadata (created)
flutter_module/pubspec.yaml (created)
flutter_module/README.md (created)
flutter_module/lib/main.dart (created)
flutter_module/flutter_module_android.iml (created)
flutter_module/.idea/libraries/Flutter_for_Android.xml (created)
flutter_module/.idea/libraries/Dart_SDK.xml (created)
flutter_module/.idea/modules.xml (created)
flutter_module/.idea/workspace.xml (created)
Running "flutter packages get" in flutter_module... 4.8s
Wrote 12 files.
All done!
Your module code is in flutter_module/lib/main.dart.
看一下创建好的项目目录:
MacBook-Pro-3:project kuangzhongwen$ cd flutter_module/
MacBook-Pro-3:flutter_module kuangzhongwen$ ls -a
. .packages
.. README.md
.DS_Store flutter_module.iml
.android flutter_module_android.iml
.gitignore lib
.idea pubspec.lock
.ios pubspec.yaml
.metadata test
在 flutter 的模块项目中包含有一个隐藏的 .android
和 .ios
目录这个目录下是可运行的 Android 和 iOS 项目,我们的 flutter代码还是在 lib
下编写,注意在 .android
和 .ios
目录下都有一个 Flutter 目录,这个是我们 flutter 的库项目了。也就是Android 用来生成 aar,iOS 用来生产 framework 的库。如果我们用 flutter create xxx
生成的纯 flutter 项目是没有这个Flutter 目录的。
- 1.3 把该项目使用 git 管理起来,稍后我们要在 native 项目中以子模块的形式添加进去
MacBook-Pro-3:flutter_module kuangzhongwen$ git init
Initialized empty Git repository in /Users/kuangzhongwen/Desktop/project/flutter_module/.git/
初始化 git 仓库后我们先编辑一下项目下的 .gitignore
文件,当前这个文件是把项目下的 .ios
和 .android
忽略掉的。这个两个项目我们需要跟踪一下,大家可以去 github 上找一下 iOS 和 Android 的 gitignore 模版文件,然后添加到这个两个目录中,然后把顶层目录的文件作出如下修改,删除 .android 和 .ios
添加 .ios/Flutter/Generated.xcconfig。
我的 .gitignore 如下:
.DS_Store
.dart_tool/
.packages
.pub/
.idea/
.vagrant/
.sconsign.dblite
.svn/
*.swp
profile
DerivedData/
.generated/
*.pbxuser
*.mode1v3
*.mode2v3
*.perspectivev3
!default.pbxuser
!default.mode1v3
!default.mode2v3
!default.perspectivev3
xcuserdata
*.moved-aside
*.pyc
*sync/
Icon?
.tags*
build/
.ios/Flutter/Generated.xcconfig
.flutter-plugins
- 1.4 提交你的 flutter 模块项目到你的 git 服务器
echo "# FlutterModuleTest" >> README.md
git init
git add .
git commit -m "first commit"
git remote add origin https://github.com/kuangzhongwen/FlutterModuleTest.git
git push -u origin master
2. 给原生 Android 项目集成 Flutter
iOS 集成的方式可以上网搜一下,这边讲讲 Android 的集成方式。
- 2.1 在原生 Android 项目中添加子模块,将上面创建的 flutter module 项目拉取到原生安卓项目中
cd NativeFlutter/
git submodule add https://github.com/kuangzhongwen/FlutterModuleTest.git
git submodule update
- 2.2 在根目录的
settings.gradle
中添加如下配置
setBinding(new Binding([gradle: this]))
evaluate(new File(
'/Users/kuangzhongwen/Desktop/project/NativeFlutter/FlutterModuleTest/.android/include_flutter.groovy'
))
- 2.3 在原生项目的 app 目录下的 build.gradle 文件中添加 Flutter 库的依赖
dependencies {
implementation project(':flutter')
}
其中 flutter 工程为创建 Flutter module 过程自动生成的,注意就是 flutter。
- 2.4 在原生项目中新建搭载 flutter view 的 Activity
public class MyFlutterActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final FrameLayout root = new FrameLayout(this);
root.setLayoutParams(new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
setContentView(root);
root.setVisibility(View.INVISIBLE);
final FlutterView flutterView = Flutter.createView(this, getLifecycle(), "route1");
root.addView(flutterView);
/**
* Android 从原生跳到 Flutter 模块的黑屏问题,在网上看到很多说设置透明主题的但是没有用,
* 后来看到一种先隐藏显示,等待渲染好第一帧后才显示 flutter 页面的方法。
*/
final FlutterView.FirstFrameListener[] listeners = new FlutterView.FirstFrameListener[1];
listeners[0] = new FlutterView.FirstFrameListener() {
@Override
public void onFirstFrame() {
root.setVisibility(View.VISIBLE);
}
};
flutterView.addFirstFrameListener(listeners[0]);
}
}
其中 createView(),第二个参数是 Lifecycle 对象, 第三个参数为 route,这个参数 Flutter 端可以通过window.defaultRouteName() 获取,利用它 flutter 可知道要创建哪个 widget。同理 Flutter.createFragment(String route) 可生成 FlutterFragment,最后别忘了在清单文件中注册。
- 2.5 在原生代码中集成 flutter 跳转到 flutter 页面
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.go_to_flutter_btn).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this, MyFlutterActivity.class);
startActivity(intent);
}
});
}
}
- 2.6 编译运行
编译运行报错:
Invoke-customs are only supported starting with Android O (--min-api 26) Message{kind=ERROR
在 app.gradle 中加入:
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
同时注意,最小 sdk 版本要改成 16 或以上:
minSdkVersion 16
flutter 的 dart 代码位于:/FlutterModuleTest/lib/main.dart。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or press Run > Flutter Hot Reload in a Flutter IDE). Notice that the
// counter didn't reset back to zero; the application is not restarted.
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Invoke "debug painting" (press "p" in the console, choose the
// "Toggle Debug Paint" action from the Flutter Inspector in Android
// Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
// to see the wireframe for each widget.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
看看效果: