内容

面向 React Native 开发者的 Flutter

内容

此文档面向希望利用现有 RN 知识使用 Flutter 构建移动应用的 React Native (RN) 开发者。如果您了解 RN 框架的基础知识,那么可以使用此文档开始学习 Flutter 开发。

此文档可作为食谱,您可以跳过并找到与您的需求最相关的疑问。

面向 JavaScript 开发人员的 Dart 简介 (ES6)

与 React Native 一样,Flutter 使用反应式视图。然而,RN 编译为原生小部件,而 Flutter 编译为原生代码。Flutter 控制屏幕上的每个像素,从而避免了 JavaScript 桥接带来的性能问题。

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

  • 提供用于构建 Web、服务器和移动应用的开源可扩展编程语言。
  • 提供面向对象单继承语言,该语言使用 AOT 编译为原生的 C 风格语法。
  • 可选择编译为 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
let 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
let name; // == undefined
// Dart
var name; // == null; raises a linter warning
int? x; // == null

DartPad 中试用。

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

检查 null 或零

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

// 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
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 中试用。

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

异步编程

Future

与 JavaScript 类似,Dart 支持单线程执行。在 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) {
      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 函数返回一个 Promise。使用 await 运算符来等待 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);
    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 中,您需要导入每个必需的组件。

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

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

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 并通过返回视图组件来实现 render 方法。

// 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 小部件。

// 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 根小部件中。

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

在编写应用程序时,你将使用两种类型的小部件:StatelessWidgetStatefulWidgetStatelessWidget 正如其名,它是一个没有状态的小部件。StatelessWidget 一经创建,便永不改变其外观。StatefulWidget 根据接收的数据或用户输入动态地更改状态。

无状态小部件和有状态小部件之间重要的区别在于,StatefulWidget 具有 State 对象,该对象存储状态数据并在树重建过程中携带该数据,因此不会丢失。

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

如何创建可重用组件?

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

// 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 中,定义一个类来创建自定义小部件,然后重用该小部件。你还可以定义和调用一个函数,该函数返回一个可重用小部件,如下面的 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

项目结构和资源

我从哪里开始编写代码?

lib/main.dart 文件开始。当你创建一个 Flutter 应用时,会自动生成此文件。

// 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 文件来识别应用所需的资产。

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

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

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

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

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

在 Flutter 中,使用小组件的构建方法中的 Image.asset 构造函数将静态图像添加到你的应用。

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

有关详细信息,请参阅 在 Flutter 中添加资源和图像

如何通过网络加载图像?

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

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

Image.network('https://docs.fluttercn.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. 如下所示将软件包导入到你的应用代码中
import 'package:flutter/material.dart';

有关详细信息,请参阅 使用软件包开发软件包和插件

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

Flutter 小部件

在 Flutter 中,您可以使用描述其视图在当前配置和状态下应如何显示的小部件来构建 UI。

小部件通常由许多小的、单一用途的小部件组成,这些小部件嵌套在一起以产生强大的效果。例如,Container 小部件由负责布局、绘制、定位和调整大小的几个小部件组成。具体来说,Container 小部件包括 LimitedBoxConstrainedBoxAlignPaddingDecoratedBoxTransform 小部件。您可以使用新颖独特的方式组合这些和其他简单小部件,而不是对 Container 进行子类化以产生自定义效果。

Center 小部件是另一个可以控制布局的示例。要使小部件居中,请将其包装在 Center 小部件中,然后使用布局小部件进行对齐、行、列和网格。这些布局小部件没有自己的可视表示形式。相反,它们的唯一目的是控制另一个小部件布局的某些方面。要了解小部件以某种方式呈现的原因,通常检查相邻的小部件会有所帮助。

有关更多信息,请参阅 Flutter 技术概述

有关 Widgets 包中的核心小部件的更多信息,请参阅 Flutter 基本小部件Flutter 小部件目录Flutter 小部件索引

视图

View 容器的等效项是什么?

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

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

相当于 FlatListSectionList 的是什么?

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

在 React Native 中,FlatListSectionList 用于呈现简单或分段列表。

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

ListView 是 Flutter 中最常用的滚动小部件。默认构造函数采用明确的子项列表。ListView 最适合少量小部件。对于大型或无限列表,请使用 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 示例。

如何使用画布进行绘制或绘画?

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

// 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 小部件在绘制阶段进行绘制。它实现了抽象类 CustomPainter,并将其传递给 CustomPaint 的 painter 属性。CustomPaint 子类必须实现 paint()shouldRepaint() 方法。

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 中,大部分布局可以通过传递给特定组件的属性来完成。例如,你可以使用 style 属性在 View 组件上指定 flexbox 属性。要将组件排列成一列,你可以指定这样的属性:flexDirection: 'column'

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

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

例如,ColumnRow 小组件分别采用一个子组件数组,并将它们垂直和水平排列。一个 Container 小组件采用布局和样式属性的组合,而一个 Center 小组件则使它的子组件居中。

@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 在其核心小组件库中提供了各种布局小组件。例如,PaddingAlignStack

有关完整列表,请参阅 布局小组件

Layout on Android
Android
Layout on iOS
iOS

如何分层小组件?

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

Flutter 使用 Stack 小组件将子组件分层排列。这些小组件可以完全或部分重叠基本小组件。

Stack 小组件根据其边框的边缘对子组件进行定位。如果你只想重叠几个子组件,这个类非常有用。

@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 将一个容器(在半透明黑色背景上显示其 Text)叠加在 CircleAvatar 上。Stack 使用对齐属性和 Alignment 坐标来偏移文本。

Stack on Android
Android
Stack on iOS
iOS

有关详细信息,请参阅 Stack 类文档。

样式

如何为我的组件设置样式?

在 React Native 中,内联样式和 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 小部件可以为其样式属性采用 TextStyle 类。如果你想在多个地方使用相同的文本样式,则可以创建一个 TextStyle 类,并将其用于多个 Text 小部件。

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 图标颜色

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 风格) 包为当前 iOS 设计语言提供了高保真小部件。要使用 CupertinoIcons 字体,请在项目的 pubspec.yaml 文件中添加对 cupertino_icons 的依赖项。

name: my_awesome_application
dependencies:
  cupertino_icons: ^1.0.6

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

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

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 属性,为几乎所有内容创建统一的样式。

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

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

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

状态管理

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

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

StatelessWidget

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

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

AboutDialogCircleAvatarText 是子类化为 StatelessWidget 的无状态组件的示例。

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 是一个会改变状态的 widget。使用 setState 方法来管理 StatefulWidget 的状态改变。调用 setState() 会告知 Flutter 框架状态中发生了某些改变,这会导致应用重新运行 build() 方法,以便应用可以反映该改变。

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

CheckboxRadioSliderInkWellFormTextFieldStatefulWidget 的子类,它们是状态化 widget 的示例。

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

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

  final String title;

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

以下状态类 _MyStatefulWidgetState 为 widget 实现 build() 方法。当状态改变时,例如,当用户切换按钮时,setState() 会使用新的切换值被调用。这会导致框架在 UI 中重新构建此 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),
              child: ElevatedButton(
                onPressed: toggleBlinkState,
                child: toggleState
                    ? const Text('Blink')
                    : const Text('Stop Blinking'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

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

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

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

在 Flutter 中,小部件要么是 Stateful,要么是 Stateless,具体取决于它们是否依赖于状态更改。

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

在 Flutter 中,有三种主要方式来管理状态

  • 小部件管理自己的状态
  • 父小部件管理小部件的状态
  • 混合匹配方法

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

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

MyStatefulWidget 类管理自己的状态,它扩展 StatefulWidget,它覆盖 createState() 方法来创建 State 对象,框架调用 createState() 来构建小部件。在此示例中,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 添加到小部件树。

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

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

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

// 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 标记的局部变量或函数,并使用在参数化构造函数中接收到的属性。

/// 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
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
import 'package:shared_preferences/shared_preferences.dart';

要实现持久数据,请使用 SharedPreferences 类提供的 setter 方法。setter 方法可用于各种基本类型,例如 setIntsetBoolsetString。要读取数据,请使用 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 中,新屏幕是新活动。在 iOS 中,新屏幕是新视图控制器。在 Flutter 中,屏幕只是小部件!要在 Flutter 中导航到新屏幕,请使用 Navigator 小部件。

如何在屏幕之间导航?

在 React Native 中,有三个主要导航器: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 中,有两个主要的小部件用于在屏幕之间导航

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

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

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函数以导航到指定的路由。

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

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

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

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

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

选项卡导航

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

// 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
显示与当前选定选项卡相对应的部件。
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 构造函数。

由可以提供 Ticker 对象的类实现的 TickerProvider 是一个界面。任何必须在触发帧时得到通知的对象都可以使用计时器,但最常见的是通过 AnimationController 间接使用。AnimationController 需要一个 TickerProvider 来获取其 Ticker。如果您正在从 State 创建 AnimationController,则可以使用 TickerProviderStateMixinSingleTickerProviderStateMixin 类来获取合适的 TickerProvider

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

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

// 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 小组件提供点击时的导航。

@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 小部件,当 DrawerScaffold 中可用时,它会自动显示一个合适的 IconButton 来显示 DrawerScaffold 会自动处理边缘滑动手势以显示 Drawer

@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 组件向组件添加侦听器。

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

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

// 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 中,向任何小部件添加手势检测。

@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 进行网络连接,您可以发出获取请求,然后接收响应以获取数据。

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

import 'dart:io';

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

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 组件显示文本输入框,然后使用回调将值存储在变量中。

// 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 中的文本。

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 对象用于保存、重置或验证每个 FormField,它是该 Form 的后代。要获取 FormState,可以使用 Form.of(),其中上下文是 Form 的祖先,或者将 GlobalKey 传递给 Form 构造函数并调用 GlobalKey.currentState()

@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)在提交时保存表单。

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 中,将使用以下实现

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

在 Flutter 中,使用以下实现

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 的有状态热重载功能可帮助您快速轻松地进行实验、构建 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 来访问菜单,或键入以下快捷键

操作 终端快捷键 调试函数和属性
应用的小部件层次结构 w debugDumpApp()
应用的渲染树 t debugDumpRenderTree()
图层 L debugDumpLayerTree()
无障碍功能 S(遍历顺序)或
U(逆命中测试顺序)
debugDumpSemantics()
切换小部件检查器 i WidgetsApp. showWidgetInspectorOverride
切换构造线显示 p debugPaintSizeEnabled
模拟不同的操作系统 o defaultTargetPlatform
显示性能覆盖 P WidgetsApp. showPerformanceOverlay
将屏幕截图保存到 flutter.png s  
退出 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() 来启动动画。

// 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 小组件中。

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 小组件,并将子小组件嵌套。

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 组件 说明
Button ElevatedButton 一个基本的凸起按钮。
  onPressed [必需] 当按钮被点击或以其他方式激活时的回调。
  Child 按钮的标签。
     
Button TextButton 一个基本的扁平按钮。
  onPressed [必需] 当按钮被点击或以其他方式激活时的回调。
  Child 按钮的标签。
     
ScrollView ListView 一个可滚动的线性排列的组件列表。
  children ( <Widget> [ ]) 要显示的子组件列表。
  controller [ ScrollController ] 可用于控制可滚动组件的对象。
  itemExtent [ double ] 如果非空,则强制子组件在滚动方向上具有给定的范围。
  scroll Direction [ Axis ] 滚动视图沿其滚动轴。
     
FlatList ListView.builder 按需创建的线性组件数组的构造函数。
  itemBuilder [必需] [IndexedWidgetBuilder] 帮助按需构建子组件。此回调仅在索引大于或等于零且小于 itemCount 时调用。
  itemCount [ int ] 提高 ListView 估计最大滚动范围的能力。
     
Image Image 显示图像的组件。
  image [必需] 要显示的图像。
  Image. asset 为指定图像的各种方式提供了多个构造函数。
  width, height, color, alignment 图像的样式和布局。
  fit 将图像嵌入到布局期间分配的空间中。
     
Modal ModalRoute 阻止与先前路由交互的路由。
  animation 驱动路由转换和先前路由正向转换的动画。
     
ActivityIndicator CircularProgressIndicator 沿圆形显示进度的组件。
  strokeWidth 用于绘制圆形的线条的宽度。
  backgroundColor 进度指示器的背景颜色。默认情况下为当前主题的 ThemeData.backgroundColor
     
ActivityIndicator LinearProgressIndicator 一个沿线显示进度的小组件。
  value 此进度指示器的值。
     
RefreshControl RefreshIndicator 支持 Material “下拉刷新”惯例的小组件。
  color 进度指示器的前景色。
  onRefresh 当用户拖动刷新指示器足够远以表明他们希望应用刷新时调用的函数。
     
View Container 环绕子小组件的小组件。
     
View Column 以垂直阵列显示其子项的小组件。
     
View Row 以水平阵列显示其子项的小组件。
     
View Center 在其内部居中其子项的小组件。
     
View Padding 通过给定的填充量缩进其子项的小组件。
  padding [必需] [ EdgeInsets ] 缩进子项的空间量。
     
TouchableOpacity GestureDetector 检测手势的小组件。
  onTap 点击发生时的回调。
  onDoubleTap 在同一位置快速连续点击两次时调用的回调。
     
TextInput TextInput 系统文本输入控件的接口。
  controller [ TextEditingController ] 用于访问和修改文本。
     
文本 文本 显示具有单一样式的文本字符串的 Text 小组件。
  data [ String ] 要显示的文本。
  textDirection [ TextAlign ] 文本流动的方向。
     
Switch Switch 一种材料设计开关。
  value [必需] [ boolean ] 此开关是打开还是关闭。
  onChanged [必需] [ callback ] 用户打开或关闭开关时调用。
     
Slider Slider 用于从一系列值中进行选择。
  value [必需] [ double ] 滑块的当前值。
  onChanged [必需] 当用户为滑块选择新值时调用。