Flutter Android 开发者指南:移动应用开发入门

Viewed 0

这篇文档帮助 Android 开发者利用已有的知识快速上手 Flutter 开发。如果你熟悉 Android 框架,那么你的技能将非常有用,因为 Flutter 依赖于 Android 操作系统的众多功能与配置。Flutter 提供了一种全新的构建移动界面的方式,并通过插件系统与 Android(及 iOS)进行非 UI 任务的通信。本文可作为你随时查阅的快速指南。

视图 (View) 在 Flutter 中对应什么?

在 Android 中,View 是屏幕上所有内容的基础,例如按钮、工具栏和输入框。在 Flutter 中,与 View 大致对应的概念是 Widget。虽然 Widget 并不完全等同于 Android 的 View,但在你熟悉 Flutter 工作原理时,可以将其理解为“声明和构建 UI 的方式”。

Widget 与 View 存在一些关键差异。首先,Widget 的生命周期不同:它们是不可变的,一旦需要变化,其生命周期就会终结。每当 Widget 或其状态发生变化时,Flutter 框架都会创建一个新的 Widget 树实例。相比之下,Android View 只绘制一次,除非调用 invalidate 才会重绘。

Widget 的不可变性使其非常轻量,因为它们本身并非视图,也不直接绘制任何内容,而只是对 UI 及其底层创建的真实视图对象的语义描述。

Flutter 内置支持 Material Components 库,它提供了遵循 Material Design 设计规范 的 Widgets。这是一个为所有平台(包括 iOS)优化的灵活设计系统。同时,Flutter 非常灵活,能够实现任何设计语言。例如,在 iOS 上,你可以使用 Cupertino widgets 来创建符合 Apple iOS 设计语言 的界面。

在 Android 中,你可以直接更新 View。但在 Flutter 中,Widget 不可变,无法直接更新,你必须通过操作 Widget 的状态来实现界面更新。

这就引出了有状态 (Stateful)无状态 (Stateless) Widget 的概念。StatelessWidget 如其名,是一个没有关联状态信息的 Widget。它适用于描述的用户界面部分不依赖于对象配置信息之外的任何东西的场景,例如 Android 中显示一个静态图标的 ImageView。这个图标在运行时不会改变,因此在 Flutter 中就使用 StatelessWidget

如果你需要根据 HTTP 请求返回的数据或用户交互来动态更新界面,就必须使用 StatefulWidget,并通知 Flutter 框架该 Widget 的 State 已更新,以便 Flutter 能相应地更新 Widget。

重要的是,无状态和有状态 Widget 在行为上本质一致,它们都会在每一帧重建。不同之处在于,StatefulWidget 拥有一个 State 对象,该对象可以跨帧存储和恢复状态数据。

一个简单的规则是:如果一个 Widget 会变化(例如由于用户交互),它就是有状态的。如果一个 Widget 仅对变化做出响应,但其父 Widget 本身不响应变化,则父 Widget 仍然可以是无状态的。

下面的例子展示了 StatelessWidget 的用法。Text Widget 本身就是一个 StatelessWidget,它只渲染传入构造器的信息。

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

但是,如果你希望动态改变文本内容,例如在点击 FloatingActionButton 时,该怎么办呢?这时,你需要将 Text Widget 嵌入一个 StatefulWidget 中,并在按钮点击时更新状态。

示例代码如下:

import 'package:flutter/material.dart';

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> {
  String textToShow = 'I Like Flutter';

  void _updateText() {
    setState(() {
      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 树来定义布局。以下示例展示如何显示一个带有内边距的简单按钮:

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

你可以在 Flutter 的 Widget 目录 中查看所有可用的布局 Widget。

在 Android 中,你可以通过调用父 View 的 addChild()removeChild() 来动态添加或删除子 View。在 Flutter 中,由于 Widget 不可变,没有直接的对应方法。但你可以通过一个返回 Widget 的函数,并根据一个布尔标记值来控制创建哪个子 Widget,从而实现类似效果。

例如,下面的代码展示了如何在点击 FloatingActionButton 时在两个 Widget 之间切换:

import 'package:flutter/material.dart';

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

在 Android 中,你可以通过 XML 或调用 animate() 方法定义动画。在 Flutter 中,你需要使用动画库,通过将 Widget 包装在动画 Widget 中来实现动画效果。Flutter 使用 AnimationControllerAnimation<double> 的子类)来控制动画的播放、暂停、停止和反转。它需要一个 Ticker 在垂直同步信号时触发,并生成一个介于 0 和 1 之间的线性插值。然后,你可以创建一个或多个 Animation 并将其绑定到控制器上。

例如,使用 CurvedAnimation 可以实现曲线插值动画。控制器决定动画进度,而 CurvedAnimation 则根据曲线计算替代默认线性进度的值。与 Widget 一样,Flutter 的动画也可以组合使用。在构建 Widget 树时,你将 Animation 对象赋值给 Widget 的动画属性(如 FadeTransition 的不透明度),然后启动控制器。

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

import 'package:flutter/material.dart';

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

class FadeAppTest extends StatelessWidget {
  const FadeAppTest({super.key});
  @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),
      ),
    );
  }
}

更多信息,请查阅 动画 Widget动画指南动画概览

在 Android 中,你使用 CanvasDrawable 在屏幕上绘制图形。Flutter 也有类似的 Canvas API,因为它们基于相同的底层渲染引擎 Skia。因此,对于 Android 开发者来说,在 Flutter 中使用画布绘图会非常熟悉。

Flutter 提供了 CustomPaintCustomPainter 两个类来辅助自定义绘制,后者用于实现你的绘制算法。例如,要实现一个签名功能,可以参考以下简化代码:

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

在 Android 中,你通常通过继承 View 类并重写特定方法来实现自定义视图。在 Flutter 中,创建自定义 Widget 的推荐方式是组合更小的 Widget,而不是继承它们。这类似于 Android 中实现自定义 ViewGroup,你复用已有的构建块,但提供不同的行为(如自定义布局逻辑)。

例如,要创建一个接收标签的 CustomButton,你可以组合 ElevatedButtonText

class CustomButton extends StatelessWidget {
  final String label;
  const CustomButton(this.label, {super.key});
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(onPressed: () {}, child: Text(label));
  }
}

然后像使用任何其他 Widget 一样使用它:

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

Intent 在 Flutter 中的对应概念是什么?

在 Android 中,Intent 主要有两个用途:在 Activity 间导航和组件间通信。Flutter 本身没有 Intent 的概念,但你可以通过原生集成(使用 插件)来启动 Intent。

实际上,Flutter 也没有 Activity 和 Fragment 的直接对应物。在 Flutter 中,你在同一个“容器”内使用 NavigatorRoute 在不同界面(屏幕)间导航。Route 是应用内屏幕或页面的抽象,而 Navigator 是管理这些路由的 Widget,其工作原理类似于堆栈。你可以通过 push() 跳转到新路由,通过 pop() 返回上一路由。

在 Android 中,你在 AndroidManifest.xml 中声明 Activity。在 Flutter 中,有多种导航方式,例如在 MaterialApp 中定义一个路由名称的映射:

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

然后通过路由名跳转:

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

Intent 的另一个常见用途是调用外部组件,如相机或文件选择器。对于这种情况,你需要创建或使用现有的 原生平台插件。了解如何开发插件,请查看 开发包和插件

在 Flutter 中如何处理从外部应用接收到的 intent?

Flutter 可以通过与 Android 层直接通信来处理接收到的 Android Intent。通常,你需要在 Android 原生端(Activity)处理 Intent 数据,然后通过 MethodChannel 将数据传递给 Flutter。

首先,在 AndroidManifest.xml 中为你的 Activity 注册一个 Intent 过滤器,例如用于接收文本分享:

<activity
  android:name=".MainActivity"
  ... >
  <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,提取出分享的文本,并通过 MethodChannel 在 Flutter 端请求时发送过去:

(Java 代码示例,展示原理)

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) && "text/plain".equals(type)) {
      handleSendText(intent); // 处理分享的文本
    }
  }

  @Override
  public void configureFlutterEngine(@NonNull FlutterEngine 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);
  }
}

最后,在 Flutter 端,通过 MethodChannel 请求数据:

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

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

class SampleApp extends StatelessWidget {
  const SampleApp({super.key});
  @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() 的对应方法是什么?

在 Flutter 中,Navigator 类负责导航,并且可以接收从新路由返回的结果。这是通过 await 等待 push 操作返回的 Future 来实现的。

例如,要打开一个让用户选择位置的路由并等待结果:

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

然后,在位置选择路由中,当用户做出选择后,调用 pop 并返回结果:

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

runOnUiThread() 在 Flutter 中的对应方法是什么?

Dart 采用单线程执行模型,同时支持 Isolate(在另一个线程运行 Dart 代码的方式)。除非你创建 Isolate,否则你的 Dart 代码都运行在主 UI 线程,并由一个事件循环驱动。Flutter 的事件循环类似于 Android 中绑定到主线程的 Looper

Dart 的单线程模型并不意味着你需要运行会阻塞 UI 的代码。与 Android 中必须保持主线程空闲不同,在 Flutter 中,你可以使用 Dart 的 async/await 语法来执行异步操作,而不会阻塞 UI。

例如,你可以这样执行网络请求而不导致 UI 挂起:

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

一旦 await 的网络操作完成,调用 setState() 来更新 UI,这会触发 Widget 子树的重建并显示新数据。

下面是一个异步加载数据并在 ListView 中显示的完整示例:

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

关于在后台执行任务,对于 I/O 密集型任务(如网络请求或数据库操作),直接使用 async/await 即可。对于计算密集型任务,为了避免阻塞事件循环(UI 线程),你应该使用 Isolate

Isolate 是独立的执行线程,不共享主线程的内存。这意味着你不能直接从 Isolate 访问主线程的变量或调用 setState()。下面的例子展示了如何使用 Isolate 来执行繁重的 JSON 解析工作:

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

  // 第一个消息是 isolate 发送的 SendPort
  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;
  });
}

// isolate 的入口函数
static Future<void> dataLoader(SendPort sendPort) async {
  ReceivePort port = ReceivePort();
  sendPort.send(port.sendPort); // 通知主 isolate 监听端口

  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));
    replyTo.send(jsonDecode(response.body)); // 发送结果回主 isolate
  }
}

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

(完整示例代码较长,此处从略,原理同上。)

OkHttp 在 Flutter 中对应什么库?

在 Flutter 中进行网络请求,使用流行的 http package 非常简单。虽然它没有 OkHttp 的所有功能,但封装了常见的网络操作。

首先,在 pubspec.yaml 中添加依赖:

dependencies:
  http: ^1.1.0

然后,你可以在异步函数中使用它:

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 中,执行耗时任务时通常会显示一个 ProgressBar。在 Flutter 中,你可以使用 ProgressIndicator Widget,并通过一个布尔状态变量来控制它的显示。

下面的例子将 build 方法逻辑拆分:如果数据为空,则显示 CircularProgressIndicator;否则,在 ListView 中渲染数据。

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

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

class SampleApp extends StatelessWidget {
  const SampleApp({super.key});
  @override
  Widget build(BuildContext context) {
    return 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 区分资源 (resources) 和资产 (assets),但 Flutter 应用只有资产 (assets)。所有原本放在 Android res/drawable-* 中的图片,在 Flutter 中都放在一个 assets 文件夹中。

Flutter 遵循基于像素密度的命名格式(类似 iOS)。图片可以是 1.0x、2.0x、3.0x 等。Flutter 没有 dp 单位,但有逻辑像素,与设备无关像素基本相同。devicePixelRatio 表示了物理像素与逻辑像素的比例。

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

  • ldpi -> 0.75x
  • mdpi -> 1.0x
  • hdpi -> 1.5x
  • xhdpi -> 2.0x
  • xxhdpi -> 3.0x
  • xxxhdpi -> 4.0x

文件可以放在任意文件夹,没有预定义的结构。你需要在 pubspec.yaml 中声明资产及其位置。例如,添加一个图片 my_icon.png,可以按如下方式组织:

images/my_icon.png       // 基础 1.0x 图片
images/2.0x/my_icon.png  // 2.0x 图片
images/3.0x/my_icon.png  // 3.0x 图片

pubspec.yaml 中声明:

assets:
 - images/my_icon.png

然后通过 AssetImageImage.asset 使用:

AssetImage('images/my_icon.png')
// 或
Image.asset('images/my_image.png')

Flutter 没有专门的字符串资源管理系统。推荐的做法是将字符串存储在 .arb(Application Resource Bundle)文件中,然后通过国际化库(如 intl)来访问。例如,在代码中你可以这样使用:Text(AppLocalizations.of(context)!.hello('John'))。更多细节请查阅 Flutter 国际化指南

Gradle 文件的对应物是什么?如何添加依赖?

在 Android 中,你在 Gradle 构建脚本中添加依赖。Flutter 使用 Dart 的构建系统和 Pub 包管理器。虽然 Flutter 项目的 android 文件夹下有 Gradle 文件,但它们仅用于添加该平台的原生依赖。通常,Flutter 项目的外部依赖在 pubspec.yaml 文件中定义。pub.dev 是查找 Flutter packages 的最佳站点。

Activity 和 Fragment

Activity 和 Fragment 在 Flutter 中的对应概念是什么?

在 Android 中,Activity 代表一个用户可以完成的独立任务,Fragment 代表一个行为或 UI 的一部分。在 Flutter 中,这两个概念都对应于 Widget。Flutter 的界面完全由 Widget 构成,你使用 Navigator 在表示不同屏幕或页面的 Route(也是 Widget)之间进行导航。

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

在 Flutter 中,你可以通过 WidgetsBindingObserver 混入并监听 didChangeAppLifecycleState() 来观察应用的生命周期状态。可观察到的状态有:

  • inactive — 应用处于非活动状态,不接收用户输入。
  • paused — 应用对用户不可见,运行在后台(对应 Android onPause())。
  • resumed — 应用可见并可响应用户输入(对应 Android onPostResume())。
  • detached — 应用与宿主视图分离。

需要注意的是,Flutter 只暴露了部分生命周期事件,因为 Flutter 引擎本身会管理大部分生命周期。如果你需要获取或释放原生资源,最好在原生端处理。

以下是一个监听生命周期状态的 Widget 示例:

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 用于水平或垂直线性排列子视图。在 Flutter 中,分别使用 RowColumn Widget 来实现。它们的子 Widget 列表是相同的,这使得构建复杂的、可变化的布局非常方便。

水平布局示例:

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

垂直布局示例:

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

RelativeLayout 的对应概念是什么?

在 Android 中,RelativeLayout 根据子视图之间的相对位置进行布局。在 Flutter 中,你可以通过组合 ColumnRowStack Widget 来实现类似效果。Stack 允许子 Widget 相对于其边框进行定位,类似于 RelativeLayout

ScrollView 的对应概念是什么?

在 Android 中,ScrollView 用于可滚动的布局。在 Flutter 中,最简单的实现是使用 ListView Widget。事实上,Flutter 的 ListView 既充当了 ScrollView,也充当了 Android 的 ListView

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

在 Flutter 中如何处理屏幕旋转?

默认情况下,Flutter 应用会自动适应屏幕旋转。如果你需要在旋转时执行特定逻辑,可以通过 WidgetsBindingObserver 监听 didChangeMetrics 事件,该事件在屏幕方向、尺寸等发生变化时触发。通常,你需要在 AndroidManifest.xml 中为 Activity 配置 android:configChanges="orientation|screenSize",但这主要是为了让 Flutter 引擎处理配置变更,而不是在 Dart 层必须的。

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

在 Flutter 中,有两种主要方式为 Widget 添加点击监听器:

  1. 如果 Widget 本身支持事件监听:直接向其 onPressed(或类似)参数传递一个函数。例如 ElevatedButton
    @override
    Widget build(BuildContext context) {
      return ElevatedButton(
        onPressed: () {
          developer.log('click');
        },
        child: const Text('Button'),
      );
    }
    
  2. 如果 Widget 不支持事件监听:使用 GestureDetector 包装该 Widget,并向其 onTap 参数传递函数:
    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
  • 双击onDoubleTap
  • 长按onLongPress
  • 垂直拖动onVerticalDragStart, onVerticalDragUpdate, onVerticalDragEnd
  • 水平拖动onHorizontalDragStart, onHorizontalDragUpdate, onHorizontalDragEnd

例如,下面的代码使用 GestureDetector 实现双击旋转 Flutter 标志:

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 中的对应概念是什么?

在 Flutter 中,对应的仍然是 ListView。由于 Widget 的不可变性,你需要向 ListViewchildren 属性传入一个 Widget 列表。Flutter 会高效地管理和渲染这些列表项。

import 'package:flutter/material.dart';

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> {
  @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;
  }
}

要为列表项添加点击监听,可以在创建每个列表项 Widget 时包裹一个 GestureDetector

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 的数据,你需要在 setState() 中更新数据源,然后让 Flutter 重建 UI。对于小型列表,可以简单地在 setState 中创建一个新的列表。但对于动态列表或大数据集,强烈推荐使用 ListView.builder。它只会构建可见的列表项,类似于 Android 的 RecyclerView,能自动回收视图,性能高效。

使用 ListView.builder 的示例结构如下:

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

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<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` 方法,它根据位置(index)返回你希望在该位置渲染的列表项 Widget。需要注意的是,上面示例中的 `onTap()` 方法不再重建整个列表,而是通过 `List.add` 操作来动态添加新项。

### 如何为 Text Widget 设置自定义字体?

在 Android 中,你可以创建字体资源文件并应用于 TextView。在 Flutter 中,首先需要将字体文件(如 `.ttf`)放入项目目录(例如 `fonts/` 文件夹),然后在 `pubspec.yaml` 文件中声明它:

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

随后,在 Text Widget 中通过 TextStylefontFamily 属性来使用自定义字体:

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

如何更改 Text Widget 的样式?

Text Widget 的样式由 TextStyle 对象控制,它提供了丰富的自定义属性,包括但不限于:

  • color:文字颜色
  • fontFamily:字体
  • fontSize:字号
  • fontWeight:字体粗细(如 FontWeight.bold
  • fontStyle:字体样式(如 FontStyle.italic
  • letterSpacing:字符间距
  • wordSpacing:单词间距
  • decoration:文本装饰(如下划线)
  • height:行高

Input 的「提示」(Hint) 功能如何实现?

在 Flutter 中,通过给 TextFielddecoration 参数传入一个 InputDecoration 对象,并设置其 hintText 属性,即可展示提示文本:

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

如何展示输入验证的错误信息?

错误信息的展示方式与提示类似。你可以在用户输入无效内容后更新状态,并为 InputDecoration 设置 errorText 属性。以下是一个邮箱验证的示例:

import 'package:flutter/material.dart';

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> {
  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: _errorText,
          ),
        ),
      ),
    );
  }
  bool isEmail(String em) {
    const emailRegexp =
        r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|'
        r'(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|'
        r'(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$';
    final regExp = RegExp(emailRegexp);
    return regExp.hasMatch(em);
  }
}

如何访问设备特定功能(GPS、相机等)?

Flutter 拥有丰富的社区插件来访问原生设备功能:

如何在 Flutter 中使用 Firebase?

Firebase 的绝大部分功能都有官方维护的插件提供支持:

如果官方插件未涵盖某些特定功能,你可以在 Pub 上查找第三方插件,或参考 开发包和插件 文档创建自己的插件。Flutter 的插件机制允许 Dart 代码与 Android/iOS 平台的原生代码进行通信。

如何在 Flutter 应用中使用 NDK?

目前不支持从 Flutter Dart 代码直接调用原生(C/C++)代码。如果你的现有 Android 应用使用了 NDK,你可以通过创建自定义插件来桥接。该插件负责在 Android 端通过 JNI 调用原生方法,然后将结果返回给 Flutter 层进行渲染。

如何自定义应用的主题?

Flutter 提供了出色的 Material Design 开箱即用支持。你可以在应用的顶层 Widget(通常是 MaterialApp)上定义整体主题。MaterialApptheme 参数接收一个 ThemeData 对象,用于统一配置颜色、字体等样式。

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 Widget)?

目前无法完全使用 Flutter 创建 Android 主屏幕小部件。它们需要使用 Jetpack Glance(推荐)或 XML 布局代码来实现。你可以借助第三方包(如 home_widget)将小部件与 Dart 代码关联,在宿主小部件中嵌入 Flutter 组件(作为图像),并在 Flutter 与主屏幕小部件之间共享数据。

为了提供更丰富的体验,建议为小部件选择器添加预览。对于运行 Android 15 及更高版本的设备,可以使用生成的动态小部件预览。

如何使用 Shared Preferences?

在 Flutter 中,可以使用 shared_preferences 插件来存储少量的键值对数据,它同时封装了 Android 的 SharedPreferences 和 iOS 的 NSUserDefaults。

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

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

如何在 Flutter 中使用 SQLite?

对于需要在 Flutter 应用中使用 SQLite 进行结构化数据存储和查询的场景,可以使用 sqflite 插件,它支持 Android、iOS 和 macOS 平台。

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

推荐使用 Flutter 开发者工具 进行调试和性能分析。这套工具集提供了丰富功能,包括:

  • 检查 Widget 树和渲染层
  • 性能图表分析
  • 调试内存泄漏和内存碎片
  • 查看日志和诊断信息
  • 单步调试和代码执行观察

如何设置推送通知?

在 Flutter 中,可以使用 firebase_messaging 插件来集成 Firebase Cloud Messaging (FCM),从而实现推送通知功能。具体配置和使用方法请参考该插件的官方文档。

0 Answers