Flutter 学习4:集成到原有的项目中

Categories: flutter

这个是我学习Flutter的一个系列文章:

  1. Flutter 学习1:开发环境、开发工具、初始化一个项目
  2. Flutter 学习2:从main.dart文件说起
  3. Flutter 学习3:转场、导航
  4. Flutter 学习4:集成到原有的项目中
  5. Flutter 学习5:开发Dart包和插件包
  6. Flutter 学习6:绘制动画
  7. Flutter 学习7:Dart语言基础
  8. Flutter 学习8:BottomSheet
  9. Flutter 学习9:Positioned、Transform等控件使用以及手势控制
  10. 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

  1. Flutter.createView kotlin代码:
 val view = Flutter.createView(this, lifecycle, "/")
 setContentView(view)
  1. 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去掉了,换成了initialRouteroutesinitialRoute定义打开Flutter App的时候第一个页面是哪个路由路径。routes就是当前有哪些路由。所以前面Android代码中Flutter.createViewFlutter.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目录中:

  1. 第一个flutter_assets目录,需要到你的flutter module下的.android目录下执行./gradlew assembleDebug命令,完成后会生成一个flutter-debug.aar里面就包含了这个flutter_assets,整个目录copy过来。
  2. 第二个flutter_shared目录,你自己新建这个目录然后里面的 dicudtl.dat文件在你的flutter sdk目录中搜索一下就会找到。

这两个目录添加完成后,再跑程序就能打开flutter页面了!😄