Flutter

Table of Contents

1. Flutter 简介

Flutter 是 Google 开发的一套全新的跨平台、开源 UI 框架,支持 Android、iOS 系统开发。

参考:
Flutter widget: Flutter widget index
Flutter API:Flutter API reference documentation
Flutter Samples:https://flutter.github.io/samples
在已有项目上进行混合开发,可参考:Add Flutter to existing app

1.1. 架构简介

Flutter 架构如图 1 所示。

flutter_system_overview.gif

Figure 1: Flutter system overview

一共分为三层,从下往上依次为:

  1. Embedder 层,每个平台下都不一样;
  2. Engine 层,主要包含 Skia、Dart 和 Text。Skia 是开源 2D 向量图形处理引擎;Dart 部分主要包括 Runtime、GC、编译模式支持等;Text 是文本渲染;
  3. Framework 层,应用程序围绕 Framework 层来构建。

1.2. 环境安装

下面以 Mac 系统为例,介绍一下 Flutter 环境的安装。

下载 Flutter SDK:

$ curl -O https://storage.googleapis.com/flutter_infra/releases/stable/macos/flutter_macos_v1.7.8+hotfix.4-stable.zip

解压到 ~/development 目录中:

$ cd ~/development
$ unzip ~/Downloads/flutter_macos_v1.7.8+hotfix.4-stable.zip

设置 PATH 环境变量:

$ export PATH="$PATH:${HOME}/development/flutter/bin"

提前下载开发过程中可能用到的二进制文件:

$ flutter precache --verbose

检测是否存在问题:

$ flutter doctor --verbose

注意观察上面命令输出的问题,进行下一步操作前先解决它们。

参考:https://flutter.dev/docs/get-started/install/macos

1.3. 第一个 Flutter 项目

创建 Flutter 项目:

$ flutter create myapp        # 会创建样板程序,主要源码在 lib/main.dart 中
$ cd myapp

先确保你的模拟器已经启动(注:终端中执行 open -a Simulator 可以启动 iOS 模拟器)。可以通过下面命令检测模拟器是否启动(输出显示已经启动了 Android 模拟器):

$ flutter devices             # 列出目前的设备
1 connected device:

Android SDK built for x86 • emulator-5554 • android-x86 • Android 9 (API 28) (emulator)

然后执行下面命令即可在模拟器中运行项目:

$ flutter run                 # 运行项目,如果有多个可用设备, 可以使用 -d 指定设备

项目成功运行后,会显示如图 2 所示的界面。每点击一下右下角的按钮,屏幕中间的数字就会加一,这个项目的分析可以参考节 3.1

flutter_default_app.gif

Figure 2: 第一个 Flutter 项目

参考:https://flutter.dev/docs/get-started/test-drive?tab=terminal

1.4. 生成 Android Apk

执行 flutter build apk 可以生成 Android apk,第一次时间可以比较长,请启用 --verbose 选项以查看进度:

$ flutter build apk --verbose             # 生成 Android apk

1.4.1. 不依赖 Android Studio 生成 Apk

要在服务器上自动打包 flutter 项目为 Android Apk,但服务器没有安装 X 环境,无法使用 Android Studio。下面介绍不依赖 Android Studio 情况下,编译 flutter 项目的步骤。

第一步,安装 Android SDK。

先安装“Command line tools”,再使用其中包含的工具 sdkmanager 来安装 Android SDK。

下面步骤将安装“Command line tools”,并将其解压到目录 ${HOME}/android-sdk/ 中。

$ mkdir ${HOME}/android-sdk/
$ cd ${HOME}/android-sdk/
$ curl -O https://dl.google.com/android/repository/commandlinetools-linux-5842447_latest.zip
$ unzip ~/commandlinetools-linux-5842447_latest.zip
$ ls -F ${HOME}/android-sdk/
tools/ commandlinetools-linux-5842447_latest.zip  # 可以看到 zip 解压后的内容都在 tools 目录中

安装完成后,设置下面环境变量:

# Android HOME
export ANDROID_HOME=${HOME}/android-sdk
export PATH=$ANDROID_HOME/tools:$PATH
export PATH=$ANDROID_HOME/tools/bin:$PATH
export PATH=$ANDROID_HOME/platform-tools:$PATH

再通过 sdkmanager 安装 Android SDK(下面安装的是 Android-29,如果有更新的版本,你可以安装更新的版本):

$ sdkmanager --sdk_root=${HOME}/android-sdk "platforms;android-29"
$ sdkmanager --sdk_root=${HOME}/android-sdk "platform-tools"
$ sdkmanager --sdk_root=${HOME}/android-sdk "patcher;v4"
$ sdkmanager --sdk_root=${HOME}/android-sdk "build-tools;29.0.2"

安装完成后,执行下面命令以接受所有 Licenses:

$ sdkmanager --licenses

第二步,安装 Flutter SDK。

前面已经介绍过,这里省略,假定 flutter 命令已经可用。

执行下面命令在 Flutter 中配置 Android SDK 的位置:

$ flutter config --android-sdk ${HOME}/android-sdk

现在环境都已经准备好,进入到 Flutter 项目根目录中,运行 flutter build apk --verbose 即可打包项目为 Android Apk 了。

参考:https://dev.to/jay_js/setting-up-flutter-without-android-studio-olo

2. Hello World 程序分析

下面是 Flutter 版本的 Hello World 程序(把 flutter create myapp 生成的 lib/main.dart 改为下面内容即可):

// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Welcome to Flutter',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Welcome to Flutter'),
        ),
        body: Center(
          child: Text('Hello World'),
        ),
      ),
    );
  }
}

说明:

  1. 上面例子中,创建了一个 MaterialApp ,Material 是 Google 推出的应用程序视觉设计语言,Flutter 中提供了大量的 Material Widget。
  2. 这个应用程序扩展自 StatelessWidget, 在 Flutter 中,几乎所有东西都是 Widget,如 alignment, padding, and layout 都是 Widget。
  3. Scaffold Widget 来自于 Material 库,可以认为是一个脚手架,它提供了侧边栏,title,还有底部的 sheets 等等。
  4. Widget 的主要工作都在其 build() 方法中。
  5. 例子中的 body 包含了一个 Center Widget(用于居中布局),而 Center Widget 包含了一个 Text Widget(用于显示文本)。

上面代码中,语法 MaterialApp(title: xx, home: yy) 是调用 MaterialApp 类的构造函数,这个构造函数采用了 Named parameters ,所以调用时要指定参数名称(如 title,home 等等)。在 Dart 2.0 中,调用构造函数时关键字 new 可以省略,如果不省略(即例子中 MaterialApp/Scaffold/AppBar/Text/Center 前面都增加一个 new 关键字),则看起来很繁琐。

MaterialApp 的构造函数还有很多其它参数,通过设置它们,可以改变其行为。例如指定参数 theme 可改变 MaterialApp 的主题颜色(如红色):

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Welcome to Flutter',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Welcome to Flutter'),
        ),
        body: Center(
          child: Text('Hello World'),
        ),
      ),
      theme: ThemeData(            // 和前面相比,增加了从本行开始往下的三行代码
        primaryColor: Colors.red,
      ),
    );
  }
}

执行上面代码,得到图 3 所示程序,它的主题颜色改为了红色(不再是默认的蓝色)。

flutter_hello_world.gif

Figure 3: Hello World(主题颜色改为了红色)

3. Widgets

Flutter 中,几乎一切都是 Widget,Widget 采用现代“响应式”框架构建,借鉴于前端框架 React。其中心思想是用 Widget 构建你的 UI。 Widget 描述了他们的视图在给定其当前配置和状态时应该看起来像什么,当 Widget 的状态发生变化时,Widget 会重新构建 UI,Flutter 会对比前后变化的不同,以确定底层渲染树从一个状态转换到下一个状态所需的最小更改(注:类似于 React/Vue 中虚拟 DOM 的 diff 算法)。

当我们编写应用程序时,往往是创建新的 Widget,它继承于 StatelessWidget(无状态组件)或者 StatefulWidget(有状态组件,即可以包含变化的数据)。Widget 的主要工作都在其 build() 函数中,在这个函数中定义其所包含的子组件。

StatelessWidget 的使用实例可以参考前面介绍的 Hello World,下面将介绍 StatefulWidget 的使用实例。

3.1. StatefulWidget

Flutter 中用 StatefulWidget 来表示“有状态组件”。下面就是使用命令 flutter create myapp 创建的项目的主要源码:

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  // This widget is the home page of your application. It is stateful, meaning
  // that it has a State object (defined below) that contains fields that affect
  // how it looks.

  // This class is the configuration for the state. It holds the values (in this
  // case the title) provided by the parent (in this case the App widget) and
  // used by the build method of the State. Fields in a Widget subclass are
  // always marked "final".

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      // This call to setState tells the Flutter framework that something has
      // changed in this State, which causes it to rerun the build method below
      // so that the display can reflect the updated values. If we changed
      // _counter without calling setState(), then the build method would not be
      // called again, and so nothing would appear to happen.
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called, for instance as done
    // by the _incrementCounter method above.
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    return Scaffold(
      appBar: AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text(widget.title),
      ),
      body: Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: Column(
          // Column is also a layout widget. It takes a list of children and
          // arranges them vertically. By default, it sizes itself to fit its
          // children horizontally, and tries to be as tall as its parent.
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

MyHomePage 有状态,它继承于 StatefulWidget。你可能会奇怪,为什么 StatefulWidget (MyHomePage) 和 State (_MyHomePageState) 被分为两个组件。 在 Flutter 中, StatefulWidgetState 具有不同的生命周期, Widget 是临时对象,用于构建应用程序的当前展示形式;而 State 对象是持久的,其数据在其 build() 方法多次调用之间不会丢失。

对状态的修改必须在 setState() 中进行,随后 build() 会自动执行。Flutter 中的状态控制如图 4 所示。

flutter_state_lifecycle.gif

Figure 4: State Lifecycle

4. Layouts

前面提到过,在 Flutter 中,几乎一切都是 Widget,布局类也是 Widget。

5 所示的界面,如果显示其布局类,则如图 6 所示。

flutter_layout_example.gif

Figure 5: 3 icons with a label under each one

flutter_layout_example_visual.gif

Figure 6: 3 icons with a label under each one

其相应的 Widget 树如图 7 所示。

flutter_layout_tree.gif

Figure 7: Diagram of the widget tree

Container 是一个重要的布局类,支持 padding, margins, borders, or background color 等等。

参考:https://flutter.dev/docs/development/ui/layout

Author: cig01

Created: <2019-01-30 Wed>

Last updated: <2020-05-30 Sat>

Creator: Emacs 27.1 (Org mode 9.4)