跳到主内容

Flutter for Android developers

了解如何将 Android 开发人员知识应用于构建 Flutter 应用程序。

本文档面向希望将现有的 Android 知识应用于使用 Flutter 构建移动应用程序的 Android 开发人员。如果您了解 Android 框架的基础知识,则可以使用本文档作为 Flutter 开发的起点。

您的 Android 知识和技能在构建 Flutter 时非常有价值,因为 Flutter 依赖于移动操作系统来实现许多功能和配置。Flutter 是一种构建移动 UI 的新方法,但它具有一个插件系统,可以与 Android(和 iOS)进行通信以执行非 UI 任务。如果您是 Android 专家,则无需重新学习所有内容即可使用 Flutter。

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

视图

#

Flutter 中 View 的等效项是什么?

#

在 Android 中,View 是显示在屏幕上的所有内容的基础。按钮、工具栏和输入,一切都是 View。在 Flutter 中,View 的粗略等效项是 Widget。Widgets 并不能完全映射到 Android views,但在您熟悉 Flutter 的工作方式时,您可以将它们视为“声明和构建 UI 的方式”。

但是,这些与 View 有一些区别。首先,widgets 具有不同的生命周期:它们是不可变的,并且仅在需要更改时才存在。每当 widgets 或其状态发生更改时,Flutter 框架都会创建一个新的 widget 实例树。相比之下,Android view 绘制一次,除非调用 invalidate,否则不会重新绘制。

Flutter 的 widgets 是轻量级的,部分原因在于它们的不可变性。由于它们本身不是 views,并且没有直接绘制任何内容,而是 UI 和语义的描述,这些描述在底层被“膨胀”成实际的 view 对象。

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

但是 Flutter 灵活且富有表现力,足以实现任何设计语言。例如,在 iOS 上,您可以使用 Cupertino widgets 来生成看起来像 Apple 的 iOS 设计语言 的界面。

如何更新 widgets?

#

在 Android 中,您通过直接修改 views 来更新它们。但是,在 Flutter 中,Widgets 是不可变的,并且不能直接更新,而是必须使用 widget 的状态。

这就是 StatefulStateless widgets 的概念由来。StatelessWidget 就是字面意思——一个没有状态信息的 widget。

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

例如,在 Android 中,这类似于放置一个带有您徽标的 ImageView。徽标在运行时不会更改,因此在 Flutter 中使用 StatelessWidget

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

重要的是要注意,无论是有状态还是无状态的 widgets,其核心行为都相同。它们每帧都会重建,区别在于 StatefulWidget 具有一个 State 对象,该对象跨帧存储状态数据并恢复它。

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

以下示例显示了如何使用 StatelessWidget。一个常见的 StatelessWidgetText widget。如果您查看 Text widget 的实现,您会发现它继承自 StatelessWidget

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

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

但是,如果您想让“I Like Flutter” 动态更改,例如,单击 FloatingActionButton 时会发生什么?

为此,将 Text widget 包装在 StatefulWidget 中,并在用户单击按钮时对其进行更新。

例如

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

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

class SampleApp extends StatelessWidget {
  const SampleApp({super.key});
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const 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),
      ),
    );
  }
}

如何布局我的 widgets?我的 XML 布局文件在哪里?

#

在 Android 中,您在 XML 中编写布局,但在 Flutter 中,您使用 widget 树编写布局。

以下示例展示了如何显示一个带填充的简单 Widget

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

您可以在 widget catalog 中查看 Flutter 提供的某些布局。

如何添加或删除布局中的组件?

#

在 Android 中,您通过在父级上调用 addChild()removeChild() 来动态添加或删除子 views。在 Flutter 中,由于 widgets 是不可变的,因此没有 addChild() 的直接等效项。相反,您可以将一个函数传递给父级,该函数返回一个 widget,并使用布尔标志来控制该子级的创建。

例如,以下是如何在单击 FloatingActionButton 时在两个 widgets 之间切换:

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

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

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const 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');
    } else {
      return ElevatedButton(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 添加动画?

#

在 Android 中,您可以使用 XML 创建动画,或者调用 view 上的 animate() 方法。在 Flutter 中,通过将 widgets 包装在动画 widget 中来为 widgets 添加动画。

在 Flutter 中,使用 AnimationController,它是一个 Animation<double>,可以暂停、查找、停止和反转动画。它需要一个 Ticker,该 ticker 在 vsync 发生时发出信号,并在运行期间每帧在 0 和 1 之间进行线性插值。然后,您创建一个或多个 Animation 并将其附加到控制器。

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

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

以下示例展示了如何编写一个 FadeTransition,当您按下 FloatingActionButton 时,该转换会将 Widget 淡入为徽标

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

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

class FadeAppTest extends StatelessWidget {
  const FadeAppTest({super.key});
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Fade Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const 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 AnimationController controller;
  late 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(
        tooltip: 'Fade',
        onPressed: () {
          controller.forward();
        },
        child: const Icon(Icons.brush),
      ),
    );
  }
}

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

如何使用 Canvas 进行绘制/绘画?

#

在 Android 中,您将使用 CanvasDrawable 将图像和形状绘制到屏幕上。Flutter 也有一个类似的 Canvas API,因为它基于相同的低级渲染引擎 Skia。因此,在 Flutter 中绘制到 canvas 对于 Android 开发人员来说是一项非常熟悉的任务。

Flutter 有两个类可以帮助您绘制到 canvas:CustomPaintCustomPainter,后者实现了您的算法来绘制到 canvas。

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

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

void main() => runApp(const MaterialApp(home: DemoApp()));

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

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

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

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

class SignatureState extends State<Signature> {
  List<Offset?> _points = <Offset>[];
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanUpdate: (details) {
        setState(() {
          RenderBox? referenceBox = context.findRenderObject() as RenderBox;
          Offset localPosition = referenceBox.globalToLocal(
            details.globalPosition,
          );
          _points = List.from(_points)..add(localPosition);
        });
      },
      onPanEnd: (details) => _points.add(null),
      child: CustomPaint(
        painter: SignaturePainter(_points),
        size: Size.infinite,
      ),
    );
  }
}

class SignaturePainter extends CustomPainter {
  SignaturePainter(this.points);
  final List<Offset?> points;
  @override
  void paint(Canvas canvas, Size size) {
    var 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;
}

如何构建自定义 widgets?

#

在 Android 中,您通常通过继承 View 或使用预先存在的 view 来覆盖并实现实现所需行为的方法。

在 Flutter 中,通过 组合 较小的 widgets(而不是扩展它们)来构建自定义 widget。这有点类似于在 Android 中实现自定义 ViewGroup,其中所有构建块都已经存在,但您提供不同的行为——例如,自定义布局逻辑。

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

dart
class CustomButton extends StatelessWidget {
  final String label;

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

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

Intents

#

Flutter 中 Intent 的等效项是什么?

#

在 Android 中,Intent 有两个主要用例:在 Activities 之间导航以及与组件通信。另一方面,Flutter 没有 intents 的概念,尽管您仍然可以通过本机集成(使用 插件)启动 intents。

Flutter 实际上并没有像 Activity 和 Fragment 那样的直接等价物;相反,在 Flutter 中,您使用 NavigatorRoute 在屏幕之间导航,所有这些都在同一个 Activity 中进行。

Route 是对应用程序的“屏幕”或“页面”的抽象,而 Navigator 是一个管理路由的 Widget。Route 大致映射到 Activity,但它不具有相同的含义。Navigator 可以推送和弹出路由,从而在屏幕之间移动。Navigator 的工作方式就像一个堆栈,您可以在其上 push() 新的路由以进行导航,并从中 pop() 路由以“返回”。

在 Android 中,您在应用程序的 AndroidManifest.xml 中声明您的 Activity。

在 Flutter 中,您有几种选择可以在页面之间导航

  • 指定一个路由名称的 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'),
      },
    ),
  );
}

通过 push 其名称到 Navigator 来导航到路由。

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

Intent 的另一个常用用途是调用外部组件,例如相机或文件选择器。为此,您需要创建一个原生平台集成(或使用现有的 插件)。

要了解如何构建原生平台集成,请参阅 开发包和插件

如何在 Flutter 中处理来自外部应用程序的传入 intents?

#

Flutter 可以通过直接与 Android 层通信并请求共享的数据来处理来自 Android 的传入 Intent。

以下示例在原生 Activity 上注册一个文本共享 Intent 过滤器,以便其他应用程序可以将文本共享给我们的 Flutter 应用程序。

基本流程是,我们首先在 Android 原生侧(在我们的 Activity 中)处理共享的文本数据,然后等待 Flutter 请求数据以使用 MethodChannel 提供它。

首先,在 AndroidManifest.xml 中注册所有 Intent 的 Intent 过滤器

xml
<activity
  android:name=".MainActivity"
  android:launchMode="singleTop"
  android:theme="@style/LaunchTheme"
  android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection"
  android:hardwareAccelerated="true"
  android:windowSoftInputMode="adjustResize">
  <!-- ... -->
  <intent-filter>
    <action android:name="android.intent.action.SEND" />
    <category android:name="android.intent.category.DEFAULT" />
    <data android:mimeType="text/plain" />
  </intent-filter>
</activity>

然后在 MainActivity 中,处理 Intent,从 Intent 中提取共享的文本,并保留它。当 Flutter 准备好处理时,它会使用平台通道请求数据,数据将从原生侧发送过来

java
package com.example.shared;

import android.content.Intent;
import android.os.Bundle;

import androidx.annotation.NonNull;

import io.flutter.plugin.common.MethodChannel;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugins.GeneratedPluginRegistrant;

public class MainActivity extends FlutterActivity {

  private String sharedText;
  private static final String CHANNEL = "app.channel.shared.data";

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Intent intent = getIntent();
    String action = intent.getAction();
    String type = intent.getType();

    if (Intent.ACTION_SEND.equals(action) && type != null) {
      if ("text/plain".equals(type)) {
        handleSendText(intent); // Handle text being sent
      }
    }
  }

  @Override
  public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
      GeneratedPluginRegistrant.registerWith(flutterEngine);

      new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
              .setMethodCallHandler(
                      (call, result) -> {
                          if (call.method.contentEquals("getSharedText")) {
                              result.success(sharedText);
                              sharedText = null;
                          }
                      }
              );
  }

  void handleSendText(Intent intent) {
    sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
  }
}

最后,当 Widget 渲染时,从 Flutter 侧请求数据

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

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

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample Shared App Handler',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const SampleAppPage(),
    );
  }
}

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

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

class _SampleAppPageState extends State<SampleAppPage> {
  static const platform = MethodChannel('app.channel.shared.data');
  String dataShared = 'No data';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(body: Center(child: Text(dataShared)));
  }

  Future<void> getSharedText() async {
    var sharedData = await platform.invokeMethod('getSharedText');
    if (sharedData != null) {
      setState(() {
        dataShared = sharedData as String;
      });
    }
  }
}

startActivityForResult() 的等效项是什么?

#

Navigator 类处理 Flutter 中的路由,并用于从您推送到堆栈上的路由中获取结果。这是通过 await 等待 push() 返回的 Future 来完成的。

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

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

然后,在您的 Location 路由内部,一旦用户选择了他们的位置,您可以 pop 堆栈并返回结果

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

Async UI

#

Flutter 中 runOnUiThread() 的等效项是什么?

#

Dart 具有单线程执行模型,并支持 Isolate(一种在另一个线程上运行 Dart 代码的方式)、事件循环和异步编程。除非您生成一个 Isolate,否则您的 Dart 代码将在主 UI 线程中运行,并由事件循环驱动。Flutter 的事件循环等同于 Android 的主 Looper——也就是说,附加到主线程的 Looper

Dart 的单线程模型并不意味着您需要将所有内容作为阻塞操作来运行,从而导致 UI 冻结。与 Android 不同,Android 要求您始终保持主线程空闲,在 Flutter 中,使用 Dart 语言提供的异步设施(例如 async/await)来执行异步工作。如果您使用 C#、Javascript 或 Kotlin 的协程,您可能熟悉 async/await 范例。

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

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

一旦 await 的网络调用完成,通过调用 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 MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const SampleAppPage(),
    );
  }
}

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

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

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

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

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

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

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

}

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

如何将工作移动到后台线程?

#

在 Android 中,当您想要访问网络资源时,通常会移动到后台线程并执行工作,以免阻塞主线程并避免 ANR。例如,您可能正在使用 AsyncTaskLiveDataIntentServiceJobScheduler 作业或使用在后台线程上运行的调度器的 RxJava 管道。

由于 Flutter 是单线程的并且运行一个事件循环(如 Node.js),因此您不必担心线程管理或生成后台线程。如果您正在执行 I/O 绑定工作,例如磁盘访问或网络调用,那么您可以安全地使用 async/await,一切就绪了。另一方面,如果您需要执行占用大量 CPU 的工作,请将其移动到 Isolate 以避免阻塞事件循环,就像您会将任何类型的工作从 Android 的主线程中移开一样。

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

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

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

在 Android 中,当您扩展 AsyncTask 时,通常会覆盖 3 个方法,即 onPreExecute()doInBackground()onPostExecute()。在 Flutter 中没有与之对应的功能,因为您 await 一个长时间运行的函数,并且 Dart 的事件循环会处理其余部分。

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

Isolate 是单独的执行线程,它们与主执行内存堆不共享任何内存。这意味着您无法访问主线程中的变量,也无法通过调用 setState() 更新 UI。与 Android 线程不同,Isolate 忠于其名称,无法共享内存(例如,以静态字段的形式)。

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

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

  // The 'echo' isolate sends its SendPort as the first message.
  SendPort sendPort = await receivePort.first as SendPort;

  final msg =
      await sendReceive(
            sendPort,
            'https://jsonplaceholder.typicode.com/posts',
          )
          as List<Object?>;
  final posts = msg.cast<Map<String, Object?>>();

  setState(() {
    widgets = posts;
  });
}

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

  // Notify any other isolates what port this isolate listens to.
  sendPort.send(port.sendPort);

  await for (var msg in port) {
    String dataUrl = msg[0] as String;
    SendPort replyTo = msg[1] as SendPort;

    http.Response response = await http.get(Uri.parse(dataUrl));
    // Lots of JSON to parse
    replyTo.send(jsonDecode(response.body));
  }
}

Future<Object?> sendReceive(SendPort port, Object? msg) {
  ReceivePort response = ReceivePort();
  port.send([msg, response.sendPort]);
  return response.first;
}

在这里,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 MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const SampleAppPage(),
    );
  }
}

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

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

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

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

  Widget getBody() {
    bool showLoadingDialog = widgets.isEmpty;
    if (showLoadingDialog) {
      return getProgressDialog();
    } else {
      return getListView();
    }
  }

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

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

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

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

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

    // The 'echo' isolate sends its SendPort as the first message.
    SendPort sendPort = await receivePort.first as SendPort;

    final msg =
        await sendReceive(
              sendPort,
              'https://jsonplaceholder.typicode.com/posts',
            )
            as List<Object?>;
    final posts = msg.cast<Map<String, Object?>>();

    setState(() {
      widgets = posts;
    });
  }

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

    // Notify any other isolates what port this isolate listens to.
    sendPort.send(port.sendPort);

    await for (var msg in port) {
      String dataUrl = msg[0] as String;
      SendPort replyTo = msg[1] as SendPort;

      http.Response response = await http.get(Uri.parse(dataUrl));
      // Lots of JSON to parse
      replyTo.send(jsonDecode(response.body));
    }
  }

  Future<Object?> sendReceive(SendPort port, Object? msg) {
    ReceivePort response = ReceivePort();
    port.send([msg, response.sendPort]);
    return response.first;
  }

}

Flutter 中 OkHttp 的等效项是什么?

#

当您使用流行的 http 时,在 Flutter 中进行网络调用非常容易。

虽然 http 包不具备 OkHttp 中的所有功能,但它抽象了您通常需要自己实现的大量网络,使其成为进行网络调用的简单方法。

要将 http 包添加为依赖项,请运行 flutter pub add

flutter pub add http

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

dart
import 'dart:developer' as developer;
import 'package:http/http.dart' as http;

Future<void> loadData() async {
  var dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
  http.Response response = await http.get(dataURL);
  developer.log(response.body);
}

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

#

在 Android 中,您通常会在 UI 中显示一个 ProgressBar 视图,同时在后台线程上执行长时间运行的任务。

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

在以下示例中,build 函数被分成三个不同的函数。如果 showLoadingDialogtrue(当 widgets.isEmpty 时),则渲染 ProgressIndicator。否则,渲染从网络调用返回的数据的 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 MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const SampleAppPage(),
    );
  }
}

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

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

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

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

  Widget getBody() {
    bool showLoadingDialog = widgets.isEmpty;
    if (showLoadingDialog) {
      return getProgressDialog();
    } else {
      return getListView();
    }
  }

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

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

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

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

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

项目结构 & 资源

#

我的分辨率相关图像文件存储在哪里?

#

虽然 Android 将资源和资产视为不同的项目,但 Flutter 应用程序只有资产。所有将位于 Android 上 res/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 会获取它们。

存储在原生资产文件夹中的资产使用 Android 的 AssetManager 在原生侧访问

kotlin
val flutterAssetStream = assetManager.open("flutter_assets/assets/my_flutter_asset.png")

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

然后,您可以使用 AssetImage 访问您的图像

dart
AssetImage('images/my_icon.png'),

或直接在 Image Widget 中

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

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

#

Flutter 目前没有专门用于字符串的资源类系统。最佳实践和推荐做法是将您的字符串存储在 .arb 文件中,作为键值对。例如

json
{
   "@@locale": "en",
   "hello":"Hello {userName}",
   "@hello":{
      "description":"A message with a single parameter",
      "placeholders":{
         "userName":{
            "type":"String",
            "example":"Bob"
         }
      }
   }
}

然后在您的代码中,您可以这样访问您的字符串

dart
Text(AppLocalizations.of(context)!.hello('John'));

有关此信息,请参阅 国际化 Flutter 应用程序

Gradle 文件的等效项是什么?如何添加依赖项?

#

在 Android 中,您通过将内容添加到 Gradle 构建脚本来添加依赖项。Flutter 使用 Dart 自己的构建系统和 Pub 包管理器。工具将构建原生 Android 和 iOS 包装器应用程序委托给各自的构建系统。

虽然在 Flutter 项目的 android 文件夹下有 Gradle 文件,但只有在添加用于平台间集成的原生依赖项时才使用它们。通常,使用 pubspec.yaml 声明要在 Flutter 中使用的外部依赖项。查找 Flutter 包的好地方是 pub.dev

Activities 和 fragments

#

Flutter 中 activities 和 fragments 的等效项是什么?

#

在 Android 中,Activity 表示用户可以执行的单个专注活动。Fragment 表示行为或用户界面的部分。Fragment 是一种模块化代码、组合用于较大屏幕的复杂用户界面以及帮助扩展应用程序 UI 的方法。在 Flutter 中,这两个概念都包含在 Widget 中。

要了解有关构建 Activity 和 Fragment 的 UI 的更多信息,请参阅社区贡献的文章:Flutter for Android Developers: How to design Activity UI in Flutter

Intents 部分所述,Flutter 中的屏幕由 Widget 表示,因为在 Flutter 中一切都是 Widget。使用 Navigator 在表示不同屏幕或页面,或者可能代表相同数据的不同状态或渲染的 Route 之间移动。

如何监听 Android activity 生命周期事件?

#

在 Android 中,您可以重写 Activity 中的方法来捕获 Activity 本身的生命周期方法,或在 Application 上注册 ActivityLifecycleCallbacks。在 Flutter 中,您既没有这些概念,但您可以改为通过挂接到 WidgetsBinding 观察器并侦听 didChangeAppLifecycleState() 更改事件来侦听生命周期事件。

可观察的生命周期事件有

  • detached — 应用程序仍然托管在 Flutter 引擎上,但已与任何宿主视图分离。
  • inactive — 应用程序处于非活动状态,未接收用户输入。
  • paused — 应用程序当前对用户不可见,未响应用户输入,并在后台运行。这相当于 Android 中的 onPause()
  • resumed — 应用程序可见并响应用户输入。这相当于 Android 中的 onPostResume()

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

正如您可能注意到的那样,只有一小部分 Activity 生命周期事件可用;虽然 FlutterActivity 确实会在内部捕获几乎所有的 Activity 生命周期事件并将其发送到 Flutter 引擎,但它们大多对您隐藏起来。Flutter 会为您负责启动和停止引擎,并且在大多数情况下不需要在 Flutter 侧观察 Activity 生命周期。如果您需要观察生命周期来获取或释放任何本机资源,那么您可能应该从本机侧进行操作。

这是一个观察包含 Activity 生命周期状态的示例

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

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

  @override
  State<LifecycleWatcher> createState() => _LifecycleWatcherState();
}

class _LifecycleWatcherState extends State<LifecycleWatcher>
    with WidgetsBindingObserver {
  AppLifecycleState? _lastLifecycleState;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    setState(() {
      _lastLifecycleState = state;
    });
  }

  @override
  Widget build(BuildContext context) {
    if (_lastLifecycleState == null) {
      return const Text(
        'This widget has not observed any lifecycle changes.',
        textDirection: TextDirection.ltr,
      );
    }

    return Text(
      'The most recent lifecycle state this widget observed was: $_lastLifecycleState.',
      textDirection: TextDirection.ltr,
    );
  }
}

void main() {
  runApp(const Center(child: LifecycleWatcher()));
}

布局

#

LinearLayout 的等效项是什么?

#

在 Android 中,LinearLayout 用于线性布局您的 Widget——水平或垂直。在 Flutter 中,使用 Row 或 Column Widget 可以实现相同的结果。

如果您注意到这两个代码示例除了 "Row" 和 "Column" Widget 之外是相同的。子项相同,并且可以利用此功能开发可以随时间变化且具有相同子项的丰富布局。

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

要了解有关构建线性布局的更多信息,请参阅社区贡献的 Medium 文章 Flutter for Android Developers: How to design LinearLayout in Flutter

RelativeLayout 的等效项是什么?

#

RelativeLayout 按照彼此之间的关系布局您的 Widget。在 Flutter 中,有几种方法可以实现相同的结果。

您可以使用 Column、Row 和 Stack Widget 的组合来实现 RelativeLayout 的效果。您可以在 Widget 构造函数中指定规则,以确定子项相对于父项的布局方式。

有关在 Flutter 中构建 RelativeLayout 的良好示例,请参阅 Collin 在 StackOverflow 上的回答。

ScrollView 的等效项是什么?

#

在 Android 中,使用 ScrollView 布局您的 Widget——如果用户的设备屏幕小于您的内容,则会滚动。

在 Flutter 中,最简单的方法是使用 ListView Widget。这可能看起来有点过头了,但 Flutter 中的 ListView Widget 既是 ScrollView 也是 Android 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 中处理横向转换?

#

FlutterView 会处理配置更改,如果 AndroidManifest.xml 包含

yaml
android:configChanges="orientation|screenSize"

手势检测和触摸事件处理

#

如何在 Flutter 中为 widget 添加 onClick 监听器?

#

在 Android 中,您可以通过调用 'setOnClickListener' 方法将 onClick 附加到诸如 button 这样的视图。

在 Flutter 中,有两种方法可以添加触摸侦听器

  1. 如果 Widget 支持事件检测,则将函数传递给它并在函数中处理它。例如,ElevatedButton 具有 onPressed 参数
dart
@override
Widget build(BuildContext context) {
  return ElevatedButton(
    onPressed: () {
      developer.log('click');
    },
    child: const Text('Button'),
  );
}
  1. 如果 Widget 不支持事件检测,则将 Widget 包装在 GestureDetector 中,并将函数传递给 onTap 参数。
dart
class SampleTapApp extends StatelessWidget {
  const SampleTapApp({super.key});

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

如何处理 widgets 上的其他手势?

#

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

  • 点击

    • onTapDown - 可能会导致点击的手指接触了屏幕上的特定位置。
    • onTapUp - 触发点击的手指停止接触屏幕上的特定位置。
    • onTap - 发生了点击。
    • onTapCancel - 之前触发 onTapDown 的手指不会导致点击。
  • 双击

    • onDoubleTap - 用户在短时间内在同一位置两次点击屏幕。
  • 长按

    • onLongPress - 手指长时间保持与屏幕上的相同位置接触。
  • 垂直拖动

    • onVerticalDragStart - 手指接触了屏幕,并且可能开始垂直移动。
    • onVerticalDragUpdate - 手指与屏幕接触并进一步垂直移动。
    • onVerticalDragEnd - 之前与屏幕接触并垂直移动的手指不再与屏幕接触,并且在停止接触屏幕时以特定速度移动。
  • 水平拖动

    • onHorizontalDragStart - 手指接触了屏幕,并且可能开始水平移动。
    • onHorizontalDragUpdate - 手指与屏幕接触并进一步水平移动。
    • onHorizontalDragEnd - 之前与屏幕接触并水平移动的手指不再与屏幕接触,并且在停止接触屏幕时以特定速度移动。

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

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

  @override
  State<SampleApp> createState() => _SampleAppState();
}

class _SampleAppState extends State<SampleApp>
    with SingleTickerProviderStateMixin {
  late AnimationController controller;
  late CurvedAnimation curve;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 2000),
    );
    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 & adapters

#

Flutter 中 ListView 的替代方案是什么?

#

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

在 Android ListView 中,您创建一个适配器并将其传递到 ListView,ListView 使用适配器返回的内容渲染每一行。但是,您必须确保回收您的行,否则您会遇到各种视觉故障和内存问题。

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

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

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

class SampleApp extends StatelessWidget {
  const SampleApp({super.key});
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const SampleAppPage(),
    );
  }
}

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

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

class _SampleAppPageState extends State<SampleAppPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Sample App')),
      body: ListView(children: _getListData()),
    );
  }

  List<Widget> _getListData() {
    List<Widget> widgets = [];
    for (int i = 0; i < 100; i++) {
      widgets.add(
        Padding(padding: const EdgeInsets.all(10), child: Text('Row $i')),
      );
    }
    return widgets;
  }
}

如何知道点击了哪个列表项?

#

在 Android ListView 中,有一个方法可以找出点击了哪个项目,即 'onItemClickListener'。在 Flutter 中,使用传递的 Widget 提供的触摸处理。

dart
import 'dart:developer' as developer;

import 'package:flutter/material.dart';

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

class SampleApp extends StatelessWidget {
  const SampleApp({super.key});
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const SampleAppPage(),
    );
  }
}

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

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

class _SampleAppPageState extends State<SampleAppPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Sample App')),
      body: ListView(children: _getListData()),
    );
  }

  List<Widget> _getListData() {
    List<Widget> widgets = [];
    for (int i = 0; i < 100; i++) {
      widgets.add(
        GestureDetector(
          onTap: () {
            developer.log('row tapped');
          },
          child: Padding(
            padding: const EdgeInsets.all(10),
            child: Text('Row $i'),
          ),
        ),
      );
    }
    return widgets;
  }
}

如何动态更新 ListView?

#

在 Android 上,您更新适配器并调用 notifyDataSetChanged

在 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 {
  const SampleApp({super.key});
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const 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));
    }
  }

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

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

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

dart
import 'dart:developer' as developer;

import 'package:flutter/material.dart';

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

class SampleApp extends StatelessWidget {
  const SampleApp({super.key});
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const 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));
    }
  }

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

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

不要创建 "ListView",而是创建一个 ListView.builder,它接受两个关键参数:列表的初始长度和 ItemBuilder 函数。

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

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

处理文本

#

如何在我的 Text widgets 上设置自定义字体?

#

在 Android SDK(截至 Android O)中,您创建一个字体资源文件并将其传递到 TextView 的 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 widgets 的样式?

#

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

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

表单输入

#

有关使用表单的更多信息,请参阅 Retrieve the value of a text field

Input 上的“提示”的等效项是什么?

#

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

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

如何显示验证错误?

#

就像使用“提示”一样,将 InputDecoration 对象传递给 Text Widget 的 decoration 构造函数。

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

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

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

class SampleApp extends StatelessWidget {
  const SampleApp({super.key});
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const SampleAppPage(),
    );
  }
}

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

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

class _SampleAppPageState extends State<SampleAppPage> {
  String? _errorText;

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

  String? _getErrorText() {
    return _errorText;
  }

  bool isEmail(String em) {
    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,}))$';

    RegExp regExp = RegExp(emailRegexp);

    return regExp.hasMatch(em);
  }
}

Flutter 插件

#

如何访问 GPS 传感器?

#

使用 geolocator 社区插件。

如何访问相机?

#

image_picker 插件非常适合访问相机。

如何使用 Facebook 登录?

#

要使用 Facebook 登录,请使用 flutter_facebook_login 社区插件。

如何使用 Firebase 功能?

#

大多数 Firebase 函数都由 第一方插件 涵盖。这些插件是第一方集成,由 Flutter 团队维护

您还可以找到一些第三方 Firebase 插件在 pub.dev 上,涵盖第一方插件未直接涵盖的领域。

如何构建我自己的自定义本机集成?

#

如果存在 Flutter 或其社区插件中缺少平台特定功能,您可以按照 开发包和插件 页面构建自己的插件。

总而言之,Flutter 的插件架构类似于在 Android 中使用事件总线:您发送一条消息,让接收者处理并向您发出结果。在这种情况下,接收者是在 Android 或 iOS 上运行的本机代码。

如何在我的 Flutter 应用程序中使用 NDK?

#

如果您在当前的 Android 应用程序中使用 NDK,并希望您的 Flutter 应用程序利用您的本机库,那么可以通过构建自定义插件来实现。

您的自定义插件首先与您的 Android 应用程序通信,您在其中通过 JNI 调用您的 native 函数。一旦准备好响应,就将消息发送回 Flutter 并渲染结果。

目前不支持直接从 Flutter 调用本机代码。

主题

#

如何设置我的应用程序的主题?

#

开箱即用,Flutter 附带了 Material Design 的精美实现,它处理了您通常需要执行的许多样式和主题需求。与 Android 不同,您在 XML 中声明主题并在 AndroidManifest.xml 中使用它将主题分配给您的应用程序,在 Flutter 中,您在顶级 Widget 中声明主题。

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

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

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

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

class SampleApp extends StatelessWidget {
  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(),
    );
  }
}

Homescreen widgets

#

如何创建一个 homescreen widget?

#

使用 Flutter 无法完全创建 Android 主屏幕小部件。 必须使用 Jetpack Glance(首选方法)或 XML 布局代码。 通过第三方软件包 home_widget,您可以将主屏幕小部件与 Dart 代码连接起来,将 Flutter 组件(作为图像)嵌入到宿主小部件中,并在 Flutter 与主屏幕小部件之间共享数据。

为了提供更丰富、更引人入胜的体验,建议添加小部件预览以包含在小部件选择器中。 对于运行 Android 15 及更高版本的设备,生成的动态小部件预览允许用户查看目标小部件的动态和个性化版本,让他们了解它在主屏幕上的显示效果。 有关生成的动态小部件预览以及旧设备的备用选项,请查看 将生成的预览添加到您的部件选择器 文档页面。

数据库和本地存储

#

如何访问 Shared Preferences?

#

在 Android 中,您可以使用 SharedPreferences API 存储少量键值对。

在 Flutter 中,使用 Shared_Preferences 插件 访问此功能。 此插件封装了 Shared Preferences 和 NSUserDefaults(iOS 等效项)的功能。

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

import 'package:shared_preferences/shared_preferences.dart';

void main() {
  runApp(
    const MaterialApp(
      home: Scaffold(
        body: Center(
          child: ElevatedButton(
            onPressed: _incrementCounter,
            child: Text('Increment Counter'),
          ),
        ),
      ),
    ),
  );
}

Future<void> _incrementCounter() async {
  SharedPreferences prefs = await SharedPreferences.getInstance();
  int counter = (prefs.getInt('counter') ?? 0) + 1;
  await prefs.setInt('counter', counter);
}

如何在 Flutter 中访问 SQLite?

#

在 Android 中,您使用 SQLite 存储可以使用 SQL 查询的结构化数据。

在 Flutter 中,对于 macOS、Android 或 iOS,使用 SQFlite 插件访问此功能。

调试

#

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

#

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

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

通知

#

如何设置推送通知?

#

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

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