Flutter 面向 React Native 开发者的介绍
了解如何在构建 Flutter 应用时应用 React Native 开发者的知识。
本文档面向希望将现有的 RN 知识应用于使用 Flutter 构建移动应用的 React Native (RN) 开发者。如果您了解 RN 框架的基础知识,则可以使用本文档作为开始学习 Flutter 开发的一种方式。
本文档可以像菜谱一样使用,您可以随意跳转并查找与您的需求最相关的问题。
Dart 语言简介(针对 JavaScript 开发者 ES6)
#与 React Native 类似,Flutter 使用类 React 样式的视图。但是,虽然 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 中试用。
有关更多信息,请参阅有关 变量的文档。
检查空值或零
#在 JavaScript 中,使用 == 比较运算符时,1 或任何非空对象的值都被视为 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 = 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 入门 文档。
如何导入组件?
#在 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 方法,通过返回 view 组件。
// 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),
),
);
}
以下图像显示了 Android 和 iOS UI 的基本 Flutter "Hello world!" 应用程序。
现在您已经看到了最基本的 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!" 应用程序中获得更多的免费功能。
在编写应用程序时,您将使用两种类型的组件:StatelessWidget 或 StatefulWidget。StatelessWidget 正如其名称所示——一个没有状态的组件。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 中,定义一个类来创建自定义 widget,然后重用该 widget。你也可以定义并调用一个返回可重用 widget 的函数,如以下示例中的 build 函数所示。
/// Flutter
class CustomCard extends StatelessWidget {
const CustomCard({super.key, required this.index, required this.onPress});
final int index;
final void Function() onPress;
@override
Widget build(BuildContext context) {
return Card(
child: Column(
children: <Widget>[
Text('Card $index'),
TextButton(onPressed: onPress, child: const Text('Press')),
],
),
);
}
}
class UseCard extends StatelessWidget {
const UseCard({super.key, required this.index});
final int index;
@override
Widget build(BuildContext context) {
/// Usage
return CustomCard(
index: index,
onPress: () {
print('Card $index');
},
);
}
}
在前面的示例中,CustomCard 类的构造函数使用 Dart 的花括号语法 { } 来表示 命名参数。
要强制要求这些字段,可以从构造函数中删除花括号,或者在构造函数中添加 required。
以下截图显示了可重用的 CustomCard 类的一个示例。
项目结构和资源
#我应该从哪里开始编写代码?
#从 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 将资产放入一个名为 asset bundle 的特殊存档中,应用从运行时读取该存档。当在 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 中,使用 Image.asset 构造函数在 widget 的 build 方法中添加静态图像。
Image.asset('assets/background.png');
有关更多信息,请参阅 在 Flutter 中添加资产和图像。
如何通过网络加载图像?
#在 React Native 中,你会在 Image 组件的 source prop 中指定 uri,并在需要时提供大小。
在 Flutter 中,使用 Image.network 构造函数包含来自 URL 的图像。
Image.network('https://docs.flutterdart.cn/assets/images/docs/owl.jpg');
如何安装包和插件包?
#Flutter 支持使用其他开发人员贡献给 Flutter 和 Dart 生态系统的共享包。这使你能够快速构建你的应用,而无需从头开始开发所有内容。包含特定于平台的代码的包称为包插件。
在 React Native 中,你将使用 yarn add {package-name} 或 npm install --save {package-name} 从命令行安装包。
在 Flutter 中,使用以下说明安装包
- 要将
google_sign_in包添加为依赖项,请运行flutter pub add
flutter pub add google_sign_in
- 通过使用
flutter pub get从命令行安装包。如果使用 IDE,它通常会为你运行flutter pub get,或者可能会提示你这样做。 - 如以下所示,将包导入到你的应用代码中
import 'package:flutter/material.dart';
你可以在 Flutter 包 部分的 pub.dev 上找到 Flutter 开发人员共享的许多包。
Flutter 组件
#在 Flutter 中,你通过描述其视图应该在给定其当前配置和状态下如何显示的 widget 来构建你的 UI。
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 最合适。对于大型或无限列表,请使用 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 prop 来指定 flexbox 属性。要将您的组件排列成一列,您将指定一个 prop,例如: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 属性和 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 组件可以接受 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 中的 ThemeData 对象。Colors 类提供来自 Material Design 颜色调色板 的颜色。
以下示例将颜色方案从 seed 设置为 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 中管理应用程序状态,请使用 StatefulWidget 与 State 对象配对。
有关在 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 类的构造函数传递 text,该 text 被标记为 final。此类扩展了 StatelessWidget——它包含不可变数据。
无状态组件的 build 方法通常仅在三种情况下调用
- 当组件插入树时
- 当父组件更改其配置时
- 当它所依赖的
InheritedWidget发生更改时
StatefulWidget
#一个 StatefulWidget 是状态发生变化的组件。使用 setState 方法来管理 StatefulWidget 的状态更改。调用 setState() 会告诉 Flutter 框架状态发生变化,这将导致应用程序重新运行 build() 方法,以便应用程序可以反映更改。
状态 是在构建组件时可以同步读取的信息,并且可能在组件的生命周期内发生变化。确保在状态发生变化时及时通知状态对象是组件实现者的责任。当组件可以动态更改时,使用 StatefulWidget。例如,组件的状态通过在表单中键入或移动滑块来更改。或者,它可能会随着时间的推移而变化——也许数据源更新了 UI。
Checkbox、Radio、Slider、InkWell、Form 和 TextField 是继承自 StatefulWidget 的有状态组件示例。
以下示例声明了一个 StatefulWidget,该组件需要一个 createState() 方法。此方法创建管理组件状态的状态对象 _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,那么它是有状态的。
- 如果组件是最终的或不可变的,那么它是无状态的。
- 确定哪个对象管理组件的状态(对于
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 添加到应用程序的 build 方法中的小部件树中。
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 中,新屏幕是新的 ViewControllers。在 Flutter 中,屏幕只是 Widgets!要导航到 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 中指定,或者可以在运行时构建,例如,在 hero 动画中。以下示例在 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<void>(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 对象的类实现的接口。Ticker 可用于任何需要通知其每当帧触发时发出的对象,但它们最常通过 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,该 widget 会自动显示适当的 IconButton 以显示 Drawer(如果 Scaffold 中有 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 中,要向 widget 添加点击(或按压)监听器,请使用具有 onPress: field 的按钮或可触摸 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 类。
进行 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 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;
});
}
表单输入
#文本字段允许用户在您的应用程序中键入文本,因此它们可用于构建表单、消息传递应用程序、搜索体验等。Flutter 提供了两个核心文本字段 widget: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 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 属性访问。
如何使用表单组件?
#在 Flutter 中,使用 Form widget,其中 TextFormField widget 和提交按钮作为子项传递。TextFormField widget 具有一个名为 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 的调试器调试您的应用程序。
如何执行热重载?
#Flutter 的 Stateful Hot Reload 功能可帮助您快速轻松地进行实验、构建 UI、添加功能和修复错误。每次更改时,您不必重新编译应用程序,而是可以立即热重载应用程序。应用程序会更新以反映您的更改,并且应用程序的当前状态将被保留。
首先,从您喜欢的 IDE,启用 autosave 和保存时热重载。
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 Simulator 的 ⌘R 和 Android 模拟器上双击 R。
在 Flutter 中,如果您使用的是 IntelliJ IDE 或 Android Studio,您可以选择 Save All (⌘s/ctrl-s),或者您可以点击工具栏上的 Hot Reload 按钮。如果您使用 flutter run 在命令行中运行该应用程序,请在 Terminal 窗口中键入 r。您也可以通过在 Terminal 窗口中键入 R 来执行完全重启。
如何访问应用内开发者菜单?
#在 React Native 中,可以通过摇动设备来访问开发人员菜单:iOS Simulator 的 ⌘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,这些 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 中创建相同的动画,创建一个 AnimationController 对象,命名为 controller 并指定持续时间。默认情况下,AnimationController 线性生成在给定持续时间内从 0.0 到 1.0 的值。动画控制器会在您的应用程序正在运行的设备准备好显示新帧时生成一个新值。通常,此速率约为每秒 60 个值。
定义 AnimationController 时,必须传入一个 vsync 对象。vsync 的存在可以防止屏幕外动画消耗不必要的资源。您可以通过将 TickerProviderStateMixin 添加到类定义中,将您的 stateful 对象用作 vsync。AnimationController 需要一个 TickerProvider,该 TickerProvider 使用构造函数中的 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()),
);
}
}
如何为卡片添加滑动动画?
#在 React Native 中,使用 PanResponder 或第三方库来实现滑动动画。
在 Flutter 中,要添加滑动动画,请使用 Dismissible Widget 并嵌套子 Widget。
return Dismissible(
key: Key(widget.key.toString()),
onDismissed: (dismissDirection) {
cards.removeLast();
},
child: Container(
//...
),
);
React Native 和 Flutter 组件等效组件
#下表列出了常用的 React Native 组件及其对应的 Flutter Widget 和常用 Widget 属性。
| React Native 组件 | Flutter Widget | 描述 |
|---|---|---|
Button |
凸起按钮
|
一个基本的凸起按钮。 |
| onPressed [必需] | 单击按钮或以其他方式激活按钮时调用的回调函数。 | |
| Child | 按钮的标签。 | |
Button |
文本按钮 |
一个基本的扁平按钮。 |
| onPressed [必需] | 单击按钮或以其他方式激活按钮时调用的回调函数。 | |
| Child | 按钮的标签。 | |
ScrollView |
ListView |
一个线性排列的 Widget 的可滚动列表。 |
| children | ( <Widget> [ ]) 要显示的子 Widget 列表。 | |
| controller | [ ScrollController ] 一个可用于控制可滚动 Widget 的对象。 |
|
| itemExtent | [ double ] 如果不为 null,则强制子项在滚动方向上具有给定的范围。 | |
| scrollDirection | [ Axis ] 滚动视图滚动的轴。 |
|
FlatList |
ListView.builder
|
用于按需创建线性 Widget 数组的构造函数。 |
| itemBuilder [必需] | [ IndexedWidgetBuilder ] 仅使用大于或等于零且小于 itemCount 的索引调用此回调函数。 |
|
| itemCount | [ int ] 提高 ListView 估计最大滚动范围的能力。 | |
Image |
Image |
一个显示图像的 Widget。 |
| image [必需] | 要显示的图像。 | |
| Image.asset | 为图像可以被指定的各种方式提供了几个构造函数。 | |
| width, height, color, alignment | 图像的样式和布局。 | |
| fit | 在布局期间分配的空间中刻画图像。 | |
Modal |
ModalRoute |
阻止与先前路由交互的路由。 |
| animation | 驱动路由过渡和先前路由前向过渡的动画。 | |
ActivityIndicator |
CircularProgressIndicator
|
一个沿圆圈显示进度的 Widget。 |
| strokeWidth | 用于绘制圆圈的线条的宽度。 | |
| backgroundColor | 进度指示器的背景颜色。默认情况下为当前主题的 ThemeData.backgroundColor。 |
|
ActivityIndicator |
LinearProgressIndicator
|
一个沿线显示进度的 Widget。 |
| value | 此进度指示器的值。 | |
RefreshControl |
RefreshIndicator
|
一个支持 Material “下拉刷新”惯例的 Widget。 |
| color | 进度指示器的前景色。 | |
| onRefresh | 当用户拖动刷新指示器足够远以表明他们希望应用程序刷新时调用的函数。 | |
View |
容器 |
一个包围子 Widget 的 Widget。 |
View |
Column |
一个垂直排列其子项的 Widget。 |
View |
Row |
一个水平排列其子项的 Widget。 |
View |
Center |
一个在其自身内部居中其子项的 Widget。 |
View |
Padding |
一个通过给定的填充插入其子项的 Widget。 |
| padding [必需] | [ EdgeInsets ] 插入子项的空间量。 | |
TouchableOpacity |
GestureDetector
|
一个检测手势的 Widget。 |
| onTap | 单击时调用的回调函数。 | |
| onDoubleTap | 快速连续地在同一位置单击时调用的回调函数。 | |
TextInput |
TextInput |
系统文本输入控件的接口。 |
| controller | [ TextEditingController ] 用于访问和修改文本。 |
|
文本 |
文本 |
Text Widget,它以单一样式显示文本字符串。 |
| data | [ String ] 要显示的文本。 | |
| textDirection | [ TextAlign ] 文本流动的方向。 |
|
Switch |
Switch |
一个 Material Design 开关。 |
| value [必需] | [ boolean ] 此开关是打开还是关闭。 | |
| onChanged [必需] | [ 回调函数 ] 在用户切换开关的打开或关闭状态时调用。 |