跳到主内容

Flutter для разработчиков Xamarin.Forms

Узнайте, как применить знания разработчика Xamarin.Forms при создании приложений Flutter.

Этот документ предназначен для разработчиков Xamarin.Forms, желающих применить свои существующие знания для создания мобильных приложений с помощью Flutter. Если вы понимаете основы фреймворка Xamarin.Forms, вы можете использовать этот документ в качестве отправной точки для разработки Flutter.

Ваши знания и навыки в области Android и iOS ценны при разработке с помощью Flutter, поскольку Flutter полагается на конфигурацию родной операционной системы, аналогично тому, как вы настраиваете свои собственные проекты Xamarin.Forms. Фреймворк Flutter также похож на то, как вы создаете единый UI, который используется на нескольких платформах.

本文档可以像菜谱一样使用,您可以随意跳转并查找与您的需求最相关的问题。

Настройка проекта

#

Как запускается приложение?

#

Для каждой платформы в Xamarin.Forms вы вызываете метод LoadApplication, который создает новое приложение и запускает ваше приложение.

csharp
LoadApplication(new App());

В Flutter точка входа по умолчанию — main, где вы загружаете свое приложение Flutter.

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

В Xamarin.Forms вы присваиваете Page свойству MainPage в классе Application.

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

В Flutter "все является виджетом", даже само приложение. Следующий пример показывает MyApp, простой виджет приложения.

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 вы указываете виджет приложения, который содержит вашу корневую страницу. Вы можете использовать виджет MaterialApp, который поддерживает Material Design, или вы можете использовать виджет CupertinoApp, который поддерживает приложение в стиле iOS, или вы можете использовать виджет нижнего уровня WidgetsApp, который можно настроить любым способом.

Следующий код определяет главную страницу, stateful виджет. В Flutter все виджеты неизменяемы, но поддерживаются два типа виджетов: Stateful и Stateless. Примеры stateless виджета — заголовки, значки или изображения.

Следующий пример использует MaterialApp, который хранит свою корневую страницу в свойстве home.

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

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

Отсюда ваша фактическая первая страница — это другой Widget, в котором вы создаете свое состояние.

Stateful виджет, такой как MyHomePage ниже, состоит из двух частей. Первая часть, которая сама по себе неизменяема, создает объект State, который хранит состояние объекта. Объект State сохраняется в течение всего жизненного цикла виджета.

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

  final String title;

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

Объект State реализует метод build() для stateful виджета.

Когда состояние дерева виджетов изменяется, вызовите setState(), что приведет к перестроению этой части UI. Убедитесь, что вызываете 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 (также известный как дерево виджетов) является неизменяемым, что означает, что вы не можете изменить его состояние после создания. Вы изменяете поля в своем классе State, а затем вызываете setState(), чтобы перестроить все дерево виджетов снова.

Этот способ генерации UI отличается от Xamarin.Forms, но у этого подхода много преимуществ.

视图

#

Что в Flutter соответствует Page или Element в Xamarin.Forms?

#

ContentPage, TabbedPage, FlyoutPage — это все типы страниц, которые вы можете использовать в приложении Xamarin.Forms. Эти страницы затем содержат Elementы для отображения различных элементов управления. В Xamarin.Forms Entry или Button являются примерами Element.

В Flutter почти все является виджетом. Page, называемая Route в Flutter, является виджетом. Кнопки, индикаторы выполнения и контроллеры анимации — все это виджеты. При создании маршрута вы создаете дерево виджетов.

Flutter включает в себя библиотеку Material Components. Это виджеты, которые реализуют руководство по Material Design. Material Design — это гибкая система дизайна оптимизированная для всех платформ, включая iOS.

Но Flutter достаточно гибок и выразителен, чтобы реализовать любой язык дизайна. Например, на iOS вы можете использовать виджеты Cupertino, чтобы создать интерфейс, который выглядит как язык дизайна Apple iOS.

Как обновить виджеты?

#

В Xamarin.Forms каждый Page или Element является stateful классом, который имеет свойства и методы. Вы обновляете свой Element, обновляя свойство, и это распространяется на нативный элемент управления.

В Flutter виджеты Widget являются неизменяемыми, и вы не можете напрямую обновлять их, изменяя свойство, вместо этого вам нужно работать с состоянием виджета.

Именно здесь возникает концепция stateful и stateless виджетов. StatelessWidget — это то, что подразумевается — виджет без информации о состоянии.

StatelessWidgets полезны, когда часть пользовательского интерфейса, которую вы описываете, не зависит ни от чего, кроме информации о конфигурации в объекте.

Например, в Xamarin.Forms это похоже на размещение Image с вашим логотипом. Логотип не будет меняться во время выполнения, поэтому используйте StatelessWidget в Flutter.

Если вы хотите динамически изменить UI на основе данных, полученных после выполнения HTTP-запроса или взаимодействия с пользователем, то вам нужно работать с StatefulWidget и сообщить Flutter, что состояние виджета было обновлено, чтобы он мог обновить этот виджет.

Важно отметить, что как stateless, так и stateful виджеты ведут себя одинаково. Они перестраиваются каждый кадр, разница в том, что StatefulWidget имеет объект State, который хранит данные состояния между кадрами и восстанавливает их.

Если вы сомневаетесь, всегда помните это правило: если виджет меняется (например, из-за взаимодействия с пользователем), он stateful. Однако, если виджет реагирует на изменения, содержащий родительский виджет все равно может быть stateless, если он сам не реагирует на изменения.

Следующий пример показывает, как использовать StatelessWidget. Распространенным StatelessWidget является виджет Text. Если вы посмотрите реализацию виджета Text, вы увидите, что он наследуется от StatelessWidget.

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

Как видите, виджет Text не имеет информации о состоянии, связанной с ним, он отображает то, что передается в его конструкторах, и ничего больше.

Но что, если вы хотите, чтобы "I Like Flutter" менялось динамически, например, при нажатии FloatingActionButton?

Чтобы добиться этого, оберните виджет Text в 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),
      ),
    );
  }
}

Как расположить виджеты? Что соответствует XAML-файлу?

#

В Xamarin.Forms большинство разработчиков пишут макеты на XAML, хотя иногда и на C#. В Flutter вы пишете свои макеты деревом виджетов в коде.

以下示例展示了如何显示一个带填充的简单 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'),
      ),
    ),
  );
}

Вы можете просмотреть макеты, которые предлагает Flutter, в каталоге виджетов.

Как добавить или удалить Element из макета?

#

В Xamarin.Forms вам приходилось удалять или добавлять Element в коде. Это включало либо установку свойства Content, либо вызов Add() или Remove(), если это был список.

В Flutter, поскольку виджеты неизменяемы, прямого эквивалента нет. Вместо этого вы можете передать функцию родителю, которая возвращает виджет, и контролировать создание дочернего элемента с помощью логического флага.

Следующий пример показывает, как переключаться между двумя виджетами при нажатии пользователем FloatingActionButton

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

Как анимировать виджет?

#

В Xamarin.Forms вы создаете простые анимации, используя ViewExtensions, которые включают методы, такие как FadeTo и TranslateTo. Вы использовали бы эти методы в представлении для выполнения необходимых анимаций.

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 时,该转换会将 Widget 淡入为徽标

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

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

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Fade Demo',
      home: MyFadeTest(title: 'Fade Demo'),
    );
  }
}

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

  final String title;

  @override
  State<MyFadeTest> createState() => _MyFadeTest();
}

class _MyFadeTest extends State<MyFadeTest> with TickerProviderStateMixin {
  late final AnimationController controller;
  late final CurvedAnimation curve;

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.title)),
      body: Center(
        child: FadeTransition(
          opacity: curve,
          child: const FlutterLogo(size: 100),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          controller.forward();
        },
        tooltip: 'Fade',
        child: const Icon(Icons.brush),
      ),
    );
  }
}

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

Как рисовать/выводить графику на экран?

#

Xamarin.Forms никогда не имел встроенного способа рисовать непосредственно на экране. Многие использовали SkiaSharp, если им требовалось пользовательское изображение. В Flutter у вас есть прямой доступ к Skia Canvas, и вы можете легко рисовать на экране.

Flutter имеет два класса, которые помогают вам рисовать на холсте: CustomPaint и CustomPainter, последний из которых реализует ваш алгоритм для рисования на холсте.

Чтобы узнать, как реализовать painter подписи в Flutter, см. ответ Коллина на Custom Paint.

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

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

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

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

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

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

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

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

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

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

  final List<Offset?> points;

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

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

Где находится прозрачность виджета?

#

В Xamarin.Forms все VisualElementы имеют Opacity. В Flutter вам нужно обернуть виджет в Opacity виджет, чтобы добиться этого.

Как создавать собственные виджеты?

#

在 Xamarin.Forms 中,你通常会继承 VisualElement,或者使用现有的 VisualElement,来重写和实现方法以达到期望的行为。

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

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

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

  final String label;

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

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

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

Как осуществлять навигацию между страницами?

#

在 Xamarin.Forms 中,NavigationPage 类提供了一种分层导航体验,用户可以通过页面进行前后导航。

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

路由大致映射到 Page。 导航器的工作方式类似于 Xamarin.Forms 的 NavigationPage,它可以根据你想要导航到视图或从视图返回来 push()pop() 路由。

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

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

以下示例构建了一个 Map

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

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

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

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

async/await 与 .NET 实现非常相似,并在 Async 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() в Xamarin.Forms?

#

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

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

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

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

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

一旦等待的网络调用完成,通过调用 setState() 来更新 UI,这将触发 widget 子树的重建并更新数据。

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

dart
import 'dart:convert';

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

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

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

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

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

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

class _SampleAppPageState extends State<SampleAppPage> {
  List<Map<String, Object?>> data = [];

  @override
  void initState() {
    super.initState();
    loadData();
  }

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

  Widget getRow(int index) {
    return Padding(
      padding: const EdgeInsets.all(10),
      child: Text('Row ${data[index]['title']}'),
    );
  }

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

有关在后台执行工作以及 Flutter 与 Android 的不同之处的更多信息,请参阅下一节。

Как перенести задачу в фоновый поток?

#

由于 Flutter 是单线程的并且运行事件循环,因此你无需担心线程管理或生成后台线程。 这与 Xamarin.Forms 非常相似。 如果你正在执行 I/O 绑定工作,例如磁盘访问或网络调用,那么你可以安全地使用 async/await,一切就绪了。

另一方面,如果你需要执行保持 CPU 繁忙的计算密集型工作,那么你希望将其移动到 Isolate 以避免阻塞事件循环,就像你将任何类型的工作移出主线程一样。 这类似于在 Xamarin.Forms 中通过 Task.Run() 将内容移动到另一个线程。

对于 I/O 密集型工作,将函数声明为 async 函数,并在函数内部 await 长时间运行的任务

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

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

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

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

以下示例以一个简单的 isolate 形式展示了如何将数据共享回主线程以更新 UI。

dart
Future<void> loadData() async {
  final ReceivePort receivePort = ReceivePort();
  await Isolate.spawn(dataLoader, receivePort.sendPort);

  // The 'echo' isolate sends its SendPort as the first message
  final SendPort sendPort = await receivePort.first as SendPort;
  final List<Map<String, dynamic>> msg = await sendReceive(
    sendPort,
    'https://jsonplaceholder.typicode.com/posts',
  );
  setState(() {
    data = msg;
  });
}

// The entry point for the isolate
static Future<void> dataLoader(SendPort sendPort) async {
  // Open the ReceivePort for incoming messages.
  final ReceivePort port = ReceivePort();

  // Notify any other isolates what port this isolate listens to.
  sendPort.send(port.sendPort);
  await for (final dynamic msg in port) {
    final String url = msg[0] as String;
    final SendPort replyTo = msg[1] as SendPort;

    final Uri dataURL = Uri.parse(url);
    final http.Response response = await http.get(dataURL);
    // Lots of JSON to parse
    replyTo.send(jsonDecode(response.body) as List<Map<String, dynamic>>);
  }
}

Future<List<Map<String, dynamic>>> sendReceive(SendPort port, String msg) {
  final ReceivePort response = ReceivePort();
  port.send(<dynamic>[msg, response.sendPort]);
  return response.first as Future<List<Map<String, dynamic>>>;
}

在这里,dataLoader() 是在自己的单独执行线程中运行的 Isolate。 在 isolate 中,你可以执行更密集的 CPU 处理(例如解析大型 JSON),或者执行计算密集型的数学运算,例如加密或信号处理。

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

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

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

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

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

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

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

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

class _SampleAppPageState extends State<SampleAppPage> {
  List<Map<String, Object?>> data = [];

  @override
  void initState() {
    super.initState();
    loadData();
  }

  bool get showLoadingDialog => data.isEmpty;

  Future<void> loadData() async {
    final ReceivePort receivePort = ReceivePort();
    await Isolate.spawn(dataLoader, receivePort.sendPort);

    // The 'echo' isolate sends its SendPort as the first message
    final SendPort sendPort = await receivePort.first as SendPort;
    final List<Map<String, dynamic>> msg = await sendReceive(
      sendPort,
      'https://jsonplaceholder.typicode.com/posts',
    );
    setState(() {
      data = msg;
    });
  }

  // The entry point for the isolate
  static Future<void> dataLoader(SendPort sendPort) async {
    // Open the ReceivePort for incoming messages.
    final ReceivePort port = ReceivePort();

    // Notify any other isolates what port this isolate listens to.
    sendPort.send(port.sendPort);
    await for (final dynamic msg in port) {
      final String url = msg[0] as String;
      final SendPort replyTo = msg[1] as SendPort;

      final Uri dataURL = Uri.parse(url);
      final http.Response response = await http.get(dataURL);
      // Lots of JSON to parse
      replyTo.send(jsonDecode(response.body) as List<Map<String, dynamic>>);
    }
  }

  Future<List<Map<String, dynamic>>> sendReceive(SendPort port, String msg) {
    final ReceivePort response = ReceivePort();
    port.send(<dynamic>[msg, response.sendPort]);
    return response.first as Future<List<Map<String, dynamic>>>;
  }

  Widget getBody() {
    if (showLoadingDialog) {
      return getProgressDialog();
    }
    return getListView();
  }

  Widget getProgressDialog() {
    return const Center(child: CircularProgressIndicator());
  }

  ListView getListView() {
    return ListView.builder(
      itemCount: data.length,
      itemBuilder: (context, index) {
        return getRow(index);
      },
    );
  }

  Widget getRow(int index) {
    return Padding(
      padding: const EdgeInsets.all(10),
      child: Text('Row ${data[index]['title']}'),
    );
  }

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

Как выполнять сетевые запросы?

#

在 Xamarin.Forms 中,你将使用 HttpClient。 当你使用流行的 http 时,在 Flutter 中进行网络调用非常容易。 这抽象了许多你通常自己实现的网络功能,使进行网络调用变得简单。

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

yaml
dependencies:
  http: ^1.4.0

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

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

Как отображать индикатор выполнения для длительной задачи?

#

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

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

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

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

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

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

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

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

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

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

class _SampleAppPageState extends State<SampleAppPage> {
  List<Map<String, Object?>> data = [];

  @override
  void initState() {
    super.initState();
    loadData();
  }

  bool get showLoadingDialog => data.isEmpty;

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

  Widget getBody() {
    if (showLoadingDialog) {
      return getProgressDialog();
    }
    return getListView();
  }

  Widget getProgressDialog() {
    return const Center(child: CircularProgressIndicator());
  }

  ListView getListView() {
    return ListView.builder(
      itemCount: data.length,
      itemBuilder: (context, index) {
        return getRow(index);
      },
    );
  }

  Widget getRow(int index) {
    return Padding(
      padding: const EdgeInsets.all(10),
      child: Text('Row ${data[index]['title']}'),
    );
  }

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

Структура проекта и ресурсы

#

Где хранить файлы изображений?

#

Xamarin.Forms 没有平台无关的存储图像的方式,你必须将图像放在 iOS 的 xcasset 文件夹中,或者在 Android 的各种 drawable 文件夹中。

虽然 Android 和 iOS 将资源和资产视为不同的项目,但 Flutter 应用程序只有资产。 将位于 Android 的 Resources/drawable-* 文件夹中的所有资源都放在 Flutter 的资产文件夹中。

Flutter 遵循类似于 iOS 的简单基于密度的格式。 资产可以是 1.0x2.0x3.0x 或任何其他乘数。 Flutter 没有 dp,但有逻辑像素,基本上与设备无关像素相同。 Flutter 的 devicePixelRatio 表达单个逻辑像素中的物理像素比率。

Android 密度限定符的等效项是

Android 密度限定符Flutter 像素比率
ldpi0.75x
mdpi1.0x
hdpi1.5x
xhdpi2.0x
xxhdpi3.0x
xxxhdpi4.0x

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

例如,要将名为 my_icon.png 的新图像资产添加到我们的 Flutter 项目,并决定它应该位于我们任意称为 images 的文件夹中,你将基本图像(1.0x)放在 images 文件夹中,并将所有其他变体放在带有适当比例乘数的子文件夹中

images/my_icon.png       // Base: 1.0x image
images/2.0x/my_icon.png  // 2.0x image
images/3.0x/my_icon.png  // 3.0x image

接下来,你需要在 pubspec.yaml 文件中声明这些图像

yaml
assets:
 - images/my_icon.png

你可以在 Image.asset widget 中直接访问你的图像

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

或者使用 AssetImage

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

有关更多详细信息,请参阅 添加资产和图像

Где хранить строки? Как обрабатывать локализацию?

#

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

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

你可以这样访问你的字符串

dart
Text(Strings.welcomeMessage);

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

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

要使用 flutter_localizations 包,请在 app widget 上指定 localizationsDelegatessupportedLocales

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

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

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

委托包含实际的本地化值,而 supportedLocales 定义了应用程序支持的语言环境。 上面的示例使用 MaterialApp,因此它同时具有用于基本 widget 本地化值的 GlobalWidgetsLocalizations 和用于 Material widget 本地化的 MaterialWidgetsLocalizations。 如果你使用 WidgetsApp 作为你的应用程序,则不需要后者。 请注意,这两个委托包含“默认”值,但如果你希望本地化它们,则需要为自己的应用程序的可本地化文案提供一个或多个委托。

初始化时,WidgetsApp(或 MaterialApp)会为你创建一个 Localizations widget,并带有你指定的委托。 设备的当前语言环境始终可以从当前上下文(以 Locale 对象的形式)的 Localizations widget 或使用 Window.locale 访问。

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

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

Где находится файл проекта?

#

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

Что соответствует NuGet? Как добавить зависимости?

#

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

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

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

通常,使用 pubspec.yaml 声明要在 Flutter 中使用的外部依赖项。 查找 Flutter 包的好地方是 pub.dev

Жизненный цикл приложения

#

Как прослушивать события жизненного цикла приложения?

#

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

可观察的生命周期事件有

inactive

应用程序处于非活动状态,未接收用户输入。 此事件仅适用于 iOS。

paused

应用程序当前对用户不可见,未响应用户输入,但在后台运行。

resumed

应用程序可见且响应用户输入。

暂停中

应用程序被暂时暂停。此事件仅适用于 Android。

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

布局

#

Что соответствует StackLayout?

#

在 Xamarin.Forms 中,您可以创建一个 StackLayout,其 Orientation 为水平或垂直。Flutter 有类似的方法,但是您将使用 RowColumn 组件。

如果您注意到这两个代码示例除了 RowColumn 组件外是相同的。子组件相同,此功能可用于开发可以随时间更改的具有相同子组件的丰富布局。

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

Что соответствует Grid?

#

Grid 的最接近等效项是 GridView。这比您在 Xamarin.Forms 中习惯的强大得多。GridView 在内容超出其可见空间时提供自动滚动。

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

您可能在 Xamarin.Forms 中使用 Grid 来实现覆盖其他组件的组件。在 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 组件。您只需将要可滚动的任何内容填充到 Widget 中即可。

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

如果您有很多想要包装在滚动中的项目,即使是不同类型的 Widget,您可能需要使用 ListView。这可能看起来有些过头,但在 Flutter 中,这比 Xamarin.Forms ListView 优化程度更高,且开销更小,后者依赖于平台特定的控件。

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

Как обрабатывать переходы в альбомную ориентацию в Flutter?

#

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

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

手势检测和触摸事件处理

#

Как добавить GestureRecognizers к виджету в Flutter?

#

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

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

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

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

Как обрабатывать другие жесты на виджетах?

#

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

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

  • 点击
onTapDown

可能导致点击的指针已在特定位置接触屏幕。

onTapUp

触发点击的指针已在特定位置停止接触屏幕。

onTap

已发生点击。

onTapCancel

先前触发 onTapDown 的指针不会导致点击。

  • 双击
onDoubleTap

用户在短时间内连续两次点击屏幕上的相同位置。

  • 长按
onLongPress

指针在同一位置长时间保持与屏幕接触。

  • 垂直拖动
onVerticalDragStart

指针已接触屏幕并可能开始垂直移动。

onVerticalDragUpdate

与屏幕接触的指针在垂直方向上移动得更远。

onVerticalDragEnd

之前与屏幕接触并垂直移动的指针不再与屏幕接触,并且在停止接触屏幕时以特定速度移动。

  • 水平拖动
onHorizontalDragStart

指针已接触屏幕并可能开始水平移动。

onHorizontalDragUpdate

与屏幕接触的指针在水平方向上移动得更远。

onHorizontalDragEnd

先前与屏幕接触并水平移动的指针不再与屏幕接触,并且在停止与屏幕接触时以特定速度移动。

以下示例展示了一个 GestureDetector,它在双击时旋转 Flutter 徽标

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

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

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

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

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

ListViews и адаптеры

#

Что в Flutter соответствует ListView?

#

Flutter 中 ListView 的等效项是……ListView

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

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

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

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

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

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

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

  List<Widget> _getListData() {
    return List<Widget>.generate(
      100,
      (index) =>
          Padding(padding: const EdgeInsets.all(10), child: Text('Row $index')),
    );
  }

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

Как узнать, на какой элемент списка был сделан клик?

#

在 Xamarin.Forms 中,ListView 有一个 ItemTapped 方法来确定单击了哪个项目。您可能还使用了许多其他技术,例如检查 SelectedItemEventToCommand 行为何时更改。

在 Flutter 中,使用传递的 Widget 提供的触摸处理。

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

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

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

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

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

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

class _SampleAppPageState extends State<SampleAppPage> {
  List<Widget> _getListData() {
    return List<Widget>.generate(
      100,
      (index) => GestureDetector(
        onTap: () {
          developer.log('Row $index tapped');
        },
        child: Padding(
          padding: const EdgeInsets.all(10),
          child: Text('Row $index'),
        ),
      ),
    );
  }

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

Как динамически обновлять ListView?

#

在 Xamarin.Forms 中,如果您将 ItemsSource 属性绑定到 ObservableCollection,则只需在 ViewModel 中更新列表。或者,您可以将新的 List 分配给 ItemSource 属性。

在 Flutter 中,事情略有不同。如果您在 setState() 方法中更新 Widget 列表,您会很快发现您的数据在视觉上没有更改。这是因为当调用 setState() 时,Flutter 渲染引擎会查看 Widget 树以查看是否有任何更改。当它到达您的 ListView 时,它会执行 == 检查,并确定这两个 ListView 相同。没有更改,因此不需要更新。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

与其创建 ListView,不如创建一个 ListView.builder,它接受两个关键参数:列表的初始长度和 item builder 函数。

item builder 函数类似于 Android adapter 中的 getView 函数;它接受一个位置,并返回要在该位置渲染的行。

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

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

Работа с текстом

#

Как установить пользовательские шрифты для текстовых виджетов?

#

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

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

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

然后将字体分配给您的 Text Widget

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

Как стилизовать текстовые виджеты?

#

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

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

表单输入

#

Как получить ввод от пользователя?

#

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

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

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

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

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

class _MyFormState extends State<MyForm> {
  /// Create a text controller and use it to retrieve the current value
  /// of the TextField.
  final TextEditingController myController = TextEditingController();

  @override
  void dispose() {
    // Clean up the controller when disposing of the widget.
    myController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Retrieve Text Input')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: TextField(controller: myController),
      ),
      floatingActionButton: FloatingActionButton(
        // When the user presses the button, show an alert dialog with the
        // text that the user has typed into our text field.
        onPressed: () {
          showDialog(
            context: context,
            builder: (context) {
              return AlertDialog(
                // Retrieve the text that the user has entered using the
                // TextEditingController.
                content: Text(myController.text),
              );
            },
          );
        },
        tooltip: 'Show me the value!',
        child: const Icon(Icons.text_fields),
      ),
    );
  }
}

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

Что соответствует Placeholder в Entry?

#

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

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

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

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

Как отображать ошибки валидации?

#

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

在 Flutter 中,您将 InputDecoration 对象传递给文本 Widget 的 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 应用程序仍然托管在 native app 的 ViewControllerActivity 中作为视图,但您没有直接访问权限,也没有本机框架的访问权限。

这并不意味着 Flutter 应用程序无法与这些本机 API 或您拥有的任何本机代码进行交互。Flutter 提供了 平台通道,这些通道与托管 Flutter 视图的 ViewControllerActivity 以及它运行的 iOS 或 Android 框架进行通信和交换数据。平台通道本质上是一种异步消息传递机制,它将 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,您可以在应用程序中共享样式。或者,目前正在预览 Theme 支持。

在 Flutter 中,您在顶级 Widget 中声明主题。

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

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

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        textSelectionTheme: const TextSelectionThemeData(
          selectionColor: Colors.red,
        ),
      ),
      home: const SampleAppPage(),
    );
  }
}

Базы данных и локальное хранилище

#

Как получить доступ к общим настройкам или UserDefaults?

#

Xamarin.Forms 开发人员可能熟悉 Xam.Plugins.Settings 插件。

在 Flutter 中,使用 shared_preferences 插件来访问等效功能。该插件封装了 UserDefaults 和 Android 等效项 SharedPreferences 的功能。

Как получить доступ к SQLite в Flutter?

#

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

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

调试

#

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

#

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

DevTools 支持性能分析、检查堆、检查 widget 树、日志诊断、调试、观察已执行代码行、调试内存泄漏和内存碎片。有关更多信息,请查看 DevTools 文档。

通知

#

Как настроить push-уведомления?

#

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

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