Flutter 即学即用系列博客——03 在旧有项目引入 Flutter

1,277 阅读13分钟

前言

其实如果打算在实际项目中引入 Flutter,完全将旧有项目改造成纯 Flutter 项目的可能性比较小,更多的是在旧有项目引入 Flutter。

因此本篇我们就说一说如何在旧有项目引入 Flutter。

官方 WIKI 有说明,但是里面坑还是不少的,变化也是存在的。

因此就让我们来看一看。

目录

1. 按照官网实现基本引入

Add Flutter to existing apps

上面为GitHub WIKI 的引入方式,通过 Module 的形式进行引入。

可以看出文档还是在不断更新的。

下面我们说下具体的步骤:

第一步:创建 Flutter Module

假设已经存在的 Android 项目路径为 /Users/nesger/Desktop/nesger_folder/project/studio/MyApp,那么我们在同级目录下面创建 Flutter Module。在终端执行如下命令:

cd /Users/nesger/Desktop/nesger_folder/project/studio/ 
flutter create -t module my_flutter

执行命令之后,就创建了一个带有 dart 代码的 Flutter Module,并且能够看到一个隐藏的文件夹 .android。

第二步:让主 APP 依赖 Flutter Module

这里,主 APP 指的就是 Android 项目 MyApp

在 MyApp 的 settings.gradle 添加下面代码:

setBinding(new Binding([gradle: this]))          
evaluate(new File(                                          
  settingsDir.parentFile,                                 
  'my_flutter/.android/include_flutter.groovy'   
))                                                                     

在需要使用 Flutter Module 的 MyApp 的对应 Module 添加依赖,比如本例子中就是到 MyApp 中的 app 的 build.gradle 添加

dependencies {
  implementation project(':flutter')
}

添加完之后有个报错如下:

Manifest merger failed : uses-sdk:minSdkVersion 15 cannot be smaller than version 16 declared in library [:flutter] /Users/nesger/Desktop/nesger_folder/project/studio/my_flutter/.android/Flutter/build/intermediates/merged_manifests/debug/processDebugManifest/merged/AndroidManifest.xml as the library might be using APIs not available in 15
	Suggestion: use a compatible library with a minSdk of at most 15,
		or increase this project's minSdk version to at least 16,
		or use tools:overrideLibrary="com.nesger.myflutter" to force usage (may lead to runtime failures)

从这里可以看到是由于我们 MyApp 的 uses-sdk:minSdkVersion 与 Flutter Module 的不一致。

控制台也给出了解决方法,我们这里简单的升下我们 MyApp 的 uses-sdk:minSdkVersion 即可。

改完编译就没问题了。

第三步:使用 Flutter Module 提供的 API 在主 APP 中创建 FlutterView

我们的主界面布局如下,就是有一个按钮而已。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <Button
        android:onClick="onClick"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="create flutter view"
        />

</RelativeLayout>

然后在代码里面对应位置添加如下代码:

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);

运行到手机上面,可以看到下面效果:

点击按钮之后,可以看到 Flutter 页面显示出来了

到这里我们基本就实现了在旧有项目引入 Flutter 了。

那么上面的代码有个地方,就是**"route1"**到底是什么呢?

顾名思义,你可以认为是一个路由。也就是用来区分不同 Flutter 页面的。

假设你的 Flutter 有多个页面,那么你如何确定要加载哪个页面呢?就可以通过这个来区分。

所以在 Flutter Module 的 main.dart 文件里面,对于存在多个页面的情况,我们可以写下面的模板代码:

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),
      );
  }
}

这段代码我们可以重点关注 switch 那一块代码。这里会根据不同的路由,返回不同的页面。

第四步:热重载和调试 dart 代码

首先定位到 Flutter Module 路径,这里为**/Users/nesger/Desktop/nesger_folder/project/studio/my_flutter**。
接着执行命令flutter attach,会看到控制台输出

Waiting for a connection from Flutter on SM G9350...

然后我们直接运行或者以 debug 模式运行项目。
接着点击按钮,触发 Flutter 代码,会看到控制台输出

Done. Syncing files to device SM G9350... 1.2s

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

这个跟我们之前讲到的热重载类似,这里就不重复了。

除了直接运行旧有项目来启动 Flutter 之外,其实更多时候我们编写 Flutter 是独立的,可以直接运行 Flutter 来调试和修改 dart 代码。

我一般倾向于直接执行 flutter run,而不是按照官网那样通过 flutter attach,然后以 debug 模型启动旧有项目。

等到 Flutter Module 都调试 OK 之后,再和旧有项目一起运行查看效果。

2. 修改配置允许 Flutter Module 在任意位置

大家可以看到,官网的例子的 Flutter Module 是在与 Android 原项目同层级的目录下面创建的。
这样其实对于我们开发不是很方便。

首先,我们需要在 Android Studio 分别打开两个项目,这样不方便修改和调试 dart 代码。
其次,一般在公司里面,项目都是用 git 之类的项目管理工具来管理的。如果按照官网的例子,其他开发者下载原项目的代码之后还需要额外下载 Flutter 代码仓库。

所以其实更多的情况,我们希望 Flutter Module 是在我们主项目下面当成主项目的代码来使用,这样不仅方便修改和调试,而且其他开发者也不需要进行额外处理。

简单回顾一下上面的引入步骤:

1.创建 Module
2.修改项目的 settings.gradle
3.添加 flutter module 依赖

其中重点需要关注的就是 2 了。因为 2 里面指定的一个文件是跟路径相关的。

我们在 MyApp 项目下面创建 sub 文件夹,移动之前的 module 到 sub 文件夹下面。

执行下面命令:(确保当前在 MyApp 项目下面)

mkdir sub  
cd sub/  
mv ../../my_flutter .  

执行完之后 module 的位置就变化了。你会发现代码里面 Flutter 相关代码和包都报错了。clean 一下,会有报错:

java.io.FileNotFoundException: /Users/nesger/Desktop/nesger_folder/project/studio/my_flutter/.android/include_flutter.groovy

提示文件找不到。

这是必然的,因为我们刚刚迁移了 flutter module 的位置。

所以说要允许 Flutter Module 配置在任意位置,重点就是第二步项目的 settings.gradle 的配置了。或者说 include_flutter.groovy 文件的位置是否指定正确。

我们看下配置信息:

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

new File(settingsDir.parentFile,'my_flutter/.android/include_flutter.groovy' ) 解读下这句话的意思就是指定 include_flutter.groovy 的所在位置。这里的意思是在 settings 文件所在目录(settingsDir)的父目录有个文件(settingsDir.parentFilemy_flutter/.android/include_flutter.groovy。看下下面的文件放置位置图就清楚了:

所以官网在跟项目同级创建 flutter module 是没问题的。但是我们现在改了,应该怎样设置呢?

上下图,然后大家考虑一下答案,再往下翻,相信聪明的你一定知道,改法有多种,下面提供一下几种方案。

Tips:注意相对路径的使用,重点是找到 include_flutter.groovy

解法一:(推荐)

include ':app'
setBinding(new Binding([gradle: this]))         
evaluate(new File(                                        
        settingsDir,                                  
        'sub/my_flutter/.android/include_flutter.groovy'    
))                                                                     

在 settings 所在目录有 sub/my_flutter/.android/include_flutter.groovy 文件

解法二:

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

在 settings 所在目录的父目录有 MyApp/sub/my_flutter/.android/include_flutter.groovy 文件

有了上面图文并茂的讲解加上一个实际的 Sample,相信不管 flutter module 放在哪里你到可以关联到了。

3. 引入自己项目报错处理方法

我们新建一个 Android 项目然后按照上述导入可以正常运行。

然而,理想很丰满,现实很骨感,本人在导入到实际工程项目时,一运行到 Flutter 相关代码,控制台就报出下面信息,并且 APP crash。

2019-02-15 09:35:00.355 4366-4366/? A/DEBUG: *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
2019-02-15 09:35:00.355 4366-4366/? A/DEBUG: Build fingerprint: 'samsung/hero2qltezc/hero2qltechn:8.0.0/R16NW/G9350ZCS3CRJ2:user/release-keys'
2019-02-15 09:35:00.355 4366-4366/? A/DEBUG: Revision: '15'
2019-02-15 09:35:00.355 4366-4366/? A/DEBUG: ABI: 'arm'
2019-02-15 09:35:00.355 4366-4366/? A/DEBUG: pid: 3072, tid: 3072, name: pkgname  >>> pkgname <<<
2019-02-15 09:35:00.355 4366-4366/? A/DEBUG: signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr --------
2019-02-15 09:35:00.360 4366-4366/? A/DEBUG: Abort message: '[FATAL:flutter/shell/common/shell.cc(212)] Check failed: vm. Must be able to initialize the VM.
    '
2019-02-15 09:35:00.360 4366-4366/? A/DEBUG:     r0 00000000  r1 00000c00  r2 00000006  r3 00000008
2019-02-15 09:35:00.360 4366-4366/? A/DEBUG:     r4 00000c00  r5 00000c00  r6 fff50940  r7 0000010c
2019-02-15 09:35:00.360 4366-4366/? A/DEBUG:     r8 00000000  r9 fff50d04  sl d74ec880  fp fff51048
2019-02-15 09:35:00.361 4366-4366/? A/DEBUG:     ip 00000000  sp fff50930  lr e9ebea17  pc e9eefb74  cpsr 200f0010
2019-02-15 09:35:00.365 4366-4366/? A/DEBUG: backtrace:
2019-02-15 09:35:00.365 4366-4366/? A/DEBUG:     #00 pc 0004bb74  /system/lib/libc.so (tgkill+12)
2019-02-15 09:35:00.365 4366-4366/? A/DEBUG:     #01 pc 0001aa13  /system/lib/libc.so (abort+54)
2019-02-15 09:35:00.365 4366-4366/? A/DEBUG:     #02 pc 0053ea03  /data/app/pkgname-nNNvK7M4bKRp1ys0OFeS7g==/lib/arm/libflutter.so (offset 0x4e5000)
2019-02-15 09:35:00.365 4366-4366/? A/DEBUG:     #03 pc 00536ba3  /data/app/pkgname-nNNvK7M4bKRp1ys0OFeS7g==/lib/arm/libflutter.so (offset 0x4e5000)
2019-02-15 09:35:00.365 4366-4366/? A/DEBUG:     #04 pc 0005d295  /data/app/pkgname-nNNvK7M4bKRp1ys0OFeS7g==/oat/arm/base.odex (offset 0x49000)

其中 pkgname 是实际项目包名,这里做了替换

此刻的心情见下图:

在经过了搜索引擎的搜索和 GitHub 上面 flutter 的相关 Issues 阅读,最终得出了解决方案。

通用解决步骤:

  1. 本项目执行清理命令。./gradlew clean
  2. 进入 flutter module 项目执行清理命令。flutter packages get;flutter clean
  3. 进入 flutter module 的 .android 项目执行清理命令和打包操作。./gradlew clean;./gradlew assemble
  4. 回到本项目执行打包命令。./gradlew assemble

通过实际例子来加深认识吧。还是以我们上面的 MyApp 为例进行说明。module 现在是在 MyApp 下面的 sub 目录下面。
那么我们直接在 terminal 执行下面命令即可:

./gradlew clean;cd sub/my_flutter/;flutter packages get;flutter clean;cd -;cd sub/my_flutter/.android/;./gradlew clean;./gradlew assemble;cd -;./gradlew assemble

分号分隔了每条命令,总结起来就是

清理项目;进入flutter module;更新包信息和清理;返回当前目录;进入flutter module .android 项目目录;清理打包;返回当前目录;打包

后续假设你 flutter module 没有更新过。那么以后修改本地项目之后,就直接执行**./gradlew assemble**。
切记不要执行 clean 或者 rebuild 。也不要点击 IDE 运行按钮。因为 IDE 运行按钮会默认先 clean。

当然上面的 assemble 命令学习 Android 的都懂,就是打出所有安装包。如果你只要 debug 包,可以改为 assembleDebug。
另外如果你要安装到设备,可以改为 installDebug。
这里就不展开了。

这里先留个悬念,打出的 debug 包可以用,但是 release 包依然会 crash。原因在后面混淆文章我们再讲。

4.推荐集成管理方式

我们知道,一般公司对于项目都有对应的管理工具。

这里假设项目是通过 GitLab 进行管理的。

那么我们要如何集成呢?

以上面为例子,假设 MyApp 项目下面有 sub 子目录,子目录下面创建了 my_flutter 模块。

因为 my_flutter 模块是跨平台使用的,除了 Android 端,iOS 端也要用。因此大概率会放到 GitLab 仓库上面。

所以如何来保证你本地的 my_flutter 是最新的,同时你做的修改能够同步到 MyApp GitLab 同时又同步到 my_flutter GitLab 呢?

这边推荐使用 git subtree 来管理。

涉及代码仓库公用的都推荐 git subtree 来管理。

如何使用呢?(以我们上面的例子来说明)

1)在主项目仓库新增子仓库。
git subtree add --prefix=sub/my_flutter 子仓库git地址 master --squash
(--squash参数表示不拉取历史信息,而只生成一条commit信息。)

上面的子仓库git地址指的是 my_flutter 所放的地址。

接下来执行git status可以看到有 commit 记录。

然后可以执行git push命令将新创建的子仓库推送到 MyApp 的代码仓库中。

2)拉取子仓库更新

使用git subtree pull命令。

比如这里 my_flutter 更新了,使用如下命令拉取:

git subtree pull --prefix=sub/my_flutter 子仓库git地址 master --squash

表示从 master 分支拉取更新。如果你想从 develop 或者其他分支拉取更新,则做对应修改即可。

3)推送更新到子仓库

使用git subtree push命令。

比如这里本地 my_flutter 修改了,使用如下命令推送:

git subtree push --prefix=sub/my_flutter 子仓库git地址 develop

表示将更新推送到 develop 分支。如果你想推送到其他分支,则将 develop 改为对应推送分支名即可。

4)简化 git subtree 命令

大家可以看到上面的命令中子仓库 git 地址比较固定而且每个命令都有用到。

并且相对比较长,比如 github.com/nesger/Flut… 这个。

因此,我们可以给这个起个 alias(别名)。

举个例子,假设上面的子仓库git地址github.com/nesger/Flut…

git remote add -f my_flutter https://github.com/nesger/FlutterNote.git

这样上面的原命令

git subtree add --prefix=sub/my_flutter https://github.com/nesger/FlutterNote.git  master --squash
git subtree pull --prefix=sub/my_flutter https://github.com/nesger/FlutterNote.git master --squash  
git subtree push --prefix=sub/my_flutter https://github.com/nesger/FlutterNote.git develop

可以对应修改为:

git subtree add --prefix=sub/my_flutter my_flutter master --squash
git subtree pull --prefix=sub/my_flutter my_flutter master --squash  
git subtree push --prefix=sub/my_flutter my_flutter develop

可以看到命令简化了很多。尤其这个命令使用比较频繁。可以提高效率。

温馨提示:

在使用git subtree pull命令进行子仓库更新之前,需要保证本地没有修改。

什么意思?

就是你在本地执行**git status .**时提示没有修改的文件。
这个时候你再去拉取才不会拉取失败。否则会有下面提示:

Working tree has modifications.  Cannot add.

所以一般 flutter module 有更新后,先推送到主项目仓库,再推送到子仓库。

如果是临时不重要修改,则先 revert 或者将修改文件保存在另外位置。

总之拉取子仓库更新的时候本地不要有修改的文件。

上述git subtree相关命令都是在主项目的目录下面执行的。

更多阅读:
Flutter 即学即用系列博客——01 环境搭建
Flutter 即学即用系列博客——02 一个纯 Flutter Demo 说明