Skip to main content
Contents

适用于 Xamarin.Forms 开发人员的 Flutter

Contents

本文档面向希望利用现有知识使用 Flutter 构建移动应用的 Xamarin.Forms 开发人员。如果您了解 Xamarin.Forms 框架的基础知识,则可以使用本文档作为 Flutter 开发的入门指南。

您在 Android 和 iOS 上的知识和技能在使用 Flutter 构建时非常宝贵,因为 Flutter 依赖于本机操作系统配置,类似于您配置本机 Xamarin.Forms 项目的方式。Flutter 框架也类似于您创建可在多个平台上使用的单个 UI 的方式。

您可以将本文档用作参考手册,随意跳转并查找最符合您需求的问题。

项目设置

#

应用如何启动?

#

在 Xamarin.Forms 的每个平台中,您都调用 LoadApplication 方法,该方法创建一个新应用程序并启动您的应用。

csharp
LoadApplication(new App());

在 Flutter 中,默认的主要入口点是 main,您可以在其中加载 Flutter 应用。

dart
void main() {
  runApp(const MyApp());
}

在 Xamarin.Forms 中,您将 Page 分配给 Application 类中的 MainPage 属性。

csharp
public class App : Application
{
    public App()
    {
        MainPage = new ContentPage
        {
            Content = new Label
            {
                Text = "Hello World",
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.Center
            }
        };
    }
}

在 Flutter 中,“一切都是小部件”,甚至应用程序本身也是如此。以下示例显示了一个简单的应用程序小部件 MyApp

dart
class MyApp extends StatelessWidget {
  /// 此小部件是应用程序的根。
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const Center(
      child: Text(
        'Hello World!',
        textDirection: TextDirection.ltr,
      ),
    );
  }
}

如何创建一个页面?

#

Xamarin.Forms 具有许多类型的页面;ContentPage 最常见。在 Flutter 中,您指定一个持有根页面的应用程序小部件。您可以使用支持Material DesignMaterialApp 小部件,或者您可以使用支持 iOS 风格应用程序的 CupertinoApp 小部件,或者您可以使用较低级别的 WidgetsApp 小部件,您可以根据需要自定义它。

以下代码定义了主页,一个有状态的小部件。在 Flutter 中,所有小部件都是不可变的,但支持两种类型的小部件:有状态无状态。无状态小部件的示例包括标题、图标或图像。

以下示例使用 MaterialApp,它在其 home 属性中持有其根页面。

dart
class MyApp extends StatelessWidget {
  /// 此小部件是应用程序的根。
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

从此处开始,您的实际第一个页面是另一个 Widget,您可以在其中创建您的状态。

一个有状态的小部件,例如下面的 MyHomePage,由两部分组成。第一部分本身是不可变的,它创建一个 State 对象,该对象保存对象的状。State 对象在小部件的生命周期中持续存在。

dart
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

State 对象为有状态小部件实现 build() 方法。

当小部件树的状态发生变化时,调用 setState(),这会触发对 UI 的该部分的构建。请确保仅在必要时且仅对已更改的小部件树部分调用 setState(),否则可能会导致 UI 性能下降。

dart
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        // 从 App.build 方法创建的 MyHomePage 对象中获取值,并使用它来设置 appbar 标题。
        title: Text(widget.title),
      ),
      body: Center(
        // Center 是一个布局小部件。它接受一个子项并将其放置在父项的中间。
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              '您已经按下按钮这么多次:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

在 Flutter 中,UI(也称为小部件树)是不可变的,这意味着一旦构建,您就无法更改其状态。您更改 State 类中的字段,然后调用 setState() 以再次重建整个小部件树。

这种生成 UI 的方式与 Xamarin.Forms 不同,但这方法有很多好处。

视图

#

Flutter 中页面或元素的等效项是什么?

#

ContentPageTabbedPageFlyoutPage 是您可能在 Xamarin.Forms 应用程序中使用的各种页面类型。然后,这些页面将保存 Element 以显示各种控件。在 Xamarin.Forms 中,EntryButtonElement 的示例。

在 Flutter 中,几乎所有东西都是小部件。Flutter 中称为 RoutePage 是一个小部件。按钮、进度条和动画控制器都是小部件。构建路由时,您会创建一个小部件树。

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

但是 Flutter 足够灵活和富有表现力,可以实现任何设计语言。例如,在 iOS 上,您可以使用Cupertino 小部件 来制作看起来像Apple 的 iOS 设计语言 的界面。

如何更新小部件?

#

在 Xamarin.Forms 中,每个 PageElement 都是一个有状态的类,它具有属性和方法。您可以通过更新属性来更新 Element,这会传播到本机控件。

在 Flutter 中,Widget 是不可变的,您不能通过更改属性来直接更新它们,而必须使用小部件的状态。

这就是有状态小部件与无状态小部件的概念来源。StatelessWidget 正如其名称所示——一个没有状态信息的小部件。

当您描述的用户界面部分不依赖于对象中的配置信息以外的任何内容时,StatelessWidgets 非常有用。

例如,在 Xamarin.Forms 中,这类似于放置带有徽标的 Image。徽标在运行时不会更改,因此在 Flutter 中使用 StatelessWidget

如果您想根据发出 HTTP 调用或用户交互后接收到的数据动态更改 UI,那么您必须使用 StatefulWidget 并告诉 Flutter 框架小部件的 State 已更新,以便它可以更新该小部件。

这里需要注意的重要一点是,从核心上讲,无状态和小部件的行为相同。它们每帧重建一次,区别在于 StatefulWidget 有一个 State 对象,该对象跨帧存储状态数据并恢复它。

如果您有疑问,请始终记住此规则:如果小部件发生更改(例如,由于用户交互),则它是可变的。但是,如果小部件对更改做出反应,则包含它的父小部件如果它本身不对更改做出反应,则仍然可以是无状态的。

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

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

如您所见,Text 小部件没有与其关联的状态信息,它呈现其构造函数中传入的内容,仅此而已。

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

为此,请将 Text 小部件包装在 StatefulWidget 中,并在用户单击按钮时更新它,如下例所示:

dart
import 'package:flutter/material.dart';

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> {
  /// 默认占位符文本
  String textToShow = 'I Like Flutter';

  void _updateText() {
    setState(() {
      // 更新文本
      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),
      ),
    );
  }
}

如何布置我的小部件?XAML 文件的等效项是什么?

#

在 Xamarin.Forms 中,大多数开发人员使用 XAML 编写布局,有时也使用 C#。在 Flutter 中,您使用代码中的小部件树编写布局。

以下示例显示如何使用填充显示简单小部件:

dart
@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: const Text('Sample App')),
    body: Center(
      child: ElevatedButton(
        style: ElevatedButton.styleFrom(
          padding: const EdgeInsets.only(left: 20, right: 30),
        ),
        onPressed: () {},
        child: const Text('Hello'),
      ),
    ),
  );
}

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

如何在我的布局中添加或删除元素?

#

在 Xamarin.Forms 中,您必须在代码中删除或添加 Element。 这涉及设置 Content 属性,或者如果是列表,则调用 Add()Remove()

在 Flutter 中,因为小部件是不可变的,所以没有直接的等效项。相反,您可以向父级传递一个返回小部件的函数,并使用布尔标志控制子级的创建。

以下示例显示了当用户单击 FloatingActionButton 时如何在两个小部件之间切换:

dart
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> {
  /// 切换的默认值
  bool toggle = true;
  void _toggle() {
    setState(() {
      toggle = !toggle;
    });
  }

  Widget _getToggleChild() {
    if (toggle) {
      return const Text('切换一');
    }
    return CupertinoButton(
      onPressed: () {},
      child: const Text('切换二'),
    );
  }

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

如何动画化小部件?

#

在 Xamarin.Forms 中,您可以使用包含诸如 FadeToTranslateTo 之类方法的 ViewExtensions 创建简单的动画。您将在视图上使用这些方法来执行所需的动画。

xml
<Image Source="{Binding MyImage}" x:Name="myImage" />

然后在代码隐藏或行为中,这将在 1 秒内淡入图像。

csharp
myImage.FadeTo(0, 1000);

在 Flutter 中,您可以通过将小部件包装在动画小部件内来使用动画库动画化小部件。使用 AnimationController,这是一个可以暂停、搜索、停止和反转动画的 Animation<double>。它需要一个 Ticker 来指示 vsync 何时发生,并在运行的每一帧上生成 0 到 1 之间的线性插值。然后,您创建一个或多个 Animation 并将其附加到控制器。

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

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

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

dart
import 'package:flutter/material.dart';

void main() {
  runApp(const FadeAppTest());
}

class FadeAppTest extends StatelessWidget {
  /// 此小部件是应用程序的根。
  const FadeAppTest({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 TickerProviderStateMixin {
  late final AnimationController controller;
  late final CurvedAnimation curve;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      duration: const Duration(milliseconds: 2000),
      vsync: this,
    );
    curve = CurvedAnimation(
      parent: controller,
      curve: Curves.easeIn,
    );
  }

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

有关更多信息,请参阅动画和动态小部件动画教程动画概述

如何在屏幕上绘图/绘画?

#

Xamarin.Forms 从未有过直接在屏幕上绘图的内置方法。如果需要绘制自定义图像,许多人会使用 SkiaSharp。在 Flutter 中,您可以直接访问 Skia Canvas 并轻松地在屏幕上绘图。

Flutter 有两个类可以帮助您在画布上绘图:CustomPaintCustomPainter,后者实现了您的算法以在画布上绘图。

要了解如何在 Flutter 中实现签名画家,请参阅 Collin 在自定义绘图 上的答案。

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
  SignatureState createState() => SignatureState();
}

class SignatureState extends State<Signature> {
  List<Offset?> _points = <Offset?>[];

  void _onPanUpdate(DragUpdateDetails details) {
    setState(() {
      final RenderBox referenceBox = context.findRenderObject() as RenderBox;
      final Offset localPosition = referenceBox.globalToLocal(
        details.globalPosition,
      );
      _points = List.from(_points)..add(localPosition);
    });
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanUpdate: _onPanUpdate,
      onPanEnd: (details) => _points.add(null),
      child: CustomPaint(
        painter: SignaturePainter(_points),
        size: Size.infinite,
      ),
    );
  }
}

class SignaturePainter extends CustomPainter {
  const 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;
}

小部件的不透明度在哪里?

#

在 Xamarin.Forms 中,所有 VisualElement 都具有不透明度。在 Flutter 中,您需要将小部件包装在Opacity 小部件 中才能实现此目的。

如何构建自定义小部件?

#

在 Xamarin.Forms 中,您通常会子类化 VisualElement 或使用预先存在的 VisualElement 来覆盖和实现实现所需行为的方法。

在 Flutter 中,通过组合较小的部件(而不是扩展它们)来构建自定义部件。这有点类似于基于添加了许多 VisualElementGrid 来实现自定义控件,同时扩展自定义逻辑。

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

导航

#

如何在页面之间导航?

#

在 Xamarin.Forms 中,NavigationPage 类提供了一种分层导航体验,用户能够向前和向后浏览页面。

Flutter 有类似的实现,使用 NavigatorRoutesRoute 是应用程序页面的抽象,而 Navigator 是管理路由的小部件

路由大致映射到一个 Page。导航器的工作方式类似于 Xamarin.Forms NavigationPage,因为它可以根据您是想导航到视图还是从视图返回来 push()pop() 路由。

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

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

以下示例构建了一个 Map

dart
void main() {
  runApp(
    MaterialApp(
      home: const MyAppHome(), // 成为名为 '/' 的路由
      routes: <String, WidgetBuilder>{
        '/a': (context) => const MyPage(title: 'page A'),
        '/b': (context) => const MyPage(title: 'page B'),
        '/c': (context) => const MyPage(title: 'page C'),
      },
    ),
  );
}

通过将路由名称推送到 Navigator 来导航到路由。

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

Navigator 是一个管理应用程序路由的堆栈。将路由推送到堆栈会移动到该路由。从堆栈中弹出路由会返回到上一个路由。这是通过等待 push() 返回的 Future 来完成的。

async/await 与 .NET 实现非常相似,并在异步 UI 中进行了更详细的解释。

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

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

然后,在您的“位置”路由中,一旦用户选择了他们的位置,就使用结果弹出堆栈:

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

如何导航到另一个应用程序?

#

在 Xamarin.Forms 中,要将用户发送到另一个应用程序,您使用特定的 URI 方案,使用 Device.OpenUrl("mailto://")

要在 Flutter 中实现此功能,请创建本机平台集成,或使用现有插件,例如 url_launcher,该插件可在pub.dev 上的其他许多包中使用。

异步 UI

#

Flutter 中 Device.BeginOnMainThread() 的等效项是什么?

#

Dart 具有单线程执行模型,支持 Isolate(一种在另一个线程上运行 Dart 代码的方法)、事件循环和异步编程。除非您生成 Isolate,否则您的 Dart 代码在主 UI 线程中运行,并由事件循环驱动。

Dart 的单线程模型并不意味着您需要将所有内容都作为阻塞操作来运行,这会导致 UI 冻结。 就像 Xamarin.Forms 一样,您需要保持 UI 线程空闲。您可以使用 async/await 来执行任务,您必须等待响应。

在 Flutter 中,使用 Dart 语言提供的异步功能(也称为 async/await)来执行异步工作。这与 C# 非常相似,对于任何 Xamarin.Forms 开发人员来说都应该很容易使用。

例如,您可以使用 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?>>();
  });
}

一旦等待的网络调用完成,通过调用 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 与 Android 的区别,请参阅下一节。

如何将工作移至后台线程?

#

由于 Flutter 是单线程的并运行事件循环,因此您不必担心线程管理或生成后台线程。这与 Xamarin.Forms 非常相似。如果您正在执行 I/O 绑定工作,例如磁盘访问或网络调用,那么您可以安全地使用 async/await,一切就绪了。

另一方面,如果您需要执行使 CPU 保持繁忙的计算密集型工作,则需要将其移动到 Isolate 以避免阻塞事件循环,就像您要将任何类型的工作都从主线程中移开一样。这类似于您在 Xamarin.Forms 中通过 Task.Run() 将内容移动到另一个线程的情况。

对于 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 是单独的执行线程,它们与主执行内存堆不共享任何内存。这是与 Task.Run() 的区别。这意味着您无法访问主线程中的变量,也无法通过调用 setState() 来更新 UI。

以下示例在一个简单的 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() {
    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 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: getBody(),
    );
  }
}

如何发出网络请求?

#

在 Xamarin.Forms 中,您将使用 HttpClient。当您使用流行的http 时,在 Flutter 中发出网络请求很容易。这抽象化了您通常自己实现的大量网络功能,从而简化了发出网络调用的过程。

要使用 http 包,请将其添加到 pubspec.yaml 中的依赖项中:

yaml
dependencies:
  http: ^1.1.0

要发出网络请求,请在 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?>>();
  });
}

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

#

在 Xamarin.Forms 中,您通常会创建一个加载指示器,可以直接在 XAML 中创建,也可以通过 AcrDialogs 等第三方插件创建。

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

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

dart
import 'dart:async';
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 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: getBody(),
    );
  }
}

项目结构和资源

#

我在哪里存储图像文件?

#

Xamarin.Forms 没有与平台无关的图像存储方式,您必须将图像放在 iOS 的 xcasset 文件夹中,或在 Android 的各个 drawable 文件夹中。

虽然 Android 和 iOS 将资源和资产视为不同的项目,但 Flutter 应用程序只有资产。所有位于 Android 上的 Resources/drawable-* 文件夹中的资源都放置在 Flutter 的 assets 文件夹中。

Flutter 遵循与 iOS 类似的简单基于密度的格式。资产可能是 1.0x2.0x3.0x 或任何其他倍数。Flutter 没有 dp,但有逻辑像素,它们基本上与设备无关像素相同。Flutter 的devicePixelRatio 表示单个逻辑像素中物理像素的比率。

Android 密度桶的等效项为:

Android 密度限定符Flutter 像素比率
ldpi0.75x
mdpi1.0x
hdpi1.5x
xhdpi2.0x
xxhdpi3.0x
xxxhdpi4.0x

资产位于任意文件夹中——Flutter 没有预定义的文件夹结构。您在 pubspec.yaml 文件中声明资产(及其位置),Flutter 会自动获取它们。

例如,要向 Flutter 项目添加一个名为 my_icon.png 的新图像资产,并决定将其放在我们任意命名的 images 文件夹中,您将把基本图像 (1.0x) 放入 images 文件夹中,并将所有其他变体放入名为相应比率倍数的子文件夹中:

images/my_icon.png       // 基本图像:1.0x
images/2.0x/my_icon.png  // 2.0x 图像
images/3.0x/my_icon.png  // 3.0x 图像

接下来,您需要在 pubspec.yaml 文件中声明这些图像:

yaml
assets:
 - images/my_icon.jpeg

您可以直接在 Image.asset 小部件中访问您的图像:

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

或使用 AssetImage

dart
@override
Widget build(BuildContext context) {
  return const Image(
    image: AssetImage('images/my_image.png'),
  );
}

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

我在哪里存储字符串?如何处理本地化?

#

与具有 resx 文件的 .NET 不同,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 # 使用 flutter_localizations 中的 intl 版本。

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

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>>[
        // 在此处添加特定于应用程序的本地化委托
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
      ],
      supportedLocales: <Locale>[
        Locale('en', 'US'), // 英语
        Locale('he', 'IL'), // 希伯来语
        // ...应用程序支持的其他语言环境
      ],
    );
  }
}

委托包含实际的本地化值,而 supportedLocales 定义应用程序支持的语言环境。上面的示例使用 MaterialApp,因此它既有用于基本小部件本地化值的 GlobalWidgetsLocalizations,也有用于 Material 小部件本地化的 MaterialWidgetsLocalizations。如果您为您的应用程序使用 WidgetsApp,则不需要后者。请注意,这两个委托包含“默认”值,但是如果您希望这些值也被本地化,则需要为自己的应用程序的可本地化副本提供一个或多个委托。

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

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

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

项目文件在哪里?

#

在 Xamarin.Forms 中,您将拥有一个 csproj 文件。Flutter 中最接近的等效项是 pubspec.yaml,其中包含包依赖项和各种项目详细信息。与 .NET Standard 类似,同一目录内的文件都被视为项目的一部分。

Nuget 的等效项是什么?如何添加依赖项?

#

在 .NET 生态系统中,本机 Xamarin 项目和 Xamarin.Forms 项目可以访问 Nuget 和内置的包管理系统。Flutter 应用程序包含本机 Android 应用程序、本机 iOS 应用程序和 Flutter 应用程序。

在 Android 中,您可以通过添加到 Gradle 构建脚本中来添加依赖项。在 iOS 中,您可以通过添加到 Podfile 中来添加依赖项。

Flutter 使用 Dart 自己的构建系统和 Pub 包管理器。这些工具将本机 Android 和 iOS 包装应用程序的构建委托给各自的构建系统。

通常,使用 pubspec.yaml 来声明要在 Flutter 中使用的外部依赖项。查找 Flutter 包的好地方是pub.dev

应用程序生命周期

#

如何监听应用程序生命周期事件?

#

在 Xamarin.Forms 中,您有一个包含 OnStartOnResumeOnSleepApplication。在 Flutter 中,您可以通过连接到 WidgetsBinding 观察器并监听 didChangeAppLifecycleState() 更改事件来监听类似的生命周期事件。

可观察的生命周期事件包括:

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

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

布局

#

StackLayout 的等效项是什么?

#

在 Xamarin.Forms 中,您可以创建一个 StackLayout,其 Orientation 为水平或垂直。Flutter 采用了类似的方法,但是您将使用 RowColumn 小部件。

如果您注意到这两个代码示例除了 RowColumn 小部件之外是相同的。子项相同,并且此功能可以用来开发随着时间的推移可以改变但子项保持不变的丰富布局。

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

Grid 的等效项是什么?

#

Grid 最接近的等效项是 GridView。这比您在 Xamarin.Forms 中习惯使用的功能强大得多。当内容超过其可见空间时,GridView 会自动滚动。

dart
@override
Widget build(BuildContext context) {
  return GridView.count(
    // 创建一个包含 2 列的网格。如果您将 scrollDirection 更改为水平方向,则会产生 2 行。
    crossAxisCount: 2,
    // 生成 100 个小部件,这些小部件显示它们在列表中的索引。
    children: List<Widget>.generate(
      100,
      (index) {
        return Center(
          child: Text(
            'Item $index',
            style: Theme.of(context).textTheme.headlineMedium,
          ),
        );
      },
    ),
  );
}

您可能在 Xamarin.Forms 中使用过 Grid 来实现覆盖其他小部件的小部件。在 Flutter 中,您可以使用 Stack 小部件来实现此目的。

此示例创建了两个相互重叠的图标。

dart
@override
Widget build(BuildContext context) {
  return const Stack(
    children: <Widget>[
      Icon(
        Icons.add_box,
        size: 24,
        color: Colors.black,
      ),
      Positioned(
        left: 10,
        child: Icon(
          Icons.add_circle,
          size: 24,
          color: Colors.black,
        ),
      ),
    ],
  );
}

ScrollView 的等效项是什么?

#

在 Xamarin.Forms 中,ScrollView 包裹在一个 VisualElement 周围,如果内容大于设备屏幕,则会滚动。

在 Flutter 中,最接近的匹配项是 SingleChildScrollView 小部件。您只需用要滚动的内容填充小部件即可。

dart
@override
Widget build(BuildContext context) {
  return const SingleChildScrollView(
    child: Text('Long Content'),
  );
}

如果您有很多项目想要包装在一个滚动视图中,即使是不同 Widget 类型的项目,您也可能想要使用 ListView。这看起来可能有点过头了,但在 Flutter 中,这比 Xamarin.Forms ListView 更加优化且效率更高,后者依赖于特定平台的控件。

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

如何在 Flutter 中处理横向转换?

#

可以通过在 AndroidManifest.xml 中设置 configChanges 属性来自动处理横向转换:

xml
<activity android:configChanges="orientation|screenSize" />

手势检测和触摸事件处理

#

如何在 Flutter 中向小部件添加 GestureRecognizers?

#

在 Xamarin.Forms 中,Element 可能包含可以附加到的点击事件。许多元素还包含与该事件绑定的 Command。或者,您可以使用 TapGestureRecognizer。在 Flutter 中有两种非常相似的方法:

  1. 如果小部件支持事件检测,则向其传递一个函数并在函数中处理它。例如,ElevatedButton 有一个 onPressed 参数:

    dart
    @override
    Widget build(BuildContext context) {
      return ElevatedButton(
        onPressed: () {
          developer.log('click');
        },
        child: const Text('Button'),
      );
    }
  2. 如果小部件不支持事件检测,则将小部件包装在 GestureDetector 中并将函数传递给 onTap 参数。

    dart
    class SampleApp extends StatelessWidget {
      const SampleApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Center(
            child: GestureDetector(
              onTap: () {
                developer.log('tap');
              },
              child: const FlutterLogo(size: 200),
            ),
          ),
        );
      }
    }

如何处理小部件上的其他手势?

#

在 Xamarin.Forms 中,您需要将 GestureRecognizer 添加到 View。除非您自己构建,否则通常仅限于 TapGestureRecognizerPinchGestureRecognizerPanGestureRecognizerSwipeGestureRecognizerDragGestureRecognizerDropGestureRecognizer

在 Flutter 中,使用 GestureDetector,您可以监听各种手势,例如:

  • 点击
onTapDown
可能导致点击的指针已在特定位置接触屏幕。
onTapUp
触发点击的指针已停止在特定位置接触屏幕。
onTap
发生了点击。
onTapCancel
之前触发 onTapDown 的指针不会导致点击。
  • 双击
onDoubleTap
用户在短时间内连续两次点击屏幕上的相同位置。
  • 长按
onLongPress
指针在同一位置与屏幕保持接触很长时间。
  • 垂直拖动
onVerticalDragStart
指针已接触屏幕,并且可能开始垂直移动。
onVerticalDragUpdate
与屏幕接触的指针已在垂直方向上进一步移动。
onVerticalDragEnd
之前与屏幕接触并垂直移动的指针不再与屏幕接触,并且在停止接触屏幕时以特定速度移动。
  • 水平拖动
onHorizontalDragStart
指针已接触屏幕,并且可能开始水平移动。
onHorizontalDragUpdate
与屏幕接触的指针已在水平方向上进一步移动。
onHorizontalDragEnd
之前与屏幕接触并水平移动的指针不再与屏幕接触,并且在停止接触屏幕时以特定速度移动。

以下示例显示了一个 GestureDetector,它在双击时旋转 Flutter 徽标:

dart
class RotatingFlutterDetector extends StatefulWidget {
  const RotatingFlutterDetector({super.key});

  @override
  State<RotatingFlutterDetector> createState() =>
      _RotatingFlutterDetectorState();
}

class _RotatingFlutterDetectorState extends State<RotatingFlutterDetector>
    with SingleTickerProviderStateMixin {
  late final AnimationController controller;
  late final CurvedAnimation curve;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      duration: const Duration(milliseconds: 2000),
      vsync: this,
    );
    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),
          ),
        ),
      ),
    );
  }
}

ListView 和适配器

#

Flutter 中 ListView 的等效项是什么?

#

Flutter 中 ListView 的等效项是……ListView

在 Xamarin.Forms ListView 中,您创建 ViewCell,可能还有 DataTemplateSelector,并将其传递给 ListViewListView 将使用 DataTemplateSelectorViewCell 返回的内容呈现每一行。但是,您通常必须确保启用单元格回收,否则您将遇到内存问题和滚动速度慢的问题。

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

dart
import 'package:flutter/material.dart';

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 StatelessWidget {
  const SampleAppPage({super.key});

  List<Widget> _getListData() {
    return List<Widget>.generate(
      100,
      (index) => Padding(
        padding: const EdgeInsets.all(10),
        child: Text('Row $index'),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Sample App')),
      body: ListView(children: _getListData()),
    );
  }
}

如何知道点击了哪个列表项?

#

在 Xamarin.Forms 中,ListView 有一个 ItemTapped 方法来找出点击了哪个项目。您可能还使用过许多其他技术,例如检查 SelectedItemEventToCommand 行为何时发生变化。

在 Flutter 中,使用传入小部件提供的触摸处理。

dart
import 'dart:developer' as developer;
import 'package:flutter/material.dart';

void main() {
  runApp(const SampleApp());
}

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> {
  List<Widget> _getListData() {
    return List<Widget>.generate(
      100,
      (index) => GestureDetector(
        onTap: () {
          developer.log('Row $index tapped');
        },
        child: Padding(
          padding: const EdgeInsets.all(10),
          child: Text('Row $index'),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Sample App')),
      body: ListView(children: _getListData()),
    );
  }
}

如何动态更新 ListView?

#

在 Xamarin.Forms 中,如果您将 ItemsSource 属性绑定到 ObservableCollection,您只需更新 ViewModel 中的列表即可。或者,您可以将新的 List 分配给 ItemSource 属性。

在 Flutter 中,事情的工作方式略有不同。如果您在 setState() 方法中更新 ListView 内的小部件列表,您会很快发现您的数据在视觉上没有改变。这是因为当调用 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});

  @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 index) {
    return GestureDetector(
      onTap: () {
        setState(() {
          widgets = List<Widget>.from(widgets);
          widgets.add(getRow(widgets.length));
          developer.log('Row $index');
        });
      },
      child: Padding(
        padding: const EdgeInsets.all(10),
        child: Text('Row $index'),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Sample App')),
      body: ListView(children: widgets),
    );
  }
}

构建列表的推荐方法、高效且有效的方法是使用 ListView.Builder。当您有动态列表或包含大量数据的列表时,此方法非常适用。这基本上等同于 Android 上的 RecyclerView,它会自动为您回收列表元素:

dart
import 'dart:developer' as developer;
import 'package:flutter/material.dart';

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<Widget> widgets = [];

  @override
  void initState() {
    super.initState();
    for (int i = 0; i < 100; i++) {
      widgets.add(getRow(i));
    }
  }

  Widget getRow(int index) {
    return GestureDetector(
      onTap: () {
        setState(() {
          widgets.add(getRow(widgets.length));
          developer.log('Row $index');
        });
      },
      child: Padding(
        padding: const EdgeInsets.all(10),
        child: Text('Row $index'),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Sample App')),
      body: ListView.builder(
        itemCount: widgets.length,
        itemBuilder: (context, index) {
          return getRow(index);
        },
      ),
    );
  }
}

不要创建 ListView,而是创建一个 ListView.builder,它有两个关键参数:列表的初始长度和项目构建器函数。

项目构建器函数类似于 Android 适配器中的 getView 函数;它接受一个位置,并返回您希望在该位置呈现的行。

最后,也是最重要的一点,请注意 onTap() 函数不再重新创建列表,而是向其中添加内容。

更多信息,请参阅你的第一个 Flutter 应用 代码实验室。

文本处理

#

如何在我的文本小部件上设置自定义字体?

#

在 Xamarin.Forms 中,您必须在每个本机项目中添加自定义字体。然后,在您的 Element 中,您将使用 filename#fontname 为此字体名称分配 FontFamily 属性,而 iOS 则仅使用 fontname

在 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

表单输入

#

如何检索用户输入?

#

Xamarin.Forms element 允许您直接查询 element 以确定其属性的状态,或者它是否绑定到 ViewModel 中的属性。

Flutter 中的检索信息由专用小部件处理,这与您习惯的方式不同。如果您有 TextFieldTextFormField,您可以提供TextEditingController 来检索用户输入:

dart
import 'package:flutter/material.dart';

class MyForm extends StatefulWidget {
  const MyForm({super.key});

  @override
  State<MyForm> createState() => _MyFormState();
}

class _MyFormState extends State<MyForm> {
  /// 创建一个文本控制器,并使用它来检索 TextField 的当前值。
  final TextEditingController myController = TextEditingController();

  @override
  void dispose() {
    // 处置小部件时清理控制器。
    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(
        // 当用户按下按钮时,显示一个带有用户在文本字段中输入的文本的警报对话框。
        onPressed: () {
          showDialog(
            context: context,
            builder: (context) {
              return AlertDialog(
                // 使用 TextEditingController 检索用户输入的文本。
                content: Text(myController.text),
              );
            },
          );
        },
        tooltip: 'Show me the value!',
        child: const Icon(Icons.text_fields),
      ),
    );
  }
}

您可以在Flutter 食谱检索文本字段的值中找到更多信息和完整的代码列表。

Entry 上占位符的等效项是什么?

#

在 Xamarin.Forms 中,一些 Elements 支持可以为其赋值的 Placeholder 属性。例如:

xml
<Entry Placeholder="This is a hint">

在 Flutter 中,您可以通过向文本小部件的 decoration 构造函数参数添加 InputDecoration 对象来轻松显示输入的“提示”或占位符文本。

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

如何显示验证错误?

#

使用 Xamarin.Forms,如果您希望提供验证错误的视觉提示,则需要创建新的属性和 VisualElement 来包围具有验证错误的 Element

在 Flutter 中,您将 InputDecoration 对象传递到文本小部件的 decoration 构造函数。

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

dart
import 'package:flutter/material.dart';

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> {
  String? _errorText;

  String? _getErrorText() {
    return _errorText;
  }

  bool isEmail(String em) {
    const 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,}))$';
    final 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: _getErrorText(),
          ),
        ),
      ),
    );
  }
}

Flutter 插件

#

与硬件、第三方服务和平台交互

#

如何与平台以及平台本机代码交互?

#

Flutter 不会直接在底层平台上运行代码;相反,构成 Flutter 应用程序的 Dart 代码会在设备上原生运行,绕过平台提供的 SDK。这意味着,例如,当您在 Dart 中执行网络请求时,它会直接在 Dart 上下文中运行。您不会使用在编写原生应用程序时通常会利用的 Android 或 iOS API。您的 Flutter 应用程序仍然作为视图托管在本机应用程序的 ViewControllerActivity 中,但您无法直接访问它或本机框架。

但这并不意味着 Flutter 应用程序无法与这些本机 API 或您拥有的任何本机代码交互。Flutter 提供平台通道,用于与托管 Flutter 视图的 ViewControllerActivity 通信和交换数据。平台通道本质上是一种异步消息机制,它将 Dart 代码与主机 ViewControllerActivity 及其运行的 iOS 或 Android 框架连接起来。例如,您可以使用平台通道在原生端执行方法,或从设备的传感器检索一些数据。

除了直接使用平台通道外,您还可以使用各种预制插件,这些插件封装了特定目标的本机和 Dart 代码。例如,您可以使用插件直接从 Flutter 访问相机胶卷和设备相机,而无需编写自己的集成。可以在 Dart 和 Flutter 的开源包存储库pub.dev 上找到插件。某些包可能支持 iOS 或 Android 或两者的本机集成。

如果您在 pub.dev 上找不到满足您需求的插件,您可以编写自己的插件,并将其发布到 pub.dev

如何访问 GPS 传感器?

#

使用geolocator 社区插件。

如何访问相机?

#

camera 插件常用于访问相机。

如何使用 Facebook 登录?

#

要使用 Facebook 登录,请使用flutter_facebook_login 社区插件。

如何使用 Firebase 功能?

#

大多数 Firebase 功能都由第一方插件 涵盖。这些插件是由 Flutter 团队维护的第一方集成:

您还可以在 pub.dev 上找到一些第三方 Firebase 插件,这些插件涵盖了第一方插件未直接涵盖的领域。

如何构建我自己的自定义本机集成?

#

如果 Flutter 或其社区插件缺少特定于平台的功能,您可以按照开发包和插件页面中的说明构建自己的集成。

简而言之,Flutter 的插件架构很像在 Android 中使用事件总线:您发出消息,让接收器处理并将结果发回给您。在这种情况下,接收器是在 Android 或 iOS 上原生端运行的代码。

主题 (样式)

#

如何设置应用程序主题?

#

Flutter 带有一个漂亮且内置的 Material Design 实现,它处理您通常会执行的许多样式和主题需求。

Xamarin.Forms 确实有一个全局 ResourceDictionary,您可以在其中在整个应用程序中共享样式。或者,目前预览版中也提供主题支持。

在 Flutter 中,您在顶级小部件中声明主题。

要充分利用应用程序中的 Material Components,您可以将顶级小部件 MaterialApp 声明为应用程序的入口点。MaterialApp 是一个便捷的小部件,它包装了许多通常需要用于实现 Material Design 的应用程序的小部件。它通过添加特定于 Material 的功能来构建在 WidgetsApp 之上。

您也可以使用 WidgetsApp 作为您的应用程序小部件,它提供一些相同的功能,但不如 MaterialApp 丰富。

要自定义任何子组件的颜色和样式,请将 ThemeData 对象传递给 MaterialApp 小部件。例如,在以下代码中,来自 seed 的颜色方案设置为深紫色,文本选择颜色为红色。

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),
        textSelectionTheme:
            const TextSelectionThemeData(selectionColor: Colors.red),
      ),
      home: const SampleAppPage(),
    );
  }
}

数据库和本地存储

#

如何访问共享首选项或 UserDefaults?

#

Xamarin.Forms 开发人员可能熟悉 Xam.Plugins.Settings 插件。

在 Flutter 中,可以使用shared_preferences 插件访问等效的功能。此插件包装了 UserDefaults 和 Android 等效项 SharedPreferences 的功能。

如何在 Flutter 中访问 SQLite?

#

在 Xamarin.Forms 中,大多数应用程序都会使用 sqlite-net-pcl 插件来访问 SQLite 数据库。

在 Flutter 中,在 macOS、Android 和 iOS 上,可以使用sqflite 插件访问此功能。

调试

#

我可以使用哪些工具来调试 Flutter 中的应用程序?

#

使用DevTools (/tools/devtools) 套件来调试 Flutter 或 Dart 应用程序。

DevTools 包括对分析、检查堆、检查小部件树、记录诊断信息、调试、观察执行的代码行、调试内存泄漏和内存碎片的支持。有关更多信息,请查看DevTools 文档。

通知

#

如何设置推送通知?

#

在 Android 中,您使用 Firebase Cloud Messaging 为您的应用程序设置推送通知。

在 Flutter 中,可以使用firebase_messaging 插件访问此功能。有关使用 Firebase Cloud Messaging API 的更多信息,请参阅firebase_messaging 插件文档。