本文档旨在帮助 Xamarin.Forms 开发人员将现有知识应用于使用 Flutter 构建移动应用。如果您了解 Xamarin.Forms 框架的基础知识,那么您可以使用本文档作为 Flutter 开发的入门指南。

在使用 Flutter 进行开发时,您的 Android 和 iOS 知识和技能非常有价值,因为 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 中,“一切皆 Widget”,甚至应用程序本身也是。以下示例展示了 MyApp,一个简单的应用程序 Widget

dart
class MyApp extends StatelessWidget {
  /// This widget is the root of your application.
  const MyApp({super.key});

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

如何创建页面?

#

Xamarin.Forms 有多种类型的页面;ContentPage 是最常见的。在 Flutter 中,您指定一个应用程序 Widget 来承载您的根页面。您可以使用支持 Material DesignMaterialApp Widget,也可以使用支持 iOS 风格应用的 CupertinoApp Widget,或者您可以使用更底层的 WidgetsApp,您可以根据需要对其进行任何自定义。

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

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

dart
class MyApp extends StatelessWidget {
  /// This widget is the root of your application.
  const MyApp({super.key});

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

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

一个有状态 (Stateful) Widget,例如下面的 MyHomePage,由两部分组成。第一部分本身是不可变的,它创建一个保存对象状态的 State 对象。State 对象在 Widget 的整个生命周期内都存在。

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

  final String title;

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

State 对象实现了有状态 Widget 的 build() 方法。

当 Widget 树的状态发生变化时,调用 setState(),这将触发 UI 该部分的重新构建。请确保仅在必要时以及仅在 Widget 树已更改的部分上调用 setState(),否则可能导致 UI 性能不佳。

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

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        // Take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set the appbar title.
        title: Text(widget.title),
      ),
      body: Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('You have pushed the button this many times:'),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

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

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

视图

#

Flutter 中 Page 或 Element 的等效项是什么?

#

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

在 Flutter 中,几乎一切都是 Widget。在 Flutter 中称为 RoutePage 是一个 Widget。按钮、进度条和动画控制器都是 Widget。构建路由时,您会创建一个 Widget 树。

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

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

如何更新 Widget?

#

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

在 Flutter 中,Widget 是不可变的,您不能通过更改属性直接更新它们,而是必须处理 Widget 的状态。

这就是有状态 (Stateful) 和无状态 (Stateless) Widget 概念的来源。StatelessWidget 正如其名——一个没有状态信息的 Widget。

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

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

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

这里需要注意的重要一点是,无状态和有状态 Widget 的核心行为相同。它们每帧都会重建,区别在于 StatefulWidget 有一个 State 对象,它在帧之间存储状态数据并恢复它。

如果您有疑问,请始终记住这条规则:如果一个 Widget 发生变化(例如由于用户交互),它就是有状态的。但是,如果一个 Widget 对变化做出反应,则如果它本身不对变化做出反应,则包含它的父 Widget 仍然可以是无状态的。

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

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

如您所见,Text Widget 没有与其关联的状态信息,它只渲染其构造函数中传递的内容,别无其他。

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

为此,将 Text Widget 包装在 StatefulWidget 中,并在用户点击按钮时更新它,如以下示例所示

dart
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> {
  /// 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?XAML 文件的等效项是什么?

#

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

以下示例展示了如何显示带有内边距的简单 Widget

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

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

如何从布局中添加或移除 Element?

#

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

在 Flutter 中,由于 Widget 是不可变的,因此没有直接的等效项。相反,您可以将一个返回 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),
      ),
    );
  }
}

如何制作 Widget 动画?

#

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

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

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

csharp
myImage.FadeTo(0, 1000);

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

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

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

以下示例展示了如何编写一个 FadeTransition,当您按下 FloatingActionButton 时,它会将 Widget 淡入到徽标。

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

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

class FadeAppTest extends StatelessWidget {
  /// This widget is the root of your application.
  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),
      ),
    );
  }
}

更多信息,请参阅 动画和运动 Widget动画教程动画概述

如何在屏幕上绘制/着色?

#

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

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

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

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

Widget 的不透明度在哪里设置?

#

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

如何构建自定义 Widget?

#

在 Xamarin.Forms 中,您通常会子类化 VisualElement,或使用现有的 VisualElement,来重写和实现方法以达到所需的行为。

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

例如,如何构建一个在构造函数中接受标签的 CustomButton?创建 CustomButton,它由带标签的 ElevatedButton 组合而成,而不是通过扩展 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 Widget 一样。

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

如何在页面之间导航?

#

在 Xamarin.Forms 中,NavigationPage 类提供分层导航体验,用户可以向前和向后导航页面。

Flutter 有一个类似的实现,使用 NavigatorRoutesRoute 是应用程序 Page 的抽象,而 Navigator 是管理路由的Widget

路由大致映射到 Page。导航器的工作方式与 Xamarin.Forms NavigationPage 类似,它可以 push()pop() 路由,具体取决于您是要导航到视图还是从视图返回。

要在页面之间导航,您有几种选择:

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

以下示例构建一个 Map

dart
void main() {
  runApp(
    MaterialApp(
      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'),
      },
    ),
  );
}

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

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

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

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

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

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

然后,在您的“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,这将触发 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 与 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。在 Flutter 中,当您使用流行的 http时,进行网络调用很容易。这抽象了您通常可能自己实现的大部分网络操作,从而简化了网络调用。

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

yaml
dependencies:
  http: ^1.4.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 Widget。通过布尔标志控制其渲染时间,以编程方式显示进度。在长时间运行的任务开始之前告诉 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 的资产文件夹中。

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 会拾取它们。

例如,要将名为 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

您可以直接在 Image.asset Widget 中访问您的图像

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 # Use version of intl from flutter_localizations.

要使用 flutter_localizations 包,请在应用程序 Widget 上指定 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>>[
        // Add app-specific localization delegate[s] here
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
      ],
      supportedLocales: <Locale>[
        Locale('en', 'US'), // English
        Locale('he', 'IL'), // Hebrew
        // ... other locales the app supports
      ],
    );
  }
}

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

初始化时,WidgetsApp(或 MaterialApp)会为您创建一个 Localizations Widget,其中包含您指定的委托。设备的当前区域设置始终可以从当前上下文的 Localizations Widget(以 Locale 对象的形式)或使用 Window.locale 访问。

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

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

我的项目文件在哪里?

#

在 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 中,您可以创建具有水平或垂直 OrientationStackLayout。Flutter 有类似的方法,但是您将使用 RowColumn Widget。

如果您注意到这两个代码示例除了 RowColumn Widget 之外是相同的。子项是相同的,并且可以利用此功能来开发随着时间推移可以使用相同子项进行更改的丰富布局。

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(
    // Create a grid with 2 columns. If you change the scrollDirection to
    // horizontal, this would produce 2 rows.
    crossAxisCount: 2,
    // Generate 100 widgets that display their index in the list.
    children: List<Widget>.generate(100, (index) {
      return Center(
        child: Text(
          'Item $index',
          style: Theme.of(context).textTheme.headlineMedium,
        ),
      );
    }),
  );
}

您可能在 Xamarin.Forms 中使用过 Grid 来实现覆盖其他 Widget 的 Widget。在 Flutter 中,您可以使用 Stack Widget 来完成此操作。

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

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 Widget。您只需用您想要滚动的内容填充 Widget 即可。

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 中为 Widget 添加 GestureRecognizer?

#

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

  1. 如果 Widget 支持事件检测,则将函数传递给它并在函数中处理。例如,ElevatedButton 有一个 onPressed 参数。

    dart
    @override
    Widget build(BuildContext context) {
      return ElevatedButton(
        onPressed: () {
          developer.log('click');
        },
        child: const Text('Button'),
      );
    }
  2. 如果 Widget 不支持事件检测,请将 Widget 包装在 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),
            ),
          ),
        );
      }
    }

如何处理 Widget 上的其他手势?

#

在 Xamarin.Forms 中,您会将 GestureRecognizer 添加到 View。您通常会受限于 TapGestureRecognizerPinchGestureRecognizerPanGestureRecognizerSwipeGestureRecognizerDragGestureRecognizerDropGestureRecognizer,除非您自己构建。

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

  • 轻触 (Tap)
onTapDown
一个可能导致轻触的指针已在特定位置接触屏幕。
onTapUp
触发轻触的指针已在特定位置停止接触屏幕。
onTap
发生了轻触。
onTapCancel
之前触发 onTapDown 的指针不会引起轻触。
  • 双击 (Double tap)
onDoubleTap
用户在同一位置快速连续轻触屏幕两次。
  • 长按 (Long press)
onLongPress
指针在同一位置与屏幕长时间保持接触。
  • 垂直拖动 (Vertical drag)
onVerticalDragStart
指针已接触屏幕,并可能开始垂直移动。
onVerticalDragUpdate
与屏幕接触的指针已在垂直方向上进一步移动。
onVerticalDragEnd
之前与屏幕接触并垂直移动的指针不再与屏幕接触,并且在停止接触屏幕时以特定速度移动。
  • 水平拖动 (Horizontal drag)
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 并将其传递给 ListView,后者会使用您的 DataTemplateSelectorViewCell 返回的内容渲染每一行。但是,您通常必须确保打开单元格回收 (Cell Recycling),否则您将遇到内存问题和缓慢的滚动速度。

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

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

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() 方法中更新 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 {
  /// 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> 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 {
  /// 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> 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 应用 代码实验室。

文本处理

#

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

#

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

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

如何设置我的文本 Widget 的样式?

#

除了字体之外,您还可以自定义 Text Widget 上的其他样式元素。Text Widget 的 style 参数接受一个 TextStyle 对象,您可以在其中自定义许多参数,例如:

  • 颜色 (color)
  • 装饰 (decoration)
  • 装饰颜色 (decorationColor)
  • 装饰样式 (decorationStyle)
  • 字体家族 (fontFamily)
  • 字体大小 (fontSize)
  • 字体样式 (fontStyle)
  • 字体粗细 (fontWeight)
  • 哈希码 (hashCode)
  • 高度 (height)
  • 继承 (inherit)
  • 字母间距 (letterSpacing)
  • 文本基线 (textBaseline)
  • 字间距 (wordSpacing)

表单输入

#

如何获取用户输入?

#

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

在 Flutter 中,信息的检索由专用 Widget 处理,这与您习惯的方式不同。如果您有 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> {
  /// Create a text controller and use it to retrieve the current value
  /// of the TextField.
  final TextEditingController 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 that the user has typed into our text field.
        onPressed: () {
          showDialog(
            context: context,
            builder: (context) {
              return AlertDialog(
                // Retrieve the text that the user has entered using the
                // TextEditingController.
                content: Text(myController.text),
              );
            },
          );
        },
        tooltip: 'Show me the value!',
        child: const Icon(Icons.text_fields),
      ),
    );
  }
}

您可以在 Flutter 指南中的获取文本字段的值中找到更多信息和完整的代码列表。

Entry 上的 Placeholder 的等效项是什么?

#

在 Xamarin.Forms 中,某些 Elements 支持 Placeholder 属性,您可以为其分配一个值。例如

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

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

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

如何显示验证错误?

#

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

在 Flutter 中,您将 InputDecoration 对象传递给文本 Widget 的装饰构造函数。

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

dart
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> {
  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 访问相机胶卷和设备相机,而无需编写自己的集成。插件可以在 pub.dev(Dart 和 Flutter 的开源包存储库)上找到。有些包可能支持 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 中,您在顶层 Widget 中声明主题。

为了充分利用应用程序中的 Material Components,您可以将顶层 Widget MaterialApp 声明为应用程序的入口点。MaterialApp 是一个便捷 Widget,它包装了实现 Material Design 的应用程序通常需要的一些 Widget。它在 WidgetsApp 的基础上添加了 Material 特定的功能。

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

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

dart
class SampleApp extends StatelessWidget {
  /// This widget is the root of your application.
  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 套件调试 Flutter 或 Dart 应用。

DevTools 支持性能分析、检查堆、检查 Widget 树、记录诊断信息、调试、观察已执行的代码行、调试内存泄漏和内存碎片。更多信息,请查阅 DevTools 文档。

通知

#

如何设置推送通知?

#

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

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