内容

面向 Xamarin.Forms 开发人员的 Flutter

内容

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

使用 Flutter 构建时,您的 Android 和 iOS 知识和技能集非常有价值,因为 Flutter 依赖于原生操作系统配置,类似于您配置原生 Xamarin.Forms 项目的方式。Flutter 框架也类似于您创建单个 UI 的方式,该 UI 可用于多个平台。

本文件可用作食谱,您可以四处浏览并找到与您的需求最相关的问答。

项目设置

应用如何启动?

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

LoadApplication(new App());

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

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

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

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

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

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 中,您指定一个包含根页面的应用程序小部件。您可以使用 MaterialApp 小部件,它支持 Material Design,或者您可以使用 CupertinoApp 小部件,它支持 iOS 风格的应用程序,或者您可以使用较低级别的 WidgetsApp,您可以根据需要对其进行自定义。

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

以下示例使用 MaterialApp,它将根页面保存在 home 属性中。

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,您可以在其中创建您的状态。

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

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

  final String title;

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

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

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

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(也称为小部件树)是不可变的,这意味着一旦构建它,您就不能更改其状态。您可以在 State 类中更改字段,然后调用 setState() 以再次重建整个小部件树。

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

视图

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

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

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

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

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

如何更新小部件?

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

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

这就是有状态与无状态小部件概念的由来。StatelessWidget 正如其名,它是一个没有状态信息的小部件。

StatelessWidgets 在您描述的用户界面部分不依赖于对象中的配置信息以外的任何信息时很有用。

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

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

这里需要注意的重要一点是,从本质上讲,无状态小部件和有状态小部件的行为相同。它们每帧都重新构建,不同之处在于 StatefulWidget 有一个 State 对象,它跨帧存储状态数据并恢复它。

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

以下示例演示如何使用 StatelessWidget。常见的 StatelessWidgetText 小部件。如果您查看 Text 小部件的实现,您会发现它子类化 StatelessWidget

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

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

但是,如果您想让“我喜爱 Flutter”动态更改,例如,在单击 FloatingActionButton 时?

要实现此目的,请将 Text 小部件包装在 StatefulWidget 中,并在用户单击按钮时对其进行更新,如下例所示

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

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

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

以下示例演示如何显示带填充的简单小部件

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

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

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

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

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

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

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

如何制作小组件动画?

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

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

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

myImage.FadeTo(0, 1000);

在 Flutter 中,通过在动画小组件内包装小组件来使用动画库对小组件进行动画处理。使用 AnimationController,它是一个 Animation<double>,可以暂停、查找、停止和反转动画。它需要一个 Ticker,该 Ticker 会在发生垂直同步时发出信号,并在运行时在 0 和 1 之间生成线性插值。然后,创建一个或多个 Animation 并将它们附加到控制器。

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

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

以下示例演示了如何编写 FadeTransition,该 FadeTransition 在按下 FloatingActionButton 时将小组件淡化为徽标

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

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

如何在屏幕上绘制/涂鸦?

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

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

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

import 'package:flutter/material.dart';

void main() {
  runApp(const MaterialApp(home: DemoApp()));
}

class DemoApp extends StatelessWidget {
  const DemoApp({super.key});

  @override
  Widget build(BuildContext context) => const Scaffold(body: Signature());
}

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

  @override
  SignatureState createState() => SignatureState();
}

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

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

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

class SignaturePainter extends CustomPainter {
  const SignaturePainter(this.points);

  final List<Offset?> points;

  @override
  void paint(Canvas canvas, Size size) {
    final Paint paint = Paint()
      ..color = Colors.black
      ..strokeCap = StrokeCap.round
      ..strokeWidth = 5;
    for (int i = 0; i < points.length - 1; i++) {
      if (points[i] != null && points[i + 1] != null) {
        canvas.drawLine(points[i]!, points[i + 1]!, paint);
      }
    }
  }

  @override
  bool shouldRepaint(SignaturePainter oldDelegate) =>
      oldDelegate.points != points;
}

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

在 Xamarin.Forms 中,所有 VisualElement 都有一个 Opacity。在 Flutter 中,你需要将一个组件包装在 Opacity 组件 中才能实现此目的。

如何构建自定义组件?

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

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

例如,如何构建一个在构造函数中接受标签的 CustomButton?创建一个将 ElevatedButton 与标签组合在一起的 CustomButton,而不是通过扩展 ElevatedButton

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 组件一样

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

如何在页面之间导航?

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

Flutter 有一个类似的实现,使用 NavigatorRoutes。一个 Route 是一个应用程序 Page 的抽象,而 Navigator 是一个管理路由的 组件

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

要在页面之间导航,你有几个选择

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

以下示例构建一个 Map

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 中来导航到一个路由。

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

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

async/await 与 .NET 实现非常相似,并在 Async UI 中进行了更详细的说明。

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

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

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

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

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

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

要在 Flutter 中实现此功能,请创建一个本机平台集成,或使用 现有插件,例如 url_launcher,它与 pub.dev 上的许多其他软件包一起提供。

Async 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 挂起的情况下运行网络代码

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

完成等待的网络调用后,通过调用 setState() 来更新 UI,这会触发小部件子树的重建并更新数据。

以下示例异步加载数据并将其显示在 ListView

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, dynamic>> data = <Map<String, dynamic>>[];

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

  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

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

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

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

隔离是单独的执行线程,不与主执行内存堆共享任何内存。这是 Task.Run() 之间的一个区别。这意味着您无法访问主线程中的变量,也无法通过调用 setState() 来更新您的 UI。

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

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。在隔离中,您可以执行更密集的 CPU 处理(例如,解析大型 JSON),或执行计算密集型数学运算,例如加密或信号处理。

您可以在下面运行完整示例

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, dynamic>> data = <Map<String, dynamic>>[];

  @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 中的依赖项中

dependencies:
  http: ^1.1.0

要进行网络请求,请对 async 函数 http.get() 调用 await

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

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

在 Xamarin.Forms 中,您通常会创建一个加载指示器,直接在 XAML 中或通过第三方插件(例如 AcrDialogs)创建。

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

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

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, dynamic>> data = <Map<String, dynamic>>[];

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

  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 像素比率
ldpi 0.75x
mdpi 1.0x
hdpi 1.5x
xhdpi 2.0x
xxhdpi 3.0x
xxxhdpi 4.0x

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

例如,要向我们的 Flutter 项目添加一个名为 my_icon.png 的新图像资产,并决定将其放在我们任意命名的 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 文件中声明这些图像

assets:
 - images/my_icon.jpeg

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

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

或使用 AssetImage

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

更详细的信息可以在 添加资产和图像 中找到。

我应将字符串存储在何处?如何处理本地化?

与具有 resx 文件的 .NET 不同,Flutter 目前没有专门用于处理字符串的系统。目前,最佳做法是将副本文本声明为类中的静态字段,然后从中访问它们。例如

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

你可以按如下方式访问你的字符串

Text(Strings.welcomeMessage);

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

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

要使用 flutter_localizations 包,请在应用小组件上指定 localizationsDelegatessupportedLocales

import 'package:flutter_localizations/flutter_localizations.dart';

class MyWidget extends StatelessWidget {
  const MyWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      localizationsDelegates: <LocalizationsDelegate<dynamic>>[
        // Add app-specific localization delegate[s] here
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
      ],
      supportedLocales: <Locale>[
        Locale('en', 'US'), // English
        Locale('he', 'IL'), // Hebrew
        // ... other locales the app supports
      ],
    );
  }
}

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

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

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

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

我的项目文件在哪里?

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

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

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

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

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

通常,使用 pubspec.yaml 来声明在 Flutter 中使用的外部依赖项。查找 Flutter 包的理想场所是 pub.dev

应用程序生命周期

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

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

可观察的生命周期事件有

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

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

布局

StackLayout 的等效项是什么?

在 Xamarin.Forms 中,可以使用 Orientation 为水平或垂直的 StackLayout。Flutter 采用类似的方法,但是可以使用 RowColumn 窗口小部件。

如果你注意到,除了 RowColumn 窗口小部件外,这两个代码示例是相同的。子元素是相同的,并且可以利用此功能来开发丰富的布局,这些布局可以使用相同的子元素随着时间的推移而发生变化。

@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'),
    ],
  );
}
@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 会自动滚动。

@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 来实现覆盖其他小组件的小组件。在 Flutter 中,你可以使用 Stack 小组件来实现此目的。

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

@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 小组件。你只需使用要滚动的内容填充小组件即可。

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

如果你想将许多项目包裹在滚动中,即使是不同 Widget 类型,你可能需要使用 ListView。这可能看起来有点小题大做,但在 Flutter 中,这比 Xamarin.Forms ListView(它支持特定于平台的控件)更优化、更省力。

@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 属性来自动处理横向过渡。

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

手势检测和触摸事件处理

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

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

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

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

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

如何在小组件上处理其他手势?

在 Xamarin.Forms 中,您需要向 View 添加 GestureRecognizer。通常情况下,您仅限于 TapGestureRecognizerPinchGestureRecognizerPanGestureRecognizerSwipeGestureRecognizerDragGestureRecognizerDropGestureRecognizer,除非您自己构建了它们。

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

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

以下示例展示了在双击时旋转 Flutter 徽标的 GestureDetector

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 的不可变小部件模式,您将小部件列表传递到 ListView 中,而 Flutter 负责确保滚动快速且流畅。

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

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() 方法中更新小部件列表,你会很快发现你的数据在视觉上没有改变。这是因为当调用 setState() 时,Flutter 渲染引擎会查看小部件树以查看是否有任何更改。当它到达你的 ListView 时,它会执行 == 检查,并确定这两个 ListView 是相同的。没有任何更改,因此不需要更新。

要更新 ListView 的一种简单方法是在 setState() 中创建一个新的 List,并将旧列表中的数据复制到新列表中。虽然这种方法很简单,但对于大型数据集不推荐使用,如下一个示例所示。

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,它会自动为你回收列表元素

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 应用 代码实验室。

使用文本

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

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

在 Flutter 中,将字体文件放在文件夹中,并在 pubspec.yaml 文件中引用它,类似于导入图像的方式。

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

然后将字体分配给 Text 小组件

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: const Text('Sample App')),
    body: const Center(
      child: Text(
        'This is a custom font text',
        style: TextStyle(fontFamily: 'MyCustomFont'),
      ),
    ),
  );
}

如何设置文本小组件的样式?

除了字体之外,您还可以自定义 Text 小组件上的其他样式元素。 Text 小组件的样式参数采用 TextStyle 对象,您可以在其中自定义许多参数,例如

  • 颜色
  • 装饰
  • 装饰颜色
  • 装饰样式
  • 字体系列
  • 字体大小
  • 字体样式
  • 字体粗细
  • 哈希码
  • 高度
  • 继承
  • 字母间距
  • 文本基线
  • 字间距

表单输入

如何检索用户输入?

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

Flutter 中的信息检索由专门的小部件处理,与你习惯的方式不同。如果你有一个 TextField 或一个 TextFormField,你可以提供一个 TextEditingController 来检索用户输入

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 食谱

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

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

<Entry Placeholder="This is a hint">

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

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

如何显示验证错误?

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

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

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

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 中,您在顶级小组件中声明主题。

要充分利用应用中的 Material Components,您可以将顶级小组件 MaterialApp 声明为应用的入口点。 MaterialApp 是一个便捷小组件,它封装了许多实现 Material Design 的应用通常需要的各种小组件。它基于 WidgetsApp,增加了特定于 Material 的功能。

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

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

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 数据库。

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

调试

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

使用 DevTools 套件来调试 Flutter 或 Dart 应用。

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

通知

如何设置推送通知?

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

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