跳至主要内容
目录

面向 Xamarin.Forms 开发者的 Flutter

目录

本文档面向希望利用现有知识使用 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。您可以使用 `MaterialApp` Widget,它支持 Material Design,或者您可以使用 `CupertinoApp` Widget,它支持 iOS 风格的应用程序,或者您可以使用更低级的 `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 的等效项是什么?

#

`ContentPage`、`TabbedPage`、`FlyoutPage` 都是您可能在 Xamarin.Forms 应用程序中使用的页面类型。然后,这些页面将保存 `Element` 以显示各种控件。在 Xamarin.Forms 中,`Entry` 或 `Button` 是 `Element` 的示例。

在 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 中,每个 `Page` 或 `Element` 都是一个有状态类,具有属性和方法。您通过更新属性来更新您的 `Element`,这会传播到本机控件。

在 Flutter 中,`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`。常见的 `StatelessWidget` 是 `Text` 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 中,您可以使用包含 FadeToTranslateTo 等方法的 ViewExtensions 创建简单的动画。您可以在视图上使用这些方法来执行所需的动画。

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

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

csharp
myImage.FadeTo(0, 1000);

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

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

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

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

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

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

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

#

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

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

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

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

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

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

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

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

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

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

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

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

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

  final List<Offset?> points;

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

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

Widget 的不透明度在哪里?

#

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

如何构建自定义 Widget?

#

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

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

例如,您如何构建一个在构造函数中获取标签的 CustomButton?创建一个组合了 ElevatedButton 和标签的 CustomButton,而不是通过扩展 ElevatedButton

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

  final String label;

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

然后使用 CustomButton,就像使用任何其他 Flutter 小部件一样。

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

如何在页面之间导航?

#

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

Flutter 有一个类似的实现,使用 NavigatorRoutesRoute 是应用程序 Page 的抽象,Navigator 是一个 小部件,用于管理路由。

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

在页面之间导航,您有几个选项。

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

以下示例构建了一个 Map

dart
void main() {
  runApp(
    MaterialApp(
      home: const MyAppHome(), // becomes the route named '/'
      routes: <String, WidgetBuilder>{
        '/a': (context) => const MyPage(title: 'page A'),
        '/b': (context) => const MyPage(title: 'page B'),
        '/c': (context) => const MyPage(title: 'page C'),
      },
    ),
  );
}

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

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

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

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

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

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

然后,在您的“location”路由中,一旦用户选择了其位置,就使用结果弹出栈。

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

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

#

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

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

异步 UI

#

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

#

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

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

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

例如,您可以使用 async/await 并让 Dart 完成繁重的工作,而不会导致 UI 挂起来运行网络代码。

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

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

以下示例异步加载数据并在 ListView 中显示它。

dart
import 'dart:convert';

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

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

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Sample App',
      home: SampleAppPage(),
    );
  }
}

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

  @override
  State<SampleAppPage> createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List<Map<String, 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 函数,并在函数内部等待长时间运行的任务。

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

这通常是您执行网络或数据库调用(这两者都是 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, 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。当您使用流行的 http 时,在 Flutter 中进行网络调用很容易。这抽象了许多您通常需要自己实现的网络功能,从而简化了网络调用的过程。

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

yaml
dependencies:
  http: ^1.1.0

要发出网络请求,请在 async 函数 http.get() 上调用 await

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

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

#

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

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

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

dart
import 'dart:async';
import 'dart:convert';

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

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

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Sample App',
      home: SampleAppPage(),
    );
  }
}

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

  @override
  State<SampleAppPage> createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List<Map<String, 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 的 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.jpeg

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

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

或使用 AssetImage

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

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

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

#

与具有 resx 文件的 .NET 不同,Flutter 目前没有用于处理字符串的专用系统。目前,最佳实践是在类中将复制文本声明为静态字段并从那里访问它们。例如。

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

您可以像这样访问您的字符串。

dart
Text(Strings.welcomeMessage);

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

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

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

dart
import 'package:flutter_localizations/flutter_localizations.dart';

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      localizationsDelegates: <LocalizationsDelegate<dynamic>>[
        // 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中使用的外部依赖项。在pub.dev上可以找到Flutter包。

应用程序生命周期

#

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

#

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

可观察的生命周期事件是

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

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

布局

#

StackLayout 的等效项是什么?

#

在Xamarin.Forms中,您可以创建一个方向为水平或垂直的StackLayout。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来实现覆盖其他小部件的小部件。在Flutter中,您可以使用Stack小部件来实现此目的。

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

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

ScrollView 的等效项是什么?

#

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

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

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

如果您有很多想要包装在滚动中的项,即使是不同Widget类型的项,您可能希望使用ListView。这看起来可能有点过头了,但在Flutter中,这比Xamarin.Forms ListView(它依赖于特定于平台的控件)更优化且更省力。

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

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

#

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

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

手势检测和触摸事件处理

#

如何在 Flutter 中向 Widget 添加 GestureRecognizers?

#

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

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

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

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

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

#

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

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

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

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

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

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

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

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: GestureDetector(
          onDoubleTap: () {
            if (controller.isCompleted) {
              controller.reverse();
            } else {
              controller.forward();
            }
          },
          child: RotationTransition(
            turns: curve,
            child: const FlutterLogo(size: 200),
          ),
        ),
      ),
    );
  }
}

ListView 和适配器

#

Flutter 中 ListView 的等效项是什么?

#

ListView的等效项在Flutter中是……ListView

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

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

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

要以简单的方式更新ListView,请在setState()内部创建一个新的List,并将数据从旧列表复制到新列表。虽然此方法很简单,但不建议用于大型数据集,如下一个示例所示。

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

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

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Sample App',
      home: SampleAppPage(),
    );
  }
}

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

  @override
  State<SampleAppPage> createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List<Widget> widgets = <Widget>[];

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

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

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

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

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

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

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Sample App',
      home: SampleAppPage(),
    );
  }
}

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

  @override
  State<SampleAppPage> createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List<Widget> widgets = [];

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

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

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

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

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

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

有关更多信息,请参阅您的第一个Flutter应用程序代码实验室。

处理文本

#

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

#

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

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

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

然后将字体分配给您的 Text 小部件

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 小部件上的其他样式元素。Text 小部件的 style 参数接受一个 TextStyle 对象,您可以在其中自定义许多参数,例如

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

表单输入

#

如何检索用户输入?

#

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

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

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

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

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

class _MyFormState extends State<MyForm> {
  /// 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 cookbook)中找到更多信息和完整的代码列表。

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

#

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

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

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

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

如何显示验证错误?

#

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

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

但是,您不希望一开始就显示错误。相反,当用户输入无效数据时,更新状态,并传递一个新的 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 中,您在顶级小部件中声明主题。

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

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

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

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

通知

#

如何设置推送通知?

#

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

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