美文网首页
Flutter混合开发实践

Flutter混合开发实践

作者: 刺客的幻影 | 来源:发表于2020-03-31 15:58 被阅读0次

一、跨平台技术的出现背景

1. 原生开发

Android基于Java或者Kotlin, iOS基于Objective-C或Swift,直接调用各自平台的SDK开发的应用程序;

  • 优点:能快速访问各种硬件功能(GPS、摄像头)速度快、性能高、可以实现复杂动画及绘制,整体用户体验好
  • 缺点: 开发成本高,一个应用要维护两套代码,一致性差

为了加快产品的开发周期,提高开发效率,保持多端的一致性,逐渐出现了多种跨平台技术,下面选取一些常用技术作为对比:

2. H5+原生混合开发

H5技术在各类app中使用的频率非常高,通过原生的网页加载控件WebView (Android)或WKWebView(iOS)来加载H5页面,而WebView实质上就是一个浏览器内核,其JavaScript依然运行在一个权限受限的沙箱中,所以对于大多数系统能力都没有访问权限,如无法访问文件系统、不能使用蓝牙等。所以我们通常要使用JsBridge来进行两端的通信,虽然web技术栈社区及资源丰富,但是性能不够好,对于复杂用户界面或动画,容易出现卡顿和掉帧。

3. React Native

React Native (简称RN)是Facebook于2015年4月开源的跨平台移动应用开发框架,是Facebook早先开源的JS框架 React 在原生移动应用平台的衍生产物,目前支持iOS和Android两个平台。RN使用Javascript语言,类似于HTML的JSX,以及CSS来开发移动应用。

4. Weex

Weex是阿里巴巴于2016年发布的跨平台移动端开发框架,思想及原理和React Native类似,最大的不同是语法层面,Weex支持Vue语法和Rax语法,Rax 的 DSL(Domain Specific Language) 语法是基于 React JSX 语法而创造。与 React 不同,在 Rax 中 JSX 是必选的,它不支持通过其它方式创建组件,所以学习 JSX 是使用 Rax 的必要基础。而React Native只支持JSX语法。

二 、Flutter的优势

Flutter和以上几种跨平台技术最大的不同是其自行实现一套渲染框架,可通过调用skia等方式完成自渲染,而不依赖于原生控件,也不用依赖于webview,使用的语言也从JavaScript变成了Dart。

1. 自绘引擎Skia

Flutter使用Skia作为其2D渲染引擎,Skia是Google的一个2D图形处理函数库,包含字型、坐标转换,以及点阵图都有高效能且简洁的表现,Skia是跨平台的,并提供了非常友好的API,目前Google Chrome浏览器和Android均采用Skia作为其绘图引擎。

2. Dart语言

Flutter使用Dart而不是JavaScript作为开发语言,主要有如下考虑:

  • 基于JIT的快速开发周期:Flutter在开发阶段采用,采用JIT模式,这样就避免了每次改动都要进行编译,极大的节省了开发时间;
  • 基于AOT的发布包: Flutter在发布时可以通过AOT生成高效的ARM代码以保证应用性能。而JavaScript则不具有这个能力。
3. 总结:
技术类型 UI渲染方式 性能 开发效率 动态化 框架代表
H5 WebView 渲染 一般 支持 Cordova、Ionic
JavaScript+原生渲染 支持 RN、Weex
Flutter 不支持 Flutter

三、Flutter框架结构

image.png
1. Flutter Framework
  • 底下两层(Foundation和Animation、Painting、Gestures)在Google的一些视频中被合并为一个dart UI层,对应的是Flutter中的dart:ui包,它是Flutter引擎暴露的底层UI库,提供动画、手势及绘制能力。
  • Rendering层,这一层是一个抽象的布局层,它依赖于dart UI层,Rendering层会构建一个UI树,当UI树有变化时,会计算出有变化的部分,然后更新UI树,最终将UI树绘制到屏幕上,这个过程类似于React中的虚拟DOM。Rendering层可以说是Flutter UI框架最核心的部分,它除了确定每个UI元素的位置、大小之外还要进行坐标变换、绘制(调用底层dart:ui)。
  • Widgets层是Flutter提供的的一套基础组件库,在基础组件库之上,Flutter还提供了 Material 和Cupertino两种视觉风格的组件库。而我们Flutter开发的大多数场景,只是和这两层打交道。
2. Flutter Engine
  • 这是一个纯 C++实现的 SDK,其中包括了 Skia引擎、Dart运行时、文字排版引擎等。在代码调用 dart:ui库时,调用最终会走到Engine层,然后实现真正的绘制逻辑。

四、Flutter 基础

五、 混合环境配置

在实际开发中,对于这种新型技术,为了降低引入的风险,一般都会逐步地迁移和体验,这样就涉及到混合项目的搭建,native和flutter两端的通信交互问题,下面以主项目MyApp为例,记录一下混合接入步骤:
开发环境:

[✓] Flutter (Channel unknown, v1.12.13+hotfix.6, on Mac OS X 10.15.3 19D76,
    locale zh-Hans-CN)
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
[✓] Android Studio (version 3.5)
1. 国内镜像配置(强烈推荐,可以极大提高分支切换和升级速度。以下两者任选其一,有时候一个不行可以切换另外一个)

镜像1:

export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn

镜像2:

export FLUTTER_STORAGE_BASE_URL=https://mirrors.sjtug.sjtu.edu.cn
export PUB_HOSTED_URL=https://dart-pub.mirrors.sjtug.sjtu.edu.cn

2. 在MyApp/app/build.gradle 添加so库相关过滤
android {
  //...
  defaultConfig {
    ndk {
      // Filter for architectures supported by Flutter.
      abiFilters 'armeabi-v7a', 'arm64-v8a'
    }
  }
}
3. 进入到MyApp的父级文件夹下,创建my_flutter module
cd some/path/
flutter create -t module  my_flutter

4. 在MyApp/app/build.gradle 文件中增加java8支持
android {
  //...
  compileOptions {
    sourceCompatibility 1.8
    targetCompatibility 1.8
  }
}
5. 在MyApp/settings.gradle中,添加以下依赖
include ':app'                                     // assumed existing content
setBinding(new Binding([gradle: this]))                                 // new
evaluate(new File(                                                      // new
  settingsDir.parentFile,                                               // new
  'my_flutter/.android/include_flutter.groovy'                          // new
))                                                                      // new
6. 在MyApp/app/build.gradle中,依赖flutter 模块
  implementation project(':flutter')

7. 在AndroidManifest.xml中注册FlutterActivity
<activity
  android:name="io.flutter.embedding.android.FlutterActivity"
  android:theme="@style/LaunchTheme"
  android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
  android:hardwareAccelerated="true"
  android:windowSoftInputMode="adjustResize"
  />

8. 任意页面添加跳转代码,以在TestActivity中为例
myButton.setOnClickListener(new OnClickListener() {
  @Override
  public void onClick(View v) {
    startActivity(
      FlutterActivity.createDefaultIntent(TestActivity.this)
    );
  }
});

六、 混合栈管理,FlutterBoost的使用

image.png
为了实现高效、便捷的栈管理,推荐使用闲鱼开源的FlutterBoost,下面以Android support包和Flutter Version v1.12.13为例,记录使用过程(实际接入的时候,androidx或者support在不同的flutter version下会出现一些不兼容的情况,增加了试错成本)。
1. 在my_flutter/pubspec.yaml目录下添加flutter端依赖
flutter_boost:
    git:
      url: 'https://github.com/alibaba/flutter_boost.git'
      ref: 'task/task_v1.12.13_support_hotfixes'
2. 在Android端 Application的onCreate中添加初始化代码
public class MyApp extends Application{
     @Override
    public void onCreate() {
      initFlutterBoost()
    }
}

 private void initFlutterBoost() {
        INativeRouter router = new INativeRouter() {
            @Override
            public void openContainer(Context context, String url, Map<String, Object> urlParams, int requestCode, Map<String, Object> exts) {
                String assembleUrl = Utils.assembleUrl(url, urlParams);
                PageRouter.openPageByUrl(context, assembleUrl, urlParams);
            }

        };


        FlutterBoost.BoostLifecycleListener boostLifecycleListener = new FlutterBoost.BoostLifecycleListener() {

            @Override
            public void beforeCreateEngine() {

            }

            @Override
            public void onEngineCreated() {
                FlutterBoost.instance().engineProvider().getPlugins().add(new FlutterLocalStoragePlugin());
                FlutterBoost.instance().engineProvider().getPlugins().add(new UserManagerPlugin());
            }

            @Override
            public void onPluginsRegistered() {

            }

            @Override
            public void onEngineDestroy() {

            }

        };


        Platform platform = new FlutterBoost
                .ConfigBuilder(this, router)
                .isDebug(true)
                .whenEngineStart(FlutterBoost.ConfigBuilder.ANY_ACTIVITY_CREATED)
                .renderMode(FlutterView.RenderMode.texture)
                .lifecycleListener(boostLifecycleListener)
                .build();

        FlutterBoost.instance().init(platform);

    }
3.在AndroidManifest.xml中注册BoostFlutterActivity
        <activity
            android:name="com.idlefish.flutterboost.containers.BoostFlutterActivity"
            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/brvah_sample_footer_loading" />

        </activity>
4.Flutter端路由注册
class _MyAppState extends State<MyApp> {
  @override
  void initState() {
    super.initState();
    FlutterBoost.singleton.registerPageBuilders({
      'userInfoPage': (pageName, params, _) {
        print("userInfo params:$params");
        return UserInfoPage(params);
      },

      'flutter://mineFragment': (pageName, params, _) {
        return MinePage(params);
      },
    });
  }


  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Test-Flutter',
        builder: FlutterBoost.init(postPush: _onRoutePushed),
        home: Container());
  }
}
5.Native端路由注册
public class PageRouter {

    public final static Map<String, String> pageName = new HashMap<String, String>() {{  
        put("flutter://userInfoPage","userInfoPage");
    }};
    public static final String NATIVE_LOGIN_ACTIVITY_URL = "native://loginActivity";

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

    public static boolean openPageByUrl(Context context, String url, Map params, int requestCode) {

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

        Log.i("openPageByUrl", path);

        try {
            if (pageName.containsKey(path)) {
                Intent intent = BoostFlutterActivity.withNewEngine().url(pageName.get(path)).params(params)
                        .backgroundMode(BoostFlutterActivity.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_LOGIN_ACTIVITY_URL)) {
                Intent intent = new Intent(context, LoginActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
                context.startActivity(intent);
                return true;
            }
            return false;

        } catch (Throwable t) {
            return false;
        }
    }
}
6.创建一个FlutterFragment
FlutterFragment   mMineFragment = new FlutterFragment.NewEngineFragmentBuilder().url("flutter://mineFragment").params(params).build();
7.从Flutter页面启动一个NativeActivity
FlutterBoost.singleton.open("native://loginActivity");
8.从Flutter页面启动一个FlutterActivity
FlutterBoost.singleton.open("flutter://userInfoPage", urlParams: _userInfo);

可以看到FlutterBoost为Native和Flutter的跳转提供了统一标准,我们只需要在Native和Flutter两端维护一个路由表,就可以方便地实现混合栈的管理和参数传递

七、Android与Flutter的通信

在实际的开发过程中,我们时常要从flutter端访问native的数据或者硬件能力,这个时候需要使用到Platform Channel进行通信

1. Platform Channel 分类和简介
  • BasicMessageChannel: 支持字符串和半结构化的数据传递
  • MethodChannel: 支持方法调用,既可以从Flutter发平台发起方法调用,也可以从平台代码向Flutter发起调用
  • EventChannel: 支持数据流通信
2. 插件的编写

使用频率最高的主要是MethodChannel,下面是其主要流程示意图


image.png

下面我们以MethodChannel的方式,实现一个退出登录的功能

2.1 Flutter端注册一个MethodeChannel并调用
class UserManager {
  MethodChannel _methodChannel;

  static UserManager _instance;

  UserManager._internal() {
    if (_methodChannel == null) {
      _methodChannel = new MethodChannel(ChannelConstants.FLUTTER_USER_MANAGER);
    }
  }

  static UserManager getInstance() {
    if (_instance == null) {
      _instance = new UserManager._internal();
    }
    return _instance;
  }

  Future<bool> logout() async {
    bool logout = await _methodChannel.invokeMethod("logout");
    return logout;
  }
}


void _logout() async {
     bool isLogout = await UserManager.getInstance().logout();
    if(isLogout){
      print("退出登录成功")
    }else{

    }
}
2.2 Android端插件的编写
public class UserManagerPlugin implements MethodChannel.MethodCallHandler, FlutterPlugin {
    @Override
    public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
        final MethodChannel methodChannel = new MethodChannel(FlutterBoost.instance().engineProvider().getDartExecutor().getBinaryMessenger(), ChannelConstants.FLUTTER_USER_MANAGER);
        methodChannel.setMethodCallHandler(this);
    }

    @Override
    public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {

    }

    @Override
    public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
        if (call.method.equals("logout")) {
            //这里的UserManager是Android端的实现,是真正的退出登录,清空token等用户数据
            UserManager.getInstance().logout();
            //这里给Flutter回调结果
            result.success(true);
        }
    }
}
2.3 Android端的插件注册

在之前Application中执行的FlutterBoost的初始化代码的onEnginCreated回调中注册

FlutterBoost.BoostLifecycleListener boostLifecycleListener = new FlutterBoost.BoostLifecycleListener() {

            @Override
            public void beforeCreateEngine() {

            }

            @Override
            public void onEngineCreated() {
                FlutterBoost.instance().engineProvider().getPlugins().add(new UserManagerPlugin());
            }

            @Override
            public void onPluginsRegistered() {

            }

            @Override
            public void onEngineDestroy() {

            }

        };

这样我们在Flutter端调用UserManager.logout时,会通过注册的MethodChannel通道,调用到Native端的UserManager.logout方法,然后通过result.success(true)拿到调用结果

具体Platform Channel的底层实现原理,后面会用单独的篇幅介绍

相关文章

网友评论

      本文标题:Flutter混合开发实践

      本文链接:https://www.haomeiwen.com/subject/tnpvuhtx.html