面向 UIKit 开发者的 Flutter
有使用 UIKit 经验的 iOS 开发者,如果想使用 Flutter 编写移动应用,应查阅本指南。本指南解释了如何将现有 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 框架都会创建一个新的组件实例树。相比之下,UIKit 视图在更改时不会重新创建,而是一个可变实体,它只绘制一次,并且在使用 `setNeedsDisplay()` 使其失效之前不会重新绘制。
此外,与 `UIView` 不同,Flutter 的组件是轻量级的,部分原因是它们的不可变性。因为它们本身不是视图,也不是直接绘制任何东西,而只是 UI 及其语义的描述,这些描述会在底层“膨胀”成实际的视图对象。
Flutter 包含 Material Components 库。这些组件实现了 Material Design 指南。Material Design 是一个灵活的设计系统,针对所有平台进行了优化,包括 iOS。
但 Flutter 足够灵活和富有表现力,可以实现任何设计语言。在 iOS 上,您可以使用 Cupertino 组件库来生成看起来像 Apple 的 iOS 设计语言的界面。
更新组件
#要在 UIKit 中更新视图,您可以直接对其进行修改。在 Flutter 中,组件是不可变的,不能直接更新。相反,您必须操作组件的状态。
这就是有状态 (Stateful) 和无状态 (Stateless) 组件概念的由来。`StatelessWidget` 顾名思义——一个没有附加状态的组件。
当您描述的用户界面部分除了组件中的初始配置信息之外不依赖任何东西时,`StatelessWidget` 很有用。
例如,对于 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 中,通过组合组件树在代码中声明布局。
以下示例展示了如何显示一个带填充的简单 Widget
@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
例如,您可以使用 `CurvedAnimation` 来实现沿着插值曲线的动画。从这个意义上说,控制器是动画进度的“主”源,而 `CurvedAnimation` 计算替换控制器默认线性运动的曲线。像组件一样,Flutter 中的动画也通过组合工作。
在构建组件树时,您将 `Animation` 分配给组件的动画属性,例如 `FadeTransition` 的不透明度,并告诉控制器开始动画。
以下示例展示了如何编写一个 FadeTransition
,当您按下 FloatingActionButton
时,该转换会将 Widget 淡入为徽标
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),
),
);
}
}
有关更多信息,请参阅动画与动作 Widgets、动画教程和动画概述。
屏幕绘制
#在 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
?通过组合带标签的 ElevatedButton
来创建 CustomButton,而不是通过扩展 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));
}
}
然后像使用任何其他 Flutter Widget 一样使用 CustomButton
@override
Widget build(BuildContext context) {
return const Center(child: CustomButton('Hello'));
}
管理依赖项
#在 iOS 中,您通过将依赖项添加到 `Podfile` 中来使用 CocoaPods 添加依赖项。Flutter 使用 Dart 的构建系统和 Pub 包管理器来处理依赖项。这些工具将原生 Android 和 iOS 封装应用的构建委托给各自的构建系统。
尽管 Flutter 项目的 iOS 文件夹中有一个 Podfile,但只有在您添加每个平台集成所需的原生依赖项时才使用它。通常,使用 `pubspec.yaml` 在 Flutter 中声明外部依赖项。一个很好的地方可以找到 Flutter 的优秀包是 pub.dev。
导航
#本文档的这一部分讨论了应用页面之间的导航、推拉机制等。
页面导航
#在 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 中的路由,并用于从已推送到堆栈的路由获取结果。这通过 `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` 包的示例代码。
视图控制器
#本文档的这一部分讨论了 Flutter 中 ViewController 的等效项以及如何监听生命周期事件。
Flutter 中视图控制器的等效项
#在 UIKit 中,`ViewController` 表示用户界面的一部分,最常用于屏幕或部分。这些组件组合在一起以构建复杂的 UI,并帮助扩展您的应用程序 UI。在 Flutter 中,这项工作由 Widgets 完成。如导航部分所述,Flutter 中的屏幕由 Widgets 表示,因为“一切都是组件!” 使用 `Navigator` 在不同的表示不同屏幕或页面,或者可能是相同数据的不同状态或渲染的 `Route` 之间移动。
监听生命周期事件
#在 UIKit 中,您可以覆盖 `ViewController` 的方法来捕获视图本身的生命周期方法,或在 `AppDelegate` 中注册生命周期回调。在 Flutter 中,您没有这些概念,但您可以通过挂钩 `WidgetsBinding` 观察器并监听 `didChangeAppLifecycleState()` 更改事件来监听生命周期事件。
可观察的生命周期事件有
不活动
- 应用程序处于非活动状态,不接收用户输入。此事件仅在 iOS 上有效,因为 Android 上没有等效事件。
已暂停
- 应用程序当前对用户不可见,不响应用户输入,但正在后台运行。
已恢复
- 应用程序可见并响应用户输入。
正在挂起
- 应用程序暂时挂起。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()` 中更新组件列表,您会很快发现数据在视觉上没有变化。这是因为当调用 `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
- 之前与屏幕接触并水平移动的指针不再与屏幕接触。
以下示例展示了一个 GestureDetector
,它在双击时旋转 Flutter 徽标
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
作为您的应用 Widget,它提供了一些相同的功能,但不如 MaterialApp
丰富。
要自定义任何子组件的颜色和样式,请将 `ThemeData` 对象传递给 `MaterialApp` 组件。例如,在下面的代码中,种子颜色方案设置为深紫色,分隔线颜色设置为灰色。
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
Widget
@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
Widget 上自定义其他样式元素。Text
Widget 的 style 参数接受一个 TextStyle
对象,您可以在其中自定义许多参数,例如
color
decoration
decorationColor
decorationStyle
fontFamily
fontSize
fontStyle
fontWeight
hashCode
height
inherit
letterSpacing
textBaseline
wordSpacing
在应用中捆绑图像
#虽然 iOS 将图像和资产视为不同的项,但 Flutter 应用程序只有资产。iOS 上放置在 `Images.xcasset` 文件夹中的资源,在 Flutter 中放置在资产文件夹中。与 iOS 一样,资产可以是任何类型的文件,而不仅仅是图像。例如,您可能在 `my-assets` 文件夹中有一个 JSON 文件
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` 访问您的图像
image: AssetImage('images/a_dot_burr.png'),
或直接在 `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 中,您可以通过为 `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) as List).cast<Map<String, Object?>>();
});
}
一旦 `await` 的网络调用完成,通过调用 `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, Object?>> data = [];
@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) as List).cast<Map<String, Object?>>();
});
}
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) as List).cast<Map<String, Object?>>();
});
}
您通常就是这样进行网络或数据库调用,它们都是 I/O 操作。
但是,有时您可能会处理大量数据并且 UI 挂起。在 Flutter 中,使用 Isolate
来利用多个 CPU 核心来执行长时间运行或计算密集型任务。
Isolate 是独立的执行线程,不与主执行内存堆共享任何内存。这意味着您无法从主线程访问变量,也无法通过调用 `setState()` 更新 UI。Isolate 名副其实,无法共享内存(例如,以静态字段的形式)。
以下示例以一个简单的 isolate 形式展示了如何将数据共享回主线程以更新 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`。在 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, Object?>> data = [];
@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) as List).cast<Map<String, Object?>>();
});
}
显示长时间运行任务的进度
#在 UIKit 中,您通常会在后台执行长时间运行的任务时使用 `UIProgressView`。
在 Flutter 中,使用 ProgressIndicator
Widget。通过布尔标志控制何时渲染它来以编程方式显示进度。在长时间运行的任务开始之前通知 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, Object?>> data = [];
@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) as List).cast<Map<String, Object?>>();
});
}
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(),
);
}
}