跳至主要内容
目录

面向 React Native 开发人员的 Flutter

目录

本文档面向希望将现有的 React Native (RN) 知识应用于使用 Flutter 构建移动应用的 React Native 开发人员。如果您了解 RN 框架的基本原理,则可以使用本文档作为学习 Flutter 开发的入门方法。

可以通过跳跃浏览并查找与您的需求最相关的疑问,将本文档用作参考手册。

面向 JavaScript 开发人员的 Dart 入门 (ES6)

#

与 React Native 类似,Flutter 使用反应式样式视图。但是,虽然 RN 编译为原生部件,但 Flutter 会一直编译到原生代码。Flutter 控制屏幕上的每个像素,从而避免了由于需要 JavaScript 桥接而导致的性能问题。

Dart 是一种易于学习的语言,并提供以下功能

  • 提供一种开源、可扩展的编程语言,用于构建 Web、服务器和移动应用。
  • 提供一种面向对象、单继承的语言,它使用 C 样式语法,并进行 AOT 编译为原生代码。
  • 可以选择性地转译为 JavaScript。
  • 支持接口和抽象类。

下面描述了一些 JavaScript 和 Dart 之间差异的示例。

入口点

#

JavaScript 没有预定义的入口函数——您需要定义入口点。

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

在 Dart 中,每个应用都必须有一个顶级 main() 函数,用作应用的入口点。

dart
/// Dart
void main() {}

DartPad 中试用。

打印到控制台

#

要打印到 Dart 控制台,请使用 print()

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

DartPad 中试用。

变量

#

Dart 是类型安全的——它结合使用静态类型检查和运行时检查,以确保变量的值始终与变量的静态类型匹配。尽管类型是强制性的,但某些类型注释是可选的,因为 Dart 会执行类型推断。

创建和赋值变量

#

在 JavaScript 中,变量不能被类型化。

Dart 中,变量必须显式类型化,或者类型系统必须自动推断出正确的类型。

js
// JavaScript
let name = 'JavaScript';
dart
/// 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 值。

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

DartPad 中试用。

有关更多信息,请参阅有关 变量 的文档。

检查空值或零值

#

在 JavaScript 中,使用 == 比较运算符时,值 1 或任何非空对象都被视为 true

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

在 Dart 中,只有布尔值 true 被视为 true。

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

DartPad 中试用。

函数

#

Dart 和 JavaScript 函数通常相似。主要区别在于声明。

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

DartPad 中试用。

有关更多信息,请参阅有关 函数 的文档。

异步编程

#

期货

#

与 JavaScript 一样,Dart 支持单线程执行。在 JavaScript 中,Promise 对象表示异步操作的最终完成(或失败)及其结果值。

Dart 使用 Future 对象来处理此问题。

js
// 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
// 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) {
      final ip = jsonDecode(response.body)['origin'] as String;
      return ip;
    });
  }
}

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

有关更多信息,请参阅有关 Future 对象的文档。

asyncawait

#

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

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

js
// 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
// 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);
    final ip = jsonDecode(response.body)['origin'] as String;
    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);
  }
}

有关更多信息,请参阅 async 和 await 的文档。

基础知识

#

如何创建一个 Flutter 应用?

#

要使用 React Native 创建应用,您需要从命令行运行 create-react-native-app

create-react-native-app <projectname>

要在 Flutter 中创建应用,请执行以下操作之一

  • 使用安装了 Flutter 和 Dart 插件的 IDE。
  • 从命令行使用 flutter create 命令。确保 Flutter SDK 在您的 PATH 中。
flutter create <projectname>

有关更多信息,请参阅 入门,其中引导您创建一个按钮点击计数器应用。创建 Flutter 项目会构建运行 Android 和 iOS 设备上的示例应用所需的所有文件。

如何运行我的应用?

#

在 React Native 中,您需要从项目目录运行 npm runyarn run

您可以通过几种方式运行 Flutter 应用

  • 在安装了 Flutter 和 Dart 插件的 IDE 中使用“运行”选项。
  • 从项目的根目录使用 flutter run

您的应用会在连接的设备、iOS 模拟器或 Android 模拟器上运行。

有关更多信息,请参阅 Flutter 的 入门 文档。

如何导入部件?

#

在 React Native 中,您需要导入每个所需的组件。

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

在 Flutter 中,要使用 Material Design 库中的部件,请导入 material.dart 包。要使用 iOS 样式部件,请导入 Cupertino 库。要使用更基本的部件集,请导入 Widgets 库。或者,您可以编写自己的部件库并导入它。

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

无论您导入哪个部件包,Dart 仅会提取应用中使用的部件。

有关更多信息,请参阅 Flutter 部件目录

Flutter 中的“Hello world!”应用相当于 React Native 中的哪个应用?

#

在 React Native 中,HelloWorldApp 类扩展 React.Component 并通过返回视图组件来实现渲染方法。

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

const App = () => {
  return (
    <View style={styles.container}>
      <Text>Hello world!</Text>
    </View>
  );
};

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

export default App;

在 Flutter 中,您可以使用核心部件库中的 CenterText 部件创建相同的“Hello world!”应用。Center 部件成为部件树的根,并具有一个子部件,即 Text 部件。

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

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

以下图片显示了基本 Flutter“Hello world!”应用的 Android 和 iOS UI。

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

既然您已经看到了最基本的 Flutter 应用,下一节将介绍如何利用 Flutter 丰富的部件库来创建现代、精致的应用。

如何使用部件并嵌套它们以形成部件树?

#

在 Flutter 中,几乎所有东西都是部件。

部件是应用用户界面的基本构建块。您可以将部件组合成一个层次结构,称为部件树。每个部件都嵌套在其父部件内,并继承其父部件的属性。甚至应用对象本身也是一个部件。没有单独的“应用”对象。相反,根部件充当此角色。

部件可以定义

  • 结构元素——例如按钮或菜单
  • 样式元素——例如字体或配色方案
  • 布局方面——例如填充或对齐方式

以下示例显示了使用 Material 库中的部件的“Hello world!”应用。在此示例中,部件树嵌套在 MaterialApp 根部件内。

dart
// 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 部件构建的“Hello world!”。与基本“Hello world!”应用相比,您可以免费获得更多功能。

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

在编写应用程序时,您将使用两种类型的 widget:StatelessWidgetStatefulWidgetStatelessWidget 正如其名称所示——一个没有状态的 widget。StatelessWidget 只创建一次,并且永远不会改变其外观。StatefulWidget 根据接收到的数据或用户输入动态更改状态。

无状态和有状态 widget 之间的重要区别在于,StatefulWidget 具有一个 State 对象,该对象存储状态数据并在树重建过程中保留它,因此不会丢失。

在简单或基本的应用程序中,嵌套 widget 很容易,但是随着代码库的增大和应用程序变得复杂,您应该将深度嵌套的 widget 分解成返回 widget 或更小类的函数。创建单独的函数和 widget 允许您在应用程序中重用组件。

如何创建可重用组件?

#

在 React Native 中,您将定义一个类来创建可重用组件,然后使用 props 方法来设置或返回所选元素的属性和值。在下面的示例中,定义了 CustomCard 类,然后在父类中使用它。

js
// React Native
const CustomCard = ({ index, onPress }) => {
  return (
    <View>
      <Text> Card {index} </Text>
      <Button
        title="Press"
        onPress={() => onPress(index)}
      />
    </View>
  );
};

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

在 Flutter 中,定义一个类来创建自定义 widget,然后重用该 widget。您还可以定义并调用一个返回可重用 widget 的函数,如以下示例中的 build 函数所示。

dart
/// 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

项目结构和资源

#

从哪里开始编写代码?

#

lib/main.dart 文件开始。在创建 Flutter 应用程序时,它会自动生成。

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

在 Flutter 中,入口点文件是 {project_name}/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.

我应该把资源和素材放在哪里,以及如何使用它们?

#

Flutter 资源或资产是与您的应用程序捆绑并部署的文件,并在运行时可访问。Flutter 应用程序可以包含以下资产类型

  • 静态数据,例如 JSON 文件
  • 配置文件
  • 图标和图像(JPEG、PNG、GIF、动画 GIF、WebP、动画 WebP、BMP 和 WBMP)

Flutter 使用位于项目根目录的 pubspec.yaml 文件来识别应用程序所需的资产。

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

assets 子部分指定应与应用程序一起包含的文件。每个资产都由相对于 pubspec.yaml 文件的显式路径标识,其中包含资产文件的位置。声明资产的顺序无关紧要。使用的实际目录(在本例中为 assets)无关紧要。但是,虽然资产可以放在任何应用程序目录中,但最佳做法是将它们放在 assets 目录中。

在构建过程中,Flutter 将资产放入一个名为“资产包”的特殊归档文件中,应用程序在运行时从中读取。当在 pubspec.yaml 的 assets 部分中指定资产的路径时,构建过程会在相邻的子目录中查找任何具有相同名称的文件。这些文件也包含在资产包中以及指定的资产中。Flutter 在为您的应用程序选择分辨率合适的图像时使用资产变体。

在 React Native 中,您可以通过将图像文件放在源代码目录中并引用它来添加静态图像。

js
<Image source={require('./my-icon.png')} />
// OR
<Image
  source={{
    url: 'https://reactnative.net.cn/img/tiny_logo.png'
  }}
/>

在 Flutter 中,使用 widget 的 build 方法中的 Image.asset 构造函数将静态图像添加到您的应用程序中。

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

有关更多信息,请参阅 在 Flutter 中添加资产和图像

如何加载网络图片?

#

在 React Native 中,您将在 Image 组件的 source 属性中指定 uri,并在需要时提供大小。

在 Flutter 中,使用 Image.network 构造函数包含来自 URL 的图像。

dart
Image.network('https://docs.flutterdart.cn/assets/images/docs/owl.jpg');

如何安装包和插件?

#

Flutter 支持使用其他开发人员贡献给 Flutter 和 Dart 生态系统的共享包。这使您能够快速构建应用程序,而无需从头开始开发所有内容。包含平台特定代码的包称为包插件。

在 React Native 中,您将使用 yarn add {package-name}npm install --save {package-name} 从命令行安装包。

在 Flutter 中,使用以下说明安装包

  1. 要将 google_sign_in 包添加为依赖项,请运行 flutter pub add
flutter pub add google_sign_in
  1. 使用 flutter pub get 从命令行安装包。如果使用 IDE,它通常会为您运行 flutter pub get,或者它可能会提示您这样做。
  2. 将包导入您的应用程序代码中,如下所示
dart
import 'package:flutter/material.dart';

有关更多信息,请参阅 使用包开发包和插件

您可以在 pub.devFlutter 包 部分找到许多 Flutter 开发人员共享的包。

Flutter 部件

#

在 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 基本 widgetFlutter widget 目录Flutter widget 索引

视图

#

View 容器的等效项是什么?

#

在 React Native 中,View 是一个支持使用 Flexbox、样式、触摸处理和辅助功能控件进行布局的容器。

在 Flutter 中,您可以使用 Widgets 库中的核心布局 widget,例如 ContainerColumnRowCenter。有关更多信息,请参阅 布局 widget 目录。

FlatListSectionList 的等效项是什么?

#

List 是一个垂直排列的组件的可滚动列表。

在 React Native 中,FlatListSectionList 用于渲染简单或分区的列表。

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

ListView 是 Flutter 最常用的滚动 widget。默认构造函数采用显式子 widget 列表。ListView 最适合少量 widget。对于大型或无限列表,请使用 ListView.builder,它按需构建其子 widget,并且只构建可见的子 widget。

dart
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 绘制或绘画?

#

在 React Native 中,画布组件不存在,因此使用 react-native-canvas 等第三方库。

js
// React Native
const CanvasComp = () => {
  const 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();
  };

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

在 Flutter 中,您可以使用 CustomPaintCustomPainter 类绘制到画布上。

以下示例显示了如何在绘制阶段使用 CustomPaint widget 进行绘制。它实现了抽象类 CustomPainter,并将其传递给 CustomPaint 的 painter 属性。CustomPaint 子类必须实现 paint()shouldRepaint() 方法。

dart
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, 200), 40, paint);
    final Paint paintRect = Paint()..color = Colors.lightBlue;
    final Rect rect = Rect.fromPoints(
      const Offset(150, 300),
      const Offset(300, 400),
    );
    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

布局

#

如何使用部件定义布局属性?

#

在 React Native 中,大多数布局可以通过传递给特定组件的 props 来完成。例如,您可以使用 View 组件上的 style 属性来指定 flexbox 属性。要将组件排列在列中,您将指定一个属性,例如:flexDirection: 'column'

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

在 Flutter 中,布局主要由专门设计用于提供布局的 widget 以及控制 widget 及其样式属性来定义。

例如,ColumnRow widget 获取子 widget 数组,并分别沿垂直和水平方向对齐它们。Container widget 采用布局和样式属性的组合,而 Center widget 居中其子 widget。

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

Flutter 在其核心 widget 库中提供了各种布局 widget。例如,PaddingAlignStack

有关完整列表,请参阅 布局 widget

Layout on Android
Android
Layout on iOS
iOS

如何分层部件?

#

在 React Native 中,可以使用 absolute 定位来分层组件。

Flutter 使用 Stack widget 将子 widget 排列成层。widget 可以完全或部分重叠基础 widget。

Stack widget 相对于其框的边缘定位其子 widget。如果您只想重叠几个子 widget,则此类很有用。

dart
@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 属性和 Alignment 坐标偏移文本。

Stack on Android
Android
Stack on iOS
iOS

有关更多信息,请参阅 Stack 类文档。

样式

#

如何设置组件的样式?

#

在 React Native 中,内联样式和 stylesheets.create 用于设置组件的样式。

js
// 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 可以为其 style 属性使用 TextStyle 类。如果要在多个地方使用相同的文本样式,则可以创建一个 TextStyle 类,并将其用于多个 Text widget。

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

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

如何使用 IconsColors

#

React Native 不包含对图标的支持,因此使用第三方库。

在 Flutter 中,导入 Material 库还会引入丰富的 Material 图标颜色 集。

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

使用 Icons 类时,请确保在项目的 pubspec.yaml 文件中设置 uses-material-design: true。这可确保包含显示图标的 MaterialIcons 字体。通常,如果您打算使用 Material 库,则应包含此行。

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

Flutter 的 Cupertino(iOS 风格) 包提供了与当前 iOS 设计语言高度一致的部件。要使用 CupertinoIcons 字体,请在项目 pubspec.yaml 文件中添加 cupertino_icons 的依赖项。

yaml
name: my_awesome_application
dependencies:
  cupertino_icons: ^1.0.8

要全局自定义组件的颜色和样式,请使用 ThemeData 指定主题各个方面的默认颜色。在 MaterialApp 中将 theme 属性设置为 ThemeData 对象。 Colors 类提供了来自 Material Design 调色板 的颜色。

以下示例将颜色方案的种子设置为 deepPurple,并将文本选择颜色设置为 red

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

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

如何添加样式主题?

#

在 React Native 中,组件的通用主题在样式表中定义,然后在组件中使用。

在 Flutter 中,可以通过在 ThemeData 类中定义样式并将其传递给 MaterialApp 部件的 theme 属性,为几乎所有内容创建统一的样式。

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

即使不使用 MaterialApp 部件,也可以应用 ThemeTheme 部件在其 data 参数中获取 ThemeData,并将 ThemeData 应用于其所有子部件。

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

状态管理

#

状态是在构建部件时可以同步读取的信息,或者是在部件生命周期内可能发生变化的信息。要管理 Flutter 中的应用程序状态,请使用与状态对象配对的 StatefulWidget

有关在 Flutter 中管理状态的方法的更多信息,请参阅 状态管理

StatelessWidget

#

Flutter 中的 StatelessWidget 是一个不需要状态更改的部件——它没有内部状态需要管理。

当您描述的用户界面部分不依赖于对象本身的配置信息和部件膨胀所在的 BuildContext 以外的任何其他内容时,无状态部件非常有用。

AboutDialogCircleAvatarText 是继承 StatelessWidget 的无状态部件示例。

dart
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——它包含不可变数据。

无状态部件的 build 方法通常仅在三种情况下调用

  • 将部件插入树中时
  • 部件的父部件更改其配置时
  • 它依赖的 InheritedWidget 发生更改时

StatefulWidget

#

StatefulWidget 是一个状态发生变化的部件。使用 setState 方法管理 StatefulWidget 的状态更改。调用 setState() 会告诉 Flutter 框架状态中某些内容发生了变化,这会导致应用程序重新运行 build() 方法,以便应用程序能够反映更改。

状态是在构建部件时可以同步读取的信息,并且可能在部件的生命周期内发生变化。部件实现者有责任确保在状态发生变化时及时通知状态对象。当部件可以动态更改时,请使用 StatefulWidget。例如,部件的状态通过在表单中键入或移动滑块来更改。或者,它可能会随着时间的推移而改变——也许数据馈送会更新 UI。

CheckboxRadioSliderInkWellFormTextField 是继承 StatefulWidget 的有状态部件示例。

以下示例声明了一个 StatefulWidget,它需要一个 createState() 方法。此方法创建管理部件状态的状态对象 _MyStatefulWidgetState

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

  final String title;

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

以下状态类 _MyStatefulWidgetState 为部件实现了 build() 方法。当状态发生变化时,例如,当用户切换按钮时,会使用新的切换值调用 setState()。这会导致框架在 UI 中重建此部件。

dart
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),
              child: ElevatedButton(
                onPressed: toggleBlinkState,
                child: toggleState
                    ? const Text('Blink')
                    : const Text('Stop Blinking'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

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

#

在设计部件时,需要考虑以下几点。

  1. 确定部件应该是 StatefulWidget 还是 StatelessWidget

在 Flutter 中,部件要么是有状态的,要么是无状态的——这取决于它们是否依赖于状态更改。

  • 如果部件发生了变化——用户与之交互或数据馈送中断 UI,则它是有状态的
  • 如果部件是最终的或不可变的,则它是无状态的
  1. 确定哪个对象管理部件的状态(对于 StatefulWidget)。

在 Flutter 中,有三种主要方法可以管理状态

  • 部件管理其自身的状态
  • 父部件管理部件的状态
  • 混合搭配方法

在决定使用哪种方法时,请考虑以下原则

  • 如果相关状态是用户数据,例如复选框的选中或未选中模式,或滑块的位置,则最好由父部件管理状态。
  • 如果相关状态是美观的,例如动画,则部件本身最适合管理状态。
  • 如有疑问,请让父部件管理子部件的状态。
  1. 子类化 StatefulWidget 和 State。

MyStatefulWidget 类管理其自身的状态——它扩展了 StatefulWidget,它重写了 createState() 方法以创建 State 对象,并且框架调用 createState() 来构建部件。在此示例中,createState() 创建了 _MyStatefulWidgetState 的实例,该实例在下一个最佳实践中实现。

dart
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 添加到部件树中。

将自定义 StatefulWidget 添加到应用程序构建方法中的部件树中。

dart
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

属性

#

在 React Native 中,大多数组件在使用不同的参数或属性(称为 props)创建时都可以自定义。这些参数可以在子组件中使用 this.props

js
// React Native
const CustomCard = ({ index, onPress }) => {
  return (
    <View>
      <Text> Card {index} </Text>
      <Button
        title='Press'
        onPress={() => onPress(index)}
      />
    </View>
  );
};

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

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

在 Flutter 中,您将一个标记为 final 的局部变量或函数分配给在参数化构造函数中接收的属性。

dart
/// 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 函数来存储和检索对应用程序持久且全局的数据。

js
// React Native
const [counter, setCounter] = useState(0)
...
await AsyncStorage.setItem( 'counterkey', json.stringify(++this.state.counter));
AsyncStorage.getItem('counterkey').then(value => {
  if (value != null) {
    setCounter(value);
  }
});

在 Flutter 中,使用 shared_preferences 插件来存储和检索对应用程序持久且全局的键值数据。shared_preferences 插件在 iOS 上包装了 NSUserDefaults,在 Android 上包装了 SharedPreferences,为简单数据提供了一个持久存储。

要将 shared_preferences 包添加为依赖项,请运行 flutter pub add

flutter pub add shared_preferences
dart
import 'package:shared_preferences/shared_preferences.dart';

要实现持久数据,请使用 SharedPreferences 类提供的 setter 方法。setter 方法可用于各种原始类型,例如 setIntsetBoolsetString。要读取数据,请使用 SharedPreferences 类提供的适当 getter 方法。对于每个 setter,都有一个相应的 getter 方法,例如 getIntgetBoolgetString

dart
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 中,新屏幕是新的 ViewControllers。在 Flutter 中,屏幕只是部件!要在 Flutter 中导航到新屏幕,请使用 Navigator 部件。

如何导航到不同屏幕?

#

在 React Native 中,有三个主要的导航器:StackNavigator、TabNavigator 和 DrawerNavigator。每个都提供了一种配置和定义屏幕的方法。

js
// 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 中,有两个主要部件用于在屏幕之间导航

  • Route 是应用程序屏幕或页面的抽象。
  • Navigator 是一个管理路由的部件。

Navigator 定义为一个部件,它使用堆栈规则管理一组子部件。导航器管理 Route 对象的堆栈,并提供用于管理堆栈的方法,如 Navigator.pushNavigator.pop。路由列表可以在 MaterialApp 部件中指定,或者可以动态构建,例如在英雄动画中。以下示例在 MaterialApp 部件中指定了命名路由。

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      //...
      routes: <String, WidgetBuilder>{
        '/a': (context) => const UsualNavScreen(),
        '/b': (context) => const DrawerNavScreen(),
      },
      //...
    );
  }
}

要导航到命名路由,可以使用 Navigator.of() 方法指定 BuildContext(部件在部件树中位置的句柄)。路由的名称传递给 pushNamed 函数以导航到指定的路由。

dart
Navigator.of(context).pushNamed('/a');

您还可以使用 Navigatorpush 方法,该方法将给定的 Route 添加到最紧密包围给定 BuildContext 的导航器的历史记录中,并过渡到该记录。在以下示例中, MaterialPageRoute 部件是一个模态路由,它使用平台自适应过渡替换整个屏幕。它将 WidgetBuilder 作为必需参数。

dart
Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => const UsualNavScreen(),
  ),
);

如何使用选项卡导航和抽屉导航?

#

在 Material Design 应用程序中,Flutter 导航有两个主要选项:选项卡和抽屉。当没有足够的空间来支持选项卡时,抽屉提供了一个很好的替代方案。

选项卡导航

#

在 React Native 中,createBottomTabNavigatorTabNavigation 用于显示选项卡和选项卡导航。

js
// React Native
import { createBottomTabNavigator } from 'react-navigation';

const MyApp = TabNavigator(
  { Home: { screen: HomeScreen }, Notifications: { screen: tabNavScreen } },
  { tabBarOptions: { activeTintColor: '#e91e63' } }
);

Flutter 提供了几个专门用于抽屉和选项卡导航的部件

TabController
协调 TabBarTabBarView 之间的选项卡选择。
TabBar
显示选项卡的水平行。
Tab
创建 Material Design TabBar 选项卡。
TabBarView
显示对应于当前选定选项卡的部件。

dart
class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
  late TabController controller = TabController(length: 2, vsync: this);

  @override
  Widget build(BuildContext context) {
    return TabBar(
      controller: controller,
      tabs: const <Tab>[
        Tab(icon: Icon(Icons.person)),
        Tab(icon: Icon(Icons.email)),
      ],
    );
  }
}

需要一个 TabController 来协调 TabBarTabBarView 之间的选项卡选择。TabController 构造函数的 length 参数是选项卡的总数。需要一个 TickerProvider 来在帧触发状态更改时触发通知。TickerProvidervsync。在创建新的 TabController 时,将 vsync: this 参数传递给 TabController 构造函数。

TickerProvider 是一个接口,由可以分配 Ticker 对象的类实现。任何必须在帧触发时收到通知的对象都可以使用滴答器,但它们最常通过 AnimationController 间接使用。AnimationController 需要一个 TickerProvider 来获取其 Ticker。如果从 State 创建 AnimationController,则可以使用 TickerProviderStateMixinSingleTickerProviderStateMixin 类来获取合适的 TickerProvider

Scaffold 组件包装了一个新的 TabBar 组件并创建了两个选项卡。TabBarView 组件作为 Scaffold 组件的 body 参数传递。所有对应于 TabBar 组件选项卡的屏幕都是 TabBarView 组件的子组件,并使用相同的 TabController

dart
class _NavigationHomePageState extends State<NavigationHomePage>
    with SingleTickerProviderStateMixin {
  late TabController controller = TabController(length: 2, vsync: this);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        bottomNavigationBar: Material(
          color: Colors.blue,
          child: TabBar(
            tabs: const <Tab>[
              Tab(
                icon: Icon(Icons.person),
              ),
              Tab(
                icon: Icon(Icons.email),
              ),
            ],
            controller: controller,
          ),
        ),
        body: TabBarView(
          controller: controller,
          children: const <Widget>[HomeScreen(), TabScreen()],
        ));
  }
}

抽屉导航

#

在 React Native 中,导入所需的 react-navigation 包,然后使用 createDrawerNavigatorDrawerNavigation

js
// React Native
export default (MyApp1 = DrawerNavigator({
  Home: {
    screen: SimpleApp
  },
  Screen2: {
    screen: drawerScreen
  }
}));

在 Flutter 中,我们可以将 Drawer 组件与 Scaffold 结合使用来创建具有 Material Design 抽屉的布局。要向应用添加 Drawer,请将其包装在 Scaffold 组件中。Scaffold 组件为遵循 Material Design 指南的应用提供了始终如一的视觉结构。它还支持特殊的 Material Design 组件,例如 DrawersAppBarsSnackBars

Drawer 组件是一个 Material Design 面板,它从 Scaffold 的边缘水平滑入,以在应用程序中显示导航链接。您可以提供 ElevatedButtonText 组件或项目列表作为 Drawer 组件的子组件显示。在以下示例中,ListTile 组件在点击时提供导航。

dart
@override
Widget build(BuildContext context) {
  return Drawer(
    elevation: 20,
    child: ListTile(
      leading: const Icon(Icons.change_history),
      title: const Text('Screen2'),
      onTap: () {
        Navigator.of(context).pushNamed('/b');
      },
    ),
  );
}

Scaffold 组件还包含一个 AppBar 组件,该组件会自动显示一个合适的 IconButton 以在 Scaffold 中有 Drawer 时显示 DrawerScaffold 自动处理边缘滑动手势以显示 Drawer

dart
@override
Widget build(BuildContext context) {
  return Scaffold(
    drawer: Drawer(
      elevation: 20,
      child: ListTile(
        leading: const Icon(Icons.change_history),
        title: const Text('Screen2'),
        onTap: () {
          Navigator.of(context).pushNamed('/b');
        },
      ),
    ),
    appBar: AppBar(title: const Text('Home')),
    body: Container(),
  );
}
Navigation on Android
Android
Navigation on iOS
iOS

手势检测和触摸事件处理

#

为了侦听和响应手势,Flutter 支持点击、拖动和缩放。Flutter 中的手势系统有两个独立的层。第一层包括原始指针事件,这些事件描述了指针在屏幕上的位置和移动情况(例如触摸、鼠标和触控笔移动)。第二层包括手势,这些手势描述由一个或多个指针移动组成的语义操作。

如何为部件添加点击或按下监听器?

#

在 React Native 中,使用 PanResponderTouchable 组件向组件添加侦听器。

js
// React Native
<TouchableOpacity
  onPress={() => {
    console.log('Press');
  }}
  onLongPress={() => {
    console.log('Long Press');
  }}
>
  <Text>Tap or Long Press</Text>
</TouchableOpacity>

对于更复杂的手势并将多个触摸组合成一个手势,使用 PanResponder

js
// React Native
const App = () => {
  const panResponderRef = useRef(null);

  useEffect(() => {
    panResponderRef.current = PanResponder.create({
      onMoveShouldSetPanResponder: (event, gestureState) =>
        !!getDirection(gestureState),
      onPanResponderMove: (event, gestureState) => true,
      onPanResponderRelease: (event, gestureState) => {
        const drag = getDirection(gestureState);
      },
      onPanResponderTerminationRequest: (event, gestureState) => true
    });
  }, []);

  return (
    <View style={styles.container} {...panResponderRef.current.panHandlers}>
      <View style={styles.center}>
        <Text>Swipe Horizontally or Vertically</Text>
      </View>
    </View>
  );
};

在 Flutter 中,要向组件添加点击(或按下)侦听器,请使用具有 onPress: field 的按钮或可触摸组件。或者,通过将任何组件包装在 GestureDetector 中来向其添加手势检测。

dart
@override
Widget build(BuildContext context) {
  return GestureDetector(
    child: Scaffold(
      appBar: AppBar(title: const Text('Gestures')),
      body: const Center(
          child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text('Tap, Long Press, Swipe Horizontally or Vertically'),
        ],
      )),
    ),
    onTap: () {
      print('Tapped');
    },
    onLongPress: () {
      print('Long Pressed');
    },
    onVerticalDragEnd: (value) {
      print('Swiped Vertically');
    },
    onHorizontalDragEnd: (value) {
      print('Swiped Horizontally');
    },
  );
}

有关更多信息,包括 Flutter GestureDetector 回调列表,请参阅 GestureDetector 类

Gestures on Android
Android
Gestures on iOS
iOS

发出 HTTP 网络请求

#

从互联网获取数据对于大多数应用来说很常见。在 Flutter 中,http 包提供了从互联网获取数据的最简单方法。

如何从 API 调用中获取数据?

#

React Native 为网络提供 Fetch API - 你发出一个 fetch 请求,然后接收响应以获取数据。

js
// React Native
const [ipAddress, setIpAddress] = useState('')

const _getIPAddress = () => {
  fetch('https://httpbin.org/ip')
    .then(response => response.json())
    .then(responseJson => {
      setIpAddress(responseJson.origin);
    })
    .catch(error => {
      console.error(error);
    });
};

Flutter 使用 http 包。

要将 http 包添加为依赖项,请运行 flutter pub add

flutter pub add http

Flutter 使用 dart:io 核心 HTTP 支持客户端。要创建 HTTP 客户端,请导入 dart:io

dart
import 'dart:io';

客户端支持以下 HTTP 操作:GET、POST、PUT 和 DELETE。

dart
final url = Uri.parse('https://httpbin.org/ip');
final httpClient = HttpClient();

Future<void> getIPAddress() async {
  final request = await httpClient.getUrl(url);
  final response = await request.close();
  final responseBody = await response.transform(utf8.decoder).join();
  final ip = jsonDecode(responseBody)['origin'] as String;
  setState(() {
    _ipAddress = ip;
  });
}
API calls on Android
Android
API calls on iOS
iOS

表单输入

#

文本字段允许用户在您的应用中键入文本,因此可用于构建表单、消息应用、搜索体验等。Flutter 提供了两个核心文本字段组件:TextFieldTextFormField

如何使用文本字段部件?

#

在 React Native 中,要输入文本,您使用 TextInput 组件显示文本输入框,然后使用回调将值存储在变量中。

js
// React Native
const [password, setPassword] = useState('')
...
<TextInput
  placeholder="Enter your Password"
  onChangeText={password => setPassword(password)}
/>
<Button title="Submit" onPress={this.validate} />

在 Flutter 中,使用 TextEditingController 类来管理 TextField 组件。每当修改文本字段时,控制器都会通知其侦听器。

侦听器读取文本和选择属性以了解用户在字段中键入了什么。可以通过控制器的 text 属性访问 TextField 中的文本。

dart
final TextEditingController _controller = TextEditingController();

@override
Widget build(BuildContext context) {
  return Column(children: [
    TextField(
      controller: _controller,
      decoration: const InputDecoration(
        hintText: 'Type something',
        labelText: 'Text Field',
      ),
    ),
    ElevatedButton(
      child: const Text('Submit'),
      onPressed: () {
        showDialog(
            context: context,
            builder: (context) {
              return AlertDialog(
                title: const Text('Alert'),
                content: Text('You typed ${_controller.text}'),
              );
            });
      },
    ),
  ]);
}

在此示例中,当用户点击提交按钮时,警报对话框会显示当前在文本字段中输入的文本。这是通过使用 AlertDialog 组件显示警报消息来实现的,并且通过 TextEditingControllertext 属性访问 TextField 中的文本。

如何使用表单部件?

#

在 Flutter 中,使用 Form 组件,其中 TextFormField 组件以及提交按钮作为子组件传递。TextFormField 组件有一个名为 onSaved 的参数,该参数接受回调并在保存表单时执行。FormState 对象用于保存、重置或验证此 Form 的每个后代 FormField。要获取 FormState,可以使用 Form.of() 和其祖先是 Form 的上下文,或者将 GlobalKey 传递给 Form 构造函数并调用 GlobalKey.currentState()

dart
@override
Widget build(BuildContext context) {
  return Form(
    key: formKey,
    child: Column(
      children: <Widget>[
        TextFormField(
          validator: (value) {
            if (value != null && value.contains('@')) {
              return null;
            }
            return 'Not a valid email.';
          },
          onSaved: (val) {
            _email = val;
          },
          decoration: const InputDecoration(
            hintText: 'Enter your email',
            labelText: 'Email',
          ),
        ),
        ElevatedButton(
          onPressed: _submit,
          child: const Text('Login'),
        ),
      ],
    ),
  );
}

以下示例显示了如何使用 Form.save()formKey(它是 GlobalKey)在提交时保存表单。

dart
void _submit() {
  final form = formKey.currentState;
  if (form != null && form.validate()) {
    form.save();
    showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
            title: const Text('Alert'),
            content: Text('Email: $_email, password: $_password'));
      },
    );
  }
}
Input on Android
Android
Input on iOS
iOS

特定平台代码

#

在构建跨平台应用时,您希望尽可能多地在平台之间重用代码。但是,在某些情况下,根据操作系统使代码不同是有意义的。这需要通过声明特定平台来进行单独的实现。

在 React Native 中,将使用以下实现

js
// React Native
if (Platform.OS === 'ios') {
  return 'iOS';
} else if (Platform.OS === 'android') {
  return 'android';
} else {
  return 'not recognised';
}

在 Flutter 中,使用以下实现

dart
final platform = Theme.of(context).platform;
if (platform == TargetPlatform.iOS) {
  return 'iOS';
}
if (platform == TargetPlatform.android) {
  return 'android';
}
if (platform == TargetPlatform.fuchsia) {
  return 'fuchsia';
}
return 'not recognized ';

调试

#

我可以用哪些工具调试 Flutter 应用?

#

使用 DevTools 套件调试 Flutter 或 Dart 应用。

DevTools 包括对分析、检查堆、检查组件树、记录诊断、调试、观察执行的代码行、调试内存泄漏和内存碎片的支持。有关更多信息,请查看 DevTools 文档。

如果使用 IDE,则可以使用 IDE 的调试器调试应用程序。

如何执行热重载?

#

Flutter 的 Stateful Hot Reload 功能可帮助您快速轻松地进行实验、构建 UI、添加功能和修复错误。无需在每次进行更改时都重新编译应用,您可以立即热重载应用。应用将更新以反映您的更改,并且应用的当前状态将保留。

在 React Native 中,快捷方式是 iOS 模拟器的 ⌘R 和 Android 模拟器上的双击 R。

在 Flutter 中,如果使用 IntelliJ IDE 或 Android Studio,可以选择“全部保存”(⌘s/ctrl-s),或者可以单击工具栏上的“热重载”按钮。如果使用 flutter run 在命令行中运行应用,请在终端窗口中键入 r。您还可以通过在终端窗口中键入 R 执行完全重启。

如何访问应用内开发者菜单?

#

在 React Native 中,可以通过摇动设备访问开发者菜单:iOS 模拟器的 ⌘D 或 Android 模拟器的 ⌘M。

在 Flutter 中,如果使用 IDE,则可以使用 IDE 工具。如果使用 flutter run 启动应用程序,也可以通过在终端窗口中键入 h 访问菜单,或键入以下快捷方式

操作终端快捷方式调试功能和属性
应用的组件层次结构wdebugDumpApp()
应用的渲染树tdebugDumpRenderTree()
图层LdebugDumpLayerTree()
可访问性S(遍历顺序)或
U(反向命中测试顺序)
debugDumpSemantics()
切换组件检查器iWidgetsApp. showWidgetInspectorOverride
切换构造线的显示pdebugPaintSizeEnabled
模拟不同的操作系统odefaultTargetPlatform
显示性能覆盖PWidgetsApp. showPerformanceOverlay
将屏幕截图保存到 flutter.pngs
退出q

动画

#

精心设计的动画使 UI 感觉直观,有助于完善应用的外观和感觉,并改善用户体验。Flutter 的动画支持使实现简单和复杂的动画变得容易。Flutter SDK 包含许多包含标准运动效果的 Material Design 组件,您可以轻松自定义这些效果以个性化您的应用。

在 React Native 中,使用 Animated API 创建动画。

在 Flutter 中,使用 Animation 类和 AnimationController 类。Animation 是一个抽象类,它了解其当前值及其状态(已完成或已关闭)。AnimationController 类允许您向前或向后播放动画,或者停止动画并将动画设置为特定值以自定义运动。

如何添加简单的淡入动画?

#

在下面的 React Native 示例中,使用 Animated API 创建了一个动画组件 FadeInView。定义了初始不透明度状态、最终状态以及转换发生的时间。动画组件添加到 Animated 组件内部,不透明度状态 fadeAnim 映射到我们要设置动画的 Text 组件的不透明度,然后调用 start() 以启动动画。

js
// React Native
const FadeInView = ({ style, children }) => {
  const fadeAnim = useRef(new Animated.Value(0)).current;

  useEffect(() => {
    Animated.timing(fadeAnim, {
      toValue: 1,
      duration: 10000
    }).start();
  }, []);

  return (
    <Animated.View style={{ ...style, opacity: fadeAnim }}>
      {children}
    </Animated.View>
  );
};
    ...
<FadeInView>
  <Text> Fading in </Text>
</FadeInView>
    ...

要在 Flutter 中创建相同的动画,请创建一个名为 controllerAnimationController 对象并指定持续时间。默认情况下,AnimationController 在给定持续时间内线性生成 0.0 到 1.0 之间的值。动画控制器会在设备准备好显示新帧时生成一个新值。通常,此速率约为每秒 60 个值。

在定义 AnimationController 时,必须传入一个 vsync 对象。vsync 的存在可以防止屏幕外动画消耗不必要的资源。您可以通过将 TickerProviderStateMixin 添加到类定义中来使用您的状态对象作为 vsyncAnimationController 需要一个 TickerProvider,该 TickerProvider 使用构造函数上的 vsync 参数进行配置。

Tween 描述了起始值和结束值之间的插值,或从输入范围到输出范围的映射。要将 Tween 对象与动画一起使用,请调用 Tween 对象的 animate() 方法,并将要修改的 Animation 对象传递给它。

在此示例中,使用了一个 FadeTransition 小部件,并将 opacity 属性映射到 animation 对象。

要启动动画,请使用 controller.forward()。还可以使用控制器执行其他操作,例如 fling()repeat()。在此示例中,FlutterLogo 小部件用于 FadeTransition 小部件内部。

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

void main() {
  runApp(const Center(child: LogoFade()));
}

class LogoFade extends StatefulWidget {
  const LogoFade({super.key});

  @override
  State<LogoFade> createState() => _LogoFadeState();
}

class _LogoFadeState extends State<LogoFade>
    with SingleTickerProviderStateMixin {
  late Animation<double> animation;
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      duration: const Duration(milliseconds: 3000),
      vsync: this,
    );
    final CurvedAnimation curve = CurvedAnimation(
      parent: controller,
      curve: Curves.easeIn,
    );
    animation = Tween(begin: 0.0, end: 1.0).animate(curve);
    controller.forward();
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return FadeTransition(
      opacity: animation,
      child: const SizedBox(
        height: 300,
        width: 300,
        child: FlutterLogo(),
      ),
    );
  }
}
Flutter fade on Android
Android
Flutter fade on iOS
iOS

如何为卡片添加滑动动画?

#

在 React Native 中,可以使用 PanResponder 或第三方库来实现滑动动画。

在 Flutter 中,要添加滑动动画,请使用 Dismissible 小部件并将子小部件嵌套。

dart
return Dismissible(
  key: Key(widget.key.toString()),
  onDismissed: (dismissDirection) {
    cards.removeLast();
  },
  child: Container(
      //...
      ),
);
Card swipe on Android
Android
Card swipe on iOS
iOS

React Native 和 Flutter 部件等效组件

#

下表列出了常用的 React Native 组件映射到相应的 Flutter 小部件和常见的小部件属性。

React Native 组件Flutter 小部件描述
ButtonElevatedButton一个基本的凸起按钮。
onPressed [必填]点击按钮或以其他方式激活按钮时的回调函数。
Child按钮的标签。
ButtonTextButton一个基本的扁平按钮。
onPressed [必填]点击按钮或以其他方式激活按钮时的回调函数。
Child按钮的标签。
ScrollViewListView一个可滚动的线性排列的小部件列表。
children( <Widget> [ ]) 要显示的子小部件列表。
controller[ ScrollController ] 可用于控制可滚动小部件的对象。
itemExtent[ double ] 如果不为空,则强制子元素在滚动方向上具有给定的范围。
scroll Direction[ Axis ] 滚动视图滚动所沿的轴。
FlatListListView.builder按需创建的小部件线性数组的构造函数。
itemBuilder [必填][IndexedWidgetBuilder] 有助于按需构建子元素。此回调仅在索引大于或等于零且小于 itemCount 时调用。
itemCount[ int ] 提高 ListView 估计最大滚动范围的能力。
ImageImage显示图像的小部件。
image [必填]要显示的图像。
Image. asset为指定图像的各种方式提供了多个构造函数。
width, height, color, alignment图像的样式和布局。
fit将图像嵌入布局期间分配的空间中。
ModalModalRoute阻止与先前路由交互的路由。
animation驱动路由转换和先前路由前向转换的动画。
ActivityIndicatorCircularProgressIndicator显示圆形进度的小部件。
strokeWidth用于绘制圆圈的线条宽度。
backgroundColor进度指示器的背景颜色。默认为当前主题的 ThemeData.backgroundColor
ActivityIndicatorLinearProgressIndicator显示线性进度的小部件。
value此进度指示器的值。
RefreshControlRefreshIndicator支持 Material “下拉刷新” 手势的小部件。
color进度指示器的前景色。
onRefresh当用户将刷新指示器拖动足够远以表明他们希望应用刷新时调用的函数。
ViewContainer围绕子小部件的小部件。
ViewColumn以垂直数组显示其子元素的小部件。
ViewRow以水平数组显示其子元素的小部件。
ViewCenter在其自身内部居中其子元素的小部件。
ViewPadding通过给定的填充插入其子元素的小部件。
padding [必填][ EdgeInsets ] 要插入子元素的空间量。
TouchableOpacityGestureDetector检测手势的小部件。
onTap点击时触发的回调函数。
onDoubleTap在同一位置快速连续点击两次时触发的回调函数。
TextInputTextInput系统文本输入控件的接口。
controller[ TextEditingController ] 用于访问和修改文本。
文本文本显示具有单个样式的文本字符串的 Text 小部件。
data[ String ] 要显示的文本。
textDirection[ TextAlign ] 文本流动的方向。
SwitchSwitch一个 Material Design 开关。
value [必填][ boolean ] 此开关是否打开或关闭。
onChanged [必填][ callback ] 当用户切换开关打开或关闭时调用。
SliderSlider用于从一系列值中进行选择。
value [必填][ double ] 滑块的当前值。
onChanged [必填]当用户为滑块选择新值时调用。