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

Flutter 是一个使用 Dart 编程语言构建跨平台应用的框架。要了解使用 Dart 编程与使用 Swift 编程之间的一些差异,请参阅Swift 开发者学习 Dart面向 Swift 开发者的 Flutter 并发

在用 Flutter 构建应用时,您的 iOS 和 UIKit 知识及经验非常有价值。Flutter 在 iOS 上运行时也会对应用行为进行一些调整。要了解如何调整,请参阅平台适配

将本指南用作一本“烹饪书”。您可以跳过部分内容,直接查找解决您最相关需求的问题。

概述

#

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

在新标签页中观看 YouTube 视频:“Flutter for iOS developers”

视图与 Widgets

#

在 UIKit 中,您在 UI 中创建的大部分内容都是通过视图对象完成的,这些视图对象是 UIView 类的实例。它们可以作为其他 UIView 类的容器,从而形成您的布局。

在 Flutter 中,与 UIView 大致等效的是 Widget。Widgets 不完全等同于 iOS 视图,但当您熟悉 Flutter 的工作原理时,您可以将它们视为“您声明和构建 UI 的方式”。

但是,它们与 UIView 有一些不同之处。首先,Widgets 的生命周期不同:它们是不可变的,并且只有在需要更改时才存在。每当 Widgets 或其状态发生变化时,Flutter 框架都会创建一个新的 Widget 实例树。相比之下,UIKit 视图在更改时不会被重新创建,而是一个可变实体,它只绘制一次,并且在使用 setNeedsDisplay() 使其失效之前不会重新绘制。

此外,与 UIView 不同,Flutter 的 Widgets 是轻量级的,部分原因是它们的不可变性。因为它们本身不是视图,也不是直接绘制任何东西,而是一种对 UI 及其语义的描述,这些描述会在底层“膨胀”成实际的视图对象。

Flutter 包含 Material 组件库。这些是实现 Material Design 指南的 Widgets。Material Design 是一个灵活的设计系统,针对所有平台进行了优化,包括 iOS。

但是 Flutter 足够灵活和富有表现力,可以实现任何设计语言。在 iOS 上,您可以使用 Cupertino Widgets 库来生成类似于 Apple 的 iOS 设计语言的界面。

更新 Widgets

#

要在 UIKit 中更新视图,您需要直接修改它们。在 Flutter 中,Widgets 是不可变的,不能直接更新。相反,您必须操纵 Widget 的状态。

这就是有状态 (Stateful) 与无状态 (Stateless) Widgets 概念的由来。一个 StatelessWidget 顾名思义——一个没有附加状态的 Widget。

当您描述的用户界面部分不依赖于 Widget 中的初始配置信息以外的任何内容时,StatelessWidgets 会很有用。

例如,在 UIKit 中,这类似于放置一个以您的徽标作为 imageUIImageView。如果徽标在运行时不会更改,请在 Flutter 中使用 StatelessWidget

如果您想根据 HTTP 调用后接收到的数据动态更改 UI,请使用 StatefulWidget。HTTP 调用完成后,通知 Flutter 框架 Widget 的 State 已更新,以便它可以更新 UI。

无状态和有状态 Widgets 之间的重要区别在于,StatefulWidget 具有一个 State 对象,该对象存储状态数据并将其跨树重建传递,因此不会丢失。

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

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

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

如果您查看上面的代码,您可能会注意到 Text Widget 没有携带任何显式状态。它只渲染其构造函数中传递的内容,别无其他。

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

为了实现这一点,将 Text Widget 包装在一个 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),
      ),
    );
  }
}

Widget 布局

#

在 UIKit 中,您可以使用 Storyboard 文件来组织视图并设置约束,或者您可以在视图控制器中以编程方式设置约束。在 Flutter 中,通过组合 Widget 树来在代码中声明您的布局。

以下示例展示了如何显示一个带填充的简单 Widget

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

您可以为任何 Widget 添加填充,这模仿了 iOS 中约束的功能。

您可以在Widget 目录中查看 Flutter 提供的布局。

移除 Widgets

#

在 UIKit 中,您在父视图上调用 addSubview() 或在子视图上调用 removeFromSuperview() 来动态添加或删除子视图。在 Flutter 中,由于 Widgets 是不可变的,所以没有与 addSubview() 直接等效的方法。相反,您可以向父级传递一个返回 Widget 的函数,并通过一个布尔标志控制该子级的创建。

以下示例展示了当用户点击 FloatingActionButton 时如何在两个 Widget 之间切换

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 中,使用动画库将 Widgets 包装在动画 Widget 中。

在 Flutter 中,使用 AnimationController,它是一个 Animation<double>,可以暂停、查找、停止和反转动画。它需要一个 Ticker,用于在 vsync 发生时发出信号,并在运行时在每帧之间生成 0 到 1 的线性插值。然后创建或多个 Animation 并将它们附加到控制器。

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

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

以下示例展示了如何编写一个 FadeTransition,当您按下 FloatingActionButton 时,该转换会将 Widget 淡入为徽标

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

有关更多信息,请参阅动画与动作 Widgets动画教程动画概述

屏幕绘制

#

在 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;
}

Widget 透明度

#

在 UIKit 中,所有内容都具有 .opacity.alpha。在 Flutter 中,大多数情况下您需要将 Widget 包装在 Opacity Widget 中才能实现此功能。

自定义 Widgets

#

在 UIKit 中,您通常会子类化 UIView,或使用现有的视图,来覆盖和实现达到所需行为的方法。在 Flutter 中,通过组合更小的 Widgets(而不是扩展它们)来构建自定义 Widget。

例如,如何构建一个在构造函数中接受标签的 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));
  }
}

然后像使用任何其他 Flutter Widget 一样使用 CustomButton

dart
@override
Widget build(BuildContext context) {
  return const Center(child: CustomButton('Hello'));
}

管理依赖项

#

在 iOS 中,您通过向 Podfile 添加内容来使用 CocoaPods 添加依赖项。Flutter 使用 Dart 的构建系统和 Pub 包管理器来处理依赖项。这些工具将原生 Android 和 iOS 包装应用的构建委托给各自的构建系统。

虽然您的 Flutter 项目的 iOS 文件夹中有一个 Podfile,但仅当您添加每个平台集成所需的原生依赖项时才使用它。通常,在 Flutter 中使用 pubspec.yaml 来声明外部依赖项。一个寻找优秀 Flutter 包的好地方是 pub.dev

#

本文档的这一部分讨论了应用页面之间的导航、push 和 pop 机制等。

#

在 UIKit 中,要在视图控制器之间跳转,您可以使用 UINavigationController 来管理要显示的视图控制器堆栈。

Flutter 有一个类似的实现,使用 NavigatorRoutes。一个 Route 是应用“屏幕”或“页面”的抽象,而 Navigator 是一个管理路由的Widget。一个路由大致对应一个 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 scheme。对于系统级应用,scheme 取决于应用。要在 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 包,请在应用 Widget 上指定 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,因此它同时具有用于基本 Widgets 本地化值的 GlobalWidgetsLocalizations 和用于 Material Widgets 本地化的 MaterialWidgetsLocalizations。如果您的应用使用 WidgetsApp,则不需要后者。请注意,这两个委托包含“默认”值,但如果您也希望本地化自己的应用的可本地化副本,则需要提供一个或多个委托。

初始化时,WidgetsApp(或 MaterialApp)会为您创建一个 Localizations Widget,并带有您指定的委托。设备的当前语言环境始终可以从当前上下文中的 Localizations Widget(以 Locale 对象的形式)或使用 Window.locale 访问。

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

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

ViewControllers

#

本文档的这一部分讨论了 Flutter 中 ViewController 的等效概念以及如何监听生命周期事件。

Flutter 中 ViewController 的等效概念

#

在 UIKit 中,一个 ViewController 代表用户界面的一部分,最常用于屏幕或部分。它们组合在一起以构建复杂的用户界面,并帮助扩展您的应用的 UI。在 Flutter 中,这项工作由 Widgets 承担。正如导航部分所提到的,Flutter 中的屏幕由 Widgets 表示,因为“一切都是 Widget!” 使用 Navigator 在不同的 Route 之间移动,这些路由代表不同的屏幕或页面,或者可能是相同数据的不同状态或渲染。

监听生命周期事件

#

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

可观察的生命周期事件有

inactive
应用程序处于非活动状态,并且未接收用户输入。此事件仅适用于 iOS,因为 Android 上没有等效事件。
paused
应用程序当前对用户不可见,不响应用户输入,但正在后台运行。
resumed
应用程序可见并响应用户输入。
suspending
应用程序暂时挂起。iOS 平台没有等效事件。

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

布局

#

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

显示列表视图

#

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

由于 Flutter 的不可变 Widget 模式,您可以将 Widget 列表传递给您的 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 中,使用传入的 Widgets 提供的触摸处理。

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() 内部更新 Widget 列表,您会很快发现您的数据在视觉上没有变化。这是因为当调用 setState() 时,Flutter 渲染引擎会查看 Widget 树以查看是否有任何更改。当它到达您的 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 Widget。它既可以作为 ScrollView,也可以作为 iOS TableView,因为您可以以垂直格式布局 Widgets。

dart
@override
Widget build(BuildContext context) {
  return ListView(
    children: const <Widget>[
      Text('Row One'),
      Text('Row Two'),
      Text('Row Three'),
      Text('Row Four'),
    ],
  );
}

有关如何在 Flutter 中布局 Widgets 的更详细文档,请参阅布局教程

手势检测和触控事件处理

#

本节讨论如何在 Flutter 中检测手势和处理不同事件,以及它们与 UIKit 的比较。

添加点击监听器

#

在 UIKit 中,您将 GestureRecognizer 附加到视图以处理点击事件。在 Flutter 中,有两种添加触摸监听器的方法

  1. 如果 Widget 支持事件检测,则向其传递一个函数,并在该函数中处理事件。例如,ElevatedButton Widget 有一个 onPressed 参数
dart
@override
Widget build(BuildContext context) {
 return ElevatedButton(
   onPressed: () {
     developer.log('click');
   },
   child: const Text('Button'),
 );
}
  1. 如果 Widget 不支持事件检测,则将 Widget 包装在 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 组件,请将顶层 Widget MaterialApp 声明为您的应用程序的入口点。MaterialApp 是一个便利 Widget,它包装了许多实现 Material Design 的应用程序通常需要的 Widget。它在 WidgetsApp 的基础上添加了 Material 特定的功能。

但是 Flutter 足够灵活和富有表现力,可以实现任何设计语言。在 iOS 上,您可以使用 Cupertino 库来生成符合 人机界面指南的界面。有关这些 Widgets 的完整集合,请参阅 Cupertino Widgets 画廊。

您还可以使用 WidgetsApp 作为您的应用 Widget,它提供了一些相同的功能,但不如 MaterialApp 丰富。

要自定义任何子组件的颜色和样式,请将 ThemeData 对象传递给 MaterialApp Widget。例如,在下面的代码中,种子颜色方案设置为深紫色,分隔线颜色设置为灰色。

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 Widget

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 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 文件中声明资产

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.png'),

或直接在 Image Widget 中

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

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

表单输入

#

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

获取用户输入

#

考虑到 Flutter 如何使用具有独立状态的不可变 Widgets,您可能想知道用户输入如何融入其中。在 UIKit 中,当需要提交用户输入或对其执行操作时,您通常会查询 Widgets 以获取其当前值。这在 Flutter 中是如何工作的?

实际上,表单在 Flutter 中由专门的 Widgets 处理,就像 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 Widget 的 decoration 构造函数参数添加一个 InputDecoration 对象来轻松显示字段的“提示”或占位符文本

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

显示验证错误

#

就像处理“提示”一样,将 InputDecoration 对象传递给 Text Widget 的 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 运行网络代码,而不会导致 UI 卡顿,让 Dart 完成繁重的工作

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,这将触发 Widget 子树的重建并更新数据。

以下示例异步加载数据并将其显示在 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(),
    );
  }
}

发起网络请求

#

在 Flutter 中进行网络调用很容易,当您使用流行的 http时。这抽象了许多您通常会自行实现的网络功能,使网络调用变得简单。

要将 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 Widget。通过布尔标志控制何时渲染它来以编程方式显示进度。在长时间运行的任务开始之前通知 Flutter 更新其状态,并在任务结束后将其隐藏。

在下面的示例中,build 函数被分为三个不同的函数。如果 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(),
    );
  }
}