给 Xamarin.Forms 开发者的 Flutter 指南

目录

本文档旨在帮助 Xamarin.Forms 开发者利用已有的知识去构建 Flutter 移动应用。如果你懂得 Xamarin.Forms 框架的基本原理,那么你就可以将本文档当作你开始 Flutter 开发的不错的起点。

你的 Android 和 iOS 知识以及技能组合在构建 Flutter 时都是有价值的,因为 Flutter 依赖的原生系统配置都与你配置 Xamarin.Forms 原生项目时一样。 Flutter 框架在创建适用于多个平台的单一界面时,与 Xamarin.Forms 是类似的。

这篇文档可以用作随时查阅以及答疑解惑的专题手册。

项目设置

app 是如何运行的?

对于 Xamarin.Forms 里的每个平台,你可以调用 LoadApplication 方法,创建一个新应用并运行你的应用。

LoadApplication(new App());

在 Flutter 中,加载 Flutter app 的默认主入口是 main

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

在 Xamarin.Forms 中,你会分配一个 PageApplication 类中的 MainPage 属性。

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

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。无状态 widget 的示例都是标题、图标或图片。

下面的示例使用了 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)。

一个 有状态的 widget,例如下面的 MyHomePage,包含两个部分。第一部分,是自身不变的 widget,创建一个状态对象来管理 widget 的状态。状态对象在 widget 的整个生命周期中持续存在。

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

  final String title;

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

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

当 widget 树的状态发生了改变,将会调用 setState() 触发 widget 当中该部分 UI 的构建。确保只在需要时调用 setState(),并且在只有部分 widget 树发生变化时调用,否则会造成糟糕的 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(也就是 widget 树)是不可变的,意味着它一旦被构建,你就无法再改变它的状态。你可以修改 State 类中的字段,并再次调用 setState 来重新构建整个 widget 树。

这样生成 UI 的方式不同于 Xamarin.Forms,但是却带来了更多好处。

视图

在 Flutter 中页面(Page)与元素(Element)的相同的是什么?

ContentPageTabbedPageFlyoutPage 就是你可以在 Xamarin.Forms 应用程序中使用的全部页面类型。这些页面会控制 Element 来显示各种控件。在 Xamarin.Forms 中,Entry 或者 Button 就是一个 Element 的示例。

在 Flutter 中,几乎所有东西都是 widget,在 Flutter 中被称作 Route 的一个 Page,也是一个 widget。按钮、进度条、动画控制器都是 widget。当构建一个路由时,就会创建一棵 widget 树。

Flutter 包含 Material 组件 库。这些都是实现了 Material Design 指南 的 widget。 Material Design 是一个灵活的 针对所有平台 的设计系统,包括 iOS。

不过,Flutter 有足够灵活和自描述性 (expressive) 去实现任何设计语言。举个例子,在 iOS 上,你可以用 Cupertino widgets 来生成一个看起来像 苹果 iOS 设计语言 的接口。

如何更新 widget?

在 Xamarin.Forms 中,每一个 Page 或者 Element 都是一个有状态的类,拥有一些属性和方法。通过更新一个属性来更新你的元素,而且这会传递到原生控件。

在 Flutter 中,widget 是不可变的,你不可以直接地通过修改一个属性来更新它们,而是应该使用 widget 的状态。

有状态 widget 和无状态 widget 的概念就是出自这里, StatelessWidget 顾名思义,就是一个没有状态信息的 widget。

当你在描绘用户界面的一个不依赖除对象中的配置信息之外任何东西的部分时, StatelessWidget 是有用的。

举个例子,在 Xamarin.Forms 中,可以轻而易举地用你的 logo 替换一张 Image。这个 logo 将不会在运行过程中修改,所以在 Flutter 会使用 StatelessWidget

如果你想基于进行了 HTTP 调用或者用户交互后接收到的数据来动态地修改 UI,你需要使用 StatefulWidget 并告诉 Flutter 框架这个 widget 的 State 已经被更新了所以它可以更新那个 widget。

这里要记下的重要内容是有状态和无状态 widget 的核心行为都是一样的。他们重建每个结构,不同的是 StatefulWidget 拥有一个 State 对象来跨结构储存状态数据和恢复它。

如果你有疑惑,那么就记住这个规则:如果一个 widget 改变了(例如是因为用户交互),它就是有状态的。相反,如果一个 widget 对修改作出反应,包含它的父 widget 如果本身没有对修改作出反应,它就是无状态的。

接下来的示例展示了如何使用一个 StatelessWidget。一个常见的 StatelessWidgetText widget。如果你阅读了 Text widget 的实现,你会发现它是 StatelessWidget 的子类。

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

如你所见,Text widget 没有状态信息与它关联,它只渲染在它的构造函数中呈现的内容。

但是,如果你想动态地修改「I Like Flutter」呢?例如在点击一个 FloatingActionButton 时进行修改。

为了实现这个目标,你需要将 Text widget 放到一个 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),
      ),
    );
  }
}

我该如何布局我的 widget 呢?什么东西可以等价于一个 XAML 文件?

在 Xamarin.Forms 中,大部分开发者用 XAML 写布局,有时也会用到 C#。在 Flutter 中编码一棵 widget 树来编写布局。

接下来的示例展示如何显示一个简单的带内边距的 widget:

@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.0, right: 30.0),
        ),
        onPressed: () {},
        child: const Text('Hello'),
      ),
    ),
  );
}

你可以查看 Flutter 在 widget 目录 中提供的布局。

如何从布局中添加或移除一个元素?

在 Xamarin.Forms 中,你需要在代码中移除或添加一个 Element。如果是一个列表,这将会涉及设置 Content 属性或者调用 Add() 或者 Remove() 方法。

在 Flutter 中,因为 widget 都是不可变的,所以没有直接对等的东西。但是你可以将一个构建 widget 的函数传递给父级,并用布尔值控制它的子 widget 的创建。

下面的示例展示当用户点击 FloatingActionButton 时,如何在两个 widget 之间切换。

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)来创建简单的动画。你需要在一个视图中使用这些方法来执行需要的动画。

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

在后面的代码或一个动作中,这个会在 1 秒内淡入这张图像。

myImage.FadeTo(0, 1000);

Flutter 通过 Animation<double> 的子类 AnimationController 来暂停、播放、停止以及逆向播放动画。它需要一个 Ticker 在垂直同步 (vsync) 的时候发出信号,并且在运行的时候创建一个介于 0 和 1 之间的线性插值。然后你就可以创建一个或多个 Animation,并将它们绑定到控制器上。

例如,你可以使用 CurvedAnimation 来实现一个曲线插值的动画。在这种情况下,控制器决定了动画进度, CurvedAnimation 计算用于替换控制器默认线性动画的曲线值。与 Widget 一样,Flutter 中的动画效果也可以组合使用。

在构建 Widget 树的时候,你需要将 Animation 对象赋值给某个 Widget 的动画属性,例如 FadeTransition 的不透明度属性,并让控制器开始动画。

下面的例子展示了如何实现一个点击 FloatingActionButton 时将一个 Widget 渐变为一个图标的 FadeTransition

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.0),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          controller.forward();
        },
        tooltip: 'Fade',
        child: const Icon(Icons.brush),
      ),
    );
  }
}

获取更多内容,请查看 动画 & 运动 Widget动画指南 以及 动画概览

如何在屏幕上绘图?

Xamarin.Forms 从来没有任何内置的方法来直接在屏幕上绘图。如果他们需要一个自定义图像绘制,大多数使用 SkiaSharp。在 Flutter 中,你可以直接访问 Skia 画布(Skia Canvas)方便地在屏幕上绘图。

Flutter 有两个帮助你用画布 (canvas) 进行绘制的类: CustomPaintCustomPainter,后者可以实现自定义的绘制算法。

如果想学习在 Flutter 中如何实现一个签名功能,可以查看 Collin 在 StackOverflow 上的回答。

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.0;
    for (int i = 0; i < points.length - 1; i++) {
      if (points[i] != null && points[i + 1] != null) {
        canvas.drawLine(points[i]!, points[i + 1]!, paint);
      }
    }
  }

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

widget 的不透明度在哪里?

Xamarin.Forms 上,所有 VisualElement 都有不透明度的属性。在 Flutter 中,你需要将 widget 放到一个 不透明度 widget 来实现。

如何构建一个自定义 widget ?

在 Xamarin.Forms 中,通常派生 VisualElement 或使用一个已有的 VisualElement ,来重写和实现所需行为的方法。

在 Flutter 中,通过 组合 更小的 Widget 来创建自定义 Widget(而不是继承它们)。这有点类似于基于 Grid 实现自定义控件,其中添加了大量 VisualElement,同时使用自定义逻辑进行扩展。

举例来说,你该如何创建一个在构造器接收标签参数的 CustomButton?你要组合 RaisedButton 和一个标签来创建自定义按钮,而不是继承 RaisedButton

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

  final String label;

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

然后就像使用其它 Flutter Widget 一样使用 CustomButton

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

如何在页面之间导航?

在 Xamarin.Forms 中,NavigationPage 类提供了一个阶级式的导航方式,让用户可以在页面之间来回进行跳转。

Flutter 也有类似的实现,使用 NavigatorRouteRoute 是应用程序里 Page 的抽象,而 Navigator 是用于管理路由的 widget

一个路由大致上映射到一个 PageNavigator 的工作方式类似于 Xamarin.Forms 的 NavigationPage,在里面可以 push()pop() 路由,取决于你是否想导航到一个视图,或者从它返回。

你有多种不同的方式在页面间导航:

  • 定义一个 route 名字的 Map。(MaterialApp)

  • 直接导航到一个 route。(WidgetApp)

接下来构建一个映射的示例。

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

通过路由名 压栈 (push) 到 Navigator 中来跳转到这个 route。

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

Navigator 管理应用程序的路由堆栈。把一个路由推入堆栈可以导航到这个路由,而从堆栈弹出一个路由可以返回到前一个路由。这是通过 awaitpush() 返回的 Future 来完成的。

async/await 与 .NET 的实现非常类似,在 Async UI 中有更详尽的解释。

举个例子,想要让用户选择他们的定位的 定位 (location) 路由,你需要以下步骤:

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

然后,在你的「定位」路由里,用户选择他们的定位后,通过 pop() 路由堆栈来返回结果。

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/wait 来执行任务,其中必须等待响应。

在 Flutter 中,可以使用 Dart 语言提供的异步工具,例如 async/await 来执行异步任务。这跟 C# 很像,并且对于 Xamarin.Forms 开发者来说应该是非常容易使用的。

例如,你可以通过使用 async/await 来运行网络代码而且不会导致 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);
  });
}

一旦用 await 修饰的网络操作完成,再调用 setState() 更新 UI,这会触发 widget 子树的重建并更新数据。

下面的例子展示了异步加载数据并将之展示在 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.0),
      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 来利用多核处理器的优势执行耗时或计算密集的任务。

Isolate 是独立执行的线程,不会和主执行内存堆分享内存。这是与 Task.Run() 的区别。这意味着你无法访问主线程的变量,或者调用 setState() 更新 UI。

下面的例子展示了一个简单的 Isolate 是如何将数据分享给主线程来更新 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。在 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.0),
      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 package package 轻松进行网络调用。它抽象了很多通常你会自己实现的网络功能,这使其本身在执行网络请求时简单易用。

要使用 http,请在 pubspec.yaml 文件中添加依赖:

dependencies:
  http: ^0.13.4

如果要发起一个网络请求,在异步 (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 widget。通过代码逻辑使用一个布尔标记值控制进度条的渲染。告诉 Flutter 在长时间运行的任务开始之前更新状态,并在结束后将其隐藏。

在下面的例子中,build 方法被拆分成三个不同的方法。如果 showLoadingDialog() 返回 true(当 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.0),
      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 将资源 (resources) 和资产 (assets) 视为不同的项目,但是 Flutter 应用只有资产文件 (assets)。所有原本在 Android 中应该放在 res/drawable-* 文件夹中的资源文件,在 Flutter 中都放在一个 assets 文件夹中。

Flutter 遵循一个简单的类似 iOS 的密度相关的格式。文件可以是一倍 (1x)、两倍 (2x)、三倍 (3x) 或其它的任意倍数。 Flutter 没有 dp 单位,但是有逻辑像素尺寸,基本和设备无关的像素尺寸是一样的。名称为 devicePixelRatio 的尺寸表示在单一逻辑像素标准下设备物理像素的比例。

与 Android 的密度分类的对照表如下:

Android density qualifier Flutter pixel ratio
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 widget 直接访问:

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

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

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

与 .NET 拥有 resx 文件不同, Flutter 当下并没有一个特定的管理字符串的资源管理系统。目前来讲,最好的办法是将字符串作为静态域存放在类中,并通过类访问它们。例如:

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

接着在你们的代码中,你可以这样访问你的字符串:

Text(Strings.welcomeMessage);

默认情况下,Flutter 只支持美式英语的本地化字符串。如果你需要添加其他语言支持,请引入 flutter_localizations 库。同时你可能还需要添加 intl package 来使用国际化机制,比如日期和时间的格式化等。

dependencies:
  flutter_localizations:
    sdk: flutter
  intl: '^0.17.0'

若你想使用 flutter_localizations package,指定应用的 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 指定了应用支持的语言,而这些 delegates 则包含了实际的本地化内容。上面的示例使用了一个 MaterialApp,所以它既使用了处理基础 widget 本地化的 GlobalWidgetsLocalizations,也使用了处理 Material widget 本地化的 MaterialWidgetsLocalizations。如果你在应用中使用的是 WidgetApp,就不需要后者了。注意,这两个 delegates 虽然都包含了「默认」值,但是如果你想要实现本地化,就必须在本地提供一个或多个 delegates 的实现副本。

当初始化的时候,WidgetsApp(或 MaterialApp)会根据你提供的 delegates 创建一个 Localizations widget。 Localizations widget 可以随时从当前上下文中中获取设备所用的语言,也可以使用 Window.locale

要使用本地化资源,使用 Localizations.of() 方法可以访问提供代理的特定本地化类。使用 intl_translation 库解压翻译的副本到 arb 文件,然后在应用中通过 intl 来引用它们。

关于 Flutter 中国际化和本地化的细节内容,请参看 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 封装应用程序的构建委托给各自的构建系统。

通常你会在 Flutter 中使用 pubspec.yaml 来声明外部依赖。你可以通过 pub.dev 来查找一些优秀的 Flutter 第三方包。

应用程序生命周期

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

在 Xamarin.Forms 中,你会有一个包含 OnStartOnResumeOnSleepApplication。在 Flutter 中,你可以通过在 WidgetsBinding 的监听器 (observer) 中添加监听,也可以通过监听 didChangeAppLifecycleState() 事件,来实现相应的功能。

可监听的生命周期事件有:

inactive
The application is in an inactive state and is not receiving user input. This event is iOS only.
inactive
应用当前处于不活跃状态,不接收用户输入事件。这个事件只在 iOS 上有效。
paused
The application is not currently visible to the user, is not responding to user input, but is running in the background.
paused
应用当前处于用户不可见状态,不接收用户输入事件,但仍在后台运行。
resumed
The application is visible and responding to user input.
resumed
应用可见,同时响应用户输入。
suspending
The application is suspended momentarily. This event is Android only.
suspending
应用被挂起。这个事件只在 Android 上有效。

有关这些状态的含义的更多细节,可参考 AppLifecycleStatus 文档

布局

什么东西与 StackLayout 等效?

在 Xamarin.Forms 中,可以创建一个带水平或垂直方向 OrientationStackLayout 。 Flutter 也有类似的方法,不过你将使用的是 RowColumn widget。

你可能会注意到除了 RowColumn widget 之外,这两个代码示例是相同的。这些子元素是相同的,可以利用这个特性开发丰富的布局,这些布局可以随着时间的推移而改变。

@override
Widget build(BuildContext context) {
  return Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: const <Widget>[
      Text('Row One'),
      Text('Row Two'),
      Text('Row Three'),
      Text('Row Four'),
    ],
  );
}
@override
Widget build(BuildContext context) {
  return Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: const <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 来实现覆盖其他 widget 的 widget。在 Flutter 中,你可以使用 Stack widget 来完成这一操作。

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

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

有什么等同于 ScrollView ?

在 Xamarin.Forms 中,ScrollView 封装了 VisualElement,如果内容大于设备屏幕,它就会滚动。

在 Flutter 中,最接近的是 SingleChildScrollView widget。你只需用想要可滚动的内容来填充 widget。

@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 中向 widget 添加手势识别器?

在 Xamarin.Forms 中,Element 可能包含一个可供附加 (attach) 的单击事件。许多元素还包含一个与此事件关联的 Command。你也可以使用 TapGestureRecognizer。而在 Flutter 中有两种非常相似的方式:

  1. 如果 Widget 支持事件监听,那么向它传入一个方法并在方法中处理事件。例如,RaisedButton 有一个 onPressed 参数:

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

我如何处理 widget 上的其他手势?

在 Xamarin.Forms 中你可以在 VisualElement 中添加一个 GestureRecognizer。你通常只能使用 TapGestureRecognizerPinchGestureRecognizerPanGestureRecognizer、, SwipeGestureRecognizerDragGestureRecognizerDropGestureRecognizer,除非你构建了自己的实现。

在 Flutter 中,使用手势检测器,你可以监听到各种各样的手势,比如:

  • 单击

onTapDown
A pointer that might cause a tap has contacted the screen at a particular location.
onTapDown
当指尖在特定位置与屏幕接触产生点击事件。
onTapUp
A pointer that triggers a tap has stopped contacting the screen at a particular location.
onTapUp
当指尖触发的点击事件已经停止在特定位置与屏幕接触。
onTap
A tap has occurred.
onTap
一个点击事件已经发生。
onTapCancel
The pointer that previously triggered the onTapDown won’t cause a tap.
onTapCancel
触发了 onTapDown 事件之后的指尖没有导致点击事件。
  • 双击

onDoubleTap
The user tapped the screen at the same location twice in quick succession.
onDoubleTap
用户在同一位置连续快速点击屏幕两次。
  • 长按

onLongPress
A pointer has remained in contact with the screen at the same location for a long period of time.
onLongPress
指尖长时间保持与屏幕在同一位置的接触。
  • 垂直拖动

onVerticalDragStart
A pointer has contacted the screen and might begin to move vertically.
onVerticalDragStart
指尖与屏幕接触后,可能开始垂直移动。
onVerticalDragUpdate
A pointer in contact with the screen has moved further in the vertical direction.
onVerticalDragUpdate
指尖与屏幕接触并在垂直方向上移动得更远。
onVerticalDragEnd
A pointer that was previously in contact with the screen and moving vertically is no longer in contact with the screen and was moving at a specific velocity when it stopped contacting the screen.
onVerticalDragEnd
指尖在之前与屏幕接触并垂直移动,当不再与屏幕接触时触发这个事件。当它停止与屏幕接触时,它会以特定的速度移动。
  • 水平拖动

onHorizontalDragStart
A pointer has contacted the screen and might begin to move horizontally.
onHorizontalDragStart
指尖与屏幕接触,开始水平移动时触发。
onHorizontalDragUpdate
A pointer in contact with the screen has moved further in the horizontal direction.
onHorizontalDragUpdate
指尖与屏幕接触并在水平方向上移动得更远。
onHorizontalDragEnd
A pointer that was previously in contact with the screen and moving horizontally is no longer in contact with the screen and was moving at a specific velocity when it stopped contacting the screen.
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.0),
          ),
        ),
      ),
    );
  }
}

列表视图和适配器

在 Flutter 中,与列表视图等价的是什么?

在 Flutter 中与 ListView 等价的是……一个 ListView

在一个 Xamarin.Forms 的 ListView 中,你可以创建一个 ViewCell 或者 DataTemplateSelector,并将其传递到 ListView 中,该视图将用你的 DataTemplateSelector 或者 ViewCell 的返回数据渲染每一行。但是,你通常必须确保打开单元格回收,否则会遇到内存问题和会使滚动速度变慢。

由于 Flutter 中 widget 的不可变特性,你需要向 ListView 传递一个 widget 列表, 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.0),
        child: Text('Row $index'),
      ),
    );
  }

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

如何确定列表中被点击的元素?

在 Xamarin.Forms 中,ListView 拥有一个 ItemTapped 方法能找出哪个列表项被单击了。还有其他一些方法,比如检查 SelectedItemEventToCommand 的行为何时会发生更改。

而在 Flutter 里,需要通过 widget 传递进来的 touch 响应处理来实现。

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.0),
          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,就只需要更新视图模型中的列表。另一种方法是,你可以给属性 ItemsSource 分配一个新的 `List 。

在 Flutter 中,情况略有不同。如果你在 setState() 中更新了 widget 列表,你会发现展示的数据并不会立刻更新。这是因为当 setState() 被调用时, Flutter 的渲染引擎回去检索 widget 树是否有改变。当它获取到 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.0),
        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.0),
        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 需要两个关键参数:初始化列表长度和 item 构建函数。

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

最后且最重要的是,要注意 onTap() 函数不再重新创建列表,而是用 .add 添加给它的。

更多信息,请访问 编写你的第一个 Flutter 应用程序,第 1 部分编写你的第一个 Flutter 应用程序,第 2 部分

文本处理

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

在 Xamarin.Forms 中,你必须在每个原生项目中添加自定义字体。然后在你的 Element 中,你会使用 filename#fontnameFontFamily 属性分配字体名,在 iOS 中使用 fontname

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

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

然后将字体赋值给你的 Text Widget:

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

如何更改 · Widget 的样式?

除了字体,你还可以自定义 Text Widget 的其它样式元素。 Text Widget 的样式参数接收一个 TextStyle 对象,你可以在这个对象里自定义很多参数,例如:

  • color
  • decoration
  • decorationColor
  • decorationStyle
  • fontFamily
  • fontSize
  • fontStyle
  • fontWeight
  • hashCode
  • height
  • inherit
  • letterSpacing
  • textBaseline
  • wordSpacing

表单输入

如何检索用户输入?

Xamarin.Forms 的 element 允许你直接查询 element 来确定它的任何属性的状态,或者它被绑定到 ViewModel 中的属性。

在 Flutter 中检索信息是由专门的 widget 处理的,这是跟原来的习惯不同的。如果你有一个 TextFieldTextFormField,你可以提供一个 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.0),
        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 实用教程 中的 获取文本框的输入值 找到更多的信息和完整的代码清单。

在入口的占位符 (Placeholder) 与什么等价?

在 Xamarin.Forms 中,一些 Element 支持设置 Placeholder 属性。如:

  <Entry Placeholder="This is a hint">

在 Flutter 中,通过在文本 widget 的装饰器构造函数参数中添加 InputDecoration 对象,可以轻松地为输入显示「提示」或占位符文本。

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

如何显示验证错误的信息?

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

在 Flutter 中,我们将 InputDecoration 对象传递给文本 widget 的装饰器构造函数。

然而,你并不想一开始就显示错误信息。相反,当用户输入了无效的信息后,更新状态并传入一个新的 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 的开源 package 仓库中找到。有些包可能支持 iOS 上的本地集成,有些支持 Android,还有两者都兼而有之的。

如果在 Pub 上找不到适合你需求的插件,你可以 编写自己的插件在 Pub 上发布

如何访问 GPS 传感器?

使用 geolocator 社区插件.

如何访问照相机?

camera 插件被常用于相机功能的使用。

如何通过 Facebook 登录?

To log in with Facebook, use the

使用 flutter_facebook_login 社区插件实现 Facebook 登录功能。

如何使用 Firebase 特性?

官方插件 提供了 Firebase 的大多数功能。这些插件都是由 Flutter 团队维护的官方集成插件:

你可以在 Pub 网站上查找一些官方插件没有直接支持的功能的第三方 Firebase 插件。

如何构建自定义的原生集成?

如果有 Flutter 官方或社区第三方插件没有涵盖的平台特定的功能,你可以参考 开发包和插件 文档创建自己的插件。

简单地说,Flutter 的插件架构很像在 Android 中使用事件总线:你发出一条消息,让接收方处理并向你发回一个结果。在这种情况下,接收方是运行在 Android 或 iOS 上的原生代码。

主题(样式)

如何对应用使用主题?

Flutter 附带了一个内建的漂亮的 Material Design 实现,它处理了许多你通常会做的样式和主题需求。

Xamarin.Forms 确实有一个全局的 ResourceDictionary,可以为你的应用程序共享样式。另外,预览版目前还支持主题。

在 Flutter 中,你可以在最顶级 widget 中声明主题。

为了在应用中利用好 Material 组件,你可以在应用中声明一个顶层 Widget MaterialApp 作为入口。 MaterialApp 是一个包装了一系列 Widget 的为你给予便利的 Widget,而这些 Widget 通常是实现 Material Design 的应用所必须的。它基于 WidgetsApp 并添加了 Material 相关的功能。

你也可以使用 WidgetApp 作为应用的 Widget,它会提供一些相同的功能,但是不如 MaterialApp 提供的功能丰富。

如果要自定义任意子组件的颜色或者样式,给 MaterialApp Widget 传入一个 ThemeData 对象即可。例如,在下面的代码中,主色调设置为蓝色,文本选中颜色设置为红色。

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

数据库与本地存储

如何访问共享首选项或用户默认值?

Xamarin.Forms 开发者可能会熟悉 Xam.Plugins.Settings 插件。

在 Flutter 中,使用 shared_preferences 插件 就可以访问相同的功能。这个插件封装了 UserDefaults 和 Android 平台上的 SharedPreferences

在 Flutter 中如何访问 SQLite

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

在 Flutter 中,使用 SQFlite 插件来访问这个功能。

调试

我可以使用什么工具调试我的 Flutter 应用?

请使用 开发者工具 调试你的 Flutter 和 Dart 应用。

开发者工具包含了性能工具、检查堆栈、检视 widget 树、诊断信息记录、调试、执行代码行观察、调试内存泄漏和内存碎片等。有关更多信息,请参阅 开发者工具 文档。

通知

如何设置通知推送?

在 Android 中,你可以使用 Firebase Cloud Messaging 来为应用设置推送通知。

在 Flutter 中,则使用 firebase_messaging 插件实现此功能。想要获得更多关于使用 Firebase Cloud Messaging API 的信息,请查阅 firebase_messaging 插件文档。