适用于 UIKit 开发者的 Flutter
有使用 UIKit 经验且希望使用 Flutter 编写移动应用程序的 iOS 开发者应查看本指南。它解释了如何将现有的 UIKit 知识应用于 Flutter。
Flutter 是一个用于构建跨平台应用程序的框架,它使用 Dart 编程语言。要了解使用 Dart 编程和使用 Swift 编程之间的一些差异,请参阅 Swift 开发者学习 Dart 和 适用于 Swift 开发者的 Flutter 并发。
在使用 Flutter 进行构建时,您的 iOS 和 UIKit 知识和经验非常宝贵。Flutter 还会对在 iOS 上运行时的应用程序行为进行一些调整。要了解如何进行调整,请参阅 平台调整。
将本指南用作食谱。四处浏览并查找解决您最相关需求的问题。
概述
作为介绍,请观看以下视频。它概述了 Flutter 在 iOS 上的工作原理以及如何使用 Flutter 构建 iOS 应用程序。
视图与小组件
在 UIKit 中,您在 UI 中创建的大部分内容都是使用视图对象完成的,这些对象是UIView
类的实例。这些可以作为其他UIView
类的容器,这些类形成您的布局。
在 Flutter 中,与UIView
大致等效的是Widget
。小组件不会完全映射到 iOS 视图,但当您熟悉 Flutter 的工作原理时,可以将它们视为“声明和构建 UI 的方式”。
但是,这些与UIView
有一些区别。首先,小组件具有不同的生命周期:它们是不可变的,并且仅在需要更改时才存在。每当小组件或其状态发生更改时,Flutter 的框架都会创建一个新的 Widget 实例树。相比之下,UIKit 视图在更改时不会重新创建,而是一个可变实体,绘制一次,并且在使用setNeedsDisplay()
使其无效之前不会重新绘制。
此外,与UIView
不同,Flutter 的小组件很轻量级,部分原因是它们不可变。因为它们本身不是视图,并且不会直接绘制任何内容,而是 UI 及其语义的描述,这些描述在底层被“充气”为实际的视图对象。
Flutter 包含 Material Components 库。这些小组件实现了 Material Design 指南。Material Design 是一个灵活的设计系统,针对所有平台进行了优化,包括 iOS。
但 Flutter 足够灵活和富有表现力,可以实现任何设计语言。在 iOS 上,您可以使用 Cupertino 小组件 来生成一个看起来像 Apple 的 iOS 设计语言 的界面。
更新小组件
要在 UIKit 中更新视图,您需要直接对其进行更改。在 Flutter 中,小组件是不可变的,不会直接更新。相反,您必须操作小组件的状态。
这就是有状态小组件与无状态小组件概念的由来。StatelessWidget
正如其名,它是一个没有附加状态的小组件。
StatelessWidgets
很有用,当您描述的用户界面部分不依赖于小组件中的初始配置信息之外的任何内容时。
例如,使用 UIKit 时,这类似于将 UIImageView
放置在您的徽标中作为 image
。如果徽标在运行时不会更改,请在 Flutter 中使用 StatelessWidget
。
如果您想在进行 HTTP 调用后根据接收到的数据动态更改 UI,请使用 StatefulWidget
。在 HTTP 调用完成后,告诉 Flutter 框架小组件的 State
已更新,以便它可以更新 UI。
无状态小组件和有状态小组件之间的重要区别在于,StatefulWidget
具有 State
对象,该对象存储状态数据并将其跨树重建进行传递,因此不会丢失。
如果您有疑问,请记住此规则:如果小组件在 build
方法之外发生更改(例如,由于运行时用户交互),则它是有状态的。如果小组件在构建后永不更改,则它是无状态的。但是,即使小组件是有状态的,如果包含它的父小组件本身没有对这些更改(或其他输入)做出反应,则它仍然可以是无状态的。
以下示例展示了如何使用 StatelessWidget
。常见的 StatelessWidget
是 Text
小组件。如果您查看 Text
小组件的实现,您会发现它继承了 StatelessWidget
。
Text(
'I like Flutter!',
style: TextStyle(fontWeight: FontWeight.bold),
);
如果您查看上面的代码,您可能会注意到 Text
窗口小部件没有携带任何显式状态。它呈现其构造函数中传递的内容,仅此而已。
但是,如果您希望使“我喜爱 Flutter”动态更改,例如在单击 FloatingActionButton
时,该怎么办?
要实现此目的,请将 Text
窗口小部件包装在 StatefulWidget
中,并在用户单击按钮时对其进行更新。
例如
class SampleApp extends StatelessWidget {
// This widget is the root of your application.
const SampleApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Sample App',
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
// Default placeholder text
String textToShow = 'I Like Flutter';
void _updateText() {
setState(() {
// Update the text
textToShow = 'Flutter is Awesome!';
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: Center(child: Text(textToShow)),
floatingActionButton: FloatingActionButton(
onPressed: _updateText,
tooltip: 'Update Text',
child: const Icon(Icons.update),
),
);
}
}
窗口小部件布局
在 UIKit 中,您可能使用 Storyboard 文件来组织视图并设置约束,或者您可能在视图控制器中以编程方式设置约束。在 Flutter 中,通过组合窗口小部件树在代码中声明布局。
以下示例显示如何显示带填充的简单窗口小部件
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: Center(
child: CupertinoButton(
onPressed: () {},
padding: const EdgeInsets.only(left: 10, right: 10),
child: const Text('Hello'),
),
),
);
}
您可以向任何窗口小部件添加填充,这模仿了 iOS 中约束的功能。
您可以在 窗口小部件目录 中查看 Flutter 提供的布局。
移除窗口小部件
在 UIKit 中,您在父级上调用 addSubview()
,或在子视图上调用 removeFromSuperview()
以动态添加或移除子视图。在 Flutter 中,由于窗口小部件是不可变的,因此没有 addSubview()
的直接等效项。相反,您可以将函数传递给返回窗口小部件的父级,并使用布尔标志控制该子级的创建。
以下示例显示如何在用户单击 FloatingActionButton
时在两个窗口小部件之间切换
class SampleApp extends StatelessWidget {
// This widget is the root of your application.
const SampleApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Sample App',
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
// Default value for toggle.
bool toggle = true;
void _toggle() {
setState(() {
toggle = !toggle;
});
}
Widget _getToggleChild() {
if (toggle) {
return const Text('Toggle One');
}
return CupertinoButton(
onPressed: () {},
child: const Text('Toggle Two'),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Sample App'),
),
body: Center(
child: _getToggleChild(),
),
floatingActionButton: FloatingActionButton(
onPressed: _toggle,
tooltip: 'Update Text',
child: const Icon(Icons.update),
),
);
}
}
动画
在 UIKit 中,您通过在视图上调用 animate(withDuration:animations:)
方法来创建动画。在 Flutter 中,使用动画库将窗口小部件包装在动画窗口小部件内。
在 Flutter 中,使用 AnimationController
,它是一个 Animation<double>
,可以暂停、查找、停止和反转动画。它需要一个 Ticker
,该 Ticker
在 vsync 发生时发出信号,并在运行时在 0 和 1 之间产生线性插值。然后,您创建一个或多个 Animation
并将其附加到控制器。
例如,您可以使用 CurvedAnimation
沿着插值曲线实现动画。从这个意义上说,控制器是动画进度的“主”源,而 CurvedAnimation
计算出替换控制器默认线性运动的曲线。与窗口小部件一样,Flutter 中的动画也与组合一起使用。
在构建窗口小部件树时,您将 Animation
分配给窗口小部件的动画属性,例如 FadeTransition
的不透明度,并告诉控制器启动动画。
以下示例展示了如何编写一个 FadeTransition
,当按下 FloatingActionButton
时,该过渡会将小组件淡化为一个徽标
import 'package:flutter/material.dart';
class SampleApp extends StatelessWidget {
// This widget is the root of your application.
const SampleApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Fade Demo',
home: MyFadeTest(title: 'Fade Demo'),
);
}
}
class MyFadeTest extends StatefulWidget {
const MyFadeTest({super.key, required this.title});
final String title;
@override
State<MyFadeTest> createState() => _MyFadeTest();
}
class _MyFadeTest extends State<MyFadeTest>
with SingleTickerProviderStateMixin {
late AnimationController controller;
late CurvedAnimation curve;
@override
void initState() {
super.initState();
controller = AnimationController(
duration: const Duration(milliseconds: 2000),
vsync: this,
);
curve = CurvedAnimation(
parent: controller,
curve: Curves.easeIn,
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
body: Center(
child: FadeTransition(
opacity: curve,
child: const FlutterLogo(size: 100),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
controller.forward();
},
tooltip: 'Fade',
child: const Icon(Icons.brush),
),
);
}
}
如需了解更多信息,请参阅 动画和动态小组件、动画教程 和 动画概述。
在屏幕上绘制
在 UIKit 中,使用 CoreGraphics
在屏幕上绘制线条和形状。Flutter 有一个基于 Canvas
类的不同 API,还有两个其他类可帮助你进行绘制:CustomPaint
和 CustomPainter
,后者实现了你的算法以绘制画布。
若要了解如何在 Flutter 中实现签名绘制器,请参阅 Collin 在 StackOverflow 上的回答。
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: DemoApp()));
class DemoApp extends StatelessWidget {
const DemoApp({super.key});
@override
Widget build(BuildContext context) => const Scaffold(body: Signature());
}
class Signature extends StatefulWidget {
const Signature({super.key});
@override
State<Signature> createState() => SignatureState();
}
class SignatureState extends State<Signature> {
List<Offset?> _points = <Offset?>[];
@override
Widget build(BuildContext context) {
return GestureDetector(
onPanUpdate: (details) {
setState(() {
RenderBox? referenceBox = context.findRenderObject() as RenderBox;
Offset localPosition =
referenceBox.globalToLocal(details.globalPosition);
_points = List.from(_points)..add(localPosition);
});
},
onPanEnd: (details) => _points.add(null),
child:
CustomPaint(
painter: SignaturePainter(_points),
size: Size.infinite,
),
);
}
}
class SignaturePainter extends CustomPainter {
SignaturePainter(this.points);
final List<Offset?> points;
@override
void paint(Canvas canvas, Size size) {
final Paint paint = Paint()
..color = Colors.black
..strokeCap = StrokeCap.round
..strokeWidth = 5;
for (int i = 0; i < points.length - 1; i++) {
if (points[i] != null && points[i + 1] != null) {
canvas.drawLine(points[i]!, points[i + 1]!, paint);
}
}
}
@override
bool shouldRepaint(SignaturePainter oldDelegate) =>
oldDelegate.points != points;
}
小组件不透明度
在 UIKit 中,所有内容都有 .opacity
或 .alpha
。在 Flutter 中,大多数情况下,你需要将小组件包装在 Opacity
小组件中才能实现此目的。
自定义小组件
在 UIKit 中,通常对 UIView
进行子类化,或使用预先存在视图,以覆盖和实现实现所需行为的方法。在 Flutter 中,通过 组合较小的组件(而不是扩展它们)来构建自定义小组件。
例如,如何构建一个在构造函数中获取标签的 CustomButton
?创建一个 CustomButton,它组合一个带有标签的 ElevatedButton
,而不是通过扩展 ElevatedButton
class CustomButton extends StatelessWidget {
const CustomButton(this.label, {super.key});
final String label;
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {},
child: Text(label),
);
}
}
然后使用 CustomButton
,就像使用任何其他 Flutter 小组件一样
@override
Widget build(BuildContext context) {
return const Center(
child: CustomButton('Hello'),
);
}
导航
本文档的本部分讨论应用页面之间的导航、push 和 pop 机制等内容。
页面之间导航
在 UIKit 中,要在视图控制器之间移动,可以使用 UINavigationController
,它管理要显示的视图控制器的堆栈。
Flutter 有一个类似的实现,它使用 Navigator
和 Routes
。 Route
是应用的“屏幕”或“页面”的抽象,而 Navigator
是管理路由的 小部件。路由大致映射到 UIViewController
。导航器的工作方式类似于 iOS UINavigationController
,因为它可以 push()
和 pop()
路由,具体取决于你是想导航到视图还是从视图返回。
要在页面之间导航,你有几个选项
- 指定路由名称的
Map
。 - 直接导航到路由。
以下示例构建 Map.
。
void main() {
runApp(
CupertinoApp(
home: const MyAppHome(), // becomes the route named '/'
routes: <String, WidgetBuilder>{
'/a': (context) => const MyPage(title: 'page A'),
'/b': (context) => const MyPage(title: 'page B'),
'/c': (context) => const MyPage(title: 'page C'),
},
),
);
}
通过将路由名称 push
到 Navigator
来导航到路由。
Navigator.of(context).pushNamed('/b');
Navigator
类处理 Flutter 中的路由,并用于从你在堆栈中 push
的路由获取结果。这是通过 await
在 push()
返回的 Future
上来完成的。
例如,要启动允许用户选择其位置的 location
路由,你可以执行以下操作
Object? coordinates = await Navigator.of(context).pushNamed('/location');
然后,在你的 location
路由中,一旦用户选择了他们的位置,就使用结果 pop()
堆栈
Navigator.of(context).pop({'lat': 43.821757, 'long': -79.226392});
导航到另一个应用
在 UIKit 中,要将用户发送到另一个应用程序,你需要使用特定的 URL 方案。对于系统级应用,该方案取决于应用。要在 Flutter 中实现此功能,请创建一个本机平台集成,或使用 现有插件,例如 url_launcher
。
手动返回
从 Dart 代码调用 SystemNavigator.pop()
会调用以下 iOS 代码
UIViewController* viewController = [UIApplication sharedApplication].keyWindow.rootViewController;
if ([viewController isKindOfClass:[UINavigationController class]]) {
[((UINavigationController*)viewController) popViewControllerAnimated:NO];
}
如果这无法满足你的需求,你可以创建自己的 平台通道 来调用任意 iOS 代码。
处理本地化
与拥有 Localizable.strings
文件的 iOS 不同,Flutter 目前没有专门用于处理字符串的系统。目前,最佳实践是在类中将你的文案文本声明为静态字段,然后从那里访问它们。例如
class Strings {
static const String welcomeMessage = 'Welcome To Flutter';
}
你可以这样访问你的字符串
Text(Strings.welcomeMessage);
默认情况下,Flutter 仅支持其字符串的美国英语。如果你需要添加对其他语言的支持,请包含 flutter_localizations
包。你可能还需要添加 Dart 的 intl
包来使用 i10n 机制,例如日期/时间格式化。
dependencies:
flutter_localizations:
sdk: flutter
intl: any # Use version of intl from flutter_localizations.
要使用 flutter_localizations
包,请在应用小部件上指定 localizationsDelegates
和 supportedLocales
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
class MyWidget extends StatelessWidget {
const MyWidget({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
localizationsDelegates: <LocalizationsDelegate<dynamic>>[
// Add app-specific localization delegate[s] here
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: <Locale>[
Locale('en', 'US'), // English
Locale('he', 'IL'), // Hebrew
// ... other locales the app supports
],
);
}
}
委托包含实际的本地化值,而 supportedLocales
定义应用支持哪些语言环境。上面的示例使用 MaterialApp
,因此它既有用于基本小部件本地化值的 GlobalWidgetsLocalizations
,又有用于 Material 小部件本地化的 MaterialWidgetsLocalizations
。如果你为你的应用使用 WidgetsApp
,则不需要后者。请注意,这两个委托包含“默认”值,但如果你希望自己的应用的本地化文案也得到本地化,则需要为自己的应用提供一个或多个委托。
初始化时,WidgetsApp
(或 MaterialApp
)会为您创建一个 Localizations
小组件,其中包含您指定的委托。设备的当前语言环境始终可从当前上下文的 Localizations
小组件(以 Locale
对象的形式)访问,或使用 Window.locale
访问。
要访问本地化资源,请使用 Localizations.of()
方法访问由给定委托提供的特定本地化类。使用 intl_translation
包将可翻译的副本提取到 arb 文件中以进行翻译,然后将其导入回应用中以与 intl
一起使用。
有关 Flutter 中的国际化和本地化的更多详细信息,请参阅 国际化指南,其中包含带有和不带有 intl
包的示例代码。
管理依赖项
在 iOS 中,您可以通过将依赖项添加到 Podfile
中,使用 CocoaPods 添加依赖项。Flutter 使用 Dart 的构建系统和 Pub 包管理器来处理依赖项。这些工具将原生 Android 和 iOS 封装应用的构建委托给各自的构建系统。
虽然在 Flutter 项目的 iOS 文件夹中有一个 Podfile,但仅当您添加每个平台集成所需的原生依赖项时才使用它。通常,使用 pubspec.yaml
在 Flutter 中声明外部依赖项。在 pub.dev 上可以找到适用于 Flutter 的优秀包。
视图控制器
本文档的此部分讨论了 Flutter 中 ViewController 的等效项以及如何侦听生命周期事件。
Flutter 中 ViewController 的等效项
在 UIKit 中,ViewController
表示用户界面的一部分,最常用于屏幕或部分。它们组合在一起构建复杂的用户界面,并帮助扩展应用程序的 UI。在 Flutter 中,此工作由 Widget 完成。如导航部分所述,Flutter 中的屏幕由 Widget 表示,因为“一切都是 Widget!”使用 Navigator
在表示不同屏幕或页面的不同 Route
之间移动,或者可能是相同数据的不同状态或渲染。
侦听生命周期事件
在 UIKit 中,你可以覆盖 ViewController
的方法来捕获视图本身的生命周期方法,或在 AppDelegate
中注册生命周期回调。在 Flutter 中,你没有这两个概念,但你可以通过连接到 WidgetsBinding
观察器并侦听 didChangeAppLifecycleState()
更改事件来侦听生命周期事件。
可观察的生命周期事件为
inactive
- 应用程序处于非活动状态,未接收用户输入。此事件仅适用于 iOS,因为 Android 上没有等效事件。
paused
- 应用程序当前对用户不可见,不响应用户输入,但在后台运行。
resumed
- 应用程序可见且响应用户输入。
suspending
- 应用程序暂时挂起。iOS 平台没有等效事件。
有关这些状态含义的更多详细信息,请参阅 AppLifecycleState
文档。
布局
本部分讨论了 Flutter 中的不同布局以及它们与 UIKit 的比较。
显示列表视图
在 UIKit 中,你可以在 UITableView
或 UICollectionView
中显示列表。在 Flutter 中,你可以使用 ListView
实现类似的功能。在 UIKit 中,这些视图具有用于决定行数、每个索引路径的单元格以及单元格大小的委托方法。
由于 Flutter 的不可变小部件模式,你可以将小部件列表传递给你的 ListView
,而 Flutter 负责确保滚动快速而流畅。
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Sample App',
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List<Widget> _getListData() {
final List<Widget> widgets = [];
for (int i = 0; i < 100; i++) {
widgets.add(Padding(
padding: const EdgeInsets.all(10),
child: Text('Row $i'),
));
}
return widgets;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Sample App'),
),
body: ListView(children: _getListData()),
);
}
}
检测点击内容
在 UIKit 中,你可以实现委托方法 tableView:didSelectRowAtIndexPath:
。在 Flutter 中,可以使用传入小部件提供的触摸处理。
import 'dart:developer' as developer;
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Sample App',
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List<Widget> _getListData() {
List<Widget> widgets = [];
for (int i = 0; i < 100; i++) {
widgets.add(
GestureDetector(
onTap: () {
developer.log('row tapped');
},
child: Padding(
padding: const EdgeInsets.all(10),
child: Text('Row $i'),
),
),
);
}
return widgets;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Sample App'),
),
body: ListView(children: _getListData()),
);
}
}
动态更新 ListView
在 UIKit 中,你可以更新列表视图的数据,并使用 reloadData
方法通知表格或集合视图。
在 Flutter 中,如果你在 setState()
中更新 ListView
中的小部件列表,你会很快发现你的数据在视觉上没有变化。这是因为当调用 setState()
时,Flutter 渲染引擎会查看小部件树以查看是否有任何更改。当它到达你的 ListView
时,它会执行 ==
检查,并确定两个 ListView
相同。没有任何更改,因此不需要更新。
要更新你的 ListView
的一种简单方法是在 setState()
中创建一个新的 List
,并将旧列表中的数据复制到新列表中。虽然这种方法简单,但不推荐用于大型数据集,如下一个示例所示。
import 'dart:developer' as developer;
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Sample App',
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List<Widget> widgets = <Widget>[];
@override
void initState() {
super.initState();
for (int i = 0; i < 100; i++) {
widgets.add(getRow(i));
}
}
Widget getRow(int i) {
return GestureDetector(
onTap: () {
setState(() {
widgets = List.from(widgets);
widgets.add(getRow(widgets.length));
developer.log('row $i');
});
},
child: Padding(
padding: const EdgeInsets.all(10),
child: Text('Row $i'),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Sample App'),
),
body: ListView(children: widgets),
);
}
}
构建列表的推荐、高效且有效的方法是使用 ListView.Builder
。当你有一个动态列表或一个包含大量数据的数据列表时,此方法非常棒。
import 'dart:developer' as developer;
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Sample App',
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List<Widget> widgets = [];
@override
void initState() {
super.initState();
for (int i = 0; i < 100; i++) {
widgets.add(getRow(i));
}
}
Widget getRow(int i) {
return GestureDetector(
onTap: () {
setState(() {
widgets.add(getRow(widgets.length));
developer.log('row $i');
});
},
child: Padding(
padding: const EdgeInsets.all(10),
child: Text('Row $i'),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Sample App'),
),
body: ListView.builder(
itemCount: widgets.length,
itemBuilder: (context, position) {
return getRow(position);
},
),
);
}
}
不要创建一个 ListView
,而是创建一个 ListView.builder
,它采用两个关键参数:列表的初始长度和一个 ItemBuilder
函数。
ItemBuilder
函数类似于 iOS 表格或集合视图中的 cellForItemAt
委托方法,因为它采用一个位置,并返回您希望在该位置呈现的单元格。
最后,但最重要的是,请注意 onTap()
函数不再重新创建列表,而是 .add
到列表中。
创建滚动视图
在 UIKit 中,您将视图包装在 ScrollView
中,它允许用户在需要时滚动您的内容。
在 Flutter 中,执行此操作最简单的方法是使用 ListView
窗口小部件。它既充当 ScrollView
,又充当 iOS TableView
,因为您可以在垂直格式中布局窗口小部件。
@override
Widget build(BuildContext context) {
return ListView(
children: const <Widget>[
Text('Row One'),
Text('Row Two'),
Text('Row Three'),
Text('Row Four'),
],
);
}
有关如何在 Flutter 中布局窗口小部件的更详细文档,请参阅 布局教程。
手势检测和触摸事件处理
本节讨论如何在 Flutter 中检测手势和处理不同事件,以及它们与 UIKit 的比较。
添加点击侦听器
在 UIKit 中,您将 GestureRecognizer
附加到视图以处理点击事件。在 Flutter 中,有两种添加触摸侦听器的方法
-
如果窗口小部件支持事件检测,请向其传递一个函数,并在函数中处理事件。例如,
ElevatedButton
窗口小部件有一个onPressed
参数@override Widget build(BuildContext context) { return ElevatedButton( onPressed: () { developer.log('click'); }, child: const Text('Button'), ); }
-
如果窗口小部件不支持事件检测,请将窗口小部件包装在 GestureDetector 中,并将函数传递给
onTap
参数。class SampleTapApp extends StatelessWidget { const SampleTapApp({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: Center( child: GestureDetector( onTap: () { developer.log('tap'); }, child: const FlutterLogo( size: 200, ), ), ), ); } }
处理其他手势
使用 GestureDetector
,您可以侦听各种手势,例如
-
轻击
onTapDown
- 可能导致轻击的指针已在特定位置接触屏幕。
onTapUp
- 触发轻击的指针已停止在特定位置接触屏幕。
onTap
- 已发生轻击。
onTapCancel
- 先前触发
onTapDown
的指针不会导致轻击。
-
双击
onDoubleTap
- 用户在同一位置快速连续轻击屏幕两次。
-
长按
onLongPress
- 指针已在同一位置长时间与屏幕保持接触。
-
垂直拖动
onVerticalDragStart
- 指针已接触屏幕,可能开始垂直移动。
onVerticalDragUpdate
- 与屏幕接触的指针已在垂直方向进一步移动。
onVerticalDragEnd
- 先前与屏幕接触并垂直移动的指针不再与屏幕接触,并且在停止与屏幕接触时以特定速度移动。
-
水平拖动
onHorizontalDragStart
- 指针已接触屏幕,可能开始水平移动。
onHorizontalDragUpdate
- 与屏幕接触的指针已在水平方向进一步移动。
onHorizontalDragEnd
- 先前与屏幕接触并水平移动的指针不再与屏幕接触。
以下示例显示了在双击时旋转 Flutter 徽标的 GestureDetector
class SampleApp extends StatefulWidget {
const SampleApp({super.key});
@override
State<SampleApp> createState() => _SampleAppState();
}
class _SampleAppState extends State<SampleApp>
with SingleTickerProviderStateMixin {
late AnimationController controller;
late CurvedAnimation curve;
@override
void initState() {
super.initState();
controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 2000),
);
curve = CurvedAnimation(
parent: controller,
curve: Curves.easeIn,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: GestureDetector(
onDoubleTap: () {
if (controller.isCompleted) {
controller.reverse();
} else {
controller.forward();
}
},
child: RotationTransition(
turns: curve,
child: const FlutterLogo(
size: 200,
),
),
),
),
);
}
}
主题、样式和媒体
Flutter 应用程序易于设置样式;您可以在浅色和深色主题之间切换、更改文本和 UI 组件的样式,等等。本部分介绍设置 Flutter 应用程序样式的各个方面,并比较在 UIKit 中如何执行相同操作。
使用主题
开箱即用,Flutter 带有 Material Design 的漂亮实现,它满足您通常会执行的大量样式和主题需求。
要在应用程序中充分利用 Material Components,请声明一个顶级小组件 MaterialApp
,作为应用程序的入口点。 MaterialApp
是一个便捷小组件,它封装了许多通常在实现 Material Design 的应用程序中必需的小组件。它通过添加 Material 特定功能来构建在 WidgetsApp
的基础上。
但是,Flutter 足够灵活且富有表现力,可以实现任何设计语言。在 iOS 上,你可以使用 Cupertino 库 来生成一个遵守 人类界面指南 的界面。有关这些小组件的完整集合,请参阅 Cupertino 小组件 图库。
你还可以使用 WidgetsApp
作为应用程序小组件,它提供了一些相同的功能,但不如 MaterialApp
丰富。
要自定义任何子组件的颜色和样式,请将 ThemeData
对象传递给 MaterialApp
小组件。例如,在下面的代码中,种子中的配色方案设置为 deepPurple,分隔符颜色为灰色。
import 'package:flutter/material.dart';
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
dividerColor: Colors.grey,
),
home: const SampleAppPage(),
);
}
}
使用自定义字体
在 UIKit 中,你可以将任何 ttf
字体文件导入到项目中,并在 info.plist
文件中创建引用。在 Flutter 中,将字体文件放在文件夹中,并在 pubspec.yaml
文件中引用它,类似于导入图像的方式。
fonts:
- family: MyCustomFont
fonts:
- asset: fonts/MyCustomFont.ttf
- style: italic
然后将字体分配给 Text
小组件
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Sample App'),
),
body: const Center(
child: Text(
'This is a custom font text',
style: TextStyle(fontFamily: 'MyCustomFont'),
),
),
);
}
设置文本样式
除了字体之外,你还可以自定义 Text
小组件上的其他样式元素。 Text
小组件的 style 参数采用 TextStyle
对象,你可以在其中自定义许多参数,例如
颜色
装饰
装饰颜色
装饰样式
字体系列
字体大小
字体样式
字体粗细
哈希码
高度
继承
字母间距
文本基线
字间距
在应用中捆绑图像
虽然 iOS 将图像和资源视为不同的项,但 Flutter 应用只有资源。放置在 iOS 上的 Images.xcasset
文件夹中的资源被放置在 Flutter 的资源文件夹中。与 iOS 一样,资源可以是任何类型的文件,而不仅仅是图像。例如,您可能有一个 JSON 文件位于 my-assets
文件夹中
my-assets/data.json
在 pubspec.yaml
文件中声明资源
assets:
- my-assets/data.json
然后使用 AssetBundle
从代码中访问它
import 'dart:async' show Future;
import 'package:flutter/services.dart' show rootBundle;
Future<String> loadAsset() async {
return await rootBundle.loadString('my-assets/data.json');
}
对于图像,Flutter 遵循类似于 iOS 的基于密度的简单格式。图像资源可能是 1.0x
、2.0x
、3.0x
或任何其他倍数。Flutter 的 devicePixelRatio
表示单个逻辑像素中物理像素的比率。
资源位于任何任意文件夹中——Flutter 没有预定义的文件夹结构。您在 pubspec.yaml
文件中声明资源(带位置),Flutter 会提取它们。
例如,要将名为 my_icon.png
的图像添加到您的 Flutter 项目,您可以决定将其存储在一个任意称为 images
的文件夹中。将基本图像 (1.0x) 放在 images
文件夹中,将其他变体放在以适当比率倍数命名的子文件夹中
images/my_icon.png // Base: 1.0x image
images/2.0x/my_icon.png // 2.0x image
images/3.0x/my_icon.png // 3.0x image
接下来,在 pubspec.yaml
文件中声明这些图像
assets:
- images/my_icon.png
您现在可以使用 AssetImage
访问您的图像
AssetImage('images/a_dot_burr.jpeg')
或直接在 Image
小部件中
@override
Widget build(BuildContext context) {
return Image.asset('images/my_image.png');
}
有关更多详细信息,请参阅 在 Flutter 中添加资源和图像。
表单输入
本节讨论如何在 Flutter 中使用表单以及它们与 UIKit 的比较。
检索用户输入
鉴于 Flutter 使用不可变小部件以及单独的状态,你可能想知道用户输入如何适应这种模式。在 UIKit 中,你通常会在提交用户输入或对其执行操作时查询小部件的当前值。Flutter 中如何实现此操作?
实际上,表单的处理方式与 Flutter 中的所有内容一样,都由专门的小部件处理。如果你有 TextField
或 TextFormField
,则可以提供 TextEditingController
来检索用户输入
class _MyFormState extends State<MyForm> {
// Create a text controller and use it to retrieve the current value.
// of the TextField!
final myController = TextEditingController();
@override
void dispose() {
// Clean up the controller when disposing of the Widget.
myController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Retrieve Text Input')),
body: Padding(
padding: const EdgeInsets.all(16),
child: TextField(controller: myController),
),
floatingActionButton: FloatingActionButton(
// When the user presses the button, show an alert dialog with the
// text the user has typed into our text field.
onPressed: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
// Retrieve the text the user has typed in using our
// TextEditingController.
content: Text(myController.text),
);
},
);
},
tooltip: 'Show me the value!',
child: const Icon(Icons.text_fields),
),
);
}
}
你可以在 检索文本字段的值 中找到更多信息和完整的代码清单,该清单来自 Flutter 烹饪书。
文本字段中的占位符
在 Flutter 中,你可以通过向 Text
小部件的 decoration 构造函数参数添加 InputDecoration
对象,轻松显示字段的“提示”或占位符文本
Center(
child: TextField(
decoration: InputDecoration(hintText: 'This is a hint'),
),
)
显示验证错误
就像使用“提示”一样,将 InputDecoration
对象传递给 Text
小部件的 decoration 构造函数。
但是,你不想一开始就显示错误。相反,当用户输入无效数据时,更新状态并传递新的 InputDecoration
对象。
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Sample App',
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
String? _errorText;
bool isEmail(String em) {
String emailRegexp =
r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|'
r'(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|'
r'(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$';
RegExp regExp = RegExp(emailRegexp);
return regExp.hasMatch(em);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Sample App'),
),
body: Center(
child: TextField(
onSubmitted: (text) {
setState(() {
if (!isEmail(text)) {
_errorText = 'Error: This is not an email';
} else {
_errorText = null;
}
});
},
decoration: InputDecoration(
hintText: 'This is a hint',
errorText: _errorText,
),
),
),
);
}
}
线程和异步性
本部分讨论 Flutter 中的并发性,以及它与 UIKit 的比较。
编写异步代码
Dart 具有单线程执行模型,支持 Isolate
(一种在另一个线程上运行 Dart 代码的方法)、事件循环和异步编程。除非你生成 Isolate
,否则你的 Dart 代码将在主 UI 线程中运行,并由事件循环驱动。Flutter 的事件循环等效于 iOS 主循环,即附加到主线程的 Looper
。
Dart 的单线程模型并不意味着你必须将所有内容作为导致 UI 冻结的阻塞操作来运行。相反,使用 Dart 语言提供的异步工具(例如 async
/await
)来执行异步工作。
例如,你可以使用 async
/await
运行网络代码,而不会导致 UI 挂起,并让 Dart 做繁重的工作
Future<void> loadData() async {
final Uri dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
final http.Response response = await http.get(dataURL);
setState(() {
data = jsonDecode(response.body);
});
}
一旦 await
ed 网络调用完成,通过调用 setState()
更新 UI,这会触发小部件子树的重建并更新数据。
以下示例异步加载数据并将其显示在 ListView
中
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Sample App',
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List<Map<String, dynamic>> data = <Map<String, dynamic>>[];
@override
void initState() {
super.initState();
loadData();
}
Future<void> loadData() async {
final Uri dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
final http.Response response = await http.get(dataURL);
setState(() {
data = jsonDecode(response.body);
});
}
Widget getRow(int index) {
return Padding(
padding: const EdgeInsets.all(10),
child: Text('Row ${data[index]['title']}'),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Sample App'),
),
body: ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) {
return getRow(index);
},
),
);
}
}
请参阅下一部分以获取有关在后台工作的更多信息,以及 Flutter 与 iOS 的不同之处。
移至后台线程
由于 Flutter 是单线程的并运行事件循环(如 Node.js),因此你不必担心线程管理或生成后台线程。如果你正在执行 I/O 绑定工作,例如磁盘访问或网络调用,那么你可以安全地使用 async
/await
,然后你就完成了。另一方面,如果你需要执行使 CPU 繁忙的计算密集型工作,则需要将其移至 Isolate
以避免阻塞事件循环。
对于 I/O 绑定工作,将函数声明为 async
函数,并在函数内部 await
长时间运行的任务
Future<void> loadData() async {
final Uri dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
final http.Response response = await http.get(dataURL);
setState(() {
data = jsonDecode(response.body);
});
}
这通常是执行网络或数据库调用的方式,它们都是 I/O 操作。
但是,有时你可能会处理大量数据,并且你的 UI 会挂起。在 Flutter 中,使用 Isolate
s 利用多个 CPU 内核来执行长时间运行或计算密集型任务。
隔离是独立的执行线程,不与主执行内存堆共享任何内存。这意味着你无法访问主线程中的变量,也无法通过调用 setState()
更新 UI。隔离忠实于其名称,并且不能共享内存(例如,以静态字段的形式)。
以下示例在一个简单的隔离中展示了如何将数据共享回主线程以更新 UI。
Future<void> loadData() async {
final ReceivePort receivePort = ReceivePort();
await Isolate.spawn(dataLoader, receivePort.sendPort);
// The 'echo' isolate sends its SendPort as the first message.
final SendPort sendPort = await receivePort.first as SendPort;
final List<Map<String, dynamic>> msg = await sendReceive(
sendPort,
'https://jsonplaceholder.typicode.com/posts',
);
setState(() {
data = msg;
});
}
// The entry point for the isolate.
static Future<void> dataLoader(SendPort sendPort) async {
// Open the ReceivePort for incoming messages.
final ReceivePort port = ReceivePort();
// Notify any other isolates what port this isolate listens to.
sendPort.send(port.sendPort);
await for (final dynamic msg in port) {
final String url = msg[0] as String;
final SendPort replyTo = msg[1] as SendPort;
final Uri dataURL = Uri.parse(url);
final http.Response response = await http.get(dataURL);
// Lots of JSON to parse
replyTo.send(jsonDecode(response.body) as List<Map<String, dynamic>>);
}
}
Future<List<Map<String, dynamic>>> sendReceive(SendPort port, String msg) {
final ReceivePort response = ReceivePort();
port.send(<dynamic>[msg, response.sendPort]);
return response.first as Future<List<Map<String, dynamic>>>;
}
此处,dataLoader()
是在自己的独立执行线程中运行的 Isolate
。在隔离中,你可以执行更多 CPU 密集型处理(例如,解析一个大的 JSON),或执行计算密集型数学,例如加密或信号处理。
你可以运行以下完整示例
import 'dart:async';
import 'dart:convert';
import 'dart:isolate';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Sample App',
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List<Map<String, dynamic>> data = <Map<String, dynamic>>[];
@override
void initState() {
super.initState();
loadData();
}
bool get showLoadingDialog => data.isEmpty;
Future<void> loadData() async {
final ReceivePort receivePort = ReceivePort();
await Isolate.spawn(dataLoader, receivePort.sendPort);
// The 'echo' isolate sends its SendPort as the first message.
final SendPort sendPort = await receivePort.first as SendPort;
final List<Map<String, dynamic>> msg = await sendReceive(
sendPort,
'https://jsonplaceholder.typicode.com/posts',
);
setState(() {
data = msg;
});
}
// The entry point for the isolate.
static Future<void> dataLoader(SendPort sendPort) async {
// Open the ReceivePort for incoming messages.
final ReceivePort port = ReceivePort();
// Notify any other isolates what port this isolate listens to.
sendPort.send(port.sendPort);
await for (final dynamic msg in port) {
final String url = msg[0] as String;
final SendPort replyTo = msg[1] as SendPort;
final Uri dataURL = Uri.parse(url);
final http.Response response = await http.get(dataURL);
// Lots of JSON to parse
replyTo.send(jsonDecode(response.body) as List<Map<String, dynamic>>);
}
}
Future<List<Map<String, dynamic>>> sendReceive(SendPort port, String msg) {
final ReceivePort response = ReceivePort();
port.send(<dynamic>[msg, response.sendPort]);
return response.first as Future<List<Map<String, dynamic>>>;
}
Widget getBody() {
bool showLoadingDialog = data.isEmpty;
if (showLoadingDialog) {
return getProgressDialog();
} else {
return getListView();
}
}
Widget getProgressDialog() {
return const Center(child: CircularProgressIndicator());
}
ListView getListView() {
return ListView.builder(
itemCount: data.length,
itemBuilder: (context, position) {
return getRow(position);
},
);
}
Widget getRow(int i) {
return Padding(
padding: const EdgeInsets.all(10),
child: Text("Row ${data[i]["title"]}"),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Sample App'),
),
body: getBody(),
);
}
}
发出网络请求
当你使用流行的 http
包 时,在 Flutter 中发出网络调用很容易。这抽象了你可能通常自己实现的大量网络,从而简化了发出网络调用的过程。
要将 http
包添加为依赖项,请运行 flutter pub add
flutter pub add http
要发出网络调用,请在 async
函数 http.get()
上调用 await
Future<void> loadData() async {
final Uri dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
final http.Response response = await http.get(dataURL);
setState(() {
data = jsonDecode(response.body);
});
}
显示长期运行任务的进度
在 UIKit 中,通常在后台执行长期运行任务时使用 UIProgressView
。
在 Flutter 中,使用 ProgressIndicator
小组件。通过控制何时通过布尔标志呈现进度指示器以编程方式显示进度。在长期运行任务开始之前告诉 Flutter 更新其状态,并在任务结束后将其隐藏。
在下面的示例中,构建函数被分成三个不同的函数。如果 showLoadingDialog
为 true
(当 widgets.length == 0
时),则呈现 ProgressIndicator
。否则,使用从网络调用返回的数据呈现 ListView
。
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Sample App',
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List<Map<String, dynamic>> data = <Map<String, dynamic>>[];
@override
void initState() {
super.initState();
loadData();
}
bool get showLoadingDialog => data.isEmpty;
Future<void> loadData() async {
final Uri dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
final http.Response response = await http.get(dataURL);
setState(() {
data = jsonDecode(response.body);
});
}
Widget getBody() {
if (showLoadingDialog) {
return getProgressDialog();
}
return getListView();
}
Widget getProgressDialog() {
return const Center(child: CircularProgressIndicator());
}
ListView getListView() {
return ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) {
return getRow(index);
},
);
}
Widget getRow(int i) {
return Padding(
padding: const EdgeInsets.all(10),
child: Text("Row ${data[i]["title"]}"),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Sample App'),
),
body: getBody(),
);
}
}