Flutter、Android混合开发实践

9,640 阅读12分钟

一、前言

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 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工程

找到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一下项目。


Sync成功后,我们可以看到项目中多了一个my_flutterd的子项目


Flutter Module添加成功后,我们需要在android/app目录下的build.gradle文件中添加该module的依赖。

implementation project(':flutter')


完成以上步骤,那么恭喜您已经成功将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即可


当你使用命令行打包时,打包完成后控制台会提示你如何使用aar


如上图所以,就是让你在想要引用aarAndroid工程下,找到android/app/build.gradle文件。并把上图中的234点提到的代码加入其中。添加完代码后Sync一下,运行工程查看正确结果。

上面的尝试都是基于Flutter1.12版本实现,若您的Flutter版本 < 1.12,请先更新Flutter版本。


-----------------------------------完整代码地址---------------------------------------------

https://github.com/JJwow/Android_Flutter_MixBuilder.git