面向 React Native 开发者的 Flutter 指南
了解如何将 React Native 开发经验应用到 Flutter 应用构建中。
本文档旨在帮助 React Native (RN) 开发者利用现有的 RN 知识构建 Flutter 移动应用。如果您理解 RN 框架的基本原理,可以使用本文档作为学习 Flutter 开发的起点。
本文档可以像菜谱一样使用,您可以随意跳转并查找与您的需求最相关的问题。
面向 JavaScript (ES6) 开发者的 Dart 简介
#与 React Native 一样,Flutter 使用响应式风格的视图。然而,RN 会转译为原生组件,而 Flutter 则直接编译为机器码。Flutter 控制屏幕上的每一个像素,从而避免了因 JavaScript 桥接而带来的性能问题。
Dart 是一种易于学习的语言,具备以下特性:
- 提供了一种用于构建 Web、服务器和移动应用的开源、可扩展的编程语言。
- 提供了一种面向对象、支持单继承、使用 C 风格语法并可 AOT 编译为原生的语言。
- 可选择性地转译为 JavaScript。
- 支持接口和抽象类。
下面描述了 JavaScript 和 Dart 之间的一些差异。
入口点
#JavaScript 没有预定义的入口函数——你可以自定义入口点。
// JavaScript
function startHere() {
// Can be used as entry point
}
在 Dart 中,每个应用都必须有一个顶级 main() 函数作为应用的入口点。
/// Dart
void main() {}
在 DartPad 中尝试一下。
控制台打印
#在 Dart 中,使用 print() 将内容输出到控制台。
// JavaScript
console.log('Hello world!');
/// Dart
print('Hello world!');
在 DartPad 中尝试一下。
变量
#Dart 是类型安全的——它结合了静态类型检查和运行时检查,以确保变量的值始终与其静态类型相匹配。虽然类型是强制性的,但由于 Dart 会执行类型推断,因此某些类型注解是可选的。
创建和赋值变量
#在 JavaScript 中,变量没有类型。
在 Dart 中,变量必须显式声明类型,或者由类型系统自动推断出正确的类型。
// JavaScript
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 才被视为真。
/// Dart
var myNull = potentiallyNull();
if (myNull == null) {
print('use "== null" to check null');
}
var zero = 0;
if (zero == 0) {
print('use "== 0" to check zero');
}
在 DartPad 中尝试一下。
函数
#Dart 和 JavaScript 的函数通常很相似。主要区别在于声明方式。
// JavaScript
function fn() {
return true;
}
/// Dart
/// You can explicitly define the return type.
bool fn() {
return true;
}
在 DartPad 中尝试一下。
有关详细信息,请参阅有关 函数 的文档。
异步编程
#Futures(未来对象)
#与 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;
}
}
/// 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 run 或 yarn run。
您可以以多种方式运行 Flutter 应用:
- 在装有 Flutter 和 Dart 插件的 IDE 中使用“运行”选项。
- 在项目的根目录中使用
flutter run。
您的应用会在连接的设备、iOS 模拟器或 Android 模拟器上运行。
有关更多信息,请参阅 Flutter 入门 文档。
如何导入组件(Widgets)?
#在 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 中是否有类似 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 中,您可以使用核心组件库中的 Center 和 Text 组件创建一个完全相同的“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。
现在您已经了解了最基础的 Flutter 应用,下一节将展示如何利用 Flutter 丰富的组件库来创建现代、精致的应用。
如何使用组件并嵌套它们来构建组件树?
#在 Flutter 中,几乎一切皆组件。
组件(Widget)是构建应用界面的基础构建块。您可以将组件组合成层次结构,称为“组件树”。每个组件都嵌套在父组件内,并从父组件继承属性。甚至应用对象本身也是一个组件,没有独立的“应用”对象。相反,根组件承担了这一角色。
组件可以定义:
- 结构元素——如按钮或菜单
- 样式元素——如字体或配色方案
- 布局细节——如内边距或对齐方式
以下示例展示了使用 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!”应用。与基础版相比,您无需额外配置即可获得更丰富的功能。
编写应用时,您会使用两种类型的组件:StatelessWidget 或 StatefulWidget。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 类的示例。
项目结构和资源
#从哪里开始编写代码?
#从 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 入口点,应用启动的开始。项目创建时自动生成,您从这里开始编写 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.net.cn/img/tiny_logo.png'
}}
/>
在 Flutter 中,通过在组件的 build 方法中使用 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 中,请按以下说明安装包:
- 要将
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 组件
#在 Flutter 中,您可以使用描述其视图在当前配置和状态下应呈现样式的组件来构建 UI。
组件通常由许多小的、单一用途的组件组合而成,这些组件嵌套在一起产生强大的效果。例如,Container 组件包含多个负责布局、绘制、定位和调整大小的组件。具体来说,Container 组件包括 LimitedBox、ConstrainedBox、Align、Padding、DecoratedBox 和 Transform 组件。与其通过继承 Container 来产生自定义效果,不如以新的独特方式组合这些简单的组件。
Center 组件是您如何控制布局的另一个例子。要将组件居中,将其包裹在 Center 组件中,然后使用布局组件进行对齐、行、列和网格排列。这些布局组件本身没有视觉表现。相反,它们唯一的目的是控制其他组件布局的某一方面。了解组件为何以某种方式渲染时,检查相邻组件通常很有帮助。
有关更多信息,请参阅 Flutter 技术概览。
有关 Widgets 包中核心组件的更多信息,请参阅 Flutter 基础组件、Flutter 组件目录 或 Flutter 组件索引。
视图
#View 容器的等价物是什么?
#
在 React Native 中,View 是一个支持 Flexbox 布局、样式、触摸处理和可访问性控制的容器。
在 Flutter 中,您可以使用 Widgets 库中的核心布局组件,例如 Container、Column、Row 和 Center。有关更多信息,请参阅 布局组件 目录。
FlatList 或 SectionList 的等价物是什么?
#
List 是一个垂直排列的滚动组件列表。
在 React Native 中,使用 FlatList 或 SectionList 渲染简单或分段列表。
// 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]);
},
);
要学习如何实现无限滚动列表,请参阅官方 infinite_list 示例。
如何使用 Canvas 进行绘制?
#在 React Native 中,没有内置 Canvas 组件,因此通常使用 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 组件进行绘制。它实现了抽象类 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()));
}
}
布局
#如何使用组件定义布局属性?
#在 React Native 中,大部分布局可以通过传递给特定组件的 props 来完成。例如,您可以使用 View 组件上的 style 属性来指定 flexbox 属性。若要按列排列组件,您可以指定类似 flexDirection: 'column' 的属性。
// React Native
<View
style={{
flex: 1,
flexDirection: 'column',
justifyContent: 'space-between',
alignItems: 'center'
}}
>
在 Flutter 中,布局主要由专门设计用于提供布局的组件,结合控制组件及其样式属性来定义。
例如,Column 和 Row 组件接受一组子组件,并分别垂直和水平对齐它们。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 在其核心组件库中提供了多种布局组件。例如 Padding、Align 和 Stack。
有关完整列表,请参阅 布局组件。
如何对组件进行层叠布局?
#在 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 将一个 Container(在半透明黑色背景上显示其 Text)覆盖在 CircleAvatar 之上。Stack 使用对齐属性和 Alignment 坐标来偏移文本。
有关更多信息,请参阅 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 组件的 style 属性可以接收 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,
),
),
],
),
);
如何使用 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 设计语言提供了高保真的组件。要使用 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 组件的 theme 属性,可以为几乎所有内容创建统一的样式。
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(primaryColor: Colors.cyan, brightness: Brightness.dark),
home: const StylingPage(),
);
}
即使不使用 MaterialApp 组件,也可以应用 Theme。Theme 组件在其 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 中管理应用状态,请使用配合 State 对象使用的 StatefulWidget。
有关在 Flutter 中管理状态方法的更多信息,请参阅 状态管理。
StatelessWidget(无状态组件)
#Flutter 中的 StatelessWidget 是不需要状态更改的组件——它没有需要管理的内部状态。
当您描述的用户界面部分不依赖于对象本身配置信息和组件填充时的 BuildContext 以外的任何内容时,无状态组件非常有用。
AboutDialog、CircleAvatar 和 Text 都是继承自 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 类的构造函数来传递被标记为 final 的 text。该类继承自 StatelessWidget——它包含不可变数据。
无状态组件的 build 方法通常仅在以下三种情况下被调用:
- 当组件被插入树中时
- 当组件的父组件更改其配置时
- 当其依赖的
InheritedWidget发生更改时
StatefulWidget(有状态组件)
#StatefulWidget 是一个会更改状态的组件。使用 setState 方法来管理 StatefulWidget 的状态更改。调用 setState() 会告诉 Flutter 框架状态发生了变化,这会导致应用重新运行 build() 方法,从而反映更改。
状态是在构建组件时可同步读取且在组件生命周期内可能发生变化的信息。确保状态对象在状态更改时得到及时通知是组件实现者的责任。当组件可以动态更改时,请使用 StatefulWidget。例如,组件状态因在表单中输入、移动滑块而改变,或者它随时间推移而改变——例如数据流更新了 UI。
Checkbox、Radio、Slider、InkWell、Form 和 TextField 都是继承自 StatefulWidget 的有状态组件示例。
以下示例声明了一个需要 createState() 方法的 StatefulWidget。该方法创建管理组件状态的状态对象:_MyStatefulWidgetState。
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({super.key, required this.title});
final String title;
@override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
下面的状态类 _MyStatefulWidgetState 实现了组件的 build() 方法。当状态发生变化(例如,用户切换按钮)时,会调用带有新切换值的 setState()。这会导致框架重建 UI 中的此组件。
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 的最佳实践是什么?
#设计组件时,请考虑以下几点:
- 确定组件应该是
StatefulWidget还是StatelessWidget。
在 Flutter 中,组件是有状态还是无状态,取决于它们是否依赖状态更改。
- 如果组件会更改——用户与其交互或数据流中断了 UI,那么它是 Stateful。
- 如果组件是 final 或不可变的,那么它是 Stateless。
- 确定哪个对象管理组件的状态(针对
StatefulWidget)。
在 Flutter 中,管理状态有三种主要方式:
- 组件管理自己的状态
- 父组件管理组件的状态
- 混合搭配的方法
决定使用哪种方法时,请考虑以下原则:
- 如果涉及的状态是用户数据(例如复选框的选中或取消选中状态,或滑块的位置),则该状态最好由父组件管理。
- 如果涉及的状态是美学相关的(例如动画),则组件本身最好管理该状态。
- 如果不确定,让父组件管理子组件的状态。
- 继承 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) {
//...
}
}
- 将 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'),
);
}
}
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');
},
);
}
}
本地存储
#如果您不需要存储大量数据且不需要复杂的结构,可以使用 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 中,屏幕只是组件!要导航到 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.push 和 Navigator.pop。可以在 MaterialApp 组件中指定路由列表,也可以动态构建它们,例如在英雄动画(Hero animations)中。以下示例在 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<void>(builder: (context) => const UsualNavScreen()),
);
如何使用标签页导航(Tabs)和侧边栏导航(Drawer)?
#在 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 提供了几种用于抽屉和标签页导航的专用组件:
-
TabController -
协调
TabBar和TabBarView之间的标签页选择。 TabBar显示一横排标签页。
Tab创建一个 Material Design 标签页。
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 来协调 TabBar 和 TabBarView 之间的标签页选择。TabController 构造函数的 length 参数是标签页的总数。需要一个 TickerProvider 在每帧触发状态更改时发送通知。TickerProvider 即 vsync。每当您创建新的 TabController 时,请将 vsync: this 参数传递给 TabController 构造函数。
TickerProvider 是由能够发放 Ticker 对象的类实现的接口。任何需要在帧触发时得到通知的对象都可以使用 Ticker,但它们最常见的是通过 AnimationController 间接使用。AnimationController 需要 TickerProvider 来获取其 Ticker。如果您正在从 State 创建 AnimationController,则可以使用 TickerProviderStateMixin 或 SingleTickerProviderStateMixin 类来获取合适的 TickerProvider。
Scaffold 组件包裹了一个新的 TabBar 组件并创建了两个标签页。TabBarView 组件作为 Scaffold 组件的 body 参数传递。对应 TabBar 组件标签页的所有屏幕都是 TabBarView 组件的子项,并使用相同的 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 组件与 Scaffold 结合使用,创建具有 Material Design 抽屉的布局。要将 Drawer 添加到应用,请将其包裹在 Scaffold 组件中。Scaffold 组件为遵循 Material Design 指南的应用提供了统一的视觉结构。它还支持特殊的 Material Design 组件,如 Drawers、AppBars 和 SnackBars。
Drawer 组件是一个 Material Design 面板,从 Scaffold 的边缘水平滑入,以显示应用中的导航链接。您可以提供 ElevatedButton、Text 组件或一系列项目作为 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 组件,当 Scaffold 中有抽屉可用时,它会自动显示一个合适的 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(),
);
}
手势检测和触摸事件处理
#为了监听并响应手势,Flutter 支持点击、拖拽和缩放。Flutter 中的手势系统有两个独立的层级。第一层包括原始指针事件,描述指针(如触摸、鼠标和手写笔移动)在屏幕上的位置和移动。第二层包括描述语义动作的手势,由一个或多个指针移动组成。
如何给组件添加点击事件监听器?
#在 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 中,要给组件添加点击(或按下)监听器,请使用具有 onPress: 字段的按钮或可触摸组件。或者,通过将任何组件包裹在 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 类。
发起 HTTP 网络请求
#从互联网获取数据是大多数应用的常见需求。在 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 Uri url = Uri.parse('https://httpbin.org/ip');
final HttpClient 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;
});
}
表单输入
#文本字段允许用户在您的应用中输入文本,因此可用于构建表单、消息应用、搜索体验等。Flutter 提供了两个核心文本字段组件:TextField 和 TextFormField。
如何使用文本输入框组件?
#在 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 组件实现的,并且通过 TextEditingController 的 text 属性访问来自 TextField 的文本。
如何使用表单组件?
#在 Flutter 中,使用 Form 组件,其中 TextFormField 组件和提交按钮作为子项传递。TextFormField 组件有一个名为 onSaved 的参数,它接受一个回调函数,并在表单保存时执行。FormState 对象用于保存、重置或验证作为此 Form 后代的每个 FormField。要获取 FormState,您可以使用祖先为 Form 的上下文调用 Form.of(),或者将 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'),
);
},
);
}
}
平台特定代码
#构建跨平台应用时,您希望尽可能多地跨平台复用代码。然而,可能会出现代码需要根据操作系统而不同的场景。这需要通过声明特定的平台来进行单独实现。
在 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 的调试器调试应用。
如何执行热重载(Hot Reload)?
#Flutter 的有状态热重载 (Stateful Hot Reload) 功能可帮助您快速轻松地进行实验、构建 UI、添加功能并修复 Bug。您无需每次更改都重新编译应用,而是可以立即热重载。应用会更新以反映您的更改,并保留应用的当前状态。
首先,从您偏好的 IDE 中启用自动保存和保存时热重载。
VS Code
将以下内容添加到你的 .vscode/settings.json 文件中
json "files.autoSave": "afterDelay", "dart.flutterHotReloadOnSave": "all", Android Studio 和 IntelliJ
* 打开 Settings > Tools > Actions on Save 并选择 Configure autosave options。 - 勾选 Save files if the IDE is idle for X seconds 选项。 - 推荐:设置较小的延迟时间。例如,2 秒。
* 打开 Settings > Languages & Frameworks > Flutter。 - 勾选 Perform hot reload on save 选项。
在 React Native 中,快捷键是 iOS 模拟器的 ⌘R,Android 模拟器则是双击 R。
在 Flutter 中,如果您使用的是 IntelliJ IDE 或 Android Studio,可以选择 Save All (⌘s/ctrl-s),或者单击工具栏上的 Hot Reload 按钮。如果您使用 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 中创建相同的动画,请创建一个名为 controller 的 AnimationController 对象并指定持续时间。默认情况下,AnimationController 在给定的持续时间内线性产生从 0.0 到 1.0 的值。每当运行应用的设备准备好显示新帧时,动画控制器就会生成一个新值。通常,这个速率约为每秒 60 个值。
定义 AnimationController 时,必须传入 vsync 对象。vsync 的存在防止屏幕外动画消耗不必要的资源。您可以将有状态对象用作 vsync,方法是将 TickerProviderStateMixin 添加到类定义中。AnimationController 需要一个 TickerProvider,它使用构造函数上的 vsync 参数进行配置。
Tween 描述了开始值和结束值之间的插值,或从输入范围到输出范围的映射。要将 Tween 对象与动画一起使用,请调用 Tween 对象的 animate() 方法,并传入您想要修改的 Animation 对象。
在此示例中,使用了 FadeTransition 组件,并将 opacity 属性映射到 animation 对象。
要开始动画,请使用 controller.forward()。其他操作也可以使用控制器执行,例如 fling() 或 repeat()。在此示例中,FadeTransition 组件内部使用了 FlutterLogo 组件。
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()),
);
}
}
如何给卡片添加滑动动画?
#在 React Native 中,使用 PanResponder 或第三方库来实现滑动动画。
在 Flutter 中,要添加滑动动画,请使用 Dismissible 组件并嵌套子组件。
return Dismissible(
key: Key(widget.key.toString()),
onDismissed: (dismissDirection) {
cards.removeLast();
},
child: Container(
//...
),
);
React Native 与 Flutter 组件对照表
#下表列出了常用 React Native 组件与相应的 Flutter 组件及其常见属性的对照。
| React Native 组件 | Flutter 组件 | 描述 |
|---|---|---|
Button |
凸起按钮
|
一个基本的凸起按钮。 |
| onPressed [必需] | 按钮被点击或以其他方式激活时的回调。 | |
| Child | 按钮的标签。 | |
Button |
文本按钮 |
一个基本的扁平按钮。 |
| onPressed [必需] | 按钮被点击或以其他方式激活时的回调。 | |
| Child | 按钮的标签。 | |
ScrollView |
ListView |
线性排列的组件的可滚动列表。 |
| children | ( <Widget> [ ]) 要显示的子组件列表。 | |
| controller | [ ScrollController ] 可用于控制可滚动组件的对象。 |
|
| itemExtent | [ double ] 如果非 null,则强制子组件在滚动方向上具有给定的范围。 | |
| scroll Direction | [ Axis ] 滚动视图滚动的轴。 |
|
FlatList |
ListView.builder
|
用于创建按需构建的线性组件数组的构造函数。 |
| itemBuilder [必需] | [ IndexedWidgetBuilder ] 有助于按需构建子组件。此回调仅在索引大于等于 0 且小于 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 |
容器 |
包裹子组件的组件。 |
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 |
Material Design 开关。 |
| value [必需] | [ boolean ] 此开关是开启还是关闭。 | |
| onChanged [必需] | [ callback ] 当用户切换开关开启或关闭时调用。 |