本文档旨在帮助希望利用现有知识使用 Flutter 构建移动应用的 Xamarin.Forms 开发者。如果您理解 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:有状态无状态。无状态 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,您在其中创建您的状态。

一个有状态的 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 应用程序中使用的页面类型。这些页面将包含 Elements 来显示各种控件。在 Xamarin.Forms 中,EntryButtonElement 的示例。

在 Flutter 中,几乎所有东西都是一个 widget。一个 Page,在 Flutter 中称为 Route,是一个 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 的状态。

这就是有状态 widget 和无状态 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,可以暂停、查找、停止和反转动画。它需要一个 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),
      ),
    );
  }
}

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

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

#

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?通过组合带标签的 ElevatedButton 来创建 CustomButton,而不是通过扩展 ElevatedButton

dart
class CustomButton extends StatelessWidget {
  const CustomButton(this.label, {super.key});

  final String label;

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(onPressed: () {}, child: Text(label));
  }
}

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

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

如何在页面之间导航?

#

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

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

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 运行网络代码,而不会导致 UI 卡顿,让 Dart 完成繁重的工作

dart
Future<void> loadData() async {
  final Uri dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
  final http.Response response = await http.get(dataURL);
  setState(() {
    data = (jsonDecode(response.body) as List).cast<Map<String, Object?>>();
  });
}

一旦等待的网络调用完成,通过调用 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 更新其状态,并在任务结束后将其隐藏。

在下面的示例中,build 函数分为三个不同的函数。如果 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 会将其拾取。

例如,要将名为 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() 更改事件来监听类似的生命周期事件。

可观察的生命周期事件有

非活跃
应用程序处于非活动状态,并且不接收用户输入。此事件仅限 iOS。
暂停
应用程序当前对用户不可见,不响应用户输入,但在后台运行。
恢复
应用程序可见并响应用户输入。
暂停中
应用程序暂时暂停。此事件仅限 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(
    // 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 添加 GestureRecognizers?

#

在 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,您可以监听各种手势,例如

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

列表视图和适配器

#

Flutter 中 ListView 的等效物是什么?

#

Flutter 中 ListView 的等效物是……ListView

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

由于 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.builder 而不是 ListView,它接受两个关键参数:列表的初始长度和项构建器函数。

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

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

有关更多信息,请参阅 您的第一个 Flutter 应用 Codelab。

处理文本

#

如何为文本 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),
      ),
    );
  }
}

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

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,如果您希望提供验证错误的视觉提示,则需要创建围绕具有验证错误的 Element 的新属性和 VisualElement

在 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 组件,您可以将顶层 widget MaterialApp 声明为应用程序的入口点。MaterialApp 是一个便利 widget,它封装了 Material Design 应用程序通常需要的一些 widget。它在 WidgetsApp 的基础上添加了 Material 特定的功能。

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

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

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 插件文档。