Contents

给 React Native 开发者的 Flutter 指南

Contents

本文面向希望基于现有的 React Native(下文统称 RN)的知识结构使用 Flutter 开发移动端应用的开发者。如果你已经对 RN 的框架有所了解,那么你可以通过这个文档入门 Flutter 开发。

本文可以当做查询手册使用,里面涉及到的问题基本上可以满足需求。

针对 JavaScript 开发者的 Dart 介绍

与 RN 一样,Flutter 使用响应式风格的界面编写方式。然而,RN 需要被转译为本地对应的 widget,而 Flutter 是直接编译成原生代码运行。 Flutter 可以控制屏幕上的每一个像素,由此可以避免使用 JavaScript Bridge 导致的性能问题。

Dart 学习起来非常简单,包含如下特性:

  • 它针对 web 服务和移动应用开发提供了一种开源的,可扩展的编程语言。

  • 它提供了一种面向对象的单继承语言,使用 C 语言风格的语法并且可通过 AOT 编译为本地代码。

  • 可转译为 JavaScript 代码。

  • 支持接口和抽象类。

下面的几个例子解释了 JavaScript 和 Dart 的区别。

入口函数

JavaScript 并没有预定义的入口函数。

// JavaScript
function startHere() {
  // Can be used as entry point
}

在 Dart 里,每个应用程序必须有一个最顶级的 main() 函数,该函数作为应用程序的入口函数。

/// Dart
void main() {}

可以在这里查看效果 DartPad

在控制台打印输出

在 Dart 中如果需要在控制台进行输出,调用 print()

// JavaScript
console.log('Hello world!');
/// Dart
print('Hello world!');

可以在这里查看效果 DartPad

变量

Dart 是类型安全的,它会结合静态类型检查和运行时检查,来保证变量的值总是和变量的静态类型相匹配。虽然类型是语法要求,有些类型标注也并不是必须要填的,因为 Dart 使用类型推断。

创建变量并赋值

在 JavaScript 中,变量是无法指定类型的。

Dart 中,变量可以显式定义类型,或者类型系统自动判断变量的类型。

// JavaScript
var name = 'JavaScript';
/// Dart
/// Both variables are acceptable.
String name = 'dart'; // Explicitly typed as a [String].
var otherName = 'Dart'; // Inferred [String] type.

可以在这里查看效果 DartPad

如果想了解更多相关信息,请参考 Dart 的类型系统

默认值

在 JavaScript 中,未初始化的变量是 undefined

在 Dart 中,未初始化的变量会有一个初始值 null。因为数字在 Dart 是对象,甚至未初始化的数字类型的变量也会是 null

// JavaScript
var name; // == undefined
// Dart
var name; // == null; raises a linter warning
int? x; // == null

可以在这里查看效果 DartPad

如果想了解更多详细内容,请查看这个文档 variables

检查 null 或者零值

在 JavaScript 中,1 或者任何非空对象在使用 == 比较操作符时都会被隐式转换为 true

// JavaScript
var myNull = null;
if (!myNull) {
  console.log('null is treated as false');
}
var zero = 0;
if (!zero) {
  console.log('0 is treated as false');
}

在 Dart 中,只有布尔类型值 true 才是 true。

/// Dart
var myNull;
var zero = 0;
if (zero == 0) {
  print('use "== 0" to check zero');
}

可以在这里查看效果 DartPad

函数

Dart 和 JavaScript 中的函数很相似。最大的区别是声明格式。

// JavaScript
function fn() {
  return true;
}
/// Dart
/// You can explicitly define the return type.
bool fn() {
  return true;
}

可以在这里查看效果 DartPad

如果想了解更多相关信息,可以参考 函数 介绍。

异步编程

Futures

Dart 与 JavaScript 类似,同样是单线程模型。在 JavaScript 中,Promise 对象代表异步操作的完成或者失败。

Dart 使用 Future 对象来实现该机制。

// JavaScript
class Example {
  _getIPAddress() {
    const url = 'https://httpbin.org/ip';
    return fetch(url)
      .then(response => response.json())
      .then(responseJson => {
        const ip = responseJson.origin;
        return ip;
      });
  }
}

function main() {
  const example = new Example();
  example
    ._getIPAddress()
    .then(ip => console.log(ip))
    .catch(error => console.error(error));
}

main();
/// Dart
import 'dart:convert';

import 'package:http/http.dart' as http;

class Example {
  Future<String> _getIPAddress() {
    final url = Uri.https('httpbin.org', '/ip');
    return http.get(url).then((response) {
      String ip = jsonDecode(response.body)['origin'];
      return ip;
    });
  }
}

void main() {
  final example = Example();
  example
      ._getIPAddress()
      .then((ip) => print(ip))
      .catchError((error) => print(error));
}

如果想了解更多相关信息,请参考 Future 的相关文档。

asyncawait

async 函数声明定义了一个异步执行的函数。

在 JavaScript 中, async 函数返回一个 Promiseawait 操作符用于等待 Promise

// JavaScript
class Example {
  async function _getIPAddress() {
    const url = 'https://httpbin.org/ip';
    const response = await fetch(url);
    const json = await response.json();
    const data = json.origin;
    return data;
  }
}

async function main() {
  const example = new Example();
  try {
    const ip = await example._getIPAddress();
    console.log(ip);
  } catch (error) {
    console.error(error);
  }
}

main();

在 Dart 中,async 函数返回一个 Future,而函数体会在未来执行, await 操作符用于等待 Future

// Dart
import 'dart:convert';

import 'package:http/http.dart' as http;

class Example {
  Future<String> _getIPAddress() async {
    final url = Uri.https('httpbin.org', '/ip');
    final response = await http.get(url);
    String ip = jsonDecode(response.body)['origin'];
    return ip;
  }
}

/// An async function returns a `Future`.
/// It can also return `void`, unless you use
/// the `avoid_void_async` lint. In that case,
/// return `Future<void>`.
void main() async {
  final example = Example();
  try {
    final ip = await example._getIPAddress();
    print(ip);
  } catch (error) {
    print(error);
  }
}

如果想了解更多相关信息,请参考 asyncawait 的相关文档

基本知识

如何创建一个 Flutter 应用?

如果要创建 RN 应用,你需要在命令行里运行 create-react-native-app

$ create-react-native-app <projectname>

要创建 Flutter 应用,完成下面其中一项即可:

  • 使用带有 Flutter 和 Dart 插件的 IDE。

  • 在命令行中运行命令 flutter create。不过要提前确认 Flutter SDK 已经在系统环境变量 PATH 中定义。

$ flutter create <projectname>

如果想要了解更多内容,详见 开始使用 Flutter,在该页面会手把手教你创建一个点击按钮进行计数的应用。创建一个 Flutter 项目就可以构建 Android 和 iOS 设备上运行应用所需的所有文件。

我如何运行应用呢?

你可以在 RN 的项目文件夹中运行 npm run 或者 yarn run 以运行应用。

而想运行 Flutter 应用,你可以通过如下几个途径进行操作:

  • 在带有 Flutter 和 Dart 插件的 IDE 中使用 “run” 选项。

  • 在项目根目录运行 flutter run

你的应用程序会在已连接的设备、iOS 模拟器或者 Android 模拟器上运行。

如果想了解更多相关信息,可以参考 Flutter 的相关文档: 开始使用 Flutter

如何导入 widget

在 RN 中,你需要导入每一个所需的组件。

// React Native
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';

在 Flutter 中,如果要使用 Material Design 库里的 widget,导入 material.dart package。如果要使用 iOS 风格的 widget,导入 Cupertino 库。如果要使用更加基本的 widget,导入 Widgets 库。或者,你可以实现自己的 widget 库并导入。

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:my_widgets/my_widgets.dart';

无论你导入哪个库,Dart 仅仅引用你应用中用到的 widget。

如果想了解更多相关信息,可以参考 核心 Widget 目录

在 Flutter 里有没有类似 React Native 中 “Hello world!” 应用程序?

在 RN 里,HelloWorldApp 继承自 React.Component 并且通过返回 view 对象实现了 render 方法。

// React Native
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';

export default class App extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <Text>Hello world!</Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center'
  }
});

在 Flutter 中,你可以使用核心 widget 库中的 CenterText widget 创建对应的「Hello world!」应用程序。 Center widget 是 widget 树中的根节点,而且只有 Text 一个子 widget。

/// Flutter
import 'package:flutter/material.dart';

void main() {
  runApp(
    const Center(
      child: Text(
        'Hello, world!',
        textDirection: TextDirection.ltr,
      ),
    ),
  );
}

下面的图片展示了 Android 和 iOS 中的基本 Flutter “Hello world!” 应用程序的界面。

Hello world app on Android
Android
Hello world app on iOS
iOS

现在大家已经明白了最基本的 Flutter 应用,接下来会告诉大家如何利用 Flutter 丰富的 widget 库来创建主流的华丽的应用程序。

我如何使用 widget 并且把它们封装起来组成一个 widget 树?

在 Flutter 中,几乎任何元素都是 widget。

Widget 是构建应用软件用户界面的基本元素。你可以将 widget 按照一定的层次组合,成为 widget 树。每个 widget 内嵌在父 widget 中,并且继承了父 widget 的属性,甚至应用程序本身就是一个 widget。并没有一个独立的应用程序对象。反而根 widget 充当了这个角色。

一个 widget 可以定义为:

  • 类似按钮或者菜单的结构化元素

  • 类似字体或者颜色方案的风格化元素

  • 类似填充区或者对齐元素布局元素

下面的示例展示了使用 Material 库里 widget 实现的「Hello world!」应用程序。在这个示例中,该 widget 树是包含在 MaterialApp root widget 里的。

/// Flutter
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

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

下面的图片为大家展示了通过 Material Design widget 所实现的「Hello world!」应用。你可以获得比「Hello world!」应用更多的功能。

Hello world app on Android
Android
Hello world app on iOS
iOS

当编写应用代码的时候,你将用到下述两种 widget: 无状态 widget 就像它的名字一样,是一个没有状态的 widget。无状态 widget 一旦创建,就不会改变。而 有状态 widget 会基于接收到的数据或者用户输入的数据动态改变状态。

无状态 widget 和有状态 widget 之间的主要区别,是有状态 widget 包含一个 State 对象会缓存状态数据,并且 widget 树的重建也会携带该数据,因此状态不会丢失。

在简单的或者基本的应用程序中,封装 widget 非常简单,但是随着代码量的增加并且应用程序的功能变得更加复杂,你应该将层级复杂的 widget 封装到函数中或者稍小一些的类。创建独立的函数和 widget 可以让你更好地复用应用中组件。

如何创建可复用的组件?

在 RN 中,你可以定义一个类来创建一个可复用的组件然后使用 props 方法来设置或者返回属性或者所选元素的值。在下面的示例中,CustomCard 类在父类中被定义和调用。

// React Native
class CustomCard extends React.Component {
  render() {
    return (
      <View>
        <Text> Card {this.props.index} </Text>
        <Button
          title="Press"
          onPress={() => this.props.onPress(this.props.index)}
        />
      </View>
    );
  }
}

// Usage
<CustomCard onPress={this.onPress} index={item.key} />

在 Flutter 中,定义一个类来创建一个自定义 widget 然后复用这个 widget。你可以定义并且调用函数来返回一个可复用的 widget,正如下面示例中 build 函数所示的那样。

/// Flutter
class CustomCard extends StatelessWidget {
  const CustomCard({
    super.key,
    required this.index,
    required this.onPress,
  });

  final int index;
  final void Function() onPress;

  @override
  Widget build(BuildContext context) {
    return Card(
        child: Column(
      children: <Widget>[
        Text('Card $index'),
        TextButton(
          onPressed: onPress,
          child: const Text('Press'),
        ),
      ],
    ));
  }
}

class UseCard extends StatelessWidget {
  const UseCard({super.key, required this.index});

  final int index;

  @override
  Widget build(BuildContext context) {
    /// Usage
    return CustomCard(
      index: index,
      onPress: () {
        print('Card $index');
      },
    );
  }
}

在之前的示例中,CustomCard 类的构造函数使用 Dart 的花括号 { } 来表示 已命名参数

如果将这些参数设定为必填参数,要么从构造函数中删掉曲括号,或者在构造函数中加上 required

下面的截图展示了可复用的 CustomCard 类的示例:

Custom cards on Android
Android
Custom cards on iOS
iOS

项目结构和资源

该从哪开始写代码呢?

main.dart 文件开始。这个文件会在你创建 Flutter 应用时自动生成。

// Dart
void main() {
  print('Hello, this is the main function.');
}

在 Flutter 中,入口文件是 {项目目录}/lib/main.dart 而程序执行是从 main 函数开始的。

Flutter 应用程序中的文件是如何组织的?

当你创建一个新的 Flutter 工程的时候,它会创建如下所示的文件夹结构。你可以自定义这个结构,不过这是整个开发的起点。

┬
└ project_name
  ┬
  ├ android      - Contains Android-specific files.
  ├ build        - Stores iOS and Android build files.
  ├ ios          - Contains iOS-specific files.
  ├ lib          - Contains externally accessible Dart source files.
    ┬
    └ src        - Contains additional source files.
    └ main.dart  - The Flutter entry point and the start of a new app.
                   This is generated automatically when you create a Flutter
                    project.
                   It's where you start writing your Dart code.
  ├ test         - Contains automated test files.
  └ pubspec.yaml - Contains the metadata for the Flutter app.
                   This is equivalent to the package.json file in React Native.
┬
└ 项目目录
  ┬
  ├ android      - 包含 Android 相关文件。
  ├ build        - 存储 iOS 和 Android 构建文件。
  ├ ios          - 包含 iOS 相关文件。
  ├ lib          - 包含外部可访问 Dart 源文件。
    ┬
    └ src        - 包含附加源文件。
    └ main.dart  - Flutter 程序入口和新应用程序的起点。当你创建 Flutter 工程的时候会自动生成这些文件。你从这里开始写 Dart 代码
  ├ test         - 包含自动测试文件。
  └ pubspec.yaml - 包含 Flutter 应用程序的元数据。这个文件相当于 RN 里的 package.json 文件。

我该把资源文件放到哪并且如何调用呢?

一个 Flutter 资源就是打包到你应用程序里的一个文件并且在程序运行的时候可以访问。 Flutter 应用程序可以包含下述几种资源类型:

  • 类似 JSON 文件的静态数据

  • 配置文件

  • 图标和图片(JPEG、PNG、GIF、WebP、BMP 和 WBMP)

Flutter 使用 pubspec.yaml 文件来确定应用程序中的资源。该文件在工程的根目录。

flutter:
  assets:
    - assets/my_icon.png
    - assets/background.png

assets 确定了需要包含在应用程序中的文件。每个资源都会在 pubspec.yaml 中定义所存储的相对路径。资源定义的顺序没有特殊要求。实际的文件夹(在这里指 assets )也没影响。但是,由于资源可以放置于程序的任何目录,所以放在 assets 文件夹是比较好的。

在构建期间,Flutter 会将资源放到一个称为 asset bundle 的归档文件中,应用程序可以在运行时访问该文件。当一个资源在 pubspec.yaml 中被声明时,构建进程会查询和这个文件相关的子文件夹路径,这些文件也会被包含在 asset bundle 中。当你为应用程序选择和屏幕显示分辨率相关的图片时, Flutter 会使用资源变体。

在 RN 中,你可以在源码文件夹中通过添加文件来增加一个静态图片并且在代码中引用它。

<Image source={require('./my-icon.png')} />

在 Flutter 中,如果要展示静态资源图片,在 widget 的 build 方法中使用 Image.asset 构造即可。

Image.asset('assets/background.png');

如果想了解更多相关信息,请参考文档 在 Flutter 中添加资源和图片

如何在网络中加载图片?

在 RN 中,你可以在 Imagesource 属性中设置 uri 和所需的尺寸。

在 Flutter 中,使用 Image.network 构造函数来实现通过地址加载图片的操作。

Image.network('https://docs.flutter.dev/assets/images/docs/owl.jpg');

我如何安装依赖包和包插件?

Flutter 支持使用开发者向 Flutter 和 Dart 生态系统贡献的 package。这样可以使大量开发者快速构建应用程序而无需重复造车轮。而平台相关的 package 就被称为插件。

在 RN 中,你可以在命令行中运行 yarn add {package-name} 或者 npm install --save {package-name} 来安装代码包。

在 Flutter 中,安装 package 需要按照如下的步骤:

  1. pubspec.yaml 的 dependencies 区域添加包名和版本。下面的例子向大家展示了如何将 google_sign_in 的 Dart package 添加到 pubspec.yaml 中。一定要检查一下 YAML 文件中的空格,因为 空格很重要!

dependencies:
  flutter:
    sdk: flutter
  google_sign_in: ^3.0.3
  1. 在命令行中输入 flutter pub get 来安装代码包。如果使用 IDE,它自己会运行 flutter pub get,或者它会提示你是不是要运行该命令。

  2. 向下面代码一样在程序中引用 package:

import 'package:flutter/material.dart';

如果想了解更多相关信息,请参考 在 Flutter 里使用 PackagesFlutter Packages 的开发和提交

你可以在 Flutter packagespub.dev 找到开发者们分享的 package。

Flutter widgets

在 Flutter 中,你可以基于 widget 打造你自己的 UI,通过 widget 当前的设置和状态会呈现相应的页面效果。

Widget 常常通过很多小的、单一功能的 widget 组成,通过这样的封装往往能够实现很棒的效果。比如,Container widget 包含多种 widget,分别负责布局、绘图、位置变化和尺寸变化。准确的说,Container widget 包括 LimitedBoxConstrainedBoxAlignPaddingDecoratedBoxTransform widget。与其继承 Container 来实现自定义效果,不如直接修改这些 widget 来实现效果。

Center widget 是另一个用于控制布局的示例。如果要居中一个 widget,就把它封装到 Center widget 中,然后使用布局 widget 来进行对齐行、列和网格。这些布局 widget 并不可见。而他们的作用就是控制其它 widget 的布局。如果想搞清楚为什么一个 widget 会有这样的效果,有效的方法是研究它临近的 widget。

如果想了解更多相关信息,请参考 Flutter 技术概览

如果想了解更多关于 Widgets 包中的核心 widget,请参考 基础 Flutter Widgets核心 Widget 目录 或是 Flutter Widget 目录

视图

View 等价容器的是什么?

在 RN 中, View 是支持 Flexbox 布局、风格化、触摸事件处理和访问性控制的容器。

在 Flutter 中,你可以使用 Widgets 库中的核心布局 widget,比如 ContainerColumnRowCenter。如果想了解更多相关信息,请参考 布局类 Widgets 目录。

FlatList 或者 SectionList 相对应的是什么?

List 是一个可以滚动的纵向排列的组件列表。

在 RN 中,FlatList 或者 SectionList 用于渲染简单的或者分组的列表。

// React Native
<FlatList
  data={[ ... ]}
  renderItem={({ item }) => <Text>{item.key}</Text>}
/>

ListView 是 Flutter 最常用的滑动 widget。默认构造函数需要一个数据列表的参数。 ListView 非常适合用于少量子 widget 的列表。如果列表的元素比较多,可以使用 ListView.builder,它会按需构建子项并且只创建可见的子项。

var data = [
  'Hello',
  'World',
];
return ListView.builder(
  itemCount: data.length,
  itemBuilder: (context, index) {
    return Text(data[index]);
  },
);
Flat list on Android
Android
Flat list on iOS
iOS

如果要了解如何实现无限滑动列表,请参考 infinite_list 示例应用。

如何使用 Canvas 绘图?

在 RN 中,canvas 组件是不可见的,所以需要使用类似 react-native-canvas 这样的组件。

// React Native
handleCanvas = canvas => {
  const ctx = canvas.getContext('2d');
  ctx.fillStyle = 'skyblue';
  ctx.beginPath();
  ctx.arc(75, 75, 50, 0, 2 * Math.PI);
  ctx.fillRect(150, 100, 300, 300);
  ctx.stroke();
};

render() {
  return (
    <View>
      <Canvas ref={this.handleCanvas} />
    </View>
  );
}

在 Flutter 中,你可以使用 CustomPaintCustomPainter 在画布上进行绘制。

下面的示例代码展示了如何使用 CustomPaint 进行绘图。它实现了抽象类 CustomPainter,然后将它赋值给 CustomPainter 的 painter 属性。 CustomPainter 子类必须实现 paintshouldRepaint 方法。

class MyCanvasPainter extends CustomPainter {
  const MyCanvasPainter();

  @override
  void paint(Canvas canvas, Size size) {
    final Paint paint = Paint()..color = Colors.amber;
    canvas.drawCircle(const Offset(100.0, 200.0), 40.0, paint);
    final Paint paintRect = Paint()..color = Colors.lightBlue;
    final Rect rect = Rect.fromPoints(
      const Offset(150.0, 300.0),
      const Offset(300.0, 400.0),
    );
    canvas.drawRect(rect, paintRect);
  }

  @override
  bool shouldRepaint(MyCanvasPainter oldDelegate) => false;
}

class MyCanvasWidget extends StatelessWidget {
  const MyCanvasWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: CustomPaint(painter: MyCanvasPainter()),
    );
  }
}
Canvas on Android
Android
Canvas on iOS
iOS

布局

如何使用 widget 来定义布局属性?

在 RN 中,大多数布局需要通过向指定的组件传递属性参数进行设置。比如,你可以使用 Viewstyle 来设置 flexbox 属性。如果要整理一列的组件,你可以使用如下的属性设置:flexDirection: “column”

// React Native
<View
  style={{
    flex: 1,
    flexDirection: 'column',
    justifyContent: 'space-between',
    alignItems: 'center'
  }}
>

在 Flutter 中,布局主要是由专门的 widget 定义的,它们同控制类 widget 和样式属性一起发挥功能。

比如,ColumnRow widget 接受一个数组的子元素并且分别按照纵向和横向进行排列。 Container widget 包含布局和样式属性的组合, Center widget 会将其自 widget 也设定居中。

@override
Widget build(BuildContext context) {
  return Center(
    child: Column(
      children: <Widget>[
        Container(
          color: Colors.red,
          width: 100.0,
          height: 100.0,
        ),
        Container(
          color: Colors.blue,
          width: 100.0,
          height: 100.0,
        ),
        Container(
          color: Colors.green,
          width: 100.0,
          height: 100.0,
        ),
      ],
    ),
  );

Flutter 在核心 widget 库中提供多种不同的布局 widget。比如 PaddingAlignStack

要得到完整的 widget 列表,请参考 Layout Widgets

Layout on Android
Android
Layout on iOS
iOS

如何为 widget 分层?

在 RN 中,组件可以通过 absolute 划分层次。

在 Flutter 中使用 Stack widget 将子 widget 进行分层。该 widget 可以将整体或者部分的子 widget 进行分层。

Stack widget 将子 widget 根据容器的边界进行布局。如果你仅仅想把子 widget 重叠摆放的话,这个 widget 非常合适。

@override
Widget build(BuildContext context) {
  return Stack(
    alignment: const Alignment(0.6, 0.6),
    children: <Widget>[
      const CircleAvatar(
        backgroundImage: NetworkImage(
          'https://avatars3.githubusercontent.com/u/14101776?v=4',
        ),
      ),
      Container(
        color: Colors.black45,
        child: const Text('Flutter'),
      ),
    ],
  );

上面的示例代码使用 Stack 将一个 Container (Text 显示在一个半透明的黑色背景上)覆盖在一个 CircleAvatar 上。 Stack 使用对齐属性和 Alignment 坐标微调文本。

Stack on Android
Android
Stack on iOS
iOS

如果想了解更多相关信息,请参考 Stack 类的文档。

风格化

如何设置组件的风格?

在 RN 中,内联风格化和 stylesheets.create 可以用于设置组件的风格。

// React Native
<View style={styles.container}>
  <Text style={{ fontSize: 32, color: 'cyan', fontWeight: '600' }}>
    This is a sample text
  </Text>
</View>

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center'
  }
});

在 Flutter 中, Text widget 可以接受 TextStyle 作为它的风格化属性。如果你想在不同的场合使用相同的文本风格,你可以创建一个 TextStyle 类,并且在多个 Text widget 中使用它。

const TextStyle textStyle = TextStyle(
  color: Colors.cyan,
  fontSize: 32.0,
  fontWeight: FontWeight.w600,
);

return const Center(
  child: Column(
    children: <Widget>[
      Text('Sample text', style: textStyle),
      Padding(
        padding: EdgeInsets.all(20.0),
        child: Icon(
          Icons.lightbulb_outline,
          size: 48.0,
          color: Colors.redAccent,
        ),
      ),
    ],
  ),
);
Styling on Android
Android
Styling on iOS
iOS

我如何使用 IconsColors 呢?

RN 并不包含默认图标,所以需要使用第三方库。

在 Flutter 中,引用 Material 库的时候就同时引入了 Material iconscolors

return const Icon(Icons.lightbulb_outline, color: Colors.redAccent);

当使用 Icons 类时,确保在项目的 pubspec.yaml 文件中设置 uses-material-design: true,这样保证 MaterialIcons 相关字体被包含在你的应用中。一般来说,如果你想用 Material 库的话,则需要包含这一行内容。

name: my_awesome_application
flutter:
  uses-material-design: true

Flutter 的 Cupertino (iOS-style) package 为 iOS 设计语言提供高分辨率的 widget。要使用 CupertinoIcons 字体,在项目的 pubspec.yaml 文件中添加 cupertino_icons 的依赖即可。

name: my_awesome_application
dependencies:
  cupertino_icons: ^0.1.0

要在全局范围内自定义组件的颜色和风格,使用 ThemeData 为不同的主题指定默认颜色。在 MaterialApp 的主题属性中设置 ThemeData 对象。 Colors 类提供 Material Design color palette 中所提供的颜色配置。

下面的示例代码将主色调设置为 blue 然后文本颜色设置为 red

class SampleApp extends StatelessWidget {
  const SampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
          primarySwatch: Colors.blue,
          textSelectionTheme:
              const TextSelectionThemeData(selectionColor: Colors.red)),
      home: const SampleAppPage(),
    );
  }
}

如何增加风格化主题?

在 React Native,常用主题都定义在样式层叠表中。

在 Flutter 中,为所有组件创建统一风格可以在 ThemeData 类中定义,并将它赋值给 MaterialApp 的主题属性。

@override
Widget build(BuildContext context) {
  return MaterialApp(
    theme: ThemeData(
      primaryColor: Colors.cyan,
      brightness: Brightness.dark,
    ),
    home: const StylingPage(),
  );
}

Theme 可以在不使用 MaterialApp widget 的情况下使用。 Theme 接受一个 ThemeData 参数,并且将 ThemeData 应用于它的全部子 widget。

@override
Widget build(BuildContext context) {
  return Theme(
    data: ThemeData(
      primaryColor: Colors.cyan,
      brightness: brightness,
    ),
    child: Scaffold(
      backgroundColor: Theme.of(context).primaryColor,
      //...
    ),
  );
}

状态管理

当 widget 被创建或者在 widget 的生命周期中有信息发生改变时所产生的信息叫做状态。要在 Flutter 中管理应用程序的状态,使用 StatefulWidget 和 State 对象。

欲知更多关于 Flutter 的状态管理相关的内容,请参访 状态管理文档 页面。

管理 StatelessWidget widget

StatelessWidget 在 Flutter 中是一个不需要状态改变的 widget,它没有内部的状态。

当你展现给用户的界面并不依赖其它任何配置信息并且使用 BuildContext 来解析 widget,则需要使用无状态 widget。

AboutDialogCircleAvatarTextStatelessWidget 的子类,并且是很典型的无状态 widget。

import 'package:flutter/material.dart';

void main() => runApp(
      const MyStatelessWidget(
        text: 'StatelessWidget Example to show immutable data',
      ),
    );

class MyStatelessWidget extends StatelessWidget {
  const MyStatelessWidget({
    super.key,
    required this.text,
  });

  final String text;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text(
        text,
        textDirection: TextDirection.ltr,
      ),
    );
  }
}

在上面的例子中,你用到了 MyStatelessWidget 类的构造函数来传递 text。并且它被标记为 final。该类继承了 StatelessWidget,它包含不可变的数据。

无状态 widget 的 build 方法通常只有在三种情况下会被调用:

  • 当 widget 被插入到 widget 树中;

  • 当 widget 的父 widget 改变了配置;

  • 当所依赖的 InheritedWidget 发生了改变。

The StatefulWidget

StatefulWidget widget

StatefulWidget 是携带状态变化的 widget。通过调用 setState 方法可以管理 StatefulWidget 的状态。当调用 setState() 的时候,程序会通知 Flutter 框架有状态发生了改变,然后会重新运行 build() 方法来更新应用的状态。

状态 是在 widget 被创建期间可以被同步读取的信息,并且在 widget 的生命周期中会发生改变。实现该 widget 的时候要注意保证党状态发生改变的时候程序能够获得相应的提醒。当 widget 能够动态改变的时候,请使用 StatefulWidget。比如,某个 widget 会随着用户填写表单或者移动滑块的时候发生改变。亦或者随着数据源更新的时候发生改变。

CheckboxRadioSliderInkWellForm、和 TextField 都是有状态的 widget,是 StatefulWidget 的子类。

下面的示例代码声明了一个 StatefulWidget,需要实现 createState() 方法。该方法创建一个对象来管理 widget 的状态,也就是 _MyStatefulWidgetState

class MyStatefulWidget extends StatefulWidget {
  const MyStatefulWidget({
    super.key,
    required this.title,
  });

  final String title;

  @override
  State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}

下面的状态类,_MyStatefulWidgetState,实现了 build() 方法。当状态发生改变的时候,比如说用户点击了开关按钮,这时 setState 就会被调用,并且将新的开关状态传进来。这就会使整体框架重构这个 widget。

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  bool showText = true;
  bool toggleState = true;
  Timer? t2;

  void toggleBlinkState() {
    setState(() {
      toggleState = !toggleState;
    });
    if (!toggleState) {
      t2 = Timer.periodic(const Duration(milliseconds: 1000), (t) {
        toggleShowText();
      });
    } else {
      t2?.cancel();
    }
  }

  void toggleShowText() {
    setState(() {
      showText = !showText;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          children: <Widget>[
            if (showText)
              const Text(
                'This execution will be done before you can blink.',
              ),
            Padding(
              padding: const EdgeInsets.only(top: 70.0),
              child: ElevatedButton(
                onPressed: toggleBlinkState,
                child: toggleState
                    ? const Text('Blink')
                    : const Text('Stop Blinking'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

StatefulWidget 和 StatelessWidget 的最佳实践是什么?

下面有一些设计原则供大家参考。

  1. 确定一个 widget 应该是 StatefulWidget 还是 StatelessWidget

在 Flutter 中, widget 要么是有状态的,要么是无状态的。这取决于 widget 是否依赖状态的改变。

  • 如果一个 widget 发生了改变,而它所处的用户界面或者数据中断了 UI,那么该 widget 就是 有状态 的。

  • 如果一个 widget 是 final 类型或者 immutable 类型的,那么该 widget 是 无状态 的。

  1. 确定哪个对象来控制 widget 的状态(针对 StatefulWidget)。

在 Flutter 中,有三种途径来管理状态:

  • widget 管理它的自身状态

  • 由其父 widget 管理 widget 状态

  • 通过混搭的方式

当决定了使用哪个途径后,要考虑下述的几个原则:

  • 如果状态信息是用户数据,比如复选框是被勾选还是未被勾选,或者滑块的位置,那么父 widget 会很好的处理当前 widget 的状态。

  • 如果状态是和外观效果相关的,比如动画,那么 widget 自己会处理状态的变化。

  • 如果无法确定,那么父 widget 会处理子 widget 的状态。

  1. 继承 StatefulWidgetState

MyStatefulWidget 类管理它自身的状态—&mdash 它继承自 StatefulWidget,重写了 createState() 方法。该方法创建了 State 对象,同时框架会调用 createState() 方法来构建 widget。在这个例子中,createState() 方法创建了一个 _MyStatefulWidgetState 实例。下面的最佳实践中也实现了类似的方法。

class MyStatefulWidget extends StatefulWidget {
  const MyStatefulWidget({
    super.key,
    required this.title,
  });

  final String title;
  @override
  State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  @override
  Widget build(BuildContext context) {
    //...
  }
}
  1. 将 StatefulWidget 添加到 widget 树中

将你自定义的 StatefulWidget 通过应用程序的 build 方法添加到 widget 树中。

class MyStatelessWidget extends StatelessWidget {
  // This widget is the root of your application.
  const MyStatelessWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: MyStatefulWidget(title: 'State Change Demo'),
    );
  }
}
State change on Android
Android
State change on iOS
iOS

Props

在 RN 中,大多数组件都可以在创建的时候通过不同的参数或者属性来自定义,叫做 props。这些参数可以在子组件中通过 this.props 进行调用。

// React Native
class CustomCard extends React.Component {
  render() {
    return (
      <View>
        <Text> Card {this.props.index} </Text>
        <Button
          title='Press'
          onPress={() => this.props.onPress(this.props.index)}
        />
      </View>
    );
  }
}
class App extends React.Component {

  onPress = index => {
    console.log('Card ', index);
  };

  render() {
    return (
      <View>
        <FlatList
          data={[ ... ]}
          renderItem={({ item }) => (
            <CustomCard onPress={this.onPress} index={item.key} />
          )}
        />
      </View>
    );
  }
}

在 Flutter 中,你可以将构造函数中的参数值赋值给标记为 final 的本地变量或者函数。

/// Flutter
class CustomCard extends StatelessWidget {
  const CustomCard({
    super.key,
    required this.index,
    required this.onPress,
  });

  final int index;
  final void Function() onPress;

  @override
  Widget build(BuildContext context) {
    return Card(
        child: Column(
      children: <Widget>[
        Text('Card $index'),
        TextButton(
          onPressed: onPress,
          child: const Text('Press'),
        ),
      ],
    ));
  }
}

class UseCard extends StatelessWidget {
  const UseCard({super.key, required this.index});

  final int index;

  @override
  Widget build(BuildContext context) {
    /// Usage
    return CustomCard(
      index: index,
      onPress: () {
        print('Card $index');
      },
    );
  }
}
Cards on Android
Android
Cards on iOS
iOS

本地存储

如果你不需要在本地存储太多数据同时也不需要存储结构化数据,那么你可以使用 shared_preferences,通过它来读写一些原始数据类型键值对,数据类型包括布尔、浮点、整数、长精度和字符串。

如何存储在应用程序中全局有效的键值对?

在 React Native,可以使用 AsyncStorage 中的 setItemgetItem 函数来存储和读取应用程序中的全局数据。

// React Native
await AsyncStorage.setItem( 'counterkey', json.stringify(++this.state.counter));
AsyncStorage.getItem('counterkey').then(value => {
  if (value != null) {
    this.setState({ counter: value });
  }
});

在 Flutter 中,使用 shared_preferences 插件来存储和访问应用程序内全局有效的键值对数据。 shared_preferences 插件封装了 iOS 中的 NSUserDefaults 和 Android 中的 SharedPreferences 来实现简单数据的持续存储。如果要使用该插件,可以在 pubspec.yaml 中添加依赖 shared_preferences,然后在 Dart 文件中引用包即可。

dependencies:
  flutter:
    sdk: flutter
  shared_preferences: ^2.0.13
import 'package:shared_preferences/shared_preferences.dart';

要实现持久数据存储,使用 SharedPreferences 类提供的 setter 方法即可。 Setter 方法适用于多种原始类型数据,比如 setIntsetBool、和 setString。要读取数据,使用 SharedPreferences 类中相应的 getter 方法。每一个 setter 方法都有对应的 getter 方法,比如,getIntgetBoolgetString

Future<void> updateCounter() async {
  final prefs = await SharedPreferences.getInstance();
  int? counter = prefs.getInt('counter');
  if (counter is int) {
    await prefs.setInt('counter', ++counter);
  }
  setState(() {
    _counter = counter;
  });
}

路径

大多数应用都会包含多个页面来显示不同类型的数据。比如,你有一个页面展示商品列表,用户可以通过点击其中的任意一个商品,在另外一个页面查看该商品的详细信息。

在 Android 中,新的页面是 Activity。在 iOS 中,新的页面是 ViewController。在 Flutter 中,页面也只是 widget,如果在 Flutter 中要切换页面,使用 Navigator widget 即可。

如何在页面之间进行切换?

在 RN 中,有三种主要的导航 widget : StackNavigator、TabNavigator 和 DrawerNavigator。每个都提供了配置和定义页面的方法。

// React Native
const MyApp = TabNavigator(
  { Home: { screen: HomeScreen }, Notifications: { screen: tabNavScreen } },
  { tabBarOptions: { activeTintColor: '#e91e63' } }
);
const SimpleApp = StackNavigator({
  Home: { screen: MyApp },
  stackScreen: { screen: StackScreen }
});
export default (MyApp1 = DrawerNavigator({
  Home: {
    screen: SimpleApp
  },
  Screen2: {
    screen: drawerScreen
  }
}));

在 Flutter 中,有两种主要的 widget 实现页面之间的切换:

  • Route 是应用程序页面的一个抽象类。

  • Navigator 是管理页面路径的 widget。

Navigator 以堆栈的方式管理子 widget。它的堆栈里存储的是 Route 对象,并且提供方法管理整个堆栈,比如 Navigator.pushNavigator.pop。路径列表需要在 MaterialApp 中指定。或者在页面切换的时候进行构建,比如 hero 动画。下面的例子在 MaterialApp widget 中指定了页面切换路径。