Flutter for UIKit 开发者
有使用 UIKit 经验的 iOS 开发者,如果希望使用 Flutter 编写移动应用,应该阅读本指南。它解释了如何将现有的 UIKit 知识应用于 Flutter。
Flutter 是一个用于构建跨平台应用程序的框架,它使用 Dart 编程语言。要了解 Dart 编程与 Swift 编程之间的一些区别,请查看 作为 Swift 开发人员学习 Dart 和 Flutter 并发性指南(适用于 Swift 开发者)。
您在 iOS 和 UIKit 方面的知识和经验在使用 Flutter 构建时非常宝贵。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 中,部件是不可变的,不会直接更新。相反,您必须操作部件的状态。
这就是有状态部件与无状态部件的概念发挥作用的地方。StatelessWidget
正如其名称所示——一个没有附加状态的部件。
StatelessWidgets
在您描述的用户界面部分不依赖于部件中的初始配置信息以外的任何内容时很有用。
例如,在 UIKit 中,这类似于放置一个带有徽标作为 image
的 UIImageView
。如果徽标在运行时不发生更改,请在 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 中,通过组合更小的 widget(而不是扩展它们)来构建自定义 widget。
例如,如何构建一个在构造函数中接收标签的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 中声明外部依赖项。在pub.dev上可以找到许多优秀的 Flutter 包。
导航
#本节内容讨论应用程序页面之间的导航、push 和 pop 机制等。
在页面之间导航
#在 UIKit 中,要在视图控制器之间切换,可以使用UINavigationController
来管理要显示的视图控制器堆栈。
Flutter 有一个类似的实现,使用Navigator
和Routes
。Route
是应用程序“屏幕”或“页面”的抽象,而Navigator
是一个widget,用于管理路由。路由大致对应于UIViewController
。Navigator 的工作方式与 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 代码。
处理本地化
#与 iOS 的Localizable.strings
文件不同,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
包,请在应用程序 widget 上指定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
,因此它既有用于基本 widget 本地化值的GlobalWidgetsLocalizations
,也有用于 Material widget 本地化的MaterialWidgetsLocalizations
。如果你使用WidgetsApp
作为你的应用程序,则不需要后者。请注意,这两个委托包含“默认”值,但如果你也希望自己的应用程序的可本地化复制内容被本地化,则需要为其提供一个或多个委托。
初始化后,WidgetsApp
(或MaterialApp
)会为你创建一个Localizations
widget,并使用你指定的委托。设备的当前语言环境始终可以通过当前上下文中Localizations
widget(以Locale
对象的格式)或使用Window.locale
访问。
要访问本地化资源,请使用Localizations.of()
方法访问由给定委托提供的特定本地化类。使用intl_translation
包将可翻译的复制内容提取到arb文件中以进行翻译,并将它们导入回应用程序以与intl
一起使用。
有关 Flutter 中国际化和本地化的更多详细信息,请参阅国际化指南,其中包含使用和不使用intl
包的示例代码。
视图控制器
#本节内容讨论 Flutter 中 ViewController 的等价物以及如何监听生命周期事件。
Flutter 中的视图控制器等价物
#在 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 的不可变 widget 模式,你将 widget 列表传递给你的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 中,使用传递的 widget 提供的触摸处理。
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()
内部更新 widget 列表,你会很快发现你的数据在视觉上没有改变。这是因为当调用setState()
时,Flutter 渲染引擎会查看 widget 树以查看是否有任何更改。当它到达你的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
widget。它既充当ScrollView
,也充当 iOS 的TableView
,因为你可以在垂直格式中布局 widget。
@override
Widget build(BuildContext context) {
return ListView(
children: const <Widget>[
Text('Row One'),
Text('Row Two'),
Text('Row Three'),
Text('Row Four'),
],
);
}
有关如何在 Flutter 中布局 widget 的更详细文档,请参阅布局教程。
手势检测和触摸事件处理
#本节讨论如何在 Flutter 中检测手势并处理不同的事件,以及它们与 UIKit 的比较。
添加点击监听器
#在 UIKit 中,你将GestureRecognizer
附加到视图以处理点击事件。在 Flutter 中,有两种添加触摸监听器的方法:
- 如果 widget 支持事件检测,请向其传递一个函数,并在函数中处理事件。例如,
ElevatedButton
widget 有一个onPressed
参数:
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
developer.log('click');
},
child: const Text('Button'),
);
}
- 如果 Widget 不支持事件检测,请将 widget 包裹在 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 组件,请声明一个顶级小部件MaterialApp
作为应用程序的入口点。MaterialApp
是一个便利小部件,它包装了许多通常需要用于实现 Material Design 的应用程序的小部件。它以WidgetsApp
为基础,添加了 Material 特定的功能。
但 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 一样,资源可以是任何类型的文件,而不仅仅是图像。例如,您可能在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.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 中,您可以通过将InputDecoration
对象添加到Text
小部件的 decoration 构造函数参数中,轻松地为您的字段显示“提示”或占位符文本。
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
利用多个 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, 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 更新其状态,并在其结束后隐藏它。
在下面的示例中,build 函数被分成三个不同的函数。如果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(),
);
}
}
除非另有说明,否则本网站上的文档反映了 Flutter 的最新稳定版本。页面上次更新于 2024-09-09。 查看源代码 或 报告问题。