Skip to main content

面向 UIKit 开发人员的 Flutter

拥有 UIKit 使用经验并希望使用 Flutter 编写移动应用的 iOS 开发人员应该阅读本指南。它解释了如何将现有的 UIKit 知识应用于 Flutter。

Flutter 是一个用于构建跨平台应用程序的框架,它使用 Dart 编程语言。要了解使用 Dart 编程和使用 Swift 编程之间的一些区别,请查看作为 Swift 开发人员学习 Dart面向 Swift 开发人员的 Flutter 并发

在使用 Flutter 构建应用程序时,您的 iOS 和 UIKit 知识和经验非常宝贵。 Flutter 在 iOS 上运行时还会对应用程序行为进行一些调整。要了解如何操作,请参阅平台适配

将本指南用作参考手册。随意跳转并查找最能满足您需求的问题。

概述

#

作为介绍,请观看以下视频。它概述了 Flutter 在 iOS 上的工作方式以及如何使用 Flutter 构建 iOS 应用。


Flutter for iOS developers

视图与部件

#

在 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,这类似于放置一个带有您的徽标作为 imageUIImageView。如果徽标在运行时没有更改,请在 Flutter 中使用 StatelessWidget

如果要根据发出 HTTP 调用后接收到的数据动态更改 UI,请使用 StatefulWidget。HTTP 调用完成后,告诉 Flutter 框架部件的 State 已更新,以便它可以更新 UI。

无状态部件和有状态部件之间的重要区别在于,StatefulWidget 具有一个 State 对象,该对象存储状态数据并在树重建过程中保留它,因此不会丢失。

如果您有疑问,请记住此规则:如果部件在 build 方法之外发生更改(例如,由于运行时用户交互),则它是可变的。如果部件一旦构建就永不更改,则它是无状态的。但是,即使部件是有状态的,如果它本身没有对这些更改(或其他输入)做出反应,则包含它的父部件仍然可以是无状态的。

以下示例显示了如何使用 StatelessWidget。一个常见的 StatelessWidgetText 部件。如果您查看 Text 部件的实现,您会发现它继承自 StatelessWidget

dart
Text(
  'I like Flutter!',
  style: TextStyle(fontWeight: FontWeight.bold),
);

如果您查看上面的代码,您可能会注意到 Text 部件没有携带任何显式状态。它呈现其构造函数中传递的内容,仅此而已。

但是,如果您想动态更改“我喜欢 Flutter”,例如单击 FloatingActionButton 时怎么办?

为此,请将 Text 部件包装在 StatefulWidget 中,并在用户单击按钮时更新它。

例如:

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: '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 中,通过组合部件树以代码形式声明您的布局。

以下示例显示了如何显示带有填充的简单部件:

dart
@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 时如何在两个部件之间切换:

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: '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 来指示 vsync 何时发生,并在其运行期间的每一帧上产生 0 到 1 之间的线性插值。然后,您可以创建一个或多个 Animation 并将其附加到控制器。

例如,您可以使用 CurvedAnimation 来沿插值曲线实现动画。从这个意义上说,控制器是动画进度的“主”源,而 CurvedAnimation 计算的曲线替换了控制器的默认线性运动。与部件一样,Flutter 中的动画也使用组合来工作。

在构建部件树时,您将 Animation 分配给部件的动画属性,例如 FadeTransition 的不透明度,并告诉控制器启动动画。

以下示例显示了如何编写一个 FadeTransition,当您按下 FloatingActionButton 时,它会将部件淡入徽标:

dart
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,以及另外两个可帮助您绘图的类:CustomPaintCustomPainter,后者实现您的算法以绘制到画布上。

要了解如何在 Flutter 中实现签名绘制器,请参阅 Collin 在StackOverflow 上的答案。

dart
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

dart
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 部件一样:

dart
@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 有类似的实现,使用 NavigatorRoutesRoute 是应用程序“屏幕”或“页面”的抽象,Navigator 是一个部件,用于管理路由。路由大致对应于 UIViewController。导航器的作用方式与 iOS UINavigationController 类似,因为它可以根据您是想导航到视图还是从视图返回来 push()pop() 路由。

要在页面之间导航,您可以选择以下几种方法:

  • 指定路由名称的 Map
  • 直接导航到路由。

以下示例构建了一个 Map

dart
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'),
      },
    ),
  );
}

通过将路由名称 pushNavigator 来导航到该路由。

dart
Navigator.of(context).pushNamed('/b');

Navigator 类处理 Flutter 中的路由,并用于从您已推送到堆栈中的路由获取结果。这是通过 await push() 返回的 Future 来完成的。

例如,要启动一个允许用户选择其位置的 location 路由,您可以执行以下操作:

dart
Object? coordinates = await Navigator.of(context).pushNamed('/location');

然后,在您的 location 路由中,一旦用户选择了其位置,就使用结果 pop() 堆栈:

dart
Navigator.of(context).pop({'lat': 43.821757, 'long': -79.226392});

导航到另一个应用程序

#

在 UIKit 中,要将用户发送到另一个应用程序,您可以使用特定的 URL 方案。对于系统级应用程序,方案取决于应用程序。要在 Flutter 中实现此功能,请创建原生平台集成,或使用现有插件,例如url_launcher

手动返回

#

从您的 Dart 代码调用 SystemNavigator.pop() 将调用以下 iOS 代码:

objc
UIViewController* viewController = [UIApplication sharedApplication].keyWindow.rootViewController;
if ([viewController isKindOfClass:[UINavigationController class]]) {
  [((UINavigationController*)viewController) popViewControllerAnimated:NO];
}

如果这不能满足您的需求,您可以创建自己的平台通道 来调用任意 iOS 代码。

处理本地化

#

与拥有 Localizable.strings 文件的 iOS 不同,Flutter 目前没有专门用于处理字符串的系统。目前,最佳实践是在类中将您的文本声明为静态字段,并从那里访问它们。例如:

dart
class Strings {
  static const String welcomeMessage = 'Welcome To Flutter';
}

您可以像这样访问您的字符串:

dart
Text(Strings.welcomeMessage);

默认情况下,Flutter 只支持其字符串的美国英语。如果您需要添加对其他语言的支持,请包含 flutter_localizations 包。您可能还需要添加 Dart 的intl 包以使用 i10n 机制,例如日期/时间格式化。

yaml
dependencies:
  flutter_localizations:
    sdk: flutter
  intl: any # Use version of intl from flutter_localizations.

要使用 flutter_localizations 包,请在应用程序部件上指定 localizationsDelegatessupportedLocales

dart
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(用于基本部件的本地化值)和 MaterialWidgetsLocalizations(用于 Material 部件的本地化)。如果您对应用程序使用 WidgetsApp,则不需要后者。请注意,这两个委托包含“默认”值,但如果您希望自己的应用程序的可本地化文本也进行本地化,则需要为自己的应用程序的可本地化文本提供一个或多个委托。

初始化时,WidgetsApp(或 MaterialApp)会为您创建一个Localizations 部件,其中包含您指定的委托。设备的当前语言环境始终可以通过当前上下文中的 Localizations 部件(以 Locale 对象的形式)或使用Window.locale 来访问。

要访问本地化资源,请使用 Localizations.of() 方法访问由给定委托提供的特定本地化类。使用intl_translation 包将可翻译文本提取到arb 文件中以进行翻译,并将它们导入回应用程序以与 intl 一起使用。

有关 Flutter 中国际化和本地化的更多详细信息,请参阅国际化指南,其中包含使用和不使用 intl 包的示例代码。

视图控制器

#

本节文档讨论 Flutter 中视图控制器的等效项以及如何监听生命周期事件。

Flutter 中视图控制器的等效项

#

在 UIKit 中,ViewController 代表用户界面的一部分,最常用于屏幕或部分。这些部件组合在一起以构建复杂的用户界面,并有助于扩展应用程序的 UI。在 Flutter 中,这项工作由部件承担。如导航部分所述,Flutter 中的屏幕由部件表示,因为“一切都是部件!”使用 Navigator 在表示不同屏幕或页面的不同 Route 之间移动,或者可能是同一数据的不同状态或呈现。

监听生命周期事件

#

在 UIKit 中,您可以覆盖 ViewController 的方法来捕获视图本身的生命周期方法,或者在 AppDelegate 中注册生命周期回调。在 Flutter 中,您既没有这个概念,而是可以通过挂接到 WidgetsBinding 观察器并监听 didChangeAppLifecycleState() 更改事件来监听生命周期事件。

可观察到的生命周期事件是:

inactive :应用程序处于非活动状态,并且没有接收用户输入。此事件仅在 iOS 上有效,因为 Android 上没有等效事件。

paused :应用程序当前对用户不可见,不响应用户输入,但在后台运行。

resumed :应用程序可见并响应用户输入。

suspending :应用程序暂时挂起。iOS 平台没有等效事件。

有关这些状态含义的更多详细信息,请参阅AppLifecycleState 文档

布局

#

本节讨论 Flutter 中的不同布局以及它们与 UIKit 的比较。

显示列表视图

#

在 UIKit 中,您可能会在 UITableViewUICollectionView 中显示列表。在 Flutter 中,您可以使用 ListView 实现类似的功能。在 UIKit 中,这些视图具有委托方法,用于确定行数、每个索引路径的单元格以及单元格的大小。

由于 Flutter 的不可变部件模式,您将部件列表传递给您的 ListView,Flutter 会确保滚动快速且流畅。

dart
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 中,使用传入部件提供的触摸处理。

dart
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,并将数据从旧列表复制到新列表。虽然这种方法很简单,但不推荐用于大型数据集,如接下来的示例所示。

dart
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。当您拥有动态列表或包含大量数据的列表时,此方法非常适用。

dart
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,因为您可以以垂直格式排列部件。

dart
@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 中,有两种添加触摸监听器的方法:

  1. 如果部件支持事件检测,则向其传递一个函数,并在函数中处理事件。例如,ElevatedButton 部件具有 onPressed 参数:
dart
@override
Widget build(BuildContext context) {
 return ElevatedButton(
   onPressed: () {
     developer.log('click');
   },
   child: const Text('Button'),
 );
}
  1. 如果部件不支持事件检测,请将部件包装在 GestureDetector 中,并将函数传递给 onTap 参数。
dart
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 徽标:

dart
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 部件。例如,在下面的代码中,从种子生成的配色方案设置为深紫色,分隔符颜色为灰色。

dart
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 文件中引用它,这与您导入图像的方式类似。

yaml
fonts:
  - family: MyCustomFont
    fonts:
      - asset: fonts/MyCustomFont.ttf
      - style: italic

然后将字体分配给您的 Text 部件:

dart
@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 对象,您可以在其中自定义许多参数,例如:

  • color
  • decoration
  • decorationColor
  • decorationStyle
  • fontFamily
  • fontSize
  • fontStyle
  • fontWeight
  • hashCode
  • height
  • inherit
  • letterSpacing
  • textBaseline
  • wordSpacing

将图像打包到应用程序中

#

虽然 iOS 将图像和资产视为不同的项目,但 Flutter 应用程序只有资产。在 iOS 上位于 Images.xcasset 文件夹中的资源位于 Flutter 的 assets 文件夹中。与 iOS 一样,资产可以是任何类型的文件,而不仅仅是图像。例如,您可能在 my-assets 文件夹中有一个 JSON 文件:

my-assets/data.json

pubspec.yaml 文件中声明资产:

yaml
assets:
 - my-assets/data.json

然后使用AssetBundle 从代码中访问它:

dart
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.0x2.0x3.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 文件中声明这些图像:

yaml
assets:
 - images/my_icon.png

您现在可以使用 AssetImage 访问您的图像:

dart
image: AssetImage('images/a_dot_burr.jpeg'),

或直接在 Image 部件中:

dart
@override
Widget build(BuildContext context) {
  return Image.asset('images/my_image.png');
}

更多详细信息,请参阅在 Flutter 中添加资产和图像

表单输入

#

本节讨论如何在 Flutter 中使用表单以及它们与 UIKit 的比较。

获取用户输入

#

鉴于 Flutter 如何使用具有单独状态的不可变部件,您可能想知道用户输入是如何融入其中的。在 UIKit 中,您通常会在需要提交用户输入或对其进行操作时查询部件的当前值。Flutter 中是如何工作的?

实际上,表单的处理方式(就像 Flutter 中的所有内容一样)是通过专用部件来处理的。如果您有 TextFieldTextFormField,您可以提供TextEditingController 来检索用户输入:

dart
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 对象,轻松显示字段的“提示”或占位符文本:

dart
Center(
  child: TextField(
    decoration: InputDecoration(hintText: 'This is a hint'),
  ),
)

显示验证错误

#

就像使用“提示”一样,将 InputDecoration 对象传递给 Text 部件的 decoration 构造函数。

但是,您不希望一开始就显示错误。相反,当用户输入无效数据时,更新状态并传递一个新的 InputDecoration 对象。

dart
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 并让 Dart 执行繁重的工作,从而在不导致 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?>>();
  });
}

一旦 awaited 网络调用完成,通过调用 setState() 来更新 UI,这将触发部件子树的重建并更新数据。

以下示例异步加载数据并在 ListView 中显示它:

dart
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 长时间运行的任务:

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?>>();
  });
}

这就是您通常执行网络或数据库调用的方式,它们都是 I/O 操作。

但是,有时您可能正在处理大量数据,并且您的 UI 冻结。在 Flutter 中,使用 Isolate 来利用多个 CPU 内核来执行长时间运行或计算密集型任务。

Isolate 是单独的执行线程,它们不与主执行内存堆共享任何内存。这意味着您无法访问主线程中的变量,也无法通过调用 setState() 来更新 UI。Isolate 名副其实,不能共享内存(例如,静态字段的形式)。

以下示例在一个简单的 Isolate 中展示了如何将数据共享回主线程以更新 UI。

dart
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),或执行计算密集型数学运算,例如加密或信号处理。

您可以运行下面的完整示例:

dart
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

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?>>();
  });
}

显示长时间运行任务的进度

#

在 UIKit 中,您通常会在后台执行长时间运行的任务时使用 UIProgressView

在 Flutter 中,使用 ProgressIndicator 部件。通过控制何时通过布尔标志呈现进度来以编程方式显示进度。在长时间运行的任务开始之前告诉 Flutter 更新其状态,并在任务结束后隐藏它。

在下面的示例中,构建函数被分成三个不同的函数。如果 showLoadingDialogtrue(当 widgets.length == 0 时),则呈现 ProgressIndicator。否则,使用从网络调用返回的数据呈现 ListView

dart
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(),
    );
  }
}
[`Window.locale`]: https://api.flutter.dev/flutter/dart-ui/Window/locale.html
[作为 Swift 开发人员学习 Dart]: https://dart.dev/guides/language/coming-from/swift-to-dart

(以上内容已翻译为中文,与英文原文一致,无需其他翻译。)