面向 React Native 开发人员的 Flutter
- JavaScript 开发人员 (ES6) 的 Dart 简介
- 基础知识
- 项目结构和资源
- Flutter widget
- 视图
- 布局
- 样式
- 状态管理
- 属性 (Props)
- 本地存储
- 路由
- 手势检测和触摸事件处理
- 进行 HTTP 网络请求
- 表单输入
- 平台特定代码
- 调试
- 动画
- React Native 和 Flutter widget 等效组件
本文档面向希望将他们现有的 React Native (RN) 知识应用于使用 Flutter 构建移动应用程序的 React Native (RN) 开发人员。如果您了解 RN 框架的基础知识,那么您可以使用本文档作为学习 Flutter 开发的入门方式。
您可以像使用菜谱一样使用本文档,随意跳转并找到与您的需求最相关的疑问。
JavaScript 开发人员 (ES6) 的 Dart 简介
#与 React Native 类似,Flutter 使用反应式视图。但是,虽然 RN 转译为原生 widget,但 Flutter 则完全编译为原生代码。Flutter 控制屏幕上的每个像素,从而避免了由于需要 JavaScript 桥接而导致的性能问题。
Dart 是一种易于学习的语言,并提供以下特性:
- 提供一种开源的、可扩展的编程语言,用于构建 Web、服务器和移动应用程序。
- 提供一种面向对象、单继承的语言,它使用 C 风格的语法,并被 AOT 编译为原生代码。
- 可选地转译为 JavaScript。
- 支持接口和抽象类。
下面描述了 JavaScript 和 Dart 之间的一些差异示例。
入口点
#JavaScript 没有预定义的入口函数——您定义入口点。
// JavaScript
function startHere() {
// 可用作入口点
}
在 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
/// 两个变量都是可接受的。
String name = 'dart'; // 显式类型化为 [String]。
var otherName = 'Dart'; // 推断为 [String] 类型。
在 DartPad 中试用。
更多信息,请参见 Dart 的类型系统。
默认值
#在 JavaScript 中,未初始化的变量为 undefined
。
在 Dart 中,未初始化的变量的初始值为 null
。因为数字在 Dart 中是对象,所以即使是具有数值类型的未初始化变量也具有 null
值。
// JavaScript
let name; // == undefined
// Dart
var name; // == null; 会引发 linter 警告
int? x; // == null
在 DartPad 中试用。
更多信息,请参见关于 变量 的文档。
检查 null 或零
#在 JavaScript 中,使用 ==
比较运算符时,值为 1 或任何非 null 对象都被视为 true
。
// JavaScript
let myNull = null;
if (!myNull) {
console.log('null 被视为 false');
}
let zero = 0;
if (!zero) {
console.log('0 被视为 false');
}
在 Dart 中,只有布尔值 true
被视为 true。
/// Dart
var myNull;
var zero = 0;
if (zero == 0) {
print('使用 "== 0" 检查零');
}
在 DartPad 中试用。
函数
#Dart 和 JavaScript 函数通常很相似。主要区别在于声明。
// JavaScript
function fn() {
return true;
}
/// Dart
/// 你可以显式地定义返回类型。
bool fn() {
return true;
}
在 DartPad 中试用。
更多信息,请参见关于 函数 的文档。
异步编程
#Futures
#与 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
对象的文档。
async
和 await
#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;
}
}
/// 异步函数返回一个 `Future`。
/// 它也可以返回 `void`,除非你使用
/// `avoid_void_async` lint。在这种情况下,
/// 返回 `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 run
或 yarn run
。
您可以通过几种方式运行 Flutter 应用程序:
- 使用安装了 Flutter 和 Dart 插件的 IDE 中的“运行”选项。
- 从项目的根目录使用
flutter run
。
您的应用程序将在连接的设备、iOS 模拟器或 Android 模拟器上运行。
更多信息,请参见 Flutter 入门 文档。
如何导入 widget?
#在 React Native 中,您需要导入每个所需的组件。
// React Native
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
在 Flutter 中,要使用 Material Design 库中的 widget,请导入 material.dart
包。要使用 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';
无论您导入哪个 widget 包,Dart 只会拉取应用程序中使用的 widget。
更多信息,请参见 Flutter Widget 目录。
Flutter 中等同于 React Native 的“Hello world!”应用程序是什么?
#在 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 中,您可以使用核心 widget 库中的 Center
和 Text
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,
),
),
);
}
下图显示了基本 Flutter“Hello world!”应用程序的 Android 和 iOS UI。
![Hello world app on Android](/assets/images/docs/get-started/android/react-native/hello-world-basic.png)
![Hello world app on iOS](/assets/images/docs/get-started/ios/react-native/hello-world-basic.png)
既然您已经看到了最基本的 Flutter 应用程序,下一节将展示如何利用 Flutter 丰富的 widget 库来创建现代化的、精美的应用程序。
如何使用 widget 并将它们嵌套以形成 widget 树?
#在 Flutter 中,几乎所有东西都是一个 widget。
Widget 是应用程序用户界面的基本构建块。您可以将 widget 组合成一个层次结构,称为 widget 树。每个 widget 都嵌套在其父 widget 内,并继承其父级的属性。即使应用程序对象本身也是一个 widget。没有单独的“应用程序”对象。相反,根 widget 扮演此角色。
一个 widget 可以定义:
- 结构元素——例如按钮或菜单
- 风格元素——例如字体或配色方案
- 布局方面——例如填充或对齐方式
以下示例展示了使用 Material 库中的 widget 的“Hello world!”应用程序。在此示例中,widget 树嵌套在 MaterialApp
根 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](/assets/images/docs/get-started/android/react-native/hello-world.png)
![Hello world app on iOS](/assets/images/docs/get-started/ios/react-native/hello-world.png)
编写应用程序时,您将使用两种类型的 widget: StatelessWidget
或 StatefulWidget
。StatelessWidget
正如其名——一个没有状态的 widget。StatelessWidget
只创建一次,并且永远不会改变其外观。StatefulWidget
根据接收到的数据或用户输入动态更改状态。
无状态和有状态 widget 之间的重要区别在于,StatefulWidget
具有一个 State
对象,该对象存储状态数据并在树重建过程中保留它,因此不会丢失。
在简单或基本的应用程序中,嵌套 widget 很容易,但是随着代码库的增大和应用程序变得复杂,您应该将深度嵌套的 widget 分解成返回 widget 或较小类的函数。创建单独的函数和 widget 允许您在应用程序中重用组件。
如何创建可重用的组件?
#在 React Native 中,您将定义一个类来创建一个可重用的组件,然后使用 props
方法来设置或返回所选元素的属性和值。在下面的示例中,定义了 CustomCard
类,然后在父类中使用它。
// React Native
const CustomCard = ({ index, onPress }) => {
return (
<View>
<Text> Card {index} </Text>
<Button
title="Press"
onPress={() => onPress(index)}
/>
</View>
);
};
// 使用方法
<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) {
/// 使用方法
return CustomCard(
index: index,
onPress: () {
print('Card $index');
},
);
}
}
在前面的示例中,CustomCard
类的构造函数使用 Dart 的花括号语法 { }
来指示命名参数。
要要求这些字段,请从构造函数中删除花括号,或向构造函数添加 required
。
以下屏幕截图显示了可重用 CustomCard
类的示例。
![Custom cards on Android](/assets/images/docs/get-started/android/react-native/custom-cards.png)
![Custom cards on iOS](/assets/images/docs/get-started/ios/react-native/custom-cards.png)
项目结构和资源
#我应该从哪里开始编写代码?
#从 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 - 包含 Android 特定的文件。
├ build - 存储 iOS 和 Android 构建文件。
├ ios - 包含 iOS 特定的文件。
├ lib - 包含外部可访问的 Dart 源文件。
┬
└ src - 包含其他源文件。
└ main.dart - Flutter 入口点和新应用程序的起点。
创建 Flutter 项目时会自动生成此文件。
这是您开始编写 Dart 代码的地方。
├ test - 包含自动化测试文件。
└ pubspec.yaml - 包含 Flutter 应用程序的元数据。
这相当于 React Native 中的 package.json 文件。
我应该将资源和资产放在哪里,以及如何使用它们?
#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
的 assets 部分指定资产的路径时,构建过程会在相邻的子目录中查找任何同名的文件。这些文件也包含在资产包中以及指定的资产一起。Flutter 在选择适合应用程序分辨率的图像时使用资产变体。
在 React Native 中,您可以通过将图像文件放在源代码目录中并引用它来添加静态图像。
<Image source={require('./my-icon.png')} />
// OR
<Image
source={{
url: 'https://reactnative.dev/img/tiny_logo.png'
}}
/>
在 Flutter 中,使用 widget 的 build 方法中的 Image.asset
构造函数将静态图像添加到您的应用程序中。
Image.asset('assets/background.png');
更多信息,请参见在 Flutter 中添加资产和图像。
如何加载网络上的图像?
#在 React Native 中,您需要在 Image
组件的 source
属性中指定 uri
,并根据需要提供大小。
在 Flutter 中,使用 Image.network
构造函数包含来自 URL 的图像。
Image.network('https://docs.flutter.dev/assets/images/docs/owl.jpg');
如何安装包和包插件?
#Flutter 支持使用其他开发者贡献给 Flutter 和 Dart 生态系统的共享包。这允许您快速构建应用程序,而无需从头开发所有内容。包含平台特定代码的包称为包插件。
在 React Native 中,您将使用 yarn add {package-name}
或 npm install --save {package-name}
从命令行安装包。
在 Flutter 中,请使用以下说明安装包:
- 要将
google_sign_in
包添加为依赖项,请运行flutter pub add
:
flutter pub add google_sign_in
- 使用
flutter pub get
从命令行安装包。如果使用 IDE,它通常会为您运行flutter pub get
,或者它可能会提示您这样做。 - 将包导入您的应用程序代码,如下所示:
import 'package:flutter/material.dart';
您可以在 pub.dev 的 Flutter 包 部分找到 Flutter 开发人员共享的许多包。
Flutter widget
#在 Flutter 中,您可以使用 widget 来构建 UI,这些 widget 描述了给定其当前配置和状态时它们的视图应该是什么样子。
widget 通常由许多小的、单一用途的 widget 组成,这些 widget 嵌套在一起以产生强大的效果。例如,Container
widget 由几个负责布局、绘制、定位和调整大小的 widget 组成。具体来说,Container
widget 包括 LimitedBox
、ConstrainedBox
、Align
、Padding
、DecoratedBox
和 Transform
widget。与其子类化 Container
来产生自定义效果,不如以新的和独特的方式组合这些和其他简单的 widget。
Center
widget 是另一个您可以控制布局的示例。要居中一个 widget,请将其包装在一个 Center
widget 中,然后使用布局 widget 进行对齐、行、列和网格。这些布局 widget 本身没有可视化表示。相反,它们的唯一目的是控制另一个 widget 布局的某些方面。要理解为什么 widget 以某种方式呈现,检查相邻的 widget 通常很有帮助。
更多信息,请参见Flutter 技术概述。
有关 Widgets
包中核心 widget 的更多信息,请参见Flutter 基本 widget、Flutter widget 目录 或Flutter widget 索引。
视图
#等效于 View
容器的是什么?
#在 React Native 中,View
是一个支持使用 Flexbox
进行布局、样式、触摸处理和辅助功能控制的容器。
在 Flutter 中,您可以使用 Widgets
库中的核心布局 widget,例如 Container
、Column
、Row
和 Center
。更多信息,请参见布局 widget 目录。
等效于 FlatList
或 SectionList
的是什么?
#List
是垂直排列的组件的可滚动列表。
在 React Native 中,FlatList
或 SectionList
用于渲染简单列表或分段列表。
// React Native
<FlatList
data={[ ... ]}
renderItem={({ item }) => <Text>{item.key}</Text>}
/>
ListView
是 Flutter 最常用的滚动 widget。默认构造函数采用显式的子 widget 列表。ListView
最适合少量 widget。对于大型或无限列表,请使用 ListView.builder
,它可以按需构建其子 widget,并且只构建可见的子 widget。
var data = [
'Hello',
'World',
];
return ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) {
return Text(data[index]);
},
);
![Flat list on Android](/assets/images/docs/get-started/android/react-native/flatlist.gif)
![Flat list on iOS](/assets/images/docs/get-started/ios/react-native/flatlist.gif)
要了解如何实现无限滚动的列表,请参见官方的 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 中,您可以使用 CustomPaint
和 CustomPainter
类在画布上绘图。
以下示例展示了如何在绘制阶段使用 CustomPaint
widget 进行绘图。它实现了抽象类 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](/assets/images/docs/get-started/android/react-native/canvas.png)
![Canvas on iOS](/assets/images/docs/get-started/ios/react-native/canvas.png)
布局
#如何使用 widget 定义布局属性?
#在 React Native 中,大部分布局都可以通过传递给特定组件的 props 来完成。例如,您可以使用 View
组件上的 style
prop 来指定 flexbox 属性。要将组件排列成一列,您可以指定一个 prop,例如:flexDirection: 'column'
。
// React Native
<View
style={{
flex: 1,
flexDirection: 'column',
justifyContent: 'space-between',
alignItems: 'center'
}}
>
在 Flutter 中,布局主要由专门设计用于提供布局的 widget 以及控制 widget 及其样式属性组合定义。
例如,Column
和 Row
widget 采用子 widget 数组,并分别沿垂直和水平方向对齐它们。Container
widget 采用布局和样式属性的组合,而 Center
widget 将其子 widget 居中。
@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。例如,Padding
、Align
和 Stack
。
完整的列表,请参见布局 widget。
![Layout on Android](/assets/images/docs/get-started/android/react-native/basic-layout.gif)
![Layout on iOS](/assets/images/docs/get-started/ios/react-native/basic-layout.gif)
如何分层 widget?
#在 React Native 中,可以使用 absolute
定位来分层组件。
Flutter 使用 Stack
widget 将子 widget 排列成多层。widget 可以完全或部分地与基础 widget 重叠。
Stack
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 属性和 Alignment
坐标偏移文本。
![Stack on Android](/assets/images/docs/get-started/android/react-native/stack.png)
![Stack on iOS](/assets/images/docs/get-started/ios/react-native/stack.png)
更多信息,请参见 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
widget 可以为其样式属性使用 TextStyle
类。如果要在多个地方使用相同的文本样式,您可以创建一个 TextStyle
类并将其用于多个 Text
widget。
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](/assets/images/docs/get-started/android/react-native/flutterstyling.gif)
![Styling on iOS](/assets/images/docs/get-started/ios/react-native/flutterstyling.gif)
如何使用 Icons
和 Colors
?
#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 设计语言提供了高保真 widget。要使用 CupertinoIcons
字体,请在项目的 pubspec.yaml
文件中添加 cupertino_icons
的依赖项。
name: my_awesome_application
dependencies:
cupertino_icons: ^1.0.8
要全局自定义组件的颜色和样式,请使用 ThemeData
为主题的各个方面指定默认颜色。将 MaterialApp
中的 theme 属性设置为 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
widget 中的 theme 属性,为几乎所有内容创建统一的样式。
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primaryColor: Colors.cyan,
brightness: Brightness.dark,
),
home: const StylingPage(),
);
}
即使不使用 MaterialApp
widget,也可以应用 Theme
。Theme
widget 在其 data
参数中采用 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
。
有关在 Flutter 中处理状态管理方法的更多信息,请参见状态管理。
无状态 widget
#Flutter 中的 StatelessWidget
是一个不需要状态变化的 widget——它没有内部状态需要管理。
当您描述的用户界面部分不依赖于对象本身的配置信息和 widget 被膨胀的 BuildContext
以外的任何其他内容时,无状态 widget 很有用。
AboutDialog
、CircleAvatar
和 Text
是继承自 StatelessWidget
的无状态 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
类的构造函数传递标记为 final
的 text
。此类扩展了 StatelessWidget
——它包含不可变数据。
无状态 widget 的 build
方法通常只在三种情况下被调用:
- 当 widget 被插入到树中时
- 当 widget 的父级更改其配置时
- 当它依赖的
InheritedWidget
发生更改时
有状态 widget
#StatefulWidget
是一个会改变状态的 widget。使用 setState
方法来管理 StatefulWidget
的状态更改。调用 setState()
会告诉 Flutter 框架状态中发生了一些更改,这会导致应用程序重新运行 build()
方法,以便应用程序可以反映更改。
状态 是在构建 widget 时可以同步读取的信息,并且可能在 widget 的生命周期内发生变化。widget 实现者的责任是确保在状态发生变化时及时通知状态对象。当 widget 可以动态更改时,使用 StatefulWidget
。例如,widget 的状态通过在表单中键入或移动滑块而发生更改。或者,它可能会随着时间的推移而改变——也许数据馈送会更新 UI。
Checkbox
、Radio
、Slider
、InkWell
、Form
和 TextField
是继承自 StatefulWidget
的有状态 widget 的示例。
以下示例声明了一个 StatefulWidget
,它需要一个 createState()
方法。此方法创建管理 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 的最佳实践是什么?
#设计 widget 时,需要考虑以下几点。
- 确定 widget 是否应该是
StatefulWidget
或StatelessWidget
。
在 Flutter 中,widget 要么是有状态的,要么是无状态的——取决于它们是否依赖于状态更改。
- 如果 widget 发生更改——用户与之交互或数据馈送中断 UI,则它是 有状态的 。
- 如果 widget 是最终的或不可变的,则它是 无状态的 。
- 确定哪个对象管理 widget 的状态(对于
StatefulWidget
)。
在 Flutter 中,主要有三种管理状态的方法:
- widget 管理其自身状态
- 父 widget 管理 widget 的状态
- 混合方法
在决定使用哪种方法时,请考虑以下原则:
- 如果相关状态是用户数据,例如复选框的选中或未选中模式,或滑块的位置,则最好由父 widget 管理状态。
- 如果相关状态是美观的,例如动画,则 widget 本身最好管理状态。
- 如果有疑问,让父 widget 管理子 widget 的状态。
- 继承 StatefulWidget 和 State。
MyStatefulWidget
类管理其自身状态——它扩展了 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) {
//...
}
}
- 将 StatefulWidget 添加到 widget 树中。
在应用程序的 build 方法中,将自定义 StatefulWidget
添加到 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](/assets/images/docs/get-started/android/react-native/state-change.gif)
![State change on iOS](/assets/images/docs/get-started/ios/react-native/state-change.gif)
属性 (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) {
/// 使用方法
return CustomCard(
index: index,
onPress: () {
print('Card $index');
},
);
}
}
![Cards on Android](/assets/images/docs/get-started/android/react-native/modular.png)
![Cards on iOS](/assets/images/docs/get-started/ios/react-native/modular.png)
本地存储
#如果您不需要存储大量数据,并且不需要结构,您可以使用 shared_preferences
,它允许您读取和写入原始数据类型的持久键值对:布尔值、浮点数、整数、长整数和字符串。
如何存储对应用程序全局有效的持久键值对?
#在 React Native 中,您可以使用 AsyncStorage
组件的 setItem
和 getItem
函数来存储和检索对应用程序持久且全局有效的数据。
// 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 方法可用于各种原始类型,例如 setInt
、setBool
和 setString
。要读取数据,请使用 SharedPreferences
类提供的相应 getter 方法。对于每个 setter,都有一个对应的 getter 方法,例如 getInt
、getBool
和 getString
。
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。
如何在屏幕之间导航?
#在 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 中,主要使用两个 widget 在屏幕之间导航:
Navigator
被定义为一个使用堆栈规则管理一组子 widget 的 widget。导航器管理一个 Route
对象的堆栈,并提供管理堆栈的方法,
例如 Navigator.push
和 Navigator.pop
。可以在 MaterialApp
widget 中指定路由列表,或者可以动态构建它们,例如在英雄动画中。以下示例在 MaterialApp
widget 中指定命名路由。
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
(widget 树中 widget 位置的句柄)。路由名称传递给 pushNamed
函数以导航到指定的路由。
Navigator.of(context).pushNamed('/a');
您还可以使用 Navigator
的 push 方法,该方法将给定的 Route
添加到最紧密包含给定 BuildContext
的导航器的历史记录中,并转换到该路由。在以下示例中,MaterialPageRoute
widget 是一个模态路由,它使用平台自适应转换替换整个屏幕。它采用 WidgetBuilder
作为必需参数。
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const UsualNavScreen(),
),
);
如何使用选项卡导航和抽屉导航?
#在 Material Design 应用中,Flutter 导航主要有两种选择:选项卡和抽屉。当空间不足以支持选项卡时,抽屉提供了一个不错的替代方案。
选项卡导航
#在 React Native 中,createBottomTabNavigator
和 TabNavigation
用于显示选项卡和选项卡导航。
// React Native
import { createBottomTabNavigator } from 'react-navigation';
const MyApp = TabNavigator(
{ Home: { screen: HomeScreen }, Notifications: { screen: tabNavScreen } },
{ tabBarOptions: { activeTintColor: '#e91e63' } }
);
Flutter 提供了几个专门用于抽屉和选项卡导航的 widget:
TabController
:协调 TabBar
和 TabBarView
之间的选项卡选择。
TabBar
:显示选项卡的水平行。
Tab
:创建一个 Material Design TabBar 选项卡。
TabBarView
:显示对应于当前选中选项卡的 widget。
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
来协调 TabBar
和 TabBarView
之间的选项卡选择。TabController
构造函数的 length
参数是选项卡的总数。需要 TickerProvider
来在帧触发状态更改时触发通知。TickerProvider
是 vsync
。在创建新的 TabController
时,将 vsync: this
参数传递给 TabController
构造函数。
TickerProvider
是一个接口,由可以分发 Ticker
对象的类实现。任何必须在帧触发时收到通知的对象都可以使用 Tickers,但它们最常用的是通过 AnimationController
间接使用。AnimationController
需要 TickerProvider
来获取其 Ticker
。如果您正在从 State 创建 AnimationController
,那么您可以使用 TickerProviderStateMixin
或 SingleTickerProviderStateMixin
类来获取合适的 TickerProvider
。
Scaffold
widget 包装一个新的 TabBar
widget 并创建两个选项卡。TabBarView
widget 作为 Scaffold
widget 的 body
参数传递。所有与 TabBar
widget 的选项卡相对应的屏幕都是 TabBarView
widget 的子项,以及相同的 TabController
。
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 包,然后使用 createDrawerNavigator
和 DrawerNavigation
。
// React Native
export default (MyApp1 = DrawerNavigator({
Home: {
screen: SimpleApp
},
Screen2: {
screen: drawerScreen
}
}));
在 Flutter 中,我们可以将 Drawer
widget 与 Scaffold
结合使用,以创建具有 Material Design 抽屉的布局。要向应用程序添加 Drawer
,请将其包装在 Scaffold
widget 中。Scaffold
widget 为遵循 Material Design 指南的应用程序提供了始终如一的视觉结构。它还支持特殊的 Material Design 组件,例如 Drawers
、AppBars
和 SnackBars
。
Drawer
widget 是一个 Material Design 面板,它从 Scaffold
的边缘水平滑入,以显示应用程序中的导航链接。您可以提供 ElevatedButton
、Text
widget 或项目列表以显示为 Drawer
widget 的子项。在以下示例中,ListTile
widget 提供点击时的导航。
@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
widget 还包括一个 AppBar
widget,当 Scaffold
中有 Drawer 时,它会自动显示适当的 IconButton 以显示 Drawer
。Scaffold
自动处理边缘滑动手势以显示 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](/assets/images/docs/get-started/android/react-native/navigation.gif)
![Navigation on iOS](/assets/images/docs/get-started/ios/react-native/navigation.gif)
手势检测和触摸事件处理
#要侦听和响应手势,Flutter 支持点击、拖动和缩放。Flutter 中的手势系统有两个独立的层。第一层包括原始指针事件,它描述了指针(例如触摸、鼠标和触笔移动)在屏幕上的位置和移动。第二层包括手势,它描述由一个或多个指针移动组成的语义操作。
如何向 widget 添加点击或按下侦听器?
#在 React Native 中,使用 PanResponder
或 Touchable
组件向组件添加侦听器。
// 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 中,要向 widget 添加点击(或按下)侦听器,请使用具有 onPress:
字段的按钮或可触摸 widget。或者,通过将 widget 包装在 GestureDetector
中来向任何 widget 添加手势检测。
@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](/assets/images/docs/get-started/android/react-native/flutter-gestures.gif)
![Gestures on iOS](/assets/images/docs/get-started/ios/react-native/flutter-gestures.gif)
进行 HTTP 网络请求
#从互联网获取数据对于大多数应用程序来说是很常见的。在 Flutter 中,
在 Flutter 中,http
包提供了从互联网获取数据的最简单方法。
如何从 API 调用中获取数据?
#React Native 提供了用于网络的 Fetch API——您可以发出 fetch 请求,然后接收响应以获取数据。
// 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](/assets/images/docs/get-started/android/react-native/api-calls.gif)
![API calls on iOS](/assets/images/docs/get-started/ios/react-native/api-calls.gif)
表单输入
#文本字段允许用户在您的应用程序中键入文本,因此它们可用于构建表单、消息应用程序、搜索体验等等。Flutter 提供了两个核心文本字段 widget: TextField
和 TextFormField
。
如何使用文本字段 widget?
#在 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
widget。每当修改文本字段时,控制器都会通知其侦听器。
侦听器读取文本和选择属性以了解用户在字段中键入了什么。您可以通过控制器的 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
widget 实现的,并且通过 TextEditingController
的 text
属性访问 TextField
中的文本。
如何使用表单 widget?
#在 Flutter 中,使用 Form
widget,其中 TextFormField
widget 与提交按钮一起作为子项传递。TextFormField
widget 有一个名为 onSaved
的参数,它接受一个回调并在保存表单时执行。FormState
对象用于保存、重置或验证此 Form
的每个子 FormField
。要获取 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](/assets/images/docs/get-started/android/react-native/input-fields.gif)
![Input on iOS](/assets/images/docs/get-started/ios/react-native/input-fields.gif)
平台特定代码
#在构建跨平台应用程序时,您希望在平台之间尽可能多地重用代码。但是,可能会出现某些情况下,代码根据操作系统而有所不同是合理的。这需要通过声明特定平台来进行单独的实现。
在 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 包括对分析、检查堆、检查 widget 树、记录诊断信息、调试、观察执行的代码行、调试内存泄漏和内存碎片的支持。更多信息,请查看 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
来访问菜单,或者键入以下快捷键:
操作 | 终端快捷键 | 调试功能和属性 |
---|---|---|
应用程序的 widget 层次结构 | w | debugDumpApp() |
应用程序的渲染树 | t | debugDumpRenderTree() |
图层 | L | debugDumpLayerTree() |
可访问性 | S (遍历顺序)或U (反向命中测试顺序) | debugDumpSemantics() |
切换 widget 检查器 | i | WidgetsApp.showWidgetInspectorOverride |
切换构造线的显示 | p | debugPaintSizeEnabled |
模拟不同的操作系统 | o | defaultTargetPlatform |
显示性能覆盖层 | P | WidgetsApp.showPerformanceOverlay |
将屏幕截图保存到 flutter.png | s | |
退出 | q |
动画
#精心设计的动画使 UI 感觉直观,有助于完善应用程序的外观和感觉,并改善用户体验。Flutter 的动画支持使实现简单和复杂的动画变得容易。Flutter SDK 包含许多包含标准运动效果的 Material Design widget,您可以轻松自定义这些效果以个性化您的应用程序。
在 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 中创建相同的动画,请创建一个名为 controller
的 AnimationController
对象并指定持续时间。默认情况下,AnimationController
在给定持续时间内线性生成范围从 0.0 到 1.0 的值。每当运行您的应用程序的设备准备好显示新帧时,动画控制器都会生成一个新值。通常,此速率约为每秒 60 个值。
在定义 AnimationController
时,您必须传入 vsync
对象。vsync
的存在可以防止屏幕外动画消耗不必要的资源。您可以通过向类定义添加 TickerProviderStateMixin
来使用您的有状态对象作为 vsync
。AnimationController
需要一个 TickerProvider
,
这是 AnimationController
的构造函数上使用 vsync
参数配置的。
Tween
描述了开始值和结束值之间的插值,或者从输入范围到输出范围的映射。要在动画中使用 Tween
对象,请调用 Tween
对象的 animate()
方法并将其传递给要修改的 Animation
对象。
在此示例中,使用了 FadeTransition
widget,并且 opacity
属性映射到 animation
对象。
要启动动画,请使用 controller.forward()
。还可以使用控制器执行其他操作,例如 fling()
或 repeat()
。在此示例中,FlutterLogo
widget 用于 FadeTransition
widget 内。
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](/assets/images/docs/get-started/android/react-native/flutter-fade.gif)
![Flutter fade on iOS](/assets/images/docs/get-started/ios/react-native/flutter-fade.gif)
如何向卡片添加滑动动画?
#在 React Native 中,使用 PanResponder
或第三方库来进行滑动动画。
在 Flutter 中,要添加滑动动画,请使用 Dismissible
widget 并嵌套子 widget。
return Dismissible(
key: Key(widget.key.toString()),
onDismissed: (dismissDirection) {
cards.removeLast();
},
child: Container(
//...
),
);
![Card swipe on Android](/assets/images/docs/get-started/android/react-native/card-swipe.gif)
![Card swipe on iOS](/assets/images/docs/get-started/ios/react-native/card-swipe.gif)
React Native 和 Flutter widget 等效组件
#下表列出了常用的 React Native 组件及其对应的 Flutter widget 和常用 widget 属性。
React Native 组件 | Flutter Widget | 说明 |
---|---|---|
Button | ElevatedButton | 一个基本的凸起按钮。 |
onPressed [required] | 点击或激活按钮时的回调。 | |
Child | 按钮的标签。 | |
Button | TextButton | 一个基本的平面按钮。 |
onPressed [required] | 点击或激活按钮时的回调。 | |
Child | 按钮的标签。 | |
ScrollView | ListView | 一个线性排列的 widget 的可滚动列表。 |
children | ( <Widget> [ ]) 要显示的子 widget 列表。 | |
controller | [ ScrollController ] 一个可用于控制可滚动 widget 的对象。 | |
itemExtent | [ double ] 如果非空,则强制子 widget 在滚动方向上具有给定的范围。 | |
scrollDirection | [ Axis ] 滚动视图滚动的轴。 | |
FlatList | ListView.builder | 按需创建 widget 的线性数组的构造函数。 |
itemBuilder [required] | [IndexedWidgetBuilder ] 帮助按需构建子项。此回调仅使用大于或等于零且小于 itemCount 的索引调用。 | |
itemCount | [ int ] 提高 ListView 估计最大滚动范围的能力。 | |
Image | Image | 显示图像的小部件。 |
image [required] | 要显示的图像。 | |
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
| 包围子 widget 的 widget。 | | | | | | View
| Column
| 以垂直数组显示其子项的 widget。 | | | | | | View
| Row
| 以水平数组显示其子项的 widget。 | | | | | | View
| Center
| 将其子项居中的 widget。 | | | | | | View
| Padding
| 通过给定填充来内嵌其子项的 widget。 | | | padding [required] | [ EdgeInsets ] 要内嵌子项的空间量。 | ||| | TouchableOpacity
| GestureDetector
| 检测手势的 widget。 | | | onTap | 发生点击时的回调。 | | | onDoubleTap | 快速连续两次在同一位置点击时的回调。 | ||| | TextInput
| TextInput
| 系统文本输入控件的接口。 | | | controller | [ TextEditingController
] 用于访问和修改文本。 | ||| | Text
| Text
| 使用单一样式显示文本字符串的 Text widget。 | | | data | [ String ] 要显示的文本。 | | | textDirection | [ TextAlign
] 文本流动的方向。 | | | | | | Switch
| Switch
| 一个 Material Design 开关。 | | | value [required] | [ boolean ] 此开关是打开还是关闭。 | | | onChanged [required] | [ callback ] 用户打开或关闭开关时调用。 | | | | | | Slider
| Slider
| 用于从一系列值中进行选择。 |
| | value [required] | [ double ] 滑块的当前值。 | | | onChanged [required] | 用户为滑块选择新值时调用。 |
除非另有说明,否则本网站上的文档反映的是 Flutter 的最新稳定版本。页面最后更新于 2025-01-30。 查看源代码 或 报告问题。