Skip to main content
Contents

Flutter for Android 开发人员

Contents

本文档面向希望利用其现有 Android 知识使用 Flutter 构建移动应用的 Android 开发人员。如果您了解 Android 框架的基础知识,则可以使用本文档作为 Flutter 开发的入门指南。

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

您可以将本文档用作参考手册,随意跳转并查找与您需求最相关的答案。

视图

#

Flutter 中与 View 等效的是什么?

#

在 Android 中,View 是屏幕上显示的所有内容的基础。按钮、工具栏和输入,所有内容都是 View。在 Flutter 中,与 View 大致等效的是 Widget。Widget 并不完全映射到 Android 视图,但在您熟悉 Flutter 的工作原理时,您可以将其视为“声明和构建 UI 的方式”。

但是,它们与 View 有几点区别。首先,widget 的生命周期不同:它们是不可变的,并且只存在于需要更改它们之前。每当 widget 或其状态发生更改时,Flutter 的框架都会创建一个新的 widget 实例树。相比之下,Android 视图只绘制一次,并且直到调用 invalidate 才会重新绘制。

Flutter 的 widget 很轻量级,部分原因在于其不可变性。因为它们本身不是视图,也不是直接绘制任何内容,而是对 UI 及其语义的描述,在后台会被“膨胀”成实际的视图对象。

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

但是 Flutter 足够灵活和富有表现力,可以实现任何设计语言。例如,在 iOS 上,您可以使用Cupertino widget 来创建一个看起来像Apple 的 iOS 设计语言 的界面。

如何更新 widget?

#

在 Android 中,您可以通过直接修改视图来更新视图。但是,在 Flutter 中,Widget 是不可变的,不会直接更新,而是必须使用 widget 的状态。

这就是 StatefulStateless widget 概念的由来。StatelessWidget 正如其名称所示——一个没有状态信息的 widget。

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

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

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

这里需要注意的重要一点是,在核心方面,无状态和有状态 widget 的行为相同。它们每帧都会重建,区别在于 StatefulWidget 具有一个 State 对象,该对象跨帧存储状态数据并恢复它。

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

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

dart
Text(
  '我喜欢 Flutter!',
  style: TextStyle(fontWeight: FontWeight.bold),
);

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

但是,如果您想动态更改“我喜欢 Flutter”,例如单击 FloatingActionButton 时怎么办?

为此,请将 Text widget 包裹在 StatefulWidget 中,并在用户单击按钮时更新它。

例如:

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

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

class SampleApp extends StatelessWidget {
  const SampleApp({super.key});
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '示例应用',
      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 textToShow = '我喜欢 Flutter';

  void _updateText() {
    setState(() {
      // 更新文本。
      textToShow = 'Flutter 太棒了!';
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('示例应用'),
      ),
      body: Center(child: Text(textToShow)),
      floatingActionButton: FloatingActionButton(
        onPressed: _updateText,
        tooltip: '更新文本',
        child: const Icon(Icons.update),
      ),
    );
  }
}

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

#

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

以下示例显示了如何显示带有填充的简单 widget:

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

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

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

#

在 Android 中,您在父级上调用 addChild()removeChild() 来动态添加或删除子视图。在 Flutter 中,因为 widget 是不可变的,所以没有直接等效于 addChild() 的方法。相反,您可以向父级传递一个返回 widget 的函数,并使用布尔标志控制子级的创建。

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

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: '示例应用',
      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> {
  // 切换的默认值。
  bool toggle = true;
  void _toggle() {
    setState(() {
      toggle = !toggle;
    });
  }

  Widget _getToggleChild() {
    if (toggle) {
      return const Text('切换一');
    } else {
      return ElevatedButton(
        onPressed: () {},
        child: const Text('切换二'),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('示例应用'),
      ),
      body: Center(
        child: _getToggleChild(),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _toggle,
        tooltip: '更新文本',
        child: const Icon(Icons.update),
      ),
    );
  }
}

如何动画化 widget?

#

在 Android 中,您可以使用 XML 创建动画,或在视图上调用 animate() 方法。在 Flutter 中,可以使用动画库通过将 widget 包裹在动画 widget 内来动画化 widget。

在 Flutter 中,使用 AnimationController,它是一个 Animation<double>,可以暂停、搜索、停止和反转动画。它需要一个 Ticker 来指示何时发生 vsync,并在运行时每帧生成 0 到 1 之间的线性插值。然后,您可以创建一个或多个 Animation 并将其附加到控制器。

例如,您可以使用 CurvedAnimation 来沿插值曲线实现动画。从这个意义上说,控制器是动画进度的“主”来源,而 CurvedAnimation 计算替换控制器默认线性运动的曲线。与 widget 一样,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: '淡入淡出演示',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const MyFadeTest(title: '淡入淡出演示'),
    );
  }
}

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: '淡入淡出',
        onPressed: () {
          controller.forward();
        },
        child: const Icon(Icons.brush),
      ),
    );
  }
}

更多信息,请参阅 动画和动态 widget动画教程动画概述

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

#

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

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

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

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

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

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

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

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

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

class SignatureState extends State<Signature> {
  List<Offset?> _points = <Offset>[];
  @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;
}

如何构建自定义 widget?

#

在 Android 中,您通常会子类化 View 或使用预先存在的视图来覆盖和实现实现所需行为的方法。

在 Flutter 中,通过组合较小的 widget(而不是扩展它们)来构建自定义 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('你好'),
  );
}

意图

#

Flutter 中与 Intent 等效的是什么?

#

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

Flutter 并没有真正与 activity 和 fragment 直接等效的内容;相反,在 Flutter 中,您使用 NavigatorRoute 在屏幕之间导航,所有这些都在同一个 Activity 中。

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

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

在 Flutter 中,您可以使用以下几种方法在页面之间导航:

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

以下示例构建了一个 Map。

dart
void main() {
  runApp(MaterialApp(
    home: const MyAppHome(), // 成为名为 '/' 的路由。
    routes: <String, WidgetBuilder>{
      '/a': (context) => const MyPage(title: '页面 A'),
      '/b': (context) => const MyPage(title: '页面 B'),
      '/c': (context) => const MyPage(title: '页面 C'),
    },
  ));
}

通过将路由名称 pushNavigator 来导航到路由。

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

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

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

如何处理来自外部应用程序的传入 intent?

#

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

以下示例在运行 Flutter 代码的本机 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); // 处理正在发送的文本
      }
    }
  }

  @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: '示例共享应用处理程序',
      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 = '无数据';

  @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 来完成的。

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

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

然后,在您的位置路由中,一旦用户选择了他们的位置,您可以使用结果 pop 堆栈:

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

异步 UI

#

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

#

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

Dart 的单线程模型并不意味着您需要将所有内容都作为阻止操作来运行,这会导致 UI 冻结。与 Android(需要您始终保持主线程空闲)不同,在 Flutter 中,使用 Dart 语言提供的异步功能,例如

async/await,来执行异步工作。如果您在 C#、Javascript 中使用过它,或者如果您使用过 Kotlin 的协程,您可能熟悉 async/await 范式。

例如,您可以使用 async/await 并让 Dart 完成繁重的工作,从而在不导致 UI 挂起的情况下运行网络代码:

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

一旦完成 awaited 网络调用,通过调用 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: '示例应用',
      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('示例应用'),
      ),
      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("行 ${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);

  // 'echo' isolate 发送其 SendPort 作为第一条消息。
  SendPort sendPort = await receivePort.first as SendPort;

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

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

// Isolate 的入口点。
static Future<void> dataLoader(SendPort sendPort) async {
  // 打开 ReceivePort 用于接收传入的消息。
  ReceivePort port = ReceivePort();

  // 通知任何其他 isolate 此 isolate 侦听哪个端口。
  sendPort.send(port.sendPort);

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

    String dataURL = data;
    http.Response response = await http.get(Uri.parse(dataURL));
    // 需要解析大量 JSON
    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: '示例应用',
      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 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('示例应用'),
      ),
      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("行 ${widgets[i]["title"]}"),
    );
  }

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

    // 'echo' isolate 发送其 SendPort 作为第一条消息。
    SendPort sendPort = await receivePort.first as SendPort;

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

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

  // Isolate 的入口点。
  static Future<void> dataLoader(SendPort sendPort) async {
    // 打开 ReceivePort 用于接收传入的消息。
    ReceivePort port = ReceivePort();

    // 通知任何其他 isolate 此 isolate 侦听哪个端口。
    sendPort.send(port.sendPort);

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

      String dataURL = data;
      http.Response response = await http.get(Uri.parse(dataURL));
      // 需要解析大量 JSON
      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 更新其状态,并在结束后隐藏它。

在下面的示例中,构建函数被分成三个不同的函数。如果 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: '示例应用',
      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('示例应用'),
      ),
      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("行 ${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 的 assets 文件夹中。

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

Android 的密度桶的等效项为:

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

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

使用 Android 的 AssetManager 从本机资产文件夹中访问存储在本机资产文件夹中的资产:

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

Flutter 无法访问本机资源或资产。

例如,要向我们的 Flutter 项目添加一个名为 my_icon.png 的新图像资产,并决定将其放在我们任意称为 images 的文件夹中,您将把基本图像 (1.0x) 放入 images 文件夹中,并将所有其他变体放入名为适当比率倍数的子文件夹中:

images/my_icon.png       // 基本:1.0x 图像
images/2.0x/my_icon.png  // 2.0x 图像
images/3.0x/my_icon.png  // 3.0x 图像

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

yaml
assets:
 - images/my_icon.jpeg

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

dart
AssetImage('images/my_icon.jpeg')

或直接在 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 对 Android 的辅助功能有基本支持,尽管此功能仍在开发中。

有关此方面的更多信息,请参阅Flutter 应用国际化

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

#

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

虽然您的 Flutter 项目的 android 文件夹下有 Gradle 文件,但只有在您添加每个平台集成所需的本机依赖项时才使用这些文件。通常,使用 pubspec.yaml 来声明要在 Flutter 中使用的外部依赖项。查找 Flutter 包的好地方是pub.dev

Activity 和 Fragment

#

Flutter 中与 Activity 和 Fragment 等效的是什么?

#

在 Android 中,Activity 代表用户可以执行的单个焦点操作。Fragment 代表行为或用户界面的部分。Fragment 是一种模块化代码、为较大屏幕组合复杂的 UI 并帮助扩展应用程序 UI 的方法。在 Flutter 中,这两个概念都属于 Widget 的范畴。

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

意图 部分所述,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(
        '此 widget 未观察到任何生命周期更改。',
        textDirection: TextDirection.ltr,
      );
    }

    return Text(
      '此 widget 最近观察到的生命周期状态是:$_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('行一'),
      Text('行二'),
      Text('行三'),
      Text('行四'),
    ],
  );
}
dart
@override
Widget build(BuildContext context) {
  return const Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
      Text('列一'),
      Text('列二'),
      Text('列三'),
      Text('列四'),
    ],
  );
}

要了解有关构建线性布局的更多信息,请参阅社区贡献的 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。这从 Android 的角度来看似乎有些过头,但在 Flutter 中,ListView widget 既是 ScrollView,也是 Android ListView。

dart
@override
Widget build(BuildContext context) {
  return ListView(
    children: const <Widget>[
      Text('行一'),
      Text('行二'),
      Text('行三'),
      Text('行四'),
    ],
  );
}

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

#

如果 AndroidManifest.xml 包含以下内容,则 FlutterView 将处理配置更改:

yaml
android:configChanges="orientation|screenSize"

手势检测和触摸事件处理

#

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

#

在 Android 中,您可以通过调用方法 'setOnClickListener' 将 onClick 附加到按钮等视图上。

在 Flutter 中,有两种添加触摸监听器的方法:

  1. 如果 Widget 支持事件检测,则向其传递一个函数并在该函数中进行处理。例如,ElevatedButton 有一个 onPressed 参数:
dart
@override
Widget build(BuildContext context) {
  return ElevatedButton(
    onPressed: () {
      developer.log('点击');
    },
    child: const Text('按钮'),
  );
}
  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('点击');
          },
          child: const FlutterLogo(
            size: 200,
          ),
        ),
      ),
    );
  }
}

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

#

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

ListView 和适配器

#

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: '示例应用',
      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('示例应用'),
      ),
      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('行 $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: '示例应用',
      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('示例应用'),
      ),
      body: ListView(children: _getListData()),
    );
  }

  List<Widget> _getListData() {
    List<Widget> widgets = [];
    for (int i = 0; i < 100; i++) {
      widgets.add(
        GestureDetector(
          onTap: () {
            developer.log('点击行');
          },
          child: Padding(
            padding: const EdgeInsets.all(10),
            child: Text('行 $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: '示例应用',
      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('示例应用'),
      ),
      body: ListView(children: widgets),
    );
  }

  Widget getRow(int i) {
    return GestureDetector(
      onTap: () {
        setState(() {
          widgets = List.from(widgets);
          widgets.add(getRow(widgets.length));
          developer.log('行 $i');
        });
      },
      child: Padding(
        padding: const EdgeInsets.all(10),
        child: Text('行 $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: '示例应用',
      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('示例应用'),
      ),
      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('行 $i');
        });
      },
      child: Padding(
        padding: const EdgeInsets.all(10),
        child: Text('行 $i'),
      ),
    );
  }
}

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

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

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

处理文本

#

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

#

在 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('示例应用'),
    ),
    body: const Center(
      child: Text(
        '这是一个自定义字体文本',
        style: TextStyle(fontFamily: 'MyCustomFont'),
      ),
    ),
  );
}

如何设置我的 Text widget 的样式?

#

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

  • color

  • decoration

  • decorationColor

  • decorationStyle

  • fontFamily

  • fontSize

  • fontStyle

  • fontWeight

  • hashCode

  • height

  • inherit

  • letterSpacing

  • textBaseline

  • wordSpacing

表单输入

#

有关使用表单的更多信息,请参阅检索文本字段的值,来自Flutter 食谱

输入的“提示”等效是什么?

#

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

dart
Center(
  child: TextField(
    decoration: InputDecoration(hintText: '这是一个提示'),
  ),
)

如何显示验证错误?

#

与“提示”一样,将 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: '示例应用',
      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('示例应用'),
      ),
      body: Center(
        child: TextField(
          onSubmitted: (text) {
            setState(() {
              if (!isEmail(text)) {
                _errorText = '错误:这不是电子邮件';
              } else {
                _errorText = null;
              }
            });
          },
          decoration: InputDecoration(
            hintText: '这是一个提示',
            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 团队维护:

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

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

#

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

简而言之,Flutter 的插件架构很像在 Android 中使用事件总线:您发出消息并让接收器处理并将结果发送回给您。在这种情况下,接收器是在 Android 或 iOS 上本机端运行的代码。

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

#

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

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

目前不支持直接从 Flutter 调用原生代码。

主题

#

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

#

Flutter 自带精美的 Material Design 实现,它可以处理您通常会执行的许多样式和主题需求。与您在 XML 中声明主题然后使用 AndroidManifest.xml 将其分配给应用程序的 Android 不同,在 Flutter 中,您在顶级 widget 中声明主题。

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

您也可以使用 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: '示例应用',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        textSelectionTheme:
            const TextSelectionThemeData(selectionColor: Colors.red),
      ),
      home: const SampleAppPage(),
    );
  }
}

数据库和本地存储

#

如何访问 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('递增计数器'),
          ),
        ),
      ),
    ),
  );
}

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 插件文档。