跳到主内容

Flutter 给 Android 开发者的指南

学习如何将 Android 开发经验应用于构建 Flutter 应用。

本文档旨在帮助 Android 开发者利用现有的 Android 知识,通过 Flutter 构建移动应用。如果你了解 Android 框架的基础知识,可以将本文档作为 Flutter 开发的入门指南。

你的 Android 知识和技能在构建 Flutter 应用时非常有价值,因为 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 及其语义的描述,这些描述在后台被“充气”(inflated) 成实际的视图对象。

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

但 Flutter 足够灵活和具有表现力,可以实现任何设计语言。例如,在 iOS 上,你可以使用 Cupertino Widget 来生成看起来像 Apple iOS 设计语言 的界面。

如何更新 Widget?

#

在 Android 中,你通过直接修改视图来更新它们。然而,在 Flutter 中,Widget 是不可变的,不能直接更新,你必须操作 Widget 的状态。

这就是 Stateful (有状态) 和 Stateless (无状态) Widget 概念的由来。StatelessWidget 正如其名——一个没有状态信息的 Widget。

当描述的 UI 部分除了对象中的配置信息外不依赖其他内容时,StatelessWidget 非常有用。

例如,在 Android 中,这类似于放置一个带有 logo 的 ImageView。logo 在运行时不会改变,所以使用 Flutter 的 StatelessWidget

如果你想根据 HTTP 请求收到的数据或用户交互来动态改变 UI,那么你必须使用 StatefulWidget,并告知 Flutter 框架该 Widget 的 State 已更新,以便它能更新该 Widget。

这里需要注意的是,本质上无状态和有状态 Widget 的表现是一样的。它们在每一帧都会重建,不同之处在于 StatefulWidget 拥有一个 State 对象,该对象可以跨帧存储状态数据并在重建时恢复它。

如果有疑问,请记住这条规则:如果一个 Widget 发生变化(例如由于用户交互),那么它就是有状态的。但是,如果一个 Widget 只是响应变化,那么只要包含它的父 Widget 本身不响应变化,它仍然可以是无状态的。

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

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

正如你所见,Text Widget 没有与之关联的状态信息,它只是渲染其构造函数中传递的内容,仅此而已。

但是,如果你想让“I Like Flutter”发生动态改变(例如点击 FloatingActionButton 时),该怎么办?

要实现这一点,请将 Text 组件包装在 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),
      ),
    );
  }
}

如何布局 Widget?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 目录 中查看 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: '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 或调用视图的 animate() 方法来创建动画。在 Flutter 中,通过将 Widget 包装在动画 Widget 中来使用动画库对 Widget 进行动画处理。

在 Flutter 中,使用 AnimationController,它是一个 Animation,可以暂停、查找、停止和反转动画。它需要一个 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。因此,对于 Android 开发者来说,在 Flutter 中进行 Canvas 绘制是一项非常熟悉的任务。

Flutter 有两个类可以帮助你在 Canvas 上绘制:CustomPaintCustomPainter,后者实现了你在 Canvas 上绘制的算法。

要了解如何在 Flutter 中实现签名绘制器,请查看 Collin 在 StackOverflow 上关于 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;
}

如何构建自定义 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('Hello'));
}

Intent

#

Flutter 中是否有 Intent 的等价物?

#

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

Flutter 实际上没有 Activity 和 Fragment 的直接等价物;相反,在 Flutter 中,你使用 NavigatorRoute 在同一 Activity 内的不同屏幕之间导航。

Route 是应用“屏幕”或“页面”的抽象,而 Navigator 是管理路由的 Widget。路由大致映射到 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'),
      },
    ),
  );
}

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

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

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

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

在 Flutter 中如何处理来自外部应用的 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,提取其中共享的文本并将其保留。当 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 来实现。

例如,要启动一个位置路由让用户选择位置,你可以这样做:

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

一旦 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: '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

要进行网络调用,在异步函数 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: '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 应用只有资产 (Assets)。所有在 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 无法访问原生资源或资产。

例如,要添加一个名为 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

Activity 和 Fragment

#

Flutter 中是否有 Activity 和 Fragment 的等价物?

#

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

要了解更多关于构建 Activity 和 Fragment UI 的内容,请参阅社区贡献的文章:Flutter 给 Android 开发者的指南:如何在 Flutter 中设计 Activity UI

正如在Intent部分提到的,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 中,使用 RowColumn Widget 来实现相同的结果。

你会发现这两个代码示例除了“Row”和“Column”Widget 外完全相同。子 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 给 Android 开发者的指南:如何在 Flutter 中设计 LinearLayout

RelativeLayout 的等价物是什么?

#

RelativeLayout 将 Widget 相对于彼此进行布局。在 Flutter 中,有几种方法可以实现相同的结果。

你可以通过结合使用 ColumnRowStack 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('Row One'),
      Text('Row Two'),
      Text('Row Three'),
      Text('Row Four'),
    ],
  );
}

如何在 Flutter 中处理横竖屏切换?

#

如果 AndroidManifest.xml 包含以下内容,FlutterView 会处理配置变更:

yaml
android:configChanges="orientation|screenSize"

手势检测和触摸事件处理

#

如何在 Flutter 中为 Widget 添加点击监听器?

#

在 Android 中,你可以通过调用方法 'setOnClickListener' 将点击事件附加到按钮等视图上。

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

如何处理 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 与 Adapter

#

Flutter 中 ListView 的替代品是什么?

#

Flutter 中 ListView 的等价物是……ListView

在 Android ListView 中,你创建一个 Adapter 并将其传入 ListView,后者会渲染 Adapter 返回的每一行。但是,你必须确保回收行,否则会遇到各种视觉故障和内存问题。

由于 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 上,你更新 Adapter 并调用 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 Adapter 中的 getView 函数;它接受一个位置,并返回你想要在该位置渲染的行。

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

处理文本

#

如何为 Text 组件设置自定义字体?

#

在 Android SDK(Android O 及更高版本)中,你创建一个字体资源文件并将其传递给 TextViewFontFamily 参数。

在 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 组件的样式?

#

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

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

表单输入

#

有关使用表单的更多信息,请参阅 获取文本字段的值

输入框中 "hint" 的等价物是什么?

#

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

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

如何显示验证错误?

#

就像使用“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 团队维护。

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

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

#

如果 Flutter 或其社区插件缺少平台特定的功能,你可以按照开发包和插件页面进行构建。

简而言之,Flutter 的插件架构非常类似于在 Android 中使用事件总线:你触发一条消息,让接收方处理并向你发射结果。在这种情况下,接收方是在 Android 或 iOS 原生端运行的代码。

如何在 Flutter 应用中使用 NDK?

#

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

你的自定义插件首先与 Android 应用通信,在这里你通过 JNI 调用 native 函数。一旦响应准备好,向 Flutter 发送消息并渲染结果。

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

主题

#

如何为应用设置主题?

#

Flutter 开箱即用地提供了精美的 Material Design 实现,它处理了你通常需要做的许多样式和主题需求。与 Android 不同(在 Android 中你在 XML 中声明主题,然后使用 AndroidManifest.xml 将其分配给应用),在 Flutter 中,你在顶层 Widget 中声明主题。

要充分利用应用中的 Material Components,你可以声明一个 MaterialApp 顶层 Widget 作为应用的入口点。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)

#

如何创建桌面小部件?

#

Android 桌面小部件不能完全使用 Flutter 创建。它们必须使用 Jetpack Glance(首选方法)或 XML 布局代码。使用第三方包 home_widget,你可以将桌面小部件连接到 Dart 代码,在宿主小部件中嵌入 Flutter 组件(作为图像),并在 Flutter 与桌面小部件之间共享数据。

为了提供更丰富、更具吸引力的体验,建议添加小部件预览以包含在小部件选择器中。对于运行 Android 15 及更高版本的设备,生成的预览允许用户看到目标小部件的动态和个性化版本,让他们一睹它在主屏幕上的确切效果。有关生成的预览和旧设备的回退选项的更多信息,请查看在小部件选择器中添加生成的预览文档页面。

数据库与本地存储

#

如何访问 SharedPreferences?

#

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