Flutter 即學即用系列部落格——03 在舊有專案引入 Flutter

前言
其實如果打算在實際專案中引入 Flutter,完全將舊有專案改造成純 Flutter 專案的可能性比較小,更多的是在舊有專案引入 Flutter。
因此本篇我們就說一說如何在舊有專案引入 Flutter。
官方 WIKI 有說明,但是裡面坑還是不少的,變化也是存在的。
因此就讓我們來看一看。
目錄

1. 按照官網實現基本引入
上面為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.parentFile ) my_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 00000000r1 00000c00r2 00000006r3 00000008 2019-02-15 09:35:00.360 4366-4366/? A/DEBUG:r4 00000c00r5 00000c00r6 fff50940r7 0000010c 2019-02-15 09:35:00.360 4366-4366/? A/DEBUG:r8 00000000r9 fff50d04sl d74ec880fp fff51048 2019-02-15 09:35:00.361 4366-4366/? A/DEBUG:ip 00000000sp fff50930lr e9ebea17pc e9eefb74cpsr 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 閱讀,最終得出瞭解決方案。
通用解決步驟:
- 本專案執行清理命令。 ./gradlew clean
- 進入 flutter module 專案執行清理命令。 flutter packages get;flutter clean
- 進入 flutter module 的 .android 專案執行清理命令和打包操作。 ./gradlew clean;./gradlew assemble
- 回到本專案執行打包命令。 ./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 地址比較固定而且每個命令都有用到。
並且相對比較長,比如 https://github.com/nesger/FlutterNote.git 這個。
因此,我們可以給這個起個 alias(別名)。
舉個例子,假設上面的 子倉庫git地址 為 https://github.com/nesger/FlutterNote.git ,那麼我們可以執行如下操作:
git remote add -f my_flutter https://github.com/nesger/FlutterNote.git
這樣上面的原命令
git subtree add --prefix=sub/my_flutter https://github.com/nesger/FlutterNote.gitmaster --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 相關命令都是在 主專案的目錄 下面執行的。
