面向 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 View,但当你熟悉 Flutter 的工作方式时,你可以将它们视为“声明和构建 UI 的方式”。
但是,它们与 View
有一些不同之处。首先,widget 具有不同的生命周期:它们是不可变的,并且只存在直到需要更改。每当 widget 或其状态发生变化时,Flutter 框架都会创建一个新的 widget 实例树。相比之下,Android View 只绘制一次,直到调用 invalidate
后才重新绘制。
Flutter 的 widget 是轻量级的,部分原因在于它们的不可变性。因为它们本身不是 View,也不直接绘制任何内容,而只是 UI 及其语义的描述,这些描述会在底层“膨胀”成实际的 View 对象。
Flutter 包含了 Material Components 库。这些 widget 实现了 Material Design 指南。Material Design 是一个灵活的设计系统,针对所有平台进行了优化,包括 iOS。
但 Flutter 足够灵活和富有表现力,可以实现任何设计语言。例如,在 iOS 上,你可以使用 Cupertino widget 来生成类似于 Apple iOS 设计语言的界面。
如何更新 widget?
#在 Android 中,你通过直接修改 View 来更新它们。然而,在 Flutter 中,Widget
是不可变的,不能直接更新,而是必须处理 widget 的状态。
这就是 Stateful
和 Stateless
widget 概念的来源。StatelessWidget
正如其名——一个不包含状态信息的 widget。
当用户界面中你所描述的部分不依赖于对象中的配置信息之外的任何内容时,StatelessWidgets
很有用。
例如,在 Android 中,这类似于放置一个带有你 logo 的 ImageView
。logo 在运行时不会改变,因此在 Flutter 中使用 StatelessWidget
。
如果你想根据 HTTP 调用后收到的数据或用户交互动态更改 UI,那么你必须使用 StatefulWidget
并告诉 Flutter 框架该 widget 的 State
已更新,以便它可以更新该 widget。
这里需要注意的重要一点是,无状态和有状态 widget 的核心行为相同。它们每帧都会重建,不同之处在于 StatefulWidget
有一个 State
对象,该对象在帧之间存储状态数据并恢复它。
如果你有疑问,请始终记住这条规则:如果 widget 发生变化(例如由于用户交互),则它是有状态的。但是,如果 widget 对变化做出反应,则其包含的父 widget 仍然可以是无状态的,如果它本身不对变化做出反应。
以下示例展示了如何使用 StatelessWidget
。一个常见的 StatelessWidget
是 Text
widget。如果你查看 Text
widget 的实现,你会发现它继承自 StatelessWidget
。
Text(
'I like Flutter!',
style: TextStyle(fontWeight: FontWeight.bold),
);
如你所见,Text
Widget 没有与之关联的状态信息,它只渲染其构造函数中传递的内容,别无其他。
但是,如果你想动态更改“I Like Flutter”,例如在点击 FloatingActionButton
时,该怎么办?
为了实现这一点,将 Text
widget 包装在 StatefulWidget
中,并在用户点击按钮时更新它。
例如
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
@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 之间切换
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
// Default value for toggle.
bool toggle = true;
void _toggle() {
setState(() {
toggle = !toggle;
});
}
Widget _getToggleChild() {
if (toggle) {
return const Text('Toggle One');
} else {
return ElevatedButton(onPressed: () {}, child: const Text('Toggle Two'));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: Center(child: _getToggleChild()),
floatingActionButton: FloatingActionButton(
onPressed: _toggle,
tooltip: 'Update Text',
child: const Icon(Icons.update),
),
);
}
}
如何为 widget 添加动画?
#在 Android 中,你可以使用 XML 创建动画,或在 View 上调用 animate()
方法。在 Flutter 中,通过将 widget 包装在动画 widget 中,使用动画库为 widget 添加动画。
在 Flutter 中,使用 AnimationController
,它是一个 Animation<double>
,可以暂停、查找、停止和反转动画。它需要一个 Ticker
,用于在 vsync 发生时发出信号,并在运行时每帧生成 0 到 1 之间的线性插值。然后创建或多个 Animation
并将它们附加到控制器。
例如,你可以使用 CurvedAnimation
来实现沿插值曲线的动画。从这个意义上说,控制器是动画进度的“主”源,而 CurvedAnimation
计算替代控制器默认线性运动的曲线。像 widget 一样,Flutter 中的动画通过组合来工作。
在构建 widget 树时,你将 Animation
分配给 widget 的动画属性,例如 FadeTransition
的不透明度,并告诉控制器开始动画。
以下示例展示了如何编写一个 FadeTransition
,当您按下 FloatingActionButton
时,该 widget 会逐渐淡入一个徽标。
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),
),
);
}
}
有关更多信息,请参阅动画和运动 widget、动画教程和动画概述。
如何使用 Canvas 进行绘制?
#在 Android 中,你会使用 Canvas
和 Drawable
在屏幕上绘制图像和形状。Flutter 也有类似的 Canvas
API,因为它基于相同的低级渲染引擎 Skia。因此,在 Flutter 中向 canvas 绘图对于 Android 开发者来说是一项非常熟悉的任务。
Flutter 有两个类可以帮助你在 canvas 上绘图:CustomPaint
和 CustomPainter
,其中后者实现了你在 canvas 上绘图的算法。
要了解如何在 Flutter 中实现签名绘图器,请参阅 Collin 在 Custom Paint 上的回答。
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
,或使用现有的 View,来重写和实现方法以达到所需的行为。
在 Flutter 中,通过组合更小的 widget(而不是继承它们)来构建自定义 widget。这与在 Android 中实现自定义 ViewGroup
有些类似,其中所有构建块都已存在,但你提供不同的行为——例如,自定义布局逻辑。
例如,你如何构建一个在其构造函数中接受标签的 CustomButton
?创建一个由 ElevatedButton
和标签组合而成的 CustomButton,而不是通过继承 ElevatedButton
。
class CustomButton extends StatelessWidget {
final String label;
const CustomButton(this.label, {super.key});
@override
Widget build(BuildContext context) {
return ElevatedButton(onPressed: () {}, child: Text(label));
}
}
然后使用 CustomButton
,就像使用任何其他 Flutter widget 一样
@override
Widget build(BuildContext context) {
return const Center(child: CustomButton('Hello'));
}
Intent
#Flutter 中 Intent 的对应概念是什么?
#在 Android 中,Intent
有两个主要用例:在 Activity 之间导航和与组件通信。另一方面,Flutter 没有 Intent 的概念,尽管你仍然可以通过原生集成(使用插件)启动 Intent。
Flutter 没有真正直接等同于 Activity 和 Fragment 的概念;相反,在 Flutter 中,你使用 Navigator
和 Route
在屏幕之间导航,所有这些都在同一个 Activity
中。
Route
是应用中“屏幕”或“页面”的抽象,而 Navigator
是管理路由的 widget。路由大致对应于 Activity
,但含义不同。导航器可以推入和弹出路由以在屏幕之间移动。导航器的工作方式类似于堆栈,你可以在其上 push()
想要导航到的新路由,并在想要“返回”时从其上 pop()
路由。
在 Android 中,你在应用的 AndroidManifest.xml
中声明 Activity。
在 Flutter 中,你有几种在页面之间导航的选项
- 指定路由名称的
Map
。(使用MaterialApp
) - 直接导航到路由。(使用
WidgetsApp
)
以下示例构建一个 Map。
void main() {
runApp(
MaterialApp(
home: const MyAppHome(), // Becomes the route named '/'.
routes: <String, WidgetBuilder>{
'/a': (context) => const MyPage(title: 'page A'),
'/b': (context) => const MyPage(title: 'page B'),
'/c': (context) => const MyPage(title: 'page C'),
},
),
);
}
通过将路由名称 push
到 Navigator
来导航到路由。
Navigator.of(context).pushNamed('/b');
Intent
的另一个常见用例是调用外部组件,例如相机或文件选择器。为此,你需要创建原生平台集成(或使用现有插件)。
要了解如何构建原生平台集成,请参阅开发包和插件。
如何在 Flutter 中处理来自外部应用的传入 intent?
#Flutter 可以通过直接与 Android 层通信并请求共享的数据来处理来自 Android 的传入 intent。
以下示例在运行我们 Flutter 代码的原生 Activity 上注册一个文本共享 intent 过滤器,以便其他应用可以与我们的 Flutter 应用共享文本。
基本流程意味着我们首先在 Android 原生端(在我们的 Activity
中)处理共享文本数据,然后等待 Flutter 请求数据,再使用 MethodChannel
提供数据。
首先,在 AndroidManifest.xml
中注册所有 intent 的 intent 过滤器
<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 准备好处理时,它使用平台通道请求数据,然后从原生端发送过去
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 端请求数据
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
上完成的。
例如,要启动一个允许用户选择其位置的位置路由,你可以执行以下操作
Object? coordinates = await Navigator.of(context).pushNamed('/location');
然后,在你的位置路由中,一旦用户选择了他们的位置,你可以使用结果 pop
堆栈
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 中使用过 async
/await
范式,或者使用过 Kotlin 的协程,你可能对此很熟悉。
例如,你可以使用 async
/await
运行网络代码,而不会导致 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?>>();
});
}
一旦 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?>>();
});
}
}
有关在后台执行工作的更多信息以及 Flutter 与 Android 的区别,请参阅下一节。
如何将工作转移到后台线程?
#在 Android 中,当你想要访问网络资源时,通常会将其转移到后台线程并执行工作,以免阻塞主线程并避免 ANR。例如,你可能正在使用 AsyncTask
、LiveData
、IntentService
、JobScheduler
作业,或者带有在后台线程上工作的调度器的 RxJava 管道。
由于 Flutter 是单线程并运行事件循环(类似于 Node.js),你无需担心线程管理或派生后台线程。如果你正在进行 I/O 密集型工作,例如磁盘访问或网络调用,那么你可以安全地使用 async
/await
,一切就绪。另一方面,如果你需要进行使 CPU 繁忙的计算密集型工作,则需要将其移至 Isolate
以避免阻塞事件循环,就像你在 Android 中将任何类型的工作移出主线程一样。
对于 I/O 密集型工作,将函数声明为 async
函数,并在函数内部 await
长时间运行的任务
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。
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?>;
setState(() {
widgets = msg;
});
}
// 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 data = msg[0] as String;
SendPort replyTo = msg[1] as SendPort;
String dataURL = data;
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),或执行计算密集型数学运算,例如加密或信号处理。
你可以在下面运行完整示例
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 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?>;
setState(() {
widgets = msg;
});
}
// 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 data = msg[0] as String;
SendPort replyTo = msg[1] as SendPort;
String dataURL = data;
http.Response response = await http.get(Uri.parse(dataURL));
// Lots of JSON to parse
replyTo.send(jsonDecode(response.body));
}
}
Future<Object?> sendReceive(SendPort port, Object? msg) {
ReceivePort response = ReceivePort();
port.send([msg, response.sendPort]);
return response.first;
}
}
Flutter 中 OkHttp 的对应概念是什么?
#当使用流行的 http
包时,在 Flutter 中进行网络调用很容易。
虽然 http 包不包含 OkHttp 中的所有功能,但它抽象了你通常会自己实现的许多网络功能,使其成为进行网络调用的简单方法。
要将 http
包添加为依赖项,运行 flutter pub add
flutter pub add http
要进行网络调用,在 async
函数 http.get()
上调用 await
import 'dart:developer' as developer;
import 'package:http/http.dart' as http;
Future<void> loadData() async {
var dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
http.Response response = await http.get(dataURL);
developer.log(response.body);
}
如何显示长时间运行任务的进度?
#在 Android 中,你通常会在 UI 中显示一个 ProgressBar
视图,同时在后台线程上执行长时间运行的任务。
在 Flutter 中,使用 ProgressIndicator
widget。通过布尔标志控制何时渲染它,从而以编程方式显示进度。告诉 Flutter 在你的长时间运行任务开始之前更新其状态,并在任务结束后将其隐藏。
在以下示例中,build 函数被分为三个不同的函数。如果 showLoadingDialog
为 true
(当 widgets.isEmpty
时),则渲染 ProgressIndicator
。否则,使用网络调用返回的数据渲染 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 将资源和资产视为不同的项,但 Flutter 应用只有资产。所有在 Android 上位于 res/drawable-*
文件夹中的资源,在 Flutter 中都放置在 assets 文件夹中。
Flutter 遵循类似于 iOS 的简单基于密度的格式。资产可以是 1.0x
、2.0x
、3.0x
或任何其他乘数。Flutter 没有 dp
,但有逻辑像素,这基本上与设备独立像素相同。Flutter 的devicePixelRatio
表示单个逻辑像素中物理像素的比率。
Android 密度桶的对应关系是
Android 密度限定符 | Flutter 像素比 |
---|---|
ldpi | 0.75x |
mdpi | 1.0x |
hdpi | 1.5x |
xhdpi | 2.0x |
xxhdpi | 3.0x |
xxxhdpi | 4.0x |
资产可以位于任何任意文件夹中——Flutter 没有预定义的文件夹结构。你在 pubspec.yaml
文件中声明资产(及其位置),Flutter 会识别它们。
存储在原生资产文件夹中的资产可以使用 Android 的 AssetManager
在原生侧访问
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
文件中声明这些图片
assets:
- images/my_icon.png
然后你可以使用 AssetImage
访问你的图片
AssetImage('images/my_icon.png'),
或直接在 Image
widget 中
@override
Widget build(BuildContext context) {
return Image.asset('images/my_image.png');
}
我将字符串存储在哪里?如何处理本地化?
#Flutter 目前没有专门的类似资源系统来处理字符串。最好和推荐的做法是将你的字符串以键值对的形式保存在 .arb
文件中,例如
{
"@@locale": "en",
"hello":"Hello {userName}",
"@hello":{
"description":"A message with a single parameter",
"placeholders":{
"userName":{
"type":"String",
"example":"Bob"
}
}
}
}
然后,在你的代码中,你可以这样访问你的字符串
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 的方式。在 Flutter 中,这两个概念都属于 Widget
的范畴。
要了解有关构建 Activity 和 Fragment 的 UI,请参阅社区贡献的 Medium 文章:面向 Android 开发者的 Flutter:如何在 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 的生命周期状态的示例
import 'package:flutter/widgets.dart';
class LifecycleWatcher extends StatefulWidget {
const LifecycleWatcher({super.key});
@override
State<LifecycleWatcher> createState() => _LifecycleWatcherState();
}
class _LifecycleWatcherState extends State<LifecycleWatcher>
with WidgetsBindingObserver {
AppLifecycleState? _lastLifecycleState;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
setState(() {
_lastLifecycleState = state;
});
}
@override
Widget build(BuildContext context) {
if (_lastLifecycleState == null) {
return const Text(
'This widget has not observed any lifecycle changes.',
textDirection: TextDirection.ltr,
);
}
return Text(
'The most recent lifecycle state this widget observed was: $_lastLifecycleState.',
textDirection: TextDirection.ltr,
);
}
}
void main() {
runApp(const Center(child: LifecycleWatcher()));
}
布局
#LinearLayout 的对应概念是什么?
#在 Android 中,LinearLayout 用于线性布局你的 widget——无论是水平还是垂直。在 Flutter 中,使用 Row 或 Column widget 达到相同的效果。
如果你注意到,除了“Row”和“Column” widget 之外,这两个代码示例是相同的。子级是相同的,可以利用此功能开发丰富的布局,这些布局可以随着时间推移使用相同的子级进行更改。
@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'),
],
);
}
要了解有关构建线性布局的更多信息,请参阅社区贡献的 Medium 文章 面向 Android 开发者的 Flutter:如何在 Flutter 中设计 LinearLayout。
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。
@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 会处理配置更改
android:configChanges="orientation|screenSize"
手势检测和触摸事件处理
#如何在 Flutter 中为 widget 添加 onClick 监听器?
#在 Android 中,你可以通过调用方法 'setOnClickListener' 将 onClick 附加到视图(例如按钮)。
在 Flutter 中,有两种添加触摸监听器的方法
- 如果 Widget 支持事件检测,则向其传递一个函数并在该函数中处理它。例如,ElevatedButton 有一个
onPressed
参数
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
developer.log('click');
},
child: const Text('Button'),
);
}
- 如果 Widget 不支持事件检测,则将 widget 包装在 GestureDetector 中,并将函数传递给
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
- 之前触发onTapDown
的指针不会引起点击。
双击
onDoubleTap
- 用户在相同位置快速连续点击屏幕两次。
长按
onLongPress
- 指针在同一位置与屏幕保持接触一段时间。
垂直拖动
onVerticalDragStart
- 指针已接触屏幕并可能开始垂直移动。onVerticalDragUpdate
- 与屏幕接触的指针在垂直方向上进一步移动。onVerticalDragEnd
- 之前与屏幕接触并垂直移动的指针不再与屏幕接触,并且在停止接触屏幕时以特定速度移动。
水平拖动
onHorizontalDragStart
- 指针已接触屏幕并可能开始水平移动。onHorizontalDragUpdate
- 与屏幕接触的指针在水平方向上进一步移动。onHorizontalDragEnd
- 之前与屏幕接触并水平移动的指针不再与屏幕接触,并且在停止接触屏幕时以特定速度移动。
以下示例展示了一个 GestureDetector
,它在双击时旋转 Flutter logo
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 会负责确保滚动快速流畅。
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 提供的触摸处理。
import 'dart:developer' as developer;
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: ListView(children: _getListData()),
);
}
List<Widget> _getListData() {
List<Widget> widgets = [];
for (int i = 0; i < 100; i++) {
widgets.add(
GestureDetector(
onTap: () {
developer.log('row tapped');
},
child: Padding(
padding: const EdgeInsets.all(10),
child: Text('Row $i'),
),
),
);
}
return widgets;
}
}
如何动态更新 ListView?
#在 Android 上,你更新适配器并调用 notifyDataSetChanged
。
在 Flutter 中,如果你在 setState()
中更新 widget 列表,你会很快发现你的数据在视觉上没有变化。这是因为当调用 setState()
时,Flutter 渲染引擎会查看 widget 树以查看是否有任何更改。当它到达你的 ListView
时,它会执行 ==
检查,并确定两个 ListView
是相同的。没有发生任何变化,因此不需要更新。
更新 ListView
的简单方法是在 setState()
中创建一个新的 List
,并将旧列表中的数据复制到新列表中。虽然这种方法很简单,但对于大型数据集不推荐使用,如下一个示例所示。
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,它会自动为你回收列表元素
import 'dart:developer' as developer;
import 'package:flutter/material.dart';
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});
@override
State<SampleAppPage> createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List<Widget> widgets = [];
@override
void initState() {
super.initState();
for (int i = 0; i < 100; i++) {
widgets.add(getRow(i));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: ListView.builder(
itemCount: widgets.length,
itemBuilder: (context, position) {
return getRow(position);
},
),
);
}
Widget getRow(int i) {
return GestureDetector(
onTap: () {
setState(() {
widgets.add(getRow(widgets.length));
developer.log('row $i');
});
},
child: Padding(padding: const EdgeInsets.all(10), child: Text('Row $i')),
);
}
}
不是创建“ListView”,而是创建一个 ListView.builder
,它接受两个关键参数:列表的初始长度和一个 ItemBuilder
函数。
ItemBuilder
函数类似于 Android 适配器中的 getView
函数;它接受一个位置,并返回你希望在该位置渲染的行。
最后,也是最重要的一点,请注意 onTap()
函数不再重新创建列表,而是向其 .add
。
处理文本
#如何为 Text widget 设置自定义字体?
#在 Android SDK 中(截至 Android O),你创建一个字体资源文件并将其传递给 TextView 的 FontFamily 参数。
在 Flutter 中,将字体文件放在一个文件夹中,并在 pubspec.yaml
文件中引用它,类似于你导入图片的方式。
fonts:
- family: MyCustomFont
fonts:
- asset: fonts/MyCustomFont.ttf
- style: italic
然后将字体分配给你的 Text
widget
@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 上的其他样式元素。Text
widget 的 style 参数接受一个 TextStyle
对象,你可以在其中自定义许多参数,例如
- 颜色
- 装饰
- 装饰颜色
- 装饰样式
- 字体家族
- 字体大小
- 字体样式
- 字体粗细
- 哈希码
- 高度
- 继承
- 字间距
- 文本基线
- 词间距
表单输入
#有关使用表单的更多信息,请参阅获取文本字段的值(来自Flutter 开发手册)。
Input 上的“提示”的对应概念是什么?
#在 Flutter 中,你可以通过向 Text Widget 的 decoration 构造函数参数添加 InputDecoration 对象,轻松为你的输入框显示“提示”或占位文本。
Center(
child: TextField(decoration: InputDecoration(hintText: 'This is a hint')),
)
如何显示验证错误?
#就像使用“提示”一样,将 InputDecoration 对象传递给 Text widget 的 decoration 构造函数。
但是,你不想一开始就显示错误。相反,当用户输入无效数据时,更新状态,并传递一个新的 InputDecoration
对象。
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 团队维护
- 用于 Flutter 的 Google 移动广告的
google_mobile_ads
- 用于 Firebase Analytics 的
firebase_analytics
- 用于 Firebase Auth 的
firebase_auth
- 用于 Firebase RTDB 的
firebase_database
- 用于 Firebase Cloud Storage 的
firebase_storage
- 用于 Firebase Messaging (FCM) 的
firebase_messaging
- 用于快速 Firebase Auth 集成(Facebook、Google、Twitter 和电子邮件)的
flutter_firebase_ui
- 用于 Firebase Cloud Firestore 的
cloud_firestore
你还可以在 pub.dev 上找到一些第三方 Firebase 插件,它们涵盖了第一方插件未直接涵盖的领域。
如何构建自己的自定义原生集成?
#如果 Flutter 或其社区插件缺少平台特定功能,你可以按照开发包和插件页面构建自己的功能。
简而言之,Flutter 的插件架构很像在 Android 中使用 Event bus:你发送一条消息,让接收方处理并向你返回结果。在这种情况下,接收方是在 Android 或 iOS 原生端运行的代码。
如何在 Flutter 应用中使用 NDK?
#如果你在当前的 Android 应用中使用了 NDK,并且希望你的 Flutter 应用利用你的原生库,那么可以通过构建自定义插件来实现。
你的自定义插件首先与你的 Android 应用通信,在那里你通过 JNI 调用你的 native
函数。一旦响应准备就绪,将消息发送回 Flutter 并渲染结果。
目前不支持直接从 Flutter 调用原生代码。
主题
#如何为我的应用设置主题?
#开箱即用,Flutter 提供了 Material Design 的精美实现,它处理了许多你通常会进行样式设置和主题化的需求。与 Android 中你在 XML 中声明主题然后使用 AndroidManifest.xml 将其分配给你的应用不同,在 Flutter 中,你在顶层 widget 中声明主题。
为了充分利用应用中的 Material Components,你可以将顶层 widget MaterialApp
声明为应用的入口点。MaterialApp 是一个便捷 widget,它封装了实现 Material Design 的应用通常需要的许多 widget。它通过添加 Material 特定功能来构建在 WidgetsApp 之上。
你也可以使用 WidgetsApp
作为你的应用 widget,它提供了一些相同的功能,但不如 MaterialApp
丰富。
要自定义任何子组件的颜色和样式,请将 ThemeData
对象传递给 MaterialApp
widget。例如,在下面的代码中,种子颜色方案设置为深紫色,文本选择颜色设置为红色。
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(),
);
}
}
数据库和本地存储
#如何访问 Shared Preferences?
#在 Android 中,你可以使用 SharedPreferences API 存储少量键值对。
在 Flutter 中,使用 Shared_Preferences 插件访问此功能。此插件封装了 Shared Preferences 和 NSUserDefaults(iOS 的对应功能)的功能。
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
插件文档。