Flutter、Android混合開發實踐

降維程式設計發表於2020-02-16

一、前言

Flutter現在如日中天,作為一隻iOS程式猿終於下定決心去深入瞭解這麼一種強大的跨平臺框架在各個平臺上的使用方式,也藉此機會瞭解Android開發。

本著低侵入的原則,將Flutter編譯成aarAndroid工程依賴的方式無疑是最優解。下面會介紹Flutter、Android混合開發,並將Flutter編譯成aar的過程,以及記錄本人實踐過程中碰到的問題。

二、Android工程新增Flutter工程

功能開發期間將FlutterAndroid兩個功能同時匯入到AndroidStudio,每次編譯Android功能即可除錯混合工程。待功能開發完成後將Flutter編譯成aar檔案,匯入Android工程。大致步驟如下:

  • 步驟一:新建Android工程
  • 步驟二:新建Flutter Module
  • 步驟三:修改工程配置檔案,將Flutter工程引入Android工程
  • 步驟四:編寫測試程式碼,編譯兩個工程檢視結果
在開始新建工程前先新建一個總檔案(這裡命名為Android_Flutter_MixBuilder),在總資料夾下新建兩個子資料夾androidmy_flutter分別用於儲存AndroidFlutter工程。


步驟一:新建Android工程

先在android目錄下新建Android project ,選擇新建Android Studio project -> 選擇Empty Activityapi版本選擇4.1(本次實踐的環境為4.1),finish。Flutter、Android混合開發實踐

Flutter、Android混合開發實踐Flutter、Android混合開發實踐


步驟二:新建Flutter Module

Flutter Module新建有兩種方式,其一是通過AndroidStudio新建,其二是通過命令列新建。

  • 方法一:命令列
開啟終端切換到之前新建的my_flutter目錄下,執行下面命令:

flutter create -t module my_flutter複製程式碼

my_fluttermodule的名字,執行完命令等待即可。

  • 方法二:使用Android Studio新建
開啟Android Studio選擇新建Flutter project -> 選擇新建Flutter Module,finish。Flutter、Android混合開發實踐Flutter、Android混合開發實踐


步驟三:修改工程配置檔案,將Flutter工程引入Android工程

找到android目錄下的build.gradle檔案,將預設庫地址修改為國內阿里雲的maven庫地址,防止不科學上網引起的更新龜速問題。

buildscript {  
  repositories {   
     maven { url 'https://maven.aliyun.com/repository/google' } 
     maven { url 'https://maven.aliyun.com/repository/jcenter' }  
     maven { url 'http://maven.aliyun.com/nexus/content/groups/public' } 
  }  
  dependencies {  
      classpath 'com.android.tools.build:gradle:3.5.3'               
      // NOTE: Do not place your application dependencies here; they belong    
      // in the individual module build.gradle files 
   }
}
allprojects { 
   repositories {  
      maven { url 'https://maven.aliyun.com/repository/google' }  
      maven { url 'https://maven.aliyun.com/repository/jcenter' }  
      maven { url 'http://maven.aliyun.com/nexus/content/groups/public' }  
      }
}複製程式碼

找到android/app目錄下的build.gradle檔案,宣告以下源相容性。

android { 
    cmpileOptions { 
     sourceCompatibility 1.8   
     targetCompatibility 1.8  
    }}  複製程式碼

在根目錄(即android)目錄下的setting.gradle檔案中加入如下程式碼:

include ':app'
rootProject.name='myAndroid'
// 加入下面配置
setBinding(new Binding([gradle: this]))
evaluate(new File( 
         settingsDir.parentFile,  
         'my_flutter/.android/include_flutter.groovy'
)) 複製程式碼

上面程式碼中的“my_flutter”為我新建的Flutter Module名稱,Sync一下專案。

Flutter、Android混合開發實踐

Sync成功後,我們可以看到專案中多了一個my_flutterd的子專案

Flutter、Android混合開發實踐

Flutter Module新增成功後,我們需要在android/app目錄下的build.gradle檔案中新增該module的依賴。

implementation project(':flutter')複製程式碼

Flutter、Android混合開發實踐

完成以上步驟,那麼恭喜您已經成功將FlutterModule新增到了Android工程中。接下來我們就可以寫一些簡單的程式碼,進行AndroidFlutter直接的互動了。

步驟四:編寫測試程式碼,編譯兩個工程檢視結果

為了解決大部分場景中的使用情況,我們主要編寫的程式碼為AndroidFlutter之間的頁面跳轉,以及跳轉時的傳值。在Android工程中,新建Android原生頁面FirstNativeActivitySecondNativeActivity,以及承載Flutter頁面的Android套殼原生頁面FirstFlutterActivity。下面介紹5AndroidFlutter互動的場景:

  • Android頁面開啟Flutter頁面並傳值
  • Flutter頁面開啟Android頁面並傳值
  • Android頁面退回Flutter頁面並傳值
  • Flutter頁面退回Android頁面並傳值
  • 解決手機系統虛擬返回按鍵破壞正常頁面的棧順序問題

在開始上面的場景前,先在Android 套殼原生頁面FirstFlutterActivity引入Flutter頁面。並在Flutter頁面定義如下內容:

  1. 新增一個textView用來顯示其他頁面傳過來的內容
  2. 新增一個button用來開啟下個原生頁面
  3. 新增一個button用來返回到上個原生頁面

思路新建FlutterView -> xml中拖一個FrameLayout ->FlutterView新增到FrameLayout -> 建立FlutterEngine,並初始化引擎指向一個Flutter頁面的路由-> FlutterView使用FlutterEngine載入內容上面的介紹和WebView的載入方式如出一轍

關鍵名詞介紹:

FlutterView:位於io.flutter.embedding.android包中,在Flutter1.12版本里,他負責建立Flutter檢視。而且FlutterView繼承於FrameLayout,所以上面思路中我們可以把他當作一個基礎的View進行操作。

FlutterEngineFlutter負責在Android端執行Dart程式碼的引擎,將Flutter編寫的UI程式碼渲染到FlutterView中。

建立FlutterView並新增到檢視中程式碼:

FlutterView flutterView = new FlutterView(this);
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams( 
       ViewGroup.LayoutParams.MATCH_PARENT, 
       ViewGroup.LayoutParams.MATCH_PARENT);
FrameLayout flContainer = findViewById(R.id.layout001);
flContainer.addView(flutterView, lp);複製程式碼

建立FlutterEngine,並渲染路由名稱為route1Flutter頁面,路由可以攜帶一些資料(字串:message)

//建立引擎
flutterEngine = new FlutterEngine(this);
String str = "route1?{\"message\":\"" + message + "\"}";
flutterEngine.getNavigationChannel().setInitialRoute(str);
flutterEngine.getDartExecutor().executeDartEntrypoint( 
       DartExecutor.DartEntrypoint.createDefault()
);
//渲染Flutter頁面
flutterView.attachToFlutterEngine(flutterEngine);複製程式碼

FlutterDart程式碼如下:

解析路由獲取本次攜帶的資料

void main() => runApp(_widgetForRoute(window.defaultRouteName));
Widget _widgetForRoute(String url) {
  // route名稱
  String route =  url.indexOf('?') == -1 ? url : url.substring(0, url.indexOf('?'));
// 引數Json字串
  String paramsJson =  url.indexOf('?') == -1 ? '{}' : url.substring(url.indexOf('?') + 1);
  Map<String, dynamic> mapJson = json.decode(paramsJson);  String message = mapJson["message"];
// 解析引數
  switch (route) {
    case 'route1':
      return MaterialApp(
        home: Scaffold(
          appBar: AppBar(
            title: Text('Flutter頁面'),
          ),
          body: Center(child: Text('頁面名字: $route',style: TextStyle(color: Colors.red), textDirection: TextDirection.ltr),),
        ),
      );
    default:
      return Center(
        child: Text('Unknown route: $route',style: TextStyle(color: Colors.red), textDirection: TextDirection.ltr),
      );
  }}複製程式碼

完成以上程式碼就可以在Android殼子中檢視Flutter頁面,下面介紹殼子頁面中AndroidFlutter是如何互動的:

思路:我們熟悉的傳統的h5頁面和原生互動時,通過中間通訊工具物件,定義好方法或者屬性進行通訊。同理,FlutterAndroid原生互動也有專門的通訊物件(Platform Channel),它有三種型別:

  • MethodChannel:用於最常見的方法傳遞,幫助Flutter和原生平臺互相呼叫方法,也是本次我們著重介紹的。
  • BasicMessageChannel:用於資料資訊的傳遞。
  • EventChannel用於事件監聽傳遞等場景。

在上面的介紹中我們可以在一個Android頁面中顯示Flutter內容,那接下來我們只需要通過MethodChannelFlutter傳送命令,以及接收訊息的回撥。那麼我們就可以在AndroidFlutter頁面呈現一些對方傳過來的資料。開整!


Android部分程式碼如下(下面的程式碼依舊在FirstFlutterActivity中編寫)

我們在開始使用MethodChannel時,先對其進行唯一性定義。注意:這裡我們定義兩個MethodChannel,一個用於對Flutter的訊息傳送,一個用於Flutter的回撥訊息接收。

//Flutter向Native發訊息
private static final String CHANNEL_NATIVE = "com.example.flutter/native";
//Native向Flutter發訊息
private static final String CHANNEL_FLUTTER = "com.example.flutter/flutter";複製程式碼

使用定義好的名字,初始化MethodChannel注意:MethodChannel初始化方法裡有兩個引數。第一個引數BinaryMessenger messenger,我們可以理解為MethodChannelFlutter頁面的繫結項,通過FlutterEnginegetDartExecutor()方法我們可以得到構造MethodChannel的第一個引數。第二個引數需要傳入我們之前定義好的唯一命名。


Android接收Flutter發來的訊息

接收Flutter訊息得先初始化一個MethodChannel,且用之前定義好的名字CHANNEL_NATIVE通過下面程式碼我們可以看到MethodChannel回撥引數有:MethodCall callMethodChannel.Result resultcall可以給我們提供本次Flutter所傳送的方法名(call.method)。result提供了一些方法可以在我們處理完邏輯後,告訴Flutter頁面我們的處理結果,例如result.success()result.notImplemented()

MethodChannel nativeChannel = new MethodChannel(flutterEngine.getDartExecutor(), CHANNEL_NATIVE);
        nativeChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
            @Override
            public void onMethodCall(MethodCall call, MethodChannel.Result result) {
                switch (call.method){
                    case "backFirstNative":
                        result.success("收到來自Flutter的訊息");
                        break;
                    default :
                        result.notImplemented();
                        break;
                }
            }
        });複製程式碼


Android給Flutter發訊息

Flutter發訊息同樣得先初始化一個MethodChannel,且用之前定義好的名字CHANNEL_FLUTTER。使用MethodChannel的方法invokeMethod就可以將本次的訊息傳送到Flutter中去啦!

Map<String, Object> result = new HashMap<>();
result.put("message", @"我是Android的發出去的資訊,我要到Flutter中去");
MethodChannel flutterChannel = new MethodChannel(flutterEngine.getDartExecutor(), CHANNEL_FLUTTER);
// 呼叫Flutter端定義的方法onActivityResult
flutterChannel.invokeMethod("onActivityResult", result);複製程式碼


上面介紹了互動時Android端的程式碼,下面介紹Flutter端的程式碼。如下:

首先我們在原來的main.dart檔案中做一下擴充套件。定義一個Widget用來顯示Android傳過來的資料,並建立一個按鈕給Android發訊息。同Android端,在main.dart檔案中我們也定義了同名MethodChannel。注意:我們在WidgetinitState()方法裡就應該寫上MethodChannel的監聽程式碼。我們可以在FlutterMethodChannel的回撥方法中通過獲取call.method、call.method.arguments來知道,Android這次想要呼叫我們什麼方法、以及帶來了什麼引數。

class ContentWidget extends StatefulWidget{
  ContentWidget({Key key, this.route,this.message}) : super(key: key);
  String route,message;
  _ContentWidgetState createState() => new _ContentWidgetState();
}
class _ContentWidgetState extends State<ContentWidget>{
  static const nativeChannel = const MethodChannel('com.example.flutter/native');
  static const flutterChannel = const MethodChannel('com.example.flutter/flutter');
  void onDataChange(val) {
    setState(() {
      widget.message = val;
    });
  }
  @override
  void initState(){
    super.initState();
    Future<dynamic> handler(MethodCall call) async{
      switch (call.method){
        case 'onActivityResult':
          onDataChange(call.arguments['message']);
          print('1234'+call.arguments['message']);
          break;
      }
    }
    flutterChannel.setMethodCallHandler(handler);
  }
  Widget build(BuildContext context) {
    // TODO: implement build
    return Center(
      child: Stack(
        children: <Widget>[
          Positioned(
            top: 100,
            left: 0,
            right: 0,
            height: 100,
            child: Text(widget.message,textAlign: TextAlign.center,),
          ),
          Positioned(
            top: 300,
            left: 100,
            right: 100,
            height: 100,
            child: RaisedButton(
                child: Text('開啟上一個原生頁面'),
                onPressed: (){
                  returnLastNativePage(nativeChannel);
                }
            ),
          ),
          Positioned(
            top: 430,
            left: 100,
            right: 100,
            height: 100,
            child: RaisedButton(
                child: Text('開啟下一個原生頁面'),
                onPressed: (){
                  openNextNativePage(nativeChannel);
                }
            ),
          )
        ],
      ),
    );
  }}複製程式碼

上面的程式碼缺少了方法:returnLastNativePageopenNextNativePage。如下:

大家肯定還記得我們之前在Android頁面接收Flutter的回撥後,還能呼叫result.success()來告訴Flutter頁面我們的處理結果。沒錯,我們在下面兩個方法中,非同步獲取這些回撥的資訊並列印。

Future<Null> returnLastNativePage(MethodChannel channel) async{
  Map<String, dynamic> para = {'message':'嗨,本文案來自Flutter頁面,回到第一個原生頁面將看到我'};
  final String result = await channel.invokeMethod('backFirstNative',para);
  print('這是在flutter中列印的'+ result);
}複製程式碼

Future<Null> openNextNativePage(MethodChannel channel) async{
  Map<String, dynamic> para = {'message':'嗨,本文案來自Flutter頁面,開啟第二個原生頁面將看到我'};
  final String result = await channel.invokeMethod('openSecondNative',para);
  print('這是在flutter中列印的'+ result);
}複製程式碼


至此,AndroidFlutter可以互通有無了。如果你在編譯的時候發現main.dartMethodChannel報錯,那麼你一定是沒有正確的引入標頭檔案比如:import 'package:flutter/services.dart'之前介紹的那些跳轉場景到最後都變成了Android之間的跳轉,以及殼子頁面對Flutter的更新。下面科普一下Android Activity之間跳轉的傳值。有Android基礎的朋友可以直接下跳到最後檢視:解決手機系統虛擬返回按鈕破壞正常頁面的棧順序


下面簡單介紹Android頁面之間的跳轉。注意:請使用startActivityForResult方法開啟Activity,這樣Activity關閉時onActivityResult方法才能接收到回撥。程式碼如下:

FirstFlutterActivity程式碼:

開啟下一個Activity。

// 跳轉原生頁面                        
Intent jumpToNativeIntent = new Intent(FirstFlutterActivity.this, SecondNativeActivity.class);
jumpToNativeIntent.putExtra("message", (String) call.argument("message"));
startActivityForResult(jumpToNativeIntent, Activity.RESULT_FIRST_USER);
result.success("成功開啟第二個原生頁面");複製程式碼
接收Activity的回撥,並將資料傳遞給Flutter進行顯示。

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    switch (requestCode) {
        case 1:
            if (data != null) {
                // NativePageActivity返回的資料
                String message = data.getStringExtra("message");
                Map<String, Object> result = new HashMap<>();
                result.put("message", message);
                // 建立MethodChannel,這裡的flutterView即Flutter.createView所返回的View
                MethodChannel flutterChannel = new MethodChannel(flutterEngine.getDartExecutor(), CHANNEL_FLUTTER);
                // 呼叫Flutter端定義的方法
                flutterChannel.invokeMethod("onActivityResult", result);
            }
            break;
        default:
            break;
    }}
複製程式碼

SecondNativeActivity程式碼:

xml裡拖了一個idtextView2textView用來顯示資訊,一個idbutton001button用來返回上個頁面。

public class SecondNativeActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second_native);
        Intent intent = getIntent();
        String content = intent.getStringExtra("message");
        TextView textView = findViewById(R.id.textView2);
        textView.setText(content);
        Button btnOpen = findViewById(R.id.button001);
        btnOpen.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent();
                intent.putExtra("message","嗨,本文案來自第二個原生頁面,將在Flutter頁面看到我");
                setResult(RESULT_OK,intent);
                finish();
            }
        });
    }}複製程式碼


解決手機系統虛擬返回按鍵破壞正常頁面的棧順序問題

場景:原生頁面A -> 開啟原生殼子頁面(顯示內容:Flutter頁面B) -> 開啟原生殼子頁面(顯示內容:Flutter頁面C)-> 點選虛擬返回按鈕

現象:原生殼子頁面(顯示內容:Flutter頁面C)直接回到了原生頁面A

上面的現象和一個不經處理的WebView頁面棧管理如出一轍,那不是我們像要的。我們想達到點選虛擬返回按鍵後,原生殼子頁面(顯示內容:Flutter頁面C)回到 原生殼子頁面(顯示內容:Flutter頁面B)。

思路:如果將點選虛擬返回按鈕的後續操作交給Flutter來處理,那麼就完美。

我們知道點選虛擬返回按鈕的後將會呼叫方法onBackPressed(),此時我們在該方法中給Flutter發訊息(即呼叫"backAction"方法)。

@Override
public void onBackPressed() {
    MethodChannel flutterChannel = new MethodChannel(flutterEngine.getDartExecutor(), CHANNEL_FLUTTER);
    flutterChannel.invokeMethod("backAction", null);
}複製程式碼

Flutter中的處理:

和之前互動的處理類似,我們增加了一個case 'backAction'這裡要用到Flutter裡的一個方法canPop()我們知道如果是棧底了還呼叫pop()方法會使程式crashcanPop()就很好的解決了這個問題。當在棧底呼叫canPop()時,會返回給我們一個布林值告訴我們是否可以繼續回退。當我們發現canPop()的結果是false時,說明當前Flutter頁面已經是最後一個頁面,此時我們應該通知Android殼子頁面退回到上一個原生頁面。程式碼如下:

void initState(){
  super.initState();
  Future<dynamic> handler(MethodCall call) async{
    switch (call.method){
      case 'onActivityResult':
        onDataChange(call.arguments['message']);
        print('1234'+call.arguments['message']);
        break;
      case 'backAction':
        if (Navigator.canPop(context)) {
          Navigator.of(context).pop();
        } else {
          nativeChannel.invokeMethod('backAction');
        }
        break;
    }
  }
  flutterChannel.setMethodCallHandler(handler);
}複製程式碼

Android殼子頁面接收到訊息,返回上一個原生頁面

MethodChannel nativeChannel = new MethodChannel(flutterEngine.getDartExecutor(), CHANNEL_NATIVE);
        nativeChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
            @Override
            public void onMethodCall(MethodCall call, MethodChannel.Result result) {
                switch (call.method){
                    case "backFirstNative":
                        result.success("收到來自Flutter的訊息");
                        break;
                    case "backAction":
                        finish();
                        result.success("成功通過虛擬按鍵返回第一個原生頁面");
                        break;
                    default :
                        result.notImplemented();
                        break;
                }
            }
        });複製程式碼


三、打包Flutter工程,以aar的形式供Android呼叫

Flutter1.12版本中,打包Flutter已經變得十分簡單,在完成Flutter程式碼編寫後,執行命令列:

flutter build aar複製程式碼

或者點選Build -> Flutter -> Build AAR即可

Flutter、Android混合開發實踐

當你使用命令列打包時,打包完成後控制檯會提示你如何使用aar

Flutter、Android混合開發實踐

如上圖所以,就是讓你在想要引用aarAndroid工程下,找到android/app/build.gradle檔案。並把上圖中的234點提到的程式碼加入其中。新增完程式碼後Sync一下,執行工程檢視正確結果。

上面的嘗試都是基於Flutter1.12版本實現,若您的Flutter版本 < 1.12,請先更新Flutter版本。


-----------------------------------完整程式碼地址---------------------------------------------

https://github.com/JJwow/Android_Flutter_MixBuilder.git複製程式碼


相關文章