前言

Flutter的UI和状态管理都学了,是时候搞一下混合开发。网上大部分的资料写的都很片面,达不到实战的效果。我觉得混合开发至少要达到以下几个效果

  • 原生跳转Flutter
  • Flutter跳转原生
  • 跳转的时候有数据的交流

本篇主要是以android为主,在现有的工程基础上接入Flutter,ios混合开发步骤大同小异,可以做为参考。

混合开发主要分为两大步骤

  1. 创建Flutter Module
  2. 接入Flutter Boost

闲鱼的Flutter-Boost可能是目前最好的混合开发框架,对于内存的把控以及跳转Flutter页面白屏都有很好的处理。

在这里多说一句,如果使用传统的方式从原生跳转到Flutter,白屏现象会非常严重,即便是在release模式下我感觉也不太能接受,毕竟还要兼顾低端手机,不能老是拿着骁龙855去测试呀!

1.创建Flutter Module

整个方案还是参考了官方建议的方式,闲鱼给出aar方案看上去确实很不错,而且对于原生的耦合度会低很多,但是上手成本较高,有兴趣的同学可以自己去了解。

1.1 创建module

1.1.1 在终端里将路径切换到项目根目录,然后使用命令行创建一个flutter module,创建命令如下

flutter create -t module my_flutter

其中“my_flutter”是module的名字,这个可以自己去定义

1.1.2 在原有项目的setting.gradle末尾中添加

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

1.1.3 在原生项目app的gradle下添加如下代码

implementation project(":flutter")

经过上面三步原生项目就可以使用Flutter的核心功能了!创建成功后的项目结构如下

android混合flutter flutter vue 混合_flutter

注:minSdk必须>=16,否则创建的时候会报错!

 

2.接入Flutter-Boost

只有接入了跳转功能,一个app的静脉才算是打通,不然就没啥luan用了。原生的跳转有很大的弊端,在Flutter页面和原生页面重叠时,每开启一个Flutter页面就创建了一个Flutter Engine,导致内存暴增,Flutter应用中全局变量在各独立页面不能共享,IOS出现内存泄漏。随着版本的迭代,Flutter-Boost把这些问题都解决了,所以是混合开发的不二之选。

项目地址:Flutter-Boost

2.1 如何接入

如果你的项目还在使用support包,在pubspec.yaml中添加如下依赖

flutter_boost:
    git:
        url: 'https://github.com/alibaba/flutter_boost.git'
        ref: '0.1.61'

如果你的项目已经切换到androidX,则使用以下方式

flutter_boost:
    git:
        url: 'https://github.com/alibaba/flutter_boost.git'
        ref: 'feature/flutter_1.9_androidx_upgrade'

注意:如果Flutter-Boost使用0.1.61版本,对应的Flutter版本不应该低于1.9.1-hotfix,否则会报错。如果你的Flutter分支没有在stable,强烈建议你切换到stable分支上。

在终端使用flutter doctor查看当前分支

android混合flutter flutter vue 混合_flutter-boost_02

如果没有在stable分支,在使用flutter channel查看所有的分支

android混合flutter flutter vue 混合_混合开发_03

使用命令flutter channel stable/beta/dev/master切换到对应的分支

2.2 Flutter集成FlutterBoost

2.2.1 创建两个Flutter页面用做跳转,在集成了FlutterBoost后我们所有的跳转动作都可以使用该框架

import 'package:flutter/material.dart';
import 'package:flutter_boost/flutter_boost.dart';
import 'package:my_flutter/constant.dart';

///第一个页面
class FirstRouteWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Page'),
      ),
      body: Container(
        alignment: Alignment.center,
        child: RaisedButton(
          child: Text('支付吧,皮卡丘'),
          onPressed: () {
            print("open pay page!");
            //利用FlutterBoost跳转到支付页面,并携带参数
            FlutterBoost.singleton.open(Routes.ROUTE_NATIVE_PAY,
                urlParams: {"need_money": '0.01'});
          },
        ),
      ),
    );
  }
}

///第二个页面
class SecondRouteWidget extends StatelessWidget {

  final Map params;

  SecondRouteWidget(this.params);

  @override
  Widget build(BuildContext context) {
    print(params);
    return Scaffold(
      appBar: AppBar(
        title: Text("支付完成页面-Flutter"),
      ),
      body: Center(
        child: RaisedButton(
          onPressed: () {
            //关闭当前页面并返回一个result
            BoostContainerSettings settings =
                BoostContainer.of(context).settings;
            FlutterBoost.singleton.close(settings.uniqueId,
                result: {"result": "data from second"});
          },
          child: Text('支付结果:${params['result']}'),
        ),
      ),
    );
  }
}

2.2.2 改造main.dart

import 'package:flutter/material.dart';
import 'package:flutter_boost/flutter_boost.dart';
import 'package:my_flutter/constant.dart';
import 'package:my_flutter/simple_page.dart';

void main() => runApp(MyApp());

class MyApp extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  void initState() {
    super.initState();
    //将需要进行跳转的页面事先注册到FlutterBoost
    //pageName是路由名称,params是参数
    FlutterBoost.singleton.registerPageBuilders({
      Routes.ROUTE_FIRST: (pageName, params, _) => FirstRouteWidget(),
      Routes.ROUTE_SECOND: (pageName, params, _) => SecondRouteWidget(params),
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      builder: FlutterBoost.init(postPush: _onRoutePushed),
      home: Container(),
    );
  }

  ///从原生跳转到Flutter的动作都会经过这里,有需要的话可以在这里做一些统一的操作
  void _onRoutePushed(
      String pageName, String uniqueId, Map params, Route route, Future _) {}
}

经过上面两步,Flutter这一侧的集成就算是完事了。如果你想使用FlutterBoost做跳转,切记将页面注册到FlutterBoost,不然跳转的时候会报错。下面我们再来看看Native侧要做那些操作。

2.3 Native集成FlutterBoost

在Flutter端集成FlutterBoost后项目结构会发生变化,最终效果如下

android混合flutter flutter vue 混合_android混合flutter_04

此时我们在app的build.gradle中添加如下依赖

implementation project(path: ':flutter_boost')

这样一来我们原生工程也能使用FlutterBoost了,在真正使用以前还需要配置一些东西。

2.3.1 PageRouter-跳转中心

这个类是用来处理原生和Flutter页面跳转逻辑的,具体的业务逻辑都包含在openPageByUrl里面,需要注意的是我的项目用的是androidX,如果你还在使用support包在类的使用上会有些许不同。

package com.zljtech.auction.flutter;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

import com.huodao.platformsdk.logic.core.route.auction.IAuctionRouteUri;
import com.huodao.platformsdk.util.launch.ActivityLaunchUtils;
import com.idlefish.flutterboost.containers.NewBoostFlutterActivity;

import java.util.HashMap;
import java.util.Map;

/**
 * @author sq
 * @date 2019/12/10
 * @describe
 */
public class PageRouter {

    public static final String NATIVE_PAY_PAGE_URL = "sample://nativePayPage";
    public static final String FLUTTER_PAGE_FIRST_URL = "first";
    public static final String FLUTTER_PAGE_SECOND_URL = "second";

    /**
     * 需要将flutter页面的url放入到该map,这样在跳转的时候才能找到对应的链接
     */
    public final static Map<String, String> PAGE_NAME = new HashMap<String, String>() {{
        put(FLUTTER_PAGE_FIRST_URL, FLUTTER_PAGE_FIRST_URL);
        put(FLUTTER_PAGE_SECOND_URL, FLUTTER_PAGE_SECOND_URL);
    }};



    public static boolean openPageByUrl(Context context, String url, Map params) {
        return openPageByUrl(context, url, params, 0);
    }

    /**
     *
     * @param context
     * @param url 跳转链接-对应具体的页面
     * @param params 携带的参数
     * @param requestCode 请求code,一般配合着startActivityForResult使用
     * @return
     */
    public static boolean openPageByUrl(Context context, String url, Map params, int requestCode) {

        String path = url.split("\\?")[0];

        Log.i("openPageByUrl", path);

        try {
            if (PAGE_NAME.containsKey(path)) {
                //所有Flutter页面的跳转都会走到这里,通过容器NewBoostFlutterActivity来装载
                Intent intent = NewBoostFlutterActivity.withNewEngine().url(PAGE_NAME.get(path)).params(params)
                        .backgroundMode(NewBoostFlutterActivity.BackgroundMode.opaque).build(context);
                if (context instanceof Activity) {
                    Activity activity = (Activity) context;
                    activity.startActivityForResult(intent, requestCode);
                } else {
                    context.startActivity(intent);
                }
                return true;
            } else if (url.startsWith(NATIVE_PAY_PAGE_URL)) {
                String needMoney = (String) params.get("need_money");
                //跳转到原生页面
                //通过Arouter进行跳转,也可以使用原生Intent
                ActivityLaunchUtils.getInstance()
                    .build(IAuctionRouteUri.ROUTE_AUCTION_ACTIVITY_PAY__ORDER_DEPOSIT)
                    .withString("need_money", needMoney)
                    .launch();
                return true;
            }

            return false;

        } catch (Throwable t) {
            return false;
        }
    }
}

2.3.2 在Application中配置FlutterBoost

这里主要是做Flutter引擎初始化工作,在Flutter跳转原生的回调方法中实现相应的跳转逻辑。

private void initFlutterBoost() {
        INativeRouter router = (context, url, urlParams, requestCode, exts) -> {
            String assembleUrl = Utils.assembleUrl(url, urlParams);
            PageRouter.openPageByUrl(context, assembleUrl, urlParams);
        };

        Platform platform = new NewFlutterBoost.ConfigBuilder(this, router)
                .isDebug(true)
                //当任意activity创建的时候就初始化flutter引擎,防止打开flutter页面时白屏
                .whenEngineStart(NewFlutterBoost.ConfigBuilder.ANY_ACTIVITY_CREATED)
                .renderMode(FlutterView.RenderMode.texture)
                .build();

        NewFlutterBoost.instance().init(platform);
    }

该方法在Application的onCreate调用即可!

2.3.2 AndroidManifest中配置

NewBoostFlutterActivity是Flutter View的承载页,所以这个页面必须在manifest中注册

<activity
            android:name="com.idlefish.flutterboost.containers.NewBoostFlutterActivity"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
            android:hardwareAccelerated="true"
            android:theme="@style/Theme.AppCompat"
            android:windowSoftInputMode="adjustResize">
            <!--            启动页面的过渡动画-->
            <meta-data
                android:name="io.flutter.embedding.android.SplashScreenDrawable"
                android:resource="@drawable/anim_loading_view" />

            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

记得在application标签中添加我们自定义的application,这样FlutterBoost才会初始化。

总结

通过上面的步骤我们就完成了FlutterBoost在Native和Flutter端的集成,具体的跳转方法如下:

1.flutter跳转到原生(携带参数)

传递的urlParams对应于PageRouter->openPageByUrl的params参数

FlutterBoost.singleton.open(Routes.ROUTE_NATIVE_PAY,
                urlParams: {"need_money": '0.01'});

2.原生跳转Flutter

Map<String, String> params = new HashMap<>();
params.put("result", "pay success");
PageRouter.openPageByUrl(this, PageRouter.FLUTTER_PAGE_SECOND_URL, params);

传递给SecondRouteWidget的参数可以在其构造方法中获取,具体请参照2.2.1

3.Flutter页面退出到原生页面携带返回值

一开始我想的是在close当前Flutter页面的时候,native侧用onActivityResult去接收返回值,毕竟native跳转到flutter的时候是使用startActivityForResult

FlutterBoost.singleton.close(settings.uniqueId,
                result: {"result": "data from second"});

这时我们再到native层debug看一下数据:

android混合flutter flutter vue 混合_实战_05

返回的数据结构有点奇怪,但毕竟是人家定义的,只能照着做了。

  • 首先我们通过data获取到一个Bundle
  • bundle中包含了一个map,key是“_flutter_result_”, value仍然是一个map
  • value里面的数据才是我们真正传递的键值对,这时候再通过我们自己定义的“result”去拿数据就可以了

4.Flutter页面返回Flutter页面携带参数

如果你想使用FlutterBoost进行Flutter页面间的跳转,传递参数什么的会简单很多,话不多说,上代码:

首先从FirstPage跳转到SecondPage

FlutterBoost.singleton.open(Routes.ROUTE_SECOND,
                urlParams: {'result': '我是第一个页面携带过来的参数'}).then((result) {
              print('接收第二个页面返回的数据=$result');
            });

urlParams是传递到下一个页面的数据,open方法会返回一个Future,自然而然地我们想到用then去接收返回的数据。

关闭SecondPage并返回数据给FirstPage

print('接收第一个页面传递过来的数据=:$params');
BoostContainerSettings settings =
        BoostContainer.of(context).settings;
FlutterBoost.singleton.close(settings.uniqueId, 
        result: {"result": "data from second"});

result是返回的数据,然后我们通过日志来验证一下操作是否正确

android混合flutter flutter vue 混合_混合开发_06

拿到的数据和我们的预期相差无几,无论是主动传递还是被动接收,数据结构都是一个map,我们通过这个map去解析自己想要的数据即可!

 

好了关于Flutter混合开发就说这么多,本篇侧重于混合开发期间的页面跳转以及数据传递。FlutterBoost还可以把Flutter页面解析成一个Fragment供原生使用,甚至是把原生控件转换为一个widget供Flutter使用,具体的姿势可以参照官网,这里就不再赘述了!

 

更新:

集成了Flutter-Boost后,页面跳转6起来了,数据传递也6了,但是我想着混合开发肯定会涉及到Flutter调用原生的方法,按理说使用MethodChannel即可,但是在实际使用中却报错了,几经折腾终于找到解决方案:

在初始化FlutterBoost的时会有一个lifecycleListener接口,我们在engineCreate回调中注册自己的Channel就可以了。

一定记得在onEngineCreated回调中注册,不然flutter engine还没有初始化就注册会报错,而且该方法只会回调一次

private void initFlutterBoost() {
        INativeRouter router = (context, url, urlParams, requestCode, exts) -> {
            String assembleUrl = Utils.assembleUrl(url, urlParams);
            PageRouter.openPageByUrl(context, assembleUrl, urlParams);
        };

        Platform platform = new NewFlutterBoost.ConfigBuilder(this, router)
                .isDebug(true)
                .whenEngineStart(NewFlutterBoost.ConfigBuilder.ANY_ACTIVITY_CREATED)
                .renderMode(FlutterView.RenderMode.texture)
                .lifecycleListener(new NewFlutterBoost.BoostLifecycleListener() {
                    @Override
                    public void onEngineCreated() {
                        Logger2.d(TAG, "onEngineCreated");
                        //注册本地MethodChannel
                        BinaryMessenger messenger = new BoostPluginRegistry(NewFlutterBoost.instance().engineProvider()).registrarFor(FlutterNativePlugin.CHANNEL).messenger();
                        MethodChannel methodChannel = new MethodChannel(messenger, FlutterNativePlugin.CHANNEL);
                        FlutterNativePlugin.registerWith(methodChannel);
                    }

                    @Override
                    public void onPluginsRegistered() {
                        Logger2.d(TAG, "onPluginsRegistered");
                    }

                    @Override
                    public void onEngineDestroy() {

                    }
                })
                .build();

        NewFlutterBoost.instance().init(platform);

    }

下面是FlutterNativePlugin的具体实现,说白了就是一个MethodChannel

public class FlutterNativePlugin implements MethodChannel.MethodCallHandler {
    private static final String TAG = "FlutterNativePlugin";
    public static final String CHANNEL = "com.zljtech.channel.method";

    private FlutterNativePlugin() {
    }

    public static void registerWith(MethodChannel channel) {
        FlutterNativePlugin instance = new FlutterNativePlugin();
        channel.setMethodCallHandler(instance);
    }

    @Override
    public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
        if ("isOk".equals(methodCall.method)) {
            boolean isOk = Math.random() > 0.5;
            Logger2.d(TAG, "isOk====" + isOk);
            result.success(isOk);
        } else {
            result.notImplemented();
        }
    }
}

然后就可以愉快的在Flutter中调用Native方法啦!