Flutter混合開發

SLPA發表於2020-12-12

混合開發簡介

使用Flutter從零開始開發App是一件輕鬆愜意的事情,但對於一些成熟的產品來說,完全摒棄原有App的歷史沉澱,全面轉向Flutter是不現實的。因此使用Flutter去統一Android、iOS技術棧,把它作為已有原生App的擴充套件能力,通過有序推進來提升移動終端的開發效率。 目前,想要在已有的原生App裡嵌入一些Flutter頁面主要有兩種方案。一種是將原生工程作為Flutter工程的子工程,由Flutter進行統一管理,這種模式稱為統一管理模式。另一種是將Flutter工程作為原生工程的子模組,維持原有的原生工程管理方式不變,這種模式被稱為三端分離模式。

在這裡插入圖片描述
在Flutter框架出現早期,由於官方提供的混編方式以及資料有限,國內較早使用Flutter進行混合開發的團隊大多使用的是統一管理模式。但是,隨著業務迭代的深入,統一管理模式的弊端也隨之顯露,不僅三端(Android、iOS和Flutter)程式碼耦合嚴重,相關工具鏈耗時也隨之大幅增長,最終導致開發效率降低。所以,後續使用Flutter進行混合開發的團隊大多使用三端程式碼分離的模式來進行依賴治理,最終實現Flutter工程的輕量級接入。 除了可以輕量級接入外,三端程式碼分離模式還可以把Flutter模組作為原生工程的子模組,從而快速地接入Flutter模組,降低原生工程的改造成本。在完成對Flutter模組的接入後,Flutter工程可以使用Android Studio進行開發,無需再開啟原生工程就可以對Dart程式碼和原生程式碼進行開發除錯。 使用三端分離模式進行Flutter混合開發的關鍵是抽離Flutter工程,將不同平臺的構建產物依照標準元件化的形式進行管理,即Android使用aar、iOS使用pod。也就是說,Flutter的混編方案其實就是將Flutter模組打包成aar或者pod庫,然後在原生工程像引用其他第三方原生元件庫那樣引入Flutter模組即可。

Flutter模組

預設情況下,新建立的Flutter工程會包含Flutter目錄和原生工程的目錄。在這種情況下,原生工程會依賴Flutter工程的庫和資源,並且無法脫離Flutter工程獨立構建和執行。 在混合開發中,原生工程對Flutter的依賴主要分為兩部分。一個是Flutter的庫和引擎,主要包含Flutter的Framework 庫和引擎庫;另一個是Flutter模組工程,即Flutter混合開發中的Flutter功能模組,主要包括Flutter工程lib目錄下的Dart程式碼實現。 對於原生工程來說,整合Flutter只需要在同級目錄建立一個Flutter模組,然後構建iOS和Android各自的Flutter依賴庫即可。接下來,我們只需要在原生專案的同級目錄下,執行Flutter提供的構建模組命令建立Flutter模組即可,如下所示。

flutter create -t module flutter_library    
複製程式碼

其中,flutter_library為Flutter模組名。執行上面的命令後,會在原生工程的同級目錄下生成一個flutter_library模組工程。Flutter模組也是Flutter工程,使用Android Studio開啟它,其目錄如下圖所示。

在這裡插入圖片描述
可以看到,和普通的Flutter工程相比,Flutter模組工程也內嵌了Android工程和iOS工程,只不過預設情況下,Android工程和iOS工程是隱藏的。因此,對於Flutter模組工程來說,也可以像普通工程一樣使用 Android Studio進行開發和除錯。 同時,相比普通的Flutter工程,Flutter模組工程的Android工程目錄下多了一個Flutter目錄,此目錄下的build.gradle配置就是我們構建aar時的打包配置。同樣,在Flutter模組工程的iOS工程目錄下也會找到一個Flutter目錄,這也是Flutter模組工程既能像Flutter普通工程一樣使用Android Studio進行開發除錯,又能打包構建aar或pod的原因。

Android整合Flutter

在原生Android工程中整合Flutter,原生工程對Flutter的依賴主要包括兩部分,分別是Flutter庫和引擎,以及Flutter工程構建產物。

  • Flutter庫和引擎:包含icudtl.dat、libFlutter.so以及一些class檔案,最終這些檔案都會被封裝到Flutter.jar中。
  • Flutter工程產物:包括應用程式資料段 isolate_snapshot_data、應用程式指令段 isolate_snapshot_instr、虛擬機器資料段vm_snapshot_data、虛擬機器指令段vm_snapshot_instr以及資原始檔flutter_assets。

和原生Android工程整合其他外掛庫的方式一樣,在原生Android工程中引入Flutter模組需要先在settings.gradle中新增如下程式碼。

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

複製程式碼

其中,flutter_library為我們建立的Flutter模組。然後,在原生Android工程的app目錄的build.gradle檔案中新增如下依賴。

dependencies {
	implementation project(":flutter")
}
複製程式碼

然後編譯並執行原生Android工程,如果沒有任何錯誤則說明整合Flutter模組成功。需要說明的是,由於Flutter支援的最低版本為16,所以需要將Android專案的minSdkVersion修改為16。 如果出現“程式包android.support.annotation不存在”的錯誤,需要使用如下的命令來建立Flutter模組,因為最新版本的Android預設使用androidx來管理包。

flutter create --androidx -t module flutter_library
複製程式碼

對於Android原生工程,如果還沒有升級到androidx,可以在原生Android工程上右鍵,然後依次選擇【Refactor】→【Migrate to Androidx】將Android工程升級到androidx包管理。 在原生Android工程中成功新增Flutter模組依賴後,開啟原生Android工程,並在應用的入口MainActivity檔案中新增如下程式碼。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        View flutterView = Flutter.createView(this, getLifecycle(), "route1");
        FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        addContentView(flutterView, layoutParams);
    }
}
複製程式碼

通過Flutter提供的createView()方法,可以將Flutter頁面構建成Android能夠識別的檢視,然後將這個檢視使用Android提供的addContentView()方法新增到父視窗即可。重新執行原生Android工程,最終效果如下圖所示。

在這裡插入圖片描述
如果原生Android的MainActivity載入的是一個FrameLayout,那麼載入只需要將Flutter頁面構建成一個Fragment即可,如下所示。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        FragmentTransaction ft= getSupportFragmentManager().beginTransaction();
        ft.replace(R.id.fragment_container, Flutter.createFragment("Hello Flutter"));
        ft.commit();
    }
}

複製程式碼

除了使用Flutter模組方式整合外,還可以將Flutter模組打包成aar,然後再新增依賴。在flutter_library根目錄下執行aar打包構建命令即可抽取Flutter依賴,如下所示。

flutter build apk --debug
複製程式碼

此命令的作用是將Flutter庫和引擎以及工程產物編譯成一個aar包,上面命令編譯的aar包是debug版本,如果需要構建release版本,只需要把命令中的debug換成release即可。 打包構建的flutter-debug.aar位於.android/Flutter/build/outputs/aar/目錄下,可以把它拷貝到原生Android工程的app/libs目錄下,然後在原生Android工程的app目錄的打包配置build.gradle中新增對它的依賴,如下所示。

dependencies {
  implementation(name: 'flutter-debug', ext: 'aar')   
}
複製程式碼

然後重新編譯一下專案,如果沒有任何錯誤提示則說明Flutter模組被成功整合到Android原生工程中。

iOS整合Flutter

原生iOS工程對Flutter的依賴包含Flutter庫和引擎,以及Flutter工程編譯產物。其中,Flutter 庫和引擎指的是Flutter.framework等,Flutter工程編譯產物指的是 App.framework等。 在原生iOS工程中整合Flutter需要先配置好CocoaPods,CocoaPods是iOS的類庫管理工具,用來管理第三方開源庫。在原生iOS工程中執行pod init命令建立一個Podfile檔案,然後在Podfile檔案中新增Flutter模組依賴,如下所示。

flutter_application_path = '../flutter_ library/
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

target 'iOSDemo' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!
  install_all_flutter_pods(flutter_application_path)

  # Pods for iOSDemo
  … //省略其他指令碼
end '

複製程式碼

然後,關閉原生iOS工程,並在原生iOS工程的根目錄執行pod install命令安裝所需的依賴包。安裝完成後,使用Xcode開啟iOSDemo.xcworkspace原生工程。 預設情況下,Flutter是不支援Bitcode的,Bitcode是一種iOS編譯程式的中間程式碼,在原生iOS工程中整合Flutter需要禁用Bitcode。在Xcode中依次選擇【TAGETS】→【Build Setttings】→【Build Options】→【Enable Bitcode】來禁用Bitcode,如下圖所示。

在這裡插入圖片描述
如果使用的是Flutter早期的版本,還需要新增build phase來支援構建Dart程式碼。依次選擇【TAGGETS】→【Build Settings】→【Enable Phases】,然後點選左上角的加號新建一個“New Run Script Phase”,新增如下指令碼程式碼。

"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed

複製程式碼

不過,最新版本的Flutter已經不需要再新增指令碼了。重新執行原生iOS工程,如果沒有任何錯誤則說明iOS成功整合Flutter模組。 除了使用Flutter模組方式外,還可以將Flutter模組打包成可以依賴的動態庫,然後再使用CocoaPods新增動態庫。首先,在flutter_library根目錄下執行打包構建命令生成framework動態庫,如下所示。

flutter build ios --debug
複製程式碼

上面命令是將Flutter工程編譯成Flutter.framework和App.framework動態庫。如果要生成release版本,只需要把命令中的debug換成release即可。 然後,在原生iOS工程的根目錄下建立一個名為FlutterEngine的目錄,並把生成的兩個framework動態庫檔案拷貝進去。不過,iOS生成模組化產物要比Android多一個步驟,因為需要把Flutter工程編譯生成的庫手動封裝成一個pod。首先,在flutter_ library該目錄下建立FlutterEngine.podspec,然後新增如下指令碼程式碼。

Pod::Spec.new do |s|
  s.name             = 'FlutterEngine'
  s.version          = '0.1.0'
  s.summary          = 'FlutterEngine'
  s.description      = <<-DESC
TODO: Add long description of the pod here.
                       DESC
  s.homepage         = 'https://github.com/xx/FlutterEngine'
  s.license          = { :type => 'MIT', :file => 'LICENSE' }
  s.author           = { 'xzh' => '1044817967@qq.com' }
  s.source       = { :git => "", :tag => "#{s.version}" }
  s.ios.deployment_target = '9.0'
  s.ios.vendored_frameworks = 'App.framework', 'Flutter.framework'
end

複製程式碼

然後,執行pod lib lint命令即可拉取Flutter模組所需的元件。接下來,在原生iOS工程的Podfile檔案新增生成的庫即可。

target 'iOSDemo' do
    pod 'FlutterEngine', :path => './'
end

複製程式碼

重新執行pod install命令安裝依賴庫,原生iOS工程整合Flutter模組就完成了。接下來,使用Xcode開啟ViewController.m檔案,然後新增如下程式碼。

#import "ViewController.h"
#import <Flutter/Flutter.h>
#import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h>

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    UIButton *button = [[UIButton alloc]init];
    [button setTitle:@"載入Flutter模組" forState:UIControlStateNormal];
    button.backgroundColor=[UIColor redColor];
    button.frame = CGRectMake(50, 50, 200, 100);
    [button setTitleColor:[UIColor redColor] forState:UIControlStateHighlighted];
    [button addTarget:self action:@selector(buttonPrint) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
}

- (void)buttonPrint{
    FlutterViewController * flutterVC = [[FlutterViewController alloc]init];
    [flutterVC setInitialRoute:@"defaultRoute"];
    [self presentViewController:flutterVC animated:true completion:nil];
}

@end
複製程式碼

在上面的程式碼中,我們在原生iOS中建立了一個按鈕,點選按鈕時就會跳轉到Flutter頁面,最終效果如下圖所示。

在這裡插入圖片描述
預設情況下,Flutter為提供了兩種呼叫方式,分別是FlutterViewController和FlutterEngine。對於FlutterViewController來說,開啟ViewController.m檔案,在裡面新增一個載入flutter頁面的方法並且新增一個按鈕看來呼叫。

Flutter模組除錯

眾所周知,Flutter的優勢之一就是在開發過程中使用熱過載功能來實現快速除錯。預設情況下,在原生工程中整合Flutter模組後熱過載功能是失效的,需要重新執行原生工程才能看到效果。如此一來,Flutter開發的熱過載優勢就失去了,並且開發效率也隨之降低。 那麼,能不能在混合專案中開啟Flutter的熱過載呢?答案是可以的,只需要經過如下步驟即可開啟熱過載功能。首先,關閉原生應用,此處所說的關閉是指關閉應用的程式,而不是簡單的退出應用。在Flutter模組的根目錄中輸入flutter attach命令,然後再次開啟原生應用,就會看到連線成功的提示,如下圖所示。

在這裡插入圖片描述
如果同時連線了多臺裝置,可以使用flutter attach -d 命令來指定連線的裝置。接下來,只需要按r鍵即可執行熱過載,按R鍵即可執行熱重啟,按d鍵即可斷開連線。 在Flutter工程中,我們可以直接點選debug按鈕來進行程式碼除錯,但在混合專案中,直接點選debug按鈕是不起作用的。此時,可以使用Android Studio提供的flutter attach按鈕來建立與flutter模組的連線,進行實現對flutter模組的程式碼除錯,如圖下圖所示。

在這裡插入圖片描述
上面只是完成了在原生工程中引入Flutter模組,具體開發時還會遇到與Flutter模組的通訊問題、路由管理問題,以及打包等。

相關文章