Flutter 学习4:集成到原有的项目中
这个是我学习Flutter的一个系列文章:
- Flutter 学习1:开发环境、开发工具、初始化一个项目
- Flutter 学习2:从main.dart文件说起
- Flutter 学习3:转场、导航
- Flutter 学习4:集成到原有的项目中
- Flutter 学习5:开发Dart包和插件包
- Flutter 学习6:绘制动画
- Flutter 学习7:Dart语言基础
- Flutter 学习8:BottomSheet
- Flutter 学习9:Positioned、Transform等控件使用以及手势控制
- Flutter 学习10:NestedScrollView、SliverAppBar、TabBar
从前面的学习中,我觉得如果用Flutter开发一个全新的App应该还是问题不大的。它的优势在于UI统一,可以用一套UI适用两端,节省开发成本。但是也有问题是毕竟是新的多端融合方案,还有很多大环境的不支持,比如第三方SDK如何集成等问题。 如果是已经有的项目呢,能够集成Flutter吗?答案是肯定的!官方有个wiki就是介绍这个的。
IOS
创建一个Flutter模块
首先要把Flutter模块建起来,按照下面的命令来创建模块:
$ cd some/path/
$ flutter create -t module ff_flutter
这里的some path就是你原来项目的父目录,敲完命令后的目录结构大概是这样的:
some/path/
MyOldApp/
MyOldApp/
AppDelegate.swift
ff_flutter/
lib/main.dart
.ios/
修改Podfile文件,添加Flutter模块到IOS项目中
接下来,在原来的项目的Podfile中添加两句话:
target 'MyOldApp' do
use_frameworks!
pod 'Moya', '~> 12.0'
....
flutter_application_path = 'some/path/ff_flutter/'
eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)
end
然后跟你以前在Podfile中添加完第三方库一样,需要在命令行中执行一下pod命令:
pod install
打开MyOldApp.xcworkspace
文件,在xcode中修改两个地方。
一个是Flutter不支持bitcode,需要把bitcode关闭。
另外一个是在Build Phases
中添加一个脚本,点击Build Phases
标签,点击左上角的➕号,选择New Run Script Phase
。
然后在新生成的Run Script
项中的脚本框内添加两行代码:
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed
全部配置完成后,将IOS项目进行Build(command+B)操作。如果一切顺利,没有异常,我们就可以添加代码了。
修改IOS代码
通过上面配置和构建模块,现在IOS项目中已经引入了flutter模块了。我们还需要在AppDeletegate中引入代码。
import UIKit
import Flutter
import FlutterPluginRegistrant
@UIApplicationMain
class AppDelegate: FlutterAppDelegate {
override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
GeneratedPluginRegistrant.register(with: self);
return super.application(application, didFinishLaunchingWithOptions: launchOptions);
}
}
原来的项目中如何打开这个Flutter写的功能页面呢,Flutter有个入口FlutterViewController
。
我在原来项目的一个Controller中添加一个按钮:
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Flutter", style: .plain, target: self, action: #selector(toFlutter))
...
@objc func toFlutter() {
let flutterViewController = FlutterViewController()
self.present(flutterViewController, animated: false, completion: nil)
}
点击这个Flutter按钮,就出现了我们很熟悉的那个Flutter Demo的画面:
这个时候我们如何回到原来的UIViewContoller呢?我们修改下这个加号按钮的点击事件:
//顶部引入services包
import 'package:flutter/services.dart';
//修改加号按钮的事件函数
void _incrementCounter() {
SystemNavigator.pop();
}
这时候问题来了,点击了没反应。没有我们预想的那样关闭了当前的FlutterViewController
。后来查了发现这个方式在Android是没有问题的,但是IOS有问题,问题在于打开FlutterViewController
的方式,如果是Navigation Controller 用:
self.navigationController?.pushViewController(flutterViewController, animated: false)
这种方式打开的FlutterViewController
,那用上面那种形式是没有问题的。
但是用:
self.present(flutterViewController, animated: false, completion: nil)
这种方式打开的就不行了。 后来我用了另外一种思路来解决这个问题。
Flutter调用Native端的代码
Flutter调用Native端的代码是Flutter官方提供的为了方便开发者能够获取Native端调用硬件信息等情况可以使用的。方式就是在Native端创建一个通道,然后在Flutter端的Dart代码执行一个方法,这个方法在Native端通过一个方法名字对应,执行一段Native的代码。
Flutter端dart代码
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
//创建一个通道,通道的name字符串要和Native端的一样
static const methodChannel = const MethodChannel('your.package/native_get');
void _incrementCounter() {
if(Platform.isIOS){
//执行Native端的一个方法,key是colseSelf
methodChannel.invokeMethod('closeSelf');
}else if(Platform.isAndroid){
SystemNavigator.pop();
}
}
...
}
Native端swift代码
@objc func toFlutter() {
let flutterViewController = FlutterViewController()
//这里的channelName和Flutter端的那个name一样
let channelName = "your.package/native_get"
let messagechannel = FlutterMethodChannel.init(name: channelName, binaryMessenger: flutterViewController)
messagechannel.setMethodCallHandler { (methodCall, result) in
if methodCall.method == "closeSelf" {
self.closeSelf()
}
}
self.present(flutterViewController, animated: false, completion: nil)
}
private func closeSelf() {
self.dismiss(animated: false, completion: nil)
}
现在就ok了,再点击那个加号按钮,就会关闭当前的页面了!😄
Android
创建一个Flutter模块
首先要把Flutter模块建起来,按照下面的命令来创建模块:
$ cd some/path/
$ flutter create -t module ff_flutter
这里的some path就是你原来项目的父目录,敲完命令后的目录结构大概是这样的:
some/path/
MyOldApp/
app/
build.gradle
...
settings.gradle
gradle.properties
...
ff_flutter/
lib/main.dart
.android/
修改Android项目的配置,引入Flutter模块
include ':app' // 这句是原来就在的,下面的几句是需要添加的
setBinding(new Binding([gradle: this]))
evaluate(new File(
settingsDir.parentFile,
'ff_flutter/.android/include_flutter.groovy'
))
这个配置添加后,就是把那个新建的Flutter模块引入到当前的Android项目中,再到app
模块中,也就是你Android项目的主模块中的build.gradle
文件中添加这个Flutter模块的依赖:
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
...
//添加flutter模块依赖
implementation project(':flutter')
}
修改Android代码
上面把flutter模块引入后就可以调用了,在Android代码中,Flutter提供了两种方式引入Flutter页面,一种是Flutter.createView
,一种是Flutter.createFragment
。
- Flutter.createView kotlin代码:
val view = Flutter.createView(this, lifecycle, "/")
setContentView(view)
- Flutter.createFragment kotlin代码:
setContentView(R.layout.activity_flutter)
val tx = supportFragmentManager.beginTransaction()
tx.replace(R.id.container, Flutter.createFragment("/"))
tx.commit()
这上面两种方式一种是生成了一个View,一种是生成了一个Fragment,具体用哪种就看自己怎么方便吧。还有就是上面两个create方法都传入了一个斜杠字符串,这个是Flutter里面的路由,就跟H5开发中经常用的路由一样的道理。
看看Flutter的代码:
void main() => runApp(MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
initialRoute: '/',
routes: {
'/': (context) => MyHomePage(title: 'Flutter Demo Home Page')
},
));
这里把原来demo中的home去掉了,换成了initialRoute
和routes
。initialRoute
定义打开Flutter App的时候第一个页面是哪个路由路径。routes
就是当前有哪些路由。所以前面Android代码中Flutter.createView
和Flutter.createFragment
创建出来的UI内容就是这个MyHomePage
的内容。
-> | ||
---|---|---|
热更新加载
前面也提到Flutter可以热更新加载,这种集成到原有的Native项目中的方式也可以热更新加载。
首先进入刚才创建的Flutter模块目录:
$ cd some/path/ff_flutter
$ flutter attach
Waiting for a connection from Flutter on KNT AL20...
然后在你的IDE中用debug模式启动App,然后打开这个Flutter的页面的时候你的命令行界面就会出现下面的字样:
Done.
Syncing files to device KNT AL20... 1.4s
🔥 To hot reload changes while running, press "r". To hot restart (and rebuild
state), press "R".
An Observatory debugger and profiler on KNT AL20 is available at:
http://127.0.0.1:60711/
For a more detailed help message, press "h". To detach, press "d"; to quit,
press "q".
然后你如果修改了Flutter端的UI界面,在命令行终端敲一个r
,它就会自动刷新页面。比如上面的页面为把测试这两个中文字删除掉。
Initializing hot reload...
Reloaded 1 of 419 libraries in 1,048ms.
The End !
PS: 后来遇到的问题 我的Flutter版本:
$ flutter --version
Flutter 1.1.8 • channel beta • https://github.com/flutter/flutter.git
Framework • revision 985ccb6d14 (6 weeks ago) • 2019-01-08 13:45:55 -0800
Engine • revision 7112b72cc2
Tools • Dart 2.1.1 (build 2.1.1-dev.0.1 ec86471ccc)
写上面的文章的时候是用一个全新的demo做的没有发现啥问题,集成进去马上就成功了。但是我在自己真实项目集成的时候Android端遇到了问题,打开Flutter的页面的时候直接就崩溃了,错误如下:
[ERROR:flutter/runtime/dart_vm.cc(259)] VM snapshot must be valid.
网上找了找发现好多人遇到这个问题,在github上有很多类似的issue,解决办法就在这个issue上面找到的 ,https://github.com/flutter/flutter/issues/19818
加两个内容到你项目的assets目录中:
- 第一个flutter_assets目录,需要到你的flutter module下的
.android
目录下执行./gradlew assembleDebug
命令,完成后会生成一个flutter-debug.aar里面就包含了这个flutter_assets,整个目录copy过来。 - 第二个flutter_shared目录,你自己新建这个目录然后里面的
dicudtl.dat
文件在你的flutter sdk目录中搜索一下就会找到。
这两个目录添加完成后,再跑程序就能打开flutter页面了!😄