1. 程式人生 > >Flutter 即學即用系列博客——03 在舊有項目引入 Flutter

Flutter 即學即用系列博客——03 在舊有項目引入 Flutter

其他 click pps 裏的 集成 學習 項目執行 到手 lead

技術分享圖片

前言

其實如果打算在實際項目中引入 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 地址比較固定而且每個命令都有用到。

並且相對比較長,比如 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.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 說明

技術分享圖片

Flutter 即學即用系列博客——03 在舊有項目引入 Flutter