前端 Flutter 劝退指南

8,852 阅读18分钟

前言

都 9012 年了,Flutter 有多火,就不需要我多说了,之前掘金首页顶着好长一段时间的 Flutter 视频教程推广足以证明。

那么正如必须要先 ”入门“ 才能 ”出门“,那么前端劝退 Flutter 前也必须得先了解 Flutter。

本文就以前端的角度来给大家捋一捋:在前端眼中,Flutter 的开发到底有何不同?

......然后劝退=_=。【想直接被劝退请滑到最后】

什么是 Flutter

首先,什么是 Flutter?

官网解释:

Flutter是一款 Google 开源的 SDK,可跨平台地为移动端,Web 端,桌面端构建高性能的应用。

当然,当然,虽说是Web端桌面端都能开发,但是我们更多地会着重于 flutter 的移动端跨平台开发功能。

那么,在 flutter之前,其实就有很多跨平台开发的框架了,知名的有 C# 的 Xamarin, 用 js 的有 nativescript ,阿里的 weex 以及大家都比较熟悉的 react native,那么名气不大的就更多了 image

所以,flutter 在一堆跨平台开发框架中凭什么脱颖而出呢?

为什么用 Flutter ?

image

  1. 低投入高产出
    一套代码,直接产出 Android + iOS 两个平台的应用。 这是跨平台开发框架的共同优势,不再多说。
  2. 高效率开发
    通过 Flutter 的 JIT(Just In Time)即时编译功能,能提供 Hot Reload 功能,快速开发应用。有没有 Hot Reload 的开发效率高低,这点前端同学应该是深有体会了。
  3. 丰富优雅的UI
    框架本身提供 Material Design 以及 Cupertino 的两种画风的 UI 组件,不局限于系统本身 OEM 的限制。
  4. 高性能应用
    和原生一样的性能。Flutter 的 AOT 将代码编译成 ARM 二进制,用自身的 自绘引擎(Skia),没有 Bridge 依赖,可直接访问系统底层服务。这些能让 Flutter 性能毫不逊色于 原生应用。

前面的这些都是其次,最关键的是什么呢?

Flutter 有个好“爹”!

大家琢磨一下,当下的主流的三大前端框架,react、react native 是 facebook 的,angular 又是 google 的,只有 vue 是没有大公司背景。事实上社区的很多开源框架,其实都是大企业内部孵化出来的。

有个好爹,背靠 Google 爸爸,含着金钥匙出生,一看就前途不可估量。框架的稳定性和成长性就能得到一定保证,给开源社区信心。

这就是,拼爹一时爽,一直拼一直爽。

言归正传,这里提到 flutter 能不局限于系统 OEM,以及相比其他跨平台框架提供更优秀的性能,那么凭啥就 flutter 那么秀呢?我们可以从 flutter 框架结构上去探索一下。

Flutter 框架结构

flutter 的框架结构图如下: image

好了,相信大家不只一次看到这一张图了。 懂的可能已经了然于胸,不懂的可能还是一脸懵逼。

这里还是简单说下

从上往下看, 首先是 Framework,Framework 是用 dart 语言写的,从上往下,

  1. 有封装好的 UI 组件 Material 和 Cupertino 【相当于前端的 Ant-Design / Element / iview 等 UI 框架】
  2. 封装好的 UI 组件 Material 和 Cupertino 由更基础的 Widget 组件拼装而成。 【这里的 Widget 相当于 前端的 HTML div, h1, span 等标签元素】
  3. 继续往下, Widget 层是由 Animation(动画), Painting(绘制), Gesture(手势) 共同通过 Rendering 构成的 对象。【这里和前端稍稍有点区别,前端 UI 的结构(html),样式(CSS),事件交互(JS 是分开的,而在 Flutter ,都是 Widget】

Framework 往下是 Engine, Framework 中的 UI 交互都是有 Engine 来进行绘制渲染的。Engine 层内部会通过 Skia 图形引擎画出 UI 组件,Skia 是 Google 开源的 2D 图形引擎,适用于多个平台系统,这也是 flutter 能跨平台的核心元素之一。这也是为什么前面说 flutter 能不局限系统 OEM 组件的限制。 也就是说,如果你想要自己封装一个 ant-design 画风的 flutter UI 框架,你可以直接通过基础的 Widget 搭建出自己的 UI 框架。如果底层基础 UI 满足不了你的需求。你可以直接用 dart 调用 Skia 图像引擎的 API,画出自己的 UI,没有任何的限制。

最后是 embedded,嵌入层,这一块是处理平台差异性的事情,从而能够把 flutter 应用嵌入到各个系统平台。

可以看到 Flutter 没有用原生系统上的 OEM,而是用 2D 渲染引擎 skia 直接渲染绘制 UI, 这使得其平台相关层很低,平台只是提供一个画布,剩余的所有渲染相关的逻辑都在Flutter内部,这就使得它具有了很好的跨端一致性。

以上就是 flutter 跨平台开发的结构了, 那么这样设计的优越性在哪呢?我们可以对比下其他应用开发的架构。

跨平台架构对比

Native

首先我们来看下原生APP开发的架构设计,一般一个 App,会分为两大块,分别是 UI 渲染和系统服务调用,我们常说的跨平台开发,其实就是跨的这两块。

image 原生 App 的 UI ,会通过平台提供的原生 OEM 控件实现,

而系统服务调用,如相机,蓝牙等传感器的使用,也会通过平台系统提供的 API 来实现

那么这就会粗线一个问题,不同平台的 OEM 控件和 系统服务调用规范,以及编程语言不统一,Android 使用 Java / Kotlin,而 iOS 使用 Objective-C / Swift,这就产生了平台差异性。

一个 app 要开发几套代码,UI 效果还不一定能保持一致,费时费力。

于是,就产生了跨平台开发的需求。

我们来看下常见的跨平台架构

Webview

image

首先最常见的跨平台方案,是直接用 webview ,这其实就是我们常说的 hybrid app 了。

虽说不同平台的 webview 内核不一定一样,但是总归会遵循 w3c 规范, 那么我们的前端的代码可以运行在平台的 Webview 上,实现 UI 上的跨平台开发

而系统服务调用这一块呢,就通过 bridge 来通过协议来调用原生的方法。

那么 hybrid app 的方案缺点也是很明显的, webview 性能比不上原生

为了解决这个 webview 的性能问题,社区又推出了另外一种方案

React Native / Weex

image 如图所示,React native ,Weex 等框架,是用前端语言描述系统 OEM 之类 实现跨平台,简单的来说,是通过写 js 配置页面布局,然后通过 react native 解析成原生的控件。

这样的做法,就明显提高了性能,因为实质上渲染出来的,还是原生的控件。

但是,即便性能提高了,但是依然达不到原生的层次,因为 RN 是通过 Jscore 解析 jsbunder 文件布局,和原生直接布局还是有那么一丁点差距的。

另外,使用 react native 并不能避免写原生的代码,如果遇到一些平台相关的复杂问题,还是不得不深入到原生库中进行必要的调整。去年 Airbnb 就因为类似的原因放弃了 rn。

那么,用 flutter 就能避免这个问题了么?我们来看下 flutter 的架构

Flutter

image

前面其实也说过了,flutter 的 UI 渲染是基于 skia 图像引擎完成的,不依赖任何一个系统平台,平台仅仅提供一个画布,让 图像渲染在画布上。

那么直接越过原生的渲染机制,从自身的渲染引擎去渲染视图,这就和原生一模一样,没有了中间商赚差价。

两者的渲染性能也提升为了 两者的渲染引擎之间的比较。

至此,我们比较了几种跨平台架构的 UI 渲染实现,

那么关于系统服务的调用呢?Flutter 并没有消除 跨平台 系统服务调用的问题,因为硬件设计层面以及编程语言的差异性是客观存在的,基本无法避免。

但是不同于之前几种用 bridge 的方式来调用系统服务,flutter 用 Platform channel 的形式去调用系统服务,这里先跳过,下面的章节会详细讲一下这里的通信机制

Web VS Flutter

开发语言

image

不同于 Web 把页面分成了 HTML,CSS,JS, 在 Flutter 中,所有东西都是 widgets 具体 widgets 类型分为:

  • 元素 widget。 如 button,menu,list
  • 样式 widget。如 font,color
  • 布局 widget。 如 padding,margin
  • ...

所有的 widget 嵌套组合在一起,就构成了一个 flutter app。

UI 语法

基本样式

关于样式语法,前端的代码我们很熟悉了,用 HTML 和CSS 能快速实现一个简单的 UI。

我们来看看一个最基本的盒子模型:

<div class="greybox">
    Lorem ipsum
</div>
<style>
.greybox {
  background-color: #e0e0e0; /* grey 300 */
  width: 320px;
  height: 240px;
  font: 900 24px Georgia;
}
</style>
var container = Container( // grey box
  child: Text(
    "Lorem ipsum",
    style: TextStyle(
      fontSize: 24.0,
      fontWeight: FontWeight.w900,
      fontFamily: "Georgia",
    ),
  ),
  width: 320.0,
  height: 240.0,
  color: Colors.grey[300],
);

在 flutter ,由于 Flutter 没有标记语言,我们需要嵌套一个个 Widget 类来实现我们的 UI,这里的 Container Widget类,其实就相当于 div 标签。

那么看到这个代码风格,如果有写过 非 jsx 的 react 的话,你会发现代码风格有点像是 React.createElement 的画风。

React.createElement("div", {
    class: "test-c",
    style: "width: 10px;"
}, "Hello", React.createElement("span", null, "world!"));

布局

实现一个 UI ,第二个比较重要的点是布局, 在 Web 前端,实现布局的核心要点是 CSS 的属性:

<div class="greybox">
  <div class="redbox">
    Lorem ipsum
  </div>
</div>
<style>
.greybox {
  background-color: #e0e0e0; /* grey 300 */
  width: 320px;
  height: 240px;
  font: 900 24px Roboto;
  display: flex;
  align-items: center;
  justify-content: center;
}
.redbox {
  background-color: #ef5350; /* red 400 */
  padding: 16px;
  color: #ffffff;
}
</style>

而在 flutter,则需要一些官方提供的样式类来实现,例如这里的 BoxDecoration 类来修饰整个盒子,Alignment 确定文本对齐方式等等

var container = Container( // gray box
  child: Center(
    child:  Container( // red box
      child: Text(
          "Lorem ipsum",
          style: bold24Roboto,
          textAlign: TextAlign.center,
        ),
        decoration: BoxDecoration(
          color: Colors.red[400],
        ),
        padding: EdgeInsets.all(16.0),
      ),
      alignment: Alignment.center,
  ),
  width: 320.0,
  height: 240.0,
  color: Colors.grey[300],
);

交互

Web:

<input name="account" />
<div onclick="handleSubmit()">Submit</div>

最后一点是交互,类似于大部分的前端 UI 框架,每个组件其实都会暴露出一些事件钩子,

通过这些钩子,我们就可以捕获到用户的行为,从而实现对应的逻辑,

这里的 demo 就简单实现了 输入校验, 按钮的点击提交等基本的交互。

Flutter:

// ...
children: <Widget>[
  TextFormField(
    decoration: InputDecoration(
        hintText: 'Email/Name/Telephone',
        labelText: 'Account *',
      ),
      onSaved: (String value) {
        loginForm.account = value;
      },
      validator: (String value) {
        if (value.isEmpty) return 'Name is required.';
      }
  ),
  RaisedButton(
    child: Text(
      'Login'
    ),
    onPressed: () {
      // print('提交操作');
      // dosomething with loginForm
      handleSubmit()
    },
  ),
]

其余的还有 路由,动画,手势等交互,这里不再多说,基本上能用 Web 技术实现的,大都能够在 flutter 实现

可以看到,flutter 的 UI 部分除了没有 jsx 之外,其余部分的设计思想与 react 大同小异。

那么简单介绍完语法,我们来实际操作看看,怎么写一个 APP

开始一个 Flutter App

首先怎么安装 flutter 开发环境这个就不多说了,官网教程教程已经很完善了

目录结构

简单说下目录结构,通过 flutter 创建出一个工程后,会自动生成这样的目录结构,

.
├── android         # Android 平台配置,flutter 自动生成
├── ios             # iOS 平台配置,flutter 自动生成
├── assets          # 静态资源目录
├── build           # 存放构建出的 release 相关文件
├── lib             # 业务代码
├──  └── main.dart  # app 入口
└── pubspec.yaml    # 包管理文件

Hello World

上手一个框架,当然要来一个经典的 Hello World。

要实现一个 flutter app, 我们需要 加载 flutter 的基本组件 import 'package:flutter/widgets.dart';, 然后执行基本的 runApp , 那么一个基本的 hello world 就完成了。

// main.dart 文件
import 'package:flutter/widgets.dart';

void main() {
  runApp(
    Center(
      child: Text(
        'Hello, world!'
      ),
    ),
  );
}

效果如下:

可以看到,如果没有样式的话,应用就是一坨黑....

就像在前端开发时我们喜欢使用的 ant design 或者 iview 之类的 UI 框架,开发 flutter 一般也会用 UI 框架

Material App

Flutter 内置两套 UI 组件,分别是 Material UI 和 Cupertino UI,

现在简单看下一个 material 风格的APP是怎么实现的, 首先 import material 组件

new 一个 MaterialApp 组件,配置 title, app bar 等信息,就简单地生成了个 material 画风的 app 了。

import 'package:flutter/material.dart';
void main() {
  runApp(
    MaterialApp(
     title: 'Hello App',
     home: Scaffold(
       appBar: AppBar(
         title: Text('Hello'),
       ),
       body: Center(
         child: Text(
           'Hello Flutter',
           style: TextStyle(
             fontSize: 30
           ),
         )
       ),
     ),
   )
  );
}

更复杂的还可以在这里配置路由相关信息,这里就不再多说。

通过这些我们知道怎么实现一个 flutter app,那么看到所有有实体的元素,都是称为 Widgets, 这里为了方便理解,我们统称为组件。

Widgets

image

而组件又细分为 Stateless Widget 和 Stateful Widget,这里可以很容易联想到 react 的 无状态组件和 有状态组件

事实上 flutter 的这两种组件确实和 react 的差不多

我们首先看下 无状态组件(StatelessWidget)

无状态组件 StatelessWidget

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Hello App',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Hello'),
        ),
        body: Center(
            child: Text(
          'Hello Flutter',
          style: TextStyle(fontSize: 30),
        )),
      ),
    );
  }
}

  • StatelessWidget 对应 react 的函数组件 Functional Component
  • 此处的 build 方法对应 react 的 render 方法

再来看看 状态组件(StatefulWidget)

状态组件 StatefulWidget

Flutter 的状态由两个类组成: 分别是 StatefulWidget 和 State 类。 写法虽然不同,但是概念都大同小异:

  • StatefulWidget 对应 React.Component
  • StatefulWidget 类管理父组件传递的 Prop
  • State 类中管理自身的 State
  • 通过 setState 更新状态
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Counter App',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Counter'),
        ),
        body: Center(
            child: Counter(10) ),
      ),
    );
  }
}


class Counter extends StatefulWidget {
  // 这个类是 state 的配置,可以在此定义父组件传递下来的 prop
  final int increaseNum;
  // 构造函数
  Counter(this.increaseNum);
  
  @override
  _CounterState createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int _counter = 0;

  void _increment() {
    print('count: $_counter');

    setState(() {
      // setState 的回调告诉 flutter 去变更 当前 State, 并且 setState() 的调用会触发 build() 从而更新视图
      _counter += widget.increaseNum;
    });
  }

  @override
  Widget build(BuildContext context) {
    // 每次调用 setState 都会触发 build 方法,同时,类似于 react 的 render 方法,
    // flutter 框架为了让 重新 build 更加快,也已经对此做了优化
    return Row(
      crossAxisAlignment: CrossAxisAlignment.center,
      mainAxisAlignment: MainAxisAlignment.center,

      children: <Widget>[
        RaisedButton(
          onPressed: _increment,
          child: Text('Increment'),
        ),
        Text('Count: $_counter'),
      ],
    );
  }
}

学过 React 的同学,是不是对此有种似曾相识的感觉呢?

生命周期

组件出来了,生命周期还远么?

类似 React ,Flutter 也有自己的组件生命周期:

  • initState: 初始化状态
  • didChangeDependencies: state依赖关系变更
  • build: 构建视图
  • didUpdateWidget: 状态变更,重新渲染视图
  • deactivate: ui 组件被暂时移除(如切换页面)
  • dispose: ui 被永久销毁

image

到此我们的 UI 组件部分就告一段落。

跨平台开发,“跨” 的除了平台 UI 部分外,还有跨了前面提到的平台系统服务调用

Native 服务调用

不管是哪一个跨平台开发的解决方案,基本上都是在UI层面去完成跨平台,一次开发运行多处,但是当你需要完成特定的功能时,比如:打开相册获取照片,这在这一层面上就无法撼动了,你依然需要使用 Native 的方式来完成。

例如 h5 本身是无法调用系统底层 API 的,在 h5 我们就会用到 jsbridge 来给 native 发送命令,从而让 native 调用系统 API。

而在 flutter, 官方提供了一些插件(plugins packages)来实现常用的功能,例如:本地图片选择,相机功能等,让我们能够简单直接地使用到不同平台的系统接口。

这里也提供了一个唤起相机的 demo :

import 'package:image_picker/image_picker.dart';

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  File _image;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Image Picker Example'),
      ),
      body: Center(
        child: _image == null
            ? Text('No image selected.')
            : Image.file(_image),
      ),
      // 点击按钮进行拍照
      floatingActionButton: FloatingActionButton(
        onPressed: getImage,
        tooltip: 'Pick Image',
        child: Icon(Icons.add_a_photo),
      ),
    );
  }


  Future getImage() async {
    // 打开相机拍摄,并获得图片资源
    var image = await ImagePicker.pickImage(source: ImageSource.camera);

    setState(() {
      _image = image;
    });
  }

}

那么 插件是怎么调用 系统服务的呢?这里就需要用到 flutter 的 methodchannel / platform channel 通信机制。

Native 服务调用-实现自己的 Flutter Plugin

image

如图所示,flutter 通过 methodchannel 机制来调用不同平台 native 层的 api。由于代码最终会被编译成机器码,所以这个调用过程基本上和原生调用差不多,都是无损的,不像通过 bridge 方式调用,需要协议转化。

要自己实现一个 底层服务调用的 FlutterPlugin,可以参考官方文档,简单来说步骤如下:

  1. 定义 plugin package 的 API
  2. 实现不同的底层逻辑
    • Android 平台的功能实现 (Java / Kotlin)
    • iOS 平台的功能实现 (Object-C / Swift)
  3. 在 dart 使用 method channel 调用原生 api

劝退

看了那么多,是不是感觉这不像劝退,而是一篇 Flutter 吹文?

别急,这就劝退了。

Flutter 虽然看起来很强大,但是实际上深入琢磨一下,其实也有不少局限性。

国内环境复杂,小程序横行

大家都知道,国内的流量几乎都被几个大公司垄断, 而 App 的推广下载成本也很高。

所以各大公司才推出了五花八门的小程序,到目前为止,已知的有:

微信/百度/支付宝/字节跳动/ QQ 小程序以及快应用等......

为了快速引流,考虑投入产出比,小公司更愿意会用 小程序/快应用/H5 方案,而不是用获客成本更高的 App 方案。

例如京东的 taro 框架或类似的跨端小程序开发框架,就比 Flutter 更加符合中国特色。

taro 是一个多端统一开发框架,支持用 React 的开发方式编写一次代码,生成能运行在微信/百度/支付宝/字节跳动/ QQ 小程序、快应用、H5、React Native 等的应用。

放弃了 Web 生态

大家可以看到,整篇下来,除了 react-style 的设计思想之外,flutter 和前端其实关系不大

江湖传言道:一切能用js实现的应用,都将用js实现。

但是很可惜的是,基于各方面的考虑,google 选择了 dart 这门并不算热门的语言作为 flutter 的开发语言,而不是 JavaScript / Typescript。给前端开发接触 flutter 设置了一定的门槛。

但是,flutter 也不是只给我们前端用的,站在前端角度,我们当然希望用 js/ts咯。 但这对于 Android/ios 等终端开发来说,其实也是同样需要一定的成本,可以算是一视同仁了。

社区活跃度欠缺

那么又由于 dart 语言这两年才被 flutter 带起来的缘故,之前一直火不起来,直到 flutter 出来后才强行续命。至此 dart 的社区生态,开源库等等都比较欠缺,不像前端社区,有丰富的 npm 包。

那么,大家可以想下,在 flutter 之前,你有听过 dart 语言么?

google 为什么用 dart 作为 flutter 的开发语言呢?

其实是因为…… dart 也有个好爹 Orz,他的爹也是Google。

看到没有,有个好爹多么重要,三线语言 dart 这不就被捧得大红大紫了么[滑稽]

纯前端的局限性

flutter 用自绘引擎彻底解决了 UI 层面的平台差异性,但是前面也提到了,系统硬件服务(如相机蓝牙等服务)的差异性是无可避免的。

作为一个纯前端来说,理想情况下,用 flutter 可以完成所有原生能实现的功能。

但现实往往是不理想的,跨端开发往往会遇到一些平台相关的问题,如 flutter plugin的相机拍照 ,在某个型号的安卓设备上有点小bug。如果你是个纯前端,运气好的话能在开源社区找到解决方案,运气不好,只能向终端(iOS/Android)开发寻求技术支持。

那么,还需要 iOS/Android 开发来兜底的跨端开发框架,还是一个跨端开发框架么?

要开发一个成熟的 App,你敢放心交给纯前端用 flutter 负责么?

当然,这并非是 flutter 弊端,而是所有跨平台方案共同的问题。要是没这问题,react native 早就一统江湖了,airbnb 也不至于弃坑 react native了。

只要跨平台框架还存在需要程序员自行解决的平台差异bug,那么 纯前端程序员全盘负责移动端开发 就是个伪命题。

那么,是不是 flutter 就与前端绝缘了呢? 也并非如此。 如果你要开发一个重 UI 展示 ,调用系统服务比较少的简单应用,那么 flutter 是个不错的选择。

总结

事实上,可以看出,最适合用 flutter 的是哪些程序员呢?

既会 iOS 开发,又懂一些 Android 开发,这不需要太精通, 能搜索解决常见终端问题即可的程序员。那么学 flutter 就是如虎添翼了。

如果真的有前端有志于做一名 flutter 开发工程师,那么不妨简单学习下 Android 和 iOS 开发。

互联网寒冬什么人才最吃香?

多面手,综合性人才,爆栈工程师...

劝退完毕。




本文首发于 github 博客
如文章对你有帮助,你的 star 是对我最大的支持
其他文章:


插播广告:
深圳 Shopee 长期内推
岗位:前端,后端(要转go),产品,UI,测试,安卓,IOS,运维 全都要。
薪酬福利:20K-50K😳,7点下班😏,免费水果😍,免费晚餐😊,15天年假👏,14天带薪病假。 岗位详情参考PS: 网投了就不能内推了哦。
简历发邮箱:chenweiyu6909@gmail.com 或者加我微信:cwy13920