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

前言

其实若是打算在实际项目中引入 Flutter,彻底将旧有项目改形成纯 Flutter 项目的可能性比较小,更多的是在旧有项目引入 Flutter。java

所以本篇咱们就说一说如何在旧有项目引入 Flutter。android

官方 WIKI 有说明,可是里面坑仍是很多的,变化也是存在的。git

所以就让咱们来看一看。github

目录

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

Add Flutter to existing appsshell

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

能够看出文档仍是在不断更新的。app

下面咱们说下具体的步骤:ide

第一步:建立 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 说明