作者 | 俞家欢

低头需要勇气,抬头需要实力

移动端跨平台技术

跨平台概念是软件开发中一个重要的概念,即不依赖于操作系统,也不依赖硬件环境。一个操作系统下开发的应用,放到另一个操作系统下依然可以运行。

移动端为什么需要跨平台技术

众所周知目前移动端主要有两大体系,一个是 Google 公司引领的 Android 体系,一个是 Apple 公司引领的 iOS 体系。两大平台应用开发技能树完全不同,因此当需要开发一个移动端应用时,需要在此两种系统体系上分别开发一遍。每一端都需要独立研发、测试,包括后续的维护工作也是如此,这样就会造成开发工作量和开发人员成倍增涨,增加研发费用和研发周期,可能会拖慢产品迭代的节奏。为了实现“偷懒”和“省钱”的目标,移动端跨平台技术便应运而生,目的是能够减小多端带来的开发成本,可以实现用一套代码跑通不同的移动平台。

移动端跨平台技术划分

移动端跨平台需要从两个维度去跨平台,一个是渲染层面,另一个逻辑层面。跨平台技术既可以是两个维度都跨平台,也可以只选一个维度进行跨平台。跨平台技术发展至今主要是是下面三种划分:

  • Web 容器 渲染使用系统原生提供的浏览器控件 (iOS 为 UIWebView/WKWebView,Android 为 WebView) 渲染 HTML5 页面,逻辑层面使用 JavaScript,定义 JS 与 原生代码的交互协议,将原生系统能力暴露给 H5。优点是可以开发效率高、维护成本低、具有动态化,缺点是渲染、性能与兼容性较差。代表框架为 Cordova。

  • 原生渲染,逻辑跨平台 渲染使用各自系统提供的渲染方式,逻辑层使用特定语言将其编译成各端可以识别的产物去执行。代表框架为 ReactNative、Weex,代表语言为 Kotlin、C/C++。

  • 自绘引擎,逻辑跨平台 渲染使用自建的跨平台绘制引擎,逻辑层使用特定语言将其编译成各端可以识别的产物去执行。代表框架为 Flutter。

Flutter 介绍

Flutter 是 Google 开发的跨平台 UI 框架,当前支持移动端、Web 以及桌面端。Flutter 使用 Dart 作为开发语言,底层使用 Skia 渲染引擎。

Skia(渲染)

Skia 是一款用 C++ 开发的、开源的、跨平台图形库。Skia 在图形转换、文字渲染、位图渲染方面都表现卓越,并提供了开发者友好的 API。

Flutter 架构于 Skia 之上 ,也因此拥有了彻底的跨平台渲染能力。通过与 Skia 的深度定制及优化,Flutter 可以最大限度地抹平平台差异,提高渲染效率与性能。底层渲染能力统一了,上层开发接口和功能体验也就随即统一了,开发者也就不用操心平台相关的渲染特性。也就是说,Skia 保证了同一套代码调用在各个平台上的渲染效果是完全一致的。Android 系统中默认集成了 Skia 绘制引擎,所以在打包 Android 产物时,不需要打包 Skia 引擎,这也是产物大小比 iOS 小的原因。

Dart(逻辑)

Dart 是快速开发多端应用的客户端优化语言。其目标是为多端开发提供高效的编程,并为应用程序框架提供灵活的执行运行时平台。

编译器

Dart 有两大类编译器

  • Native 平台

    针对移动端以及桌面端设备应用。Dart Native 包括使用即时编译(JIT)的 Dart VM 和用于生成机器码的预先编译(AOT)编译器。

    在应用开发过程中,高效开发至关重要。Dart VM 提供了即时编译(JIT),具有增量式编译(热更新/热重启)的特性以及丰富的调试支持。当应用程序准备部署到生产环境中时,Dart AOT 编译器可以提前编译成本地 ARM 或 x86_64 机器码。AOT 编译的代码运行在一个高效的 Dart 运行时中,该运行时强制开启类型安全检查,并使用快速对象分配和分代垃圾收集器管理内存。

  • Web 平台

    针对 Web 应用。Dart Web 包括开发时编译器(dartdevc,增量开发)和生产时编译器(dart2js),两者都会将 Dart 代码翻译成 JavaScript 语言。

"线程"模型

在 Dart Native 中,线程被抽象成了 isolate。每个 isolate 独享内存,包含两个事件队列和一个事件循环,执行过程如下图所示:

优先执行 MicroTaskQueue 里的任务,MicroTaskQueue 中的任务都执行完了,再去执行 EventQueue 任务,EventQueue 中每执行完一个任务,都会去检查 MicroTaskQueue 是否有任务,有任务的话,就会优先循环执行完 MicroTaskQueue。

  • Event Queue

    负责处理 I/O 事件、绘制事件、手势事件、接收其他 isolate 消息等事件。

  • Microtask Queue

    事件的优先级比 Event Queue高,Flutter 内部会将手势识别、文本输入、滚动视图、保存页面等一些高优先级的事件放在此事件队列中。

异步任务

Dart 为 Event Queue 的任务建立提供了一层封装,叫作 Future,借助 await 与 async 关键字,可以通过事件循环实现非阻塞的同步等待。Dart 会将调用体的函数也视作异步函数,将等待语句的上下文放入 Event Queue 中,一旦有了结果,事件循环就会把它从 Event Queue 中取出,等待代码继续执行。

Flutter 技术架构

Flutter 在技术架构分为三层,从上至下依次是 Dart Framework, C/C++ Engine,Platform-specific Embedder。

  • Framework

    用 Dart 编写,封装整个 Flutter 框架的核心功能,封装整个 Flutter 架构的核心功能,包括 Widget、动画、绘制、手势等功能,有Material(Android 风格 UI)和 Cupertino(iOS 风格)的 UI 界面,可构建 Widget 控件以及实现UI布局。

  • Engine

    用 C++ 编写,用于高质量移动应用的轻量级运行时环境,实现了 Flutter 核心库,包括 Dart 虚拟机、动画和图形、文字渲染、通信通道、事件通知、插件架构等。

  • Embedder

    平台中间层。

在不同的平台上,Framwork 和 Engine 是一样的,Embedder 中间层的实现是不同的。

Task Runner

Engine 不创建和管理线程,会要求 Embedder 创建 Platform、UI、Raster(GPU)以及 IO 四个 Task Runner 供 Engine 使用。Engine 并不关心这四个 Task Runner 分别具体跑在哪个线程中。但是为了获得最佳性能,Embedder 应该为每个 Task Runner 专门创建一个线程。

  • Platform Task Runner

    对应平台(Android、iOS)的主线程,负责Embedder 和 Engine 层的交互,处理平台的消息。

  • UI Task Runner

    对应平台(Android、iOS)的子线程,负责 Flutter Engine 执行 Root Isolate 中的所有 Dart 代码(所有 Dart Framework 层的代码都在此 Runner 中运行),执行渲染与处理 Vsync 信号,将 Widget 转换生成 Layer Tree。

  • Raster Task Runner

    对应平台(Android、iOS)的子线程,真正执行渲染任务,光栅化所有从 UI Task Runner 中提交过来的任务,最终渲染到屏幕上。

  • IO Task Runner

    对应平台(Android、iOS)的子线程,从磁盘/网络中读取图片,将图片数据进行处理为 GPU Task Runner 所需的格式进行渲染。

当前 iOS 和 Android 平台 上每个 Flutter Engine 实例启动都会为 UI、Raster 以及 IO 创建新线程,所有 Engine 实例共享一个 Platform Task Runner 和线程。

Platform Channel

Flutter 官方提供了一种 Platform Channel 的方案,用于 Dart 和平台之间相互通信。其核心流程是:

  • Flutter 通过 Platform Channel 将传递的数据编码成消息的形式发送到平台。

  • 平台接收到 Platform Channel 的消息后,调用相应平台的 API。

  • 执行完成后将结果数据通过同样方式原路返回。

Platform Channel 涉及三个要素:

  • Channel Name

    一个 Flutter 应用中可能存在多个 Channel,每个 Channel 在创建时必须指定一个独一无二的 Channel Name,Channel 之间使用 Channel Name 来区分彼此。当有消息从Flutter 发送到平台时,会根据其传递过来的 Channel Name 找到该 Channel 对应的消息处理器。

  • BinaryMessager

    BinaryMessenger 是平台与 Flutter 通信的工具,通信使用的消息格式为二进制数据。当初始化一个 Platform Channel,并向该 Platform Channel 注册处理器时,会生成一个与之对应的 BinaryMessageHandler,并以 Channel Name 为键值,注册到 BinaryMessenger 中。当 Flutter 发送消息时,BinaryMessenger 会根据其传入的 Channel Name 找到对应的 BinaryMessageHandler,并交由其处理。由于 Platform Channel 从 BinaryMessageHandler 接收到的消息是二进制格式数据,无法直接使用。因此 Platform Channel 会将该二进制消息通过Codec(消息编解码器)解码为能识别的消息数据。

  • Codec

    消息编解码器 Codec 主要用于将二进制格式的数据转化为能够识别的数据,Flutter 定义了两种 Codec,分别是 MessageCodec 和 MethodCodec。MessageCodec 用于二进制格式数据与基础数据之间的编解码。BasicMessageChannel 所使用的编解码器就是 MessageCodec。MethodCodec 用于二进制数据与方法调用和方法返回结果之间的编解码。MethodChannel 和 EventChannel 所使用的编解码器均为 MethodCodec。BasicMessageChannel、MethodChannel 以及 EventChannel 是 Flutter 提供的三种不同类型的 Platform Channel,在开发实践小节中会介绍其用法。

Flutter 默认的消息编解码器其支持的数据类型如下:

Widget

Widget 是 Flutter 世界里对视图的一种结构化描述,是控件实现的基本逻辑单位,里面存储的是有关视图渲染的配置信息,包括布局、渲染属性、事件响应信息等。Widget 可以是一个按钮,一种字体,一种颜色,一种布局,在 Flutter 的世界里"一切皆为 Widget"。Widget 被设计成是不可变的,所以当视图渲染的配置信息发生变化时,Flutter 会选择重建 Widget 树的方式进行数据更新。Widget 可以分为 StatelessWidget(无状态) 和 StatefulWidget(有状态)。

  • StatelessWidget

    一旦创建成功就不再关心、也不响应任何数据变化进行重绘。如 Text、Container 等。

  • StatefulWidget

    此类 Widget 创建完成后,还需要关心和响应数据变化来进行重绘。如 Image 等。调用 setState 方法会触发当前 Widget 以及其子 Widget 的重建。

State 生命周期

  • initState

    当 Widget 创建后调用的第一个方法,整个生命周期内只调用一次。

  • didChangeDependencies

    第一次构建 Widget 时,会在 initState 之后立即调用此方法。build 方法总是在此方法被调用之后调用。

  • didUpdateWidget

    组建重建时会调用此方法,如父 Widget 调用了 setState 方法。build 方法总是在此方法被调用之后调用。

  • deactivate

    当 State 从树中移除的时候被调用。

  • dispose

    当 State 永久被移除的时候被调用。

Flutter 开发实践

本文对应的 Flutter 开发环境如下:

Flutter 1.22.5 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 7891006299 (6 weeks ago) • 2020-12-10 11:54:40 -0800
Engine • revision ae90085a84
Tools • Dart 2.10.4

Flutter 开发 IDE 可以选择 vscode 也可以选择 Android Studio,本文将以 AndroidStudio 为例,插件版本如下:

Dart: 201.9317
Flutter: 52.1.1

屏幕适配

Flutter 在表示一个控件大小时是不需要写单位的,如:

Container(width: 100,height: 100,
);

此 Container 在不同像素密度的屏幕上表示的像素大小是不同的。对于一倍屏,像素大小就为 100px_100px。两倍屏就表示为 200px_200px,以此类推。为了让 UI 设计能够在不同分辨率以及不同尺寸的屏幕下显示效果保持一致,可以借助小程序的屏幕适配思想,设计一个单位叫 rpx,让所有设备的宽度都变为 375 个长度单位。

class SizeFit {static double rpx;static void initialize(BuildContext context, {double standardWidth = 375}) {if (rpx != null) {return;}rpx = MediaQuery.of(context).size.width / standardWidth;}static double setRpx(num size) {return SizeFit.rpx * size;}
}

利用 Dart 语言的扩展方法,可以给 num 类型的值扩展 rpx 属性

extension NumberExt on num {double get rpx {return SizeFit.setRpx(this);}
}

那么这样之后对于一个控件的大小表示就变为:

Container(width: 100.0.rpx,height: 100.0.rpx,
);

100.rpx 在设备上显示的像素大小就为 【宽度】*100/375。

原生通信

从上面的小节知道,Flutter 与原生通信是通过 Flutter Platform Channel 实现的,Flutter 定义了三种不同类型的 Channel。

BasicMessageChannel

双向通信,用于传递字符串和半结构化信息。

  • Flutter

    BasicMessageChannel messageChannel = BasicMessageChannel("CHANNEL_NAME", StandardMessageCodec());
    
    messageChannel.send(message);
    
    messageChannel.setMessageHandler((message) async {return "";
    });
    
    • 消息监听

    • 消息发送

    • 定义

  • Android

    • 定义

      val messageChannel = BasicMessageChannel(binding.binaryMessenger, "CHANNEL_NAME", StandardMessageCodec())
      
    • 消息发送

      messageChannel.send(message:Any)
      
    • 消息监听

      messageChannel.setMessageHandler(object: BasicMessageChannel.MessageHandler<Any> {override fun onMessage(message: Any?, reply: BasicMessageChannel.Reply<Any>) {}
      })
      

MethodChannel

双向通信,用于传递方法调用。

  • Flutter

    • 定义

      MethodChannel methodChannel = MethodChannel("CHANNEL_NAME");
      
    • 方法调用

      methodChannel.invokeMethod("METHOD_NAME", arguments);
      
    • 方法调用监听

      _methodChannel.setMethodCallHandler((MethodCall call) async {
      });
      
  • Android

    • 定义

      val methodChannel = MethodChannel(binding.binaryMessenger, "CHANNEL_NAME")
      
    • 方法调用

      methodChannel.invokeMethod("METHOD_NAME", arguments)
      
    • 方法调用监听

      channel.setMethodCallHandler(object : MethodChannel.MethodCallHandler{override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {}
      })
      

EventChannel

单向通信(原生 => Flutter),用于事件通知。

  • Flutter

    • 定义

      EventChannel eventChannel = EventChannel("CHANNEL_NAME");
      
    • 事件监听

       StreamSubscription eventListener = eventChannel.receiveBroadcastStream().listen((event) { });
      
    • 取消监听事件

      eventListener.cancel();
      
  • Android

    • 定义

      val eventChannel = EventChannel(binding.binaryMessenger, "CHANNEL_NAME")
      
    • 事件通知

      eventChannel.setStreamHandler(object: EventChannel.StreamHandler {override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { }override fun onCancel(arguments: Any?) {}
      })
      

异常捕获处理

使用 try catch finally 来捕获异常,并处理异常

try {String a = null;print(a.length);
} catch (e) {// 异常处理
} finally {print("finally");
}

Flutter 框架异常捕获处理

Flutter 框架为我们在很多关键的地方进行了异常捕获。

举个例子,当布局发生越界或不和规范时,Flutter 就会自动弹出一个错误界面,这是因为 Flutter 已经在执行 build 方法时添加了异常捕获。

@override
void performRebuild() {...try {// 执行build方法  built = build();} catch (e, stack) {// 有异常时则弹出错误提示  built = ErrorWidget.builder(_debugReportException('building $this', e, stack));} ...
}

可以看到,在发生异常时,Flutter 默认的处理方式是弹一个ErrorWidget,

继续追踪源码可以发现,Flutter 会调用 FlutterError 类的 onError 方法。那么只需要在 main 方法中,给 FlutterError 类的 onError 赋值需要的处理异常的逻辑即可。

void main() {FlutterError.onError = (FlutterErrorDetails details) {// 异常处理};
}

其他异常捕获处理

在 Dart 中,异常分两类:同步异常和异步异常,同步异常可以通过 try/catch 捕获,而异步异常则比较麻烦,如下面的代码是捕获不了Future的异常的:

try {Future.delayed(Duration(seconds: 1)).then((e) => Future.error("..."));
} catch (e) {print(e)
}

Dart 中有一个 runZoned 方法,可以给执行对象指定一个 Zone。Zone 表示一个代码执行的环境范围,可以理解为沙箱。runZoned 可以捕获被包裹代码块中所有发生的未捕获的异常,包括异步异常。这样一来,就可以在 main 方法中做如下处理

void main() {runZoned(() {runApp(MyApp());}, onError: (Object obj, StackTrace stack) {// 异常处理});
}

原生项目中嵌入Flutter 项目

一般来说,不可能将现有的原生应用用 Flutter 重写一遍,大都数情况都是将 Flutter 作为一个库或模块,集成进现有的原生应用当中,去实现一些业务场景。目前 Flutter 混合开发是有在杏仁医生项目中应用,我们将原有通过 RN 编写的业务用 Flutter 重写了一下。

创建 Flutter module

flutter create --org com.example --template=module hello

module 类型的 Flutter 工程就是专门用于集成到现有原生项目中的工程模版,此类型的 Flutter 工程不包含原生的代码,原生代码都需要编写在原生工程中。

项目集成

集成方式
  • Android

    Android 项目有两种集成方式:一种是主工程直接依赖 Flutter 工程源码,另一种是主工程依赖 Flutter 工程的 aar 产物。这里只介绍第二种方式,这种方法更适合团队开发,符合组件化的思想。在 Flutter 工程根目录运行:

    flutter build aar
    

    执行完毕后就会在 ${rootProject}/build/host/repo 下生成工程本身以及依赖的 aar 产物。如果不指定任何参数,默认会生成三个变体的 aar 包,分别是 debug、profile 以及 release。如果不想构建 profile 变体,则可以尝试如下命令:

    flutter build aar --no-profile
    

    产物构建完毕后,在原生项目中直接引用即可:

    debugImplementation 'xxxxxxxxxx:flutter_debug:1.0'
    releaseImplementation 'xxxxxxxxxx:flutter_release:1.0'
    
添加 Flutter 页面
  • Android

    • 创建 Activity 类

      // SampleFlutterActivity.kt
      class SampleFlutterActivity : FlutterActivity() {
      }
      
    • 覆写方法,返回需要跳转的路由

      // SampleFlutterActivity.kt
      class SampleFlutterActivity : FlutterActivity() {override fun getInitialRoute(): String {return "${Flutter 页面路由}"}
      }
      
    • AndroidManifest 添加 FlutterActivity 的声明

      <activityandroid:name="io.flutter.embedding.android.FlutterActivity"android:theme="@style/LaunchTheme"      android:har dwareAccelerated="true"android:windowSoftInputMode="adjustResize"android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"/>
      
    • 启动 Activity

      // SomeActivity/Fragment.kt
      startActivity(FlutterActivity.withNewEngine().initialRoute("${Flutter 页面路由}").build(this)
      )
      
    • 常规方式

      以上这种方式比较适合可以直接打开不需要传递参数的 Flutter 页面,需要处理参数的可以使用下面介绍的【继承方式】。

    • 继承方式

      以上方式就可以在 onCreate 回调中处理业务参数,跳转不同的路由或向 Flutter 页面中传递不同的参数。

预加载 Flutter 引擎

每个 FlutterActivity 都会创建一个默认 FlutterEngine。每个 FlutterEngine 都会有一个启动预热时间。在原生跳转 Flutter 模块页面时会有一个 FlutterEngine 初始化延迟,在 debug 模式下,可以非常明显的感觉到,在 release 模式下就比较难感受到这个延迟。要最小化这种延迟,可以在启动 FlutterActivity 之前,预先创建一个 FlutterEngine 并初始化它。

  • Android

    • 初始化 FlutterEngine

      // MyApplication.kt
      class MyApplication : Application() {lateinit var flutterEngine : FlutterEngineoverride fun onCreate() {super.onCreate()flutterEngine = FlutterEngine(this)flutterEngine.getNavigationChannel().setInitialRoute("${Flutter 页面路由}");flutterEngine.dartExecutor.executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault())FlutterEngineCache.getInstance().put("${flutter_engine_id}", flutterEngine) // 此处填写一个自定义的 FlutterEngine ID }
      }
      
    • 启动的时候带上 FlutterEngine Id

      // SomeActivity/Fragment.kt
      startActivity(FlutterActivity.withCachedEngine("${flutter_engine_id}")  .build(this)
      )
      
    • 【继承方式】重写相应方法

      // SampleFlutterActivity.kt
      class SampleFlutterActivity : FlutterActivity() {override fun getCachedEngineId(): String? {return "${flutter_engine_id}"}
      }
      
    • 清除 FlutterEngine 的相关资源

      FlutterEngineCache.getInstance().get("${flutter_engine_id}")?.destroy()
      

添加过度页

在 FlutterEngine 启动过程中,我们可以添加一个过渡加载页面友好显示。

  • Android

    添加过渡页只适合【继承方式】

    // SampleFlutterActivity.kt
    class SampleFlutterActivity : FlutterActivity() {override fun provideSplashScreen(): SplashScreen? {return FlutterSplashScreen()}
    }
    
    // FlutterSplashScreen.kt
    class FlutterSplashScreen : SplashScreen {override fun createSplashView(context: Context, savedInstanceState: Bundle?): View? {val frameLayout = FrameLayout(context)val progressBar = ProgressBar(context)frameLayout.addView(View(context),FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT))frameLayout.addView(progressBar,FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT).apply {gravity = Gravity.CENTER})return frameLayout}override fun transitionToFlutter(onTransitionComplete: Runnable) {onTransitionComplete.run()}
    }
    

开发调试

独立开发调试

Flutter 模块如何脱离原生工程直接运行呢?这里有两种情况。

  • Flutter 模块中不牵扯与任何与原生工程的交互

    可以直接运行。默认程序的入口是 main.dart 中的 main 方法,可以在此文件中修改初始路由去直接跳转对应的页面。

    // main.dart
    ...
    void main() {runApp(App());
    }class App extends StatelessWidget {@overrideWidget build(BuildContext context) {return MaterialApp(...initialRoute: window.defaultRouteName,  // 此处可以修改初始路由...);}
    }
    
  • Flutter 模块中需要与原生工程交互

    此种情况下直接独立运行会因为找不到 MethodChannel 而报错。为了解决此问题,在杏仁医生项目中我们的处理方式是定一个标记来表示当前 Flutter 模块是【独立运行】还是【集成运行】的。我们定一个 main_dev.dart 文件,通过 IDE 修改启动入口为 main_dev.dart,在 main_dev.dart 中修改标记为【独立运行】。在原生交互层对于识别到【独立运行】标记时,程序会 mock 与原生交互的数据。

    // main_dev.dart
    ...
    void main() {AppConfig.isRunInFlutter = true;runApp(App());
    }class App extends StatelessWidget {@overrideWidget build(BuildContext context) {return MaterialApp(...initialRoute: window.defaultRouteName,  // 此处可以修改初始路由...);}
    }
    
    // 原生交互层
    ...
    Future<Map<String, dynamic>> getHttpBaseUrl() {if (AppConfig.isRunInFlutter) {// mock 交互数据return Future.value({"baseUrl": "https://xingren.com",});}return _callHandler("getHttpBaseUrl");
    }
    ...
    

    AndroidStudio 程序入口配置如下:

  • 集成开发调试

  1. 从 App 中打开 Flutter 模块开发的页面

  2. AndroidStudio 中点击 Flutter Attach

  3. 等待文件同步完成

  4. 同步完成之后,就可以 Hot Reload 或者 Hot Restart

Package 与 Plugin 开发

Package

纯 Dart 库,不涉及平台调用。

  • 创建 Package

    flutter create --org com.example --template=package hello
    

    这条指令会在当前位置创建一个文件夹名为 hello 的 Package 项目。目录结构如下:

    ├── CHANGELOG.md
    ├── LICENSE
    ├── README.md
    ├── hello.iml
    ├── lib
    │   └── hello.dart
    ├── pubspec.lock
    ├── pubspec.yaml
    └── test└── hello_test.dart
    
  • 实现 Package

    对于纯 Dart 库的 Package,只要在 /hello/lib 目录中创建 Dart 文件编写 Dart 代码实现功能。

Plugin

使用 Dart 编写,按需使用 Java/Kotlin 和 OC/Swift 分别在 Android 和 iOS 平台实现的库。

  • 创建 Plugin

    flutter create --org com.example --template=plugin --platforms=android,ios hello
    

    这条指令会在当前位置创建一个文件夹名为 hello 的 Plugin 项目。目录结构如下:

    ├── CHANGELOG.md
    ├── LICENSE
    ├── README.md
    ├── android
    │   ├── build.gradle
    │   ├── gradle
    │   ├── gradle.properties
    │   ├── hello_android.iml
    │   ├── local.properties
    │   ├── settings.gradle
    │   └── src
    ├── example
    │   ├── README.md
    │   ├── android
    │   ├── hello_example.iml
    │   ├── ios
    │   ├── lib
    │   ├── pubspec.lock
    │   ├── pubspec.yaml
    │   └── test
    ├── hello.iml
    ├── ios
    │   ├── Assets
    │   ├── Classes
    │   └── hello.podspec
    ├── lib
    │   └── hello.dart
    ├── pubspec.lock
    ├── pubspec.yaml
    └── test└── hello_test.dart
    
  • 实现 Plugin

    • 使用 AndroidStudio 打开 /hello/example/android 文件夹。

    • 在工程中找到 /hello/android 路径的 module,在此 module 中编写 Android 平台相关的代码。

    • 在 /hello/lib 目录下创建 Dart 文件编写 Dart 代码。

    • 添加 Android 平台代码

基于 Flutter 的小程序(动态化)方案思路

动态化也需要两个维度分别考虑去实现,一个是渲染,一个是逻辑。Flutter 本身是不支持动态化的。

  • 渲染层面

    可以通过某种形式(AST)描述页面,在运行时将此描述动态转成 Flutter 的 Widget 实现 UI 动态化。

  • 逻辑层面(由于 iOS 平台的限制,逻辑层面也没有其他方案可以选择)

    iOS 平台使用自带的 JSCore,Android 平台使用 V8 引擎来实现动态化。因此逻辑代码需要使用 JavaScript 来编写。

完整的流程应该是这样的:

  • 前端可以使用 HTML/CSS/JS 开发页面以及功能逻辑。

  • 前端编译器将开发好的程序转成特殊的描述方式。

  • Flutter 应用可以在运行时解析此描述方式进行 UI 渲染和事件绑定,利用 JSCore 和 V8 引擎加载 Javascript 脚本。

参考

[Flutter 官方文档] : https://flutter.dev/docs

[Flutter 开发文档] : https://github.com/flutter/flutter

[Dart 官方文档] : https://dart.dev/

[移动端跨平台开发的深度解析] : https://zhuanlan.zhihu.com/p/41595544

[跨平台历史发展逻辑] : https://cloud.tencent.com/developer/article/1485193

全文完


以下文章您可能也会感兴趣:

  • 简单说说spring的循环依赖

  • Mysql redo log 漫游

  • 一个 AOP 缓存失效问题的排查

  • 小程序开发的几个好的实践

  • RabbitMQ 如何保证消息可靠性

  • 压力测试必知必会

  • 分布式 Session 之 Spring Session 架构与设计

  • 缓存的那些事

  • Java 并发编程 -- 线程池源码实战

  • Lombok Builder 构建器做了哪些事情?

  • WePY 2.0 新特性

  • SSL证书的自动化管理

  • 了解一下第三方登录

  • 分布式 ID 生成策略

我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 rd-hr@xingren.com 。

Flutter 移动端介绍与实践相关推荐

  1. 闲鱼基于 Flutter 的移动端跨平台应用实践

    闲鱼为什么使用 Flutter Flutter 作为 Google 新一代的跨平台框架,有较多的优点,但跟其他跨平台解决方案相比,最吸引我们的是它的高性能,可以轻松构建更流畅的 UI.虽然各跨平台方案 ...

  2. 语雀桌面端技术架构实践

    作者:易芝林(维骏) 语雀桌面端作为语雀为用户提供的生产力工具,上线两年多来一直保持高频的迭代和健康的业务增长.本次主要介绍我们在做桌面端时的一些技术架构思考和实践,同时也将分享我们沉淀的一些通用桌面 ...

  3. Flutter:从入门到实践

    课程内容 开篇:迎合未来主流趋势,把握新技术主动权 移动开发的前方突破口在哪里? 小团队如何面向未来做技术选型? 想要独立开发一个产品,能不能做到省心省力? 我用两个关键词来回答这些问题:跨平台.Fl ...

  4. 如何动态的生成某种类型的集合呢_知乎画报」的移动端动态化工程实践

    本文基于移动端动态化方案在知乎原生推广落地页「知乎画报」上的实践经验,对该方案技术升级过程中的思考以及技术关键细节做了详尽的解读. 商业化是互联网公司发展的重要阶段,App 端的商业广告业务对移动端动 ...

  5. RabbitMQ系列(三)RabbitMQ交换器Exchange介绍与实践

    RabbitMQ交换器Exchange介绍与实践 RabbitMQ系列文章 RabbitMQ在Ubuntu上的环境搭建 深入了解RabbitMQ工作原理及简单使用 RabbitMQ交换器Exchang ...

  6. 技术教程 | 基于 Web 端的屏幕共享实践

    屏幕共享的英文叫做 DesktopSharing,通俗点讲就是将自己电脑的画面分享给其他人, 被分享的可以是整个电脑屏幕.应用程序或者某一个打开的网页等等. 而随着音视频领域的深入发展,完备的功能在用 ...

  7. 美图App的移动端DNS优化实践:HTTPS请求耗时减小近半...

    本文引用了颜向群发表于高可用架构公众号上的文章<聊聊HTTPS环境DNS优化:美图App请求耗时节约近半案例>的部分内容,感谢原作者. 1.引言 移动互联网时代,APP 厂商之间的竞争非常 ...

  8. 红鸟沙龙(12)|李泽湘:端到端创业教育与实践探索

    所谓大众创业,万众创新,如今越来越多的同学在毕业之后准备投入到创业大军当中,那么我们的大学教育是否为这种趋势做好了准备?我们有没有为这些同学打通一条从象牙塔到创业者的道路?如果我们做的还不够的话,我们 ...

  9. Flutter原理与美团的实践

    Flutter是Google开发的一套全新的跨平台.开源UI框架,支持iOS.Android系统开发,并且是未来新操作系统Fuchsia的默认开发套件.自从2017年5月发布第一个版本以来,目前Flu ...

最新文章

  1. 你了解欧拉回路吗?(附Java实现代码)
  2. jmeter测试java代码
  3. linux Vi搜索和替换字符串
  4. Codeforces Round #379 (Div. 2) E. Anton and Tree
  5. C++vector容器-赋值操作
  6. 使用Java查询DynamoDB项
  7. php date 有warning,php提示PHP Warning: date(): It is not safe to rely on the......错误的解决办法...
  8. 小强的HTML5移动开发之路(27)—— JavaScript回顾2
  9. HTML页面点击下载文件的简单实现方法
  10. wordpress主题模板开发制作教程
  11. 百度搜索正式升级冰桶算法5.0!
  12. Android 判断当前身份证格式是否正确
  13. 《失控》摘录与读后感
  14. 决策树实现及调参的R与python方法对比——以泰坦尼克幸存者数据为例
  15. 实时即未来,车联网项目之电子围栏分析【六】
  16. NTFS格式和FAT格式的区别
  17. 竞业达录像服务器怎么修改,竞业达全国电子监考调试说明.doc
  18. AC的LAN3接口我在WEB里改为三层模式后,再改回二层就不能供电了 AP起不来了怎么恢复?
  19. 超好用的java反编译工具(Java Decompiler)
  20. C++十进制二进制十六进制转换

热门文章

  1. begining...
  2. 优质项目坚实支撑 BIM技术前途一片光明
  3. Android Studio未配置appkey或配置错误
  4. 移动开发大全之 macOS上使用.NET MAUI开发应用程序支持M1 和intel芯片(教程含环境配置过程)
  5. 双城记 A Tale of Two Cities
  6. updates/7/x86_64/primary_db FAILED
  7. 用R语言进行基本统计分析
  8. CANoe以太网配置 Network-Based Access Mode
  9. 【双向指针】接雨水问题
  10. 故障模块名称: NetdiskExt64.dll的解决之法