Flutter 移动端介绍与实践

作者 | 俞家欢

Flutter 移动端介绍与实践_第1张图片

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

移动端跨平台技术

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

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

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

移动端跨平台技术划分

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

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

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

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

Flutter 介绍

Flutter 移动端介绍与实践_第2张图片

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

Skia(渲染)

Flutter 移动端介绍与实践_第3张图片

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 语言。

Flutter 移动端介绍与实践_第4张图片

"线程"模型

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

Flutter 移动端介绍与实践_第5张图片

优先执行 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 移动端介绍与实践_第6张图片

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。

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

Flutter 移动端介绍与实践_第7张图片

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 默认的消息编解码器其支持的数据类型如下:

Flutter 移动端介绍与实践_第8张图片

Widget

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

  • StatelessWidget

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

  • StatefulWidget

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

State 生命周期

Flutter 移动端介绍与实践_第9张图片

  • 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 {
        override fun onMessage(message: Any?, reply: BasicMessageChannel.Reply) {
        }
      })
      

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 的声明

      
      
    • 启动 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 : FlutterEngine
      
        override 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 {
      @override
      Widget 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 {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
       	   ...
          initialRoute: window.defaultRouteName,  // 此处可以修改初始路由
          ...
        );
      }
    }
    
    // 原生交互层
    ...
    Future> getHttpBaseUrl() {
      if (AppConfig.isRunInFlutter) {
        // mock 交互数据
        return Future.value({
          "baseUrl": "https://xingren.com",
        });
      }
      return _callHandler("getHttpBaseUrl");
    }
    ...
    

    AndroidStudio 程序入口配置如下: 

    Flutter 移动端介绍与实践_第10张图片

    Flutter 移动端介绍与实践_第11张图片

  • 集成开发调试

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

  2. AndroidStudio 中点击 Flutter Attach 

  3. 等待文件同步完成 

    Flutter 移动端介绍与实践_第12张图片

  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 工程师,欢迎有兴趣的同学投递简历到 [email protected]

Flutter 移动端介绍与实践_第13张图片

你可能感兴趣的:(android,编程语言,java,dart,移动开发)