Android工程內嵌Flutter,跨平臺的漸進式解決方案

NightFarmer發表於2018-08-31

其實2017年的時候就已經接觸Flutter了,但也只是寫了個HelloWorld,一方面是Flutter在那時候還只是preview版本,另一方面ReactNative在那時候非常火熱,忙於用ReactNative重構專案,錯過了入坑Flutter的第一梯隊。
在谷歌的2018IO大會上Flutter再一次成為了跨平臺方案的焦點,而ReactNative也在隨著Airbnb的棄用熱度逐漸冷卻,其實在寫下這篇文章的時候我已經再次入坑了不短的一段時間,Flutter的各種特性也基本上都接觸到了,demo專案也寫了一些,但致使我迫不及待的寫下這篇文章的直接原因是Flutter的這個能力:
Flutter能夠無感知的嵌入到Android工程中,不管是從開發者角度還是使用者角度,你甚至可以只從一個view開始來讓Flutter參與到你的專案中去,接著替換或者開發某一個頁面甚至功能,然後你就會對它愛不釋手,讓你會有用它重構專案和開發新專案的衝動。

  • 使用者:毫秒級的載入速度,無論是view還是頁面,基本上和原生無異。
  • 開發:只作為一個module引入工程,程式碼入侵極小,Android工程和Flutter工程互不相干。
    Android工程內嵌Flutter

注意:當前日期是2018-07-29,flutter的beta版本還沒有加入這個新功能,使用命令flutter channel [分支]切換到dev或master分支才能使用,如果你閱讀本篇文章離這個時間點是很久之後可以忽略這段。

建立一個Android工程模擬你的現有工程

為了讓Android工程和Flutter工程互不干擾,這裡不再以Android工程為工程的跟目錄,而是讓Android工程和平級的Flutter工程的公共目錄作為根目錄。 最終的目錄結構應該是下面這樣的

你的專案根目錄(隨便什麼你喜歡的地方)
  ├── 原生安卓工程(FlutterInAndroid)
  └── Flutter工程 (my_flutter)
複製程式碼

所以首先在你的專案根目錄下用AS建立一個新的Android原生專案,可以勾選上kotlin支援,這樣更舒服。 建立完成後你會得到一個這樣的結構

你的專案根目錄(隨便什麼你喜歡的地方)
  └── FlutterInAndroid
複製程式碼

FlutterInAndroid目錄內是一個完整的Android工程

module模式建立Flutter工程

接下來使用Flutter命令來建立module工程,在你的專案根目錄下執行:

flutter create -t module my_flutter
複製程式碼

建立完成後你會得到一個這樣的結構

你的專案根目錄(隨便什麼你喜歡的地方)
  ├── FlutterInAndroid
  └── my_flutter
複製程式碼

my_flutter是一個Flutter的module工程,用來供Android專案引入

在Android工程中引入依賴

在FlutterInAndroid這個Android工程的setting.gradle檔案中追加flutter工程的引入
你的專案跟目錄/FlutterInAndroid/setting.gradle

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

複製程式碼

在app的build.gradle檔案中加入工程依賴
你的專案跟目錄/FlutterInAndroid/app/build.gradle

...
dependencies {
    ...
    // 加入下面配置
    implementation project(':flutter')
}
複製程式碼

使用AS開啟FlutterInAndroid工程,重新構建專案,即可成功的將Flutter加入Android工程。

在Android工程中建立Flutter的View

Flutter提供了兩種方式讓Android工程來引用元件,一種是View,一種是Fragment,這裡選用View來進行講解,Fragment同理。 這裡我們用兩種方式來引入FLutter,本質是還是是作為一個view引入佈局還是將FlutterView作為Activity的根View。

以單個view引入佈局

val flutterView = Flutter.createView(this,lifecycle,"route1")
複製程式碼

通過上面很簡單的一個方法,我們就能通過Flutter建立出一個view,這個方法提供三個引數,第一個是Activity,第二個引數是一個Lifecycle物件,我們之間取Activity的lifecycle即可,第三個引數是告訴Flutter我們要建立一個什麼樣的view,這個字串引數可以在Flutter工程中獲取得到。
建立出這個FlutterView之後就可以按常規的操作來將它加入到任何你想要的佈局中去了。

以根view作為Activity

建立一個空的Activity,用Flutter建立一個View作為頁面的根View:

class FlutterActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_flutter)
        val flutterView = Flutter.createView(this@FlutterActivity,lifecycle,"route1")
        val layout = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
        addContentView(flutterView, layout)
    }
}
複製程式碼

這裡我們並沒有使用setContentView而是是用了addContentView這個方法,原因是這樣的:
雖然FLutter的載入速度非常快,但是這個過程依然存在,在建立FLutterView之前我們先給ContentView設定了一個R.layout.activity_flutter佈局,這個佈局可以作為FlutterView載入完成之前展示給使用者的介面,當然大部分情況下使用者根本感知不到這個介面Flutter已經載入完成了,但我們仍需要它,因為debug模式下造成Flutter的載入速度並不是非常快,這個介面可以給開發人員看,還有就是如果沒有這個介面的話在Activity的載入過程會出現一個黑色的閃屏,而這個情況對使用者來說並不友好。

在Flutter工程中根據不同的route建立不同的元件

用AndroidStudio在你的專案跟目錄/my_flutter開啟Flutter工程,這時候AndroidStudio外掛會識別到Flutter工程並以Flutter工程進行載入。
忽略掉.android和.ios資料夾之後你會發現,這個FLutter工程和完整的Flutter工程並沒有任何不同,你依然能夠以完整Flutter工程的流程來進行Flutter開發並啟動除錯,這是一個非常人性化的設計。
上面我們在原生Android工程中以View的形式呼叫了Flutter,而Flutter本質上是隻有一個入口的,也就是main.dart檔案中的main函式:

void main() => runApp(new MyApp());
複製程式碼

我們的目的是根據原生工程的呼叫讓Flutter生成不同的元件作為View來供原生工程使用,那麼我們就可以從這個main函式來入手。
通過文件我們可以通過window的全域性變數中獲取到當前的routeName,這個值正是上面通過原生工程傳給Flutter的標識,有了這個標識就可以簡單的做判斷來進行不同的元件建立了:

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),
      );
  }
}
複製程式碼

讓Flutter模組支援熱載入

首先在Flutter目錄下啟動監聽服務,在你的專案根目錄/my_flutter下執行

flutter attach
複製程式碼

執行後,監聽服務會等待並監聽debug應用中flutter的狀態
然後在開啟FlutterInAndroid專案的AS中以正常方式除錯執行,在真機或模擬器中執行app後並不會立即出發flutter的監聽服務,當flutter的view或Fragment啟用時才會觸發。
當flutter的監聽服務和app建立連線後,終端會出現如下輸出:

$ flutter attach -d W8
Waiting for a connection from Flutter on PLK UL00...
Done.
Syncing files to device PLK UL00...                          8.7s

?  To hot reload changes while running, press "r". To hot restart (and rebuild state), press "R".
An Observatory debugger and profiler on PLK UL00 is available at: http://127.0.0.1:54218/
For a more detailed help message, press "h". To quit, press "q".

複製程式碼

這時我們修改flutter工程中的dart程式碼檔案,儲存後在終端中點選r鍵即可進行熱載入,R鍵進行熱重啟。

簽名打包

引入flutter工程後,對Android原生工程的構建基本上沒有影響,打包按常規操作即可。

Flutter建立的module工程中的Android工程與純Flutter工程的中Android工程的比較

區別 Flutter的module工程中的Android工程 純Flutter工程中的Android工程
資料夾名稱 .android android
包含的module app和Flutter app
說明1 app只提供了入口Activity,Flutter包含了外掛擴充套件及原生工程呼叫的介面 app包含入口Activity及外掛擴充套件
說明2 app供Flutter自身開發除錯,Flutter作為module供Android原生呼叫 app作為Android工程執行及打包

為了方便描述我們稱前者為module工程,後者為完整工程。

由此可見,雖然module工程中提供了名為Flutter的module供原生工程呼叫,但仍然保留了app工程,這樣非常大程度的方便了flutter工程師來單獨開發flutter專案,無需依賴任何原生的呼叫,自身即可啟動除錯。


參考
官方wiki

相關文章
騰訊NOW直播團隊方案
閒魚團隊方案
美團技術團隊方案


更多幹貨移步我的個人部落格 www.nightfarmer.top/

相關文章