面向 Android 开发者的 Flutter
本文件面向希望运用其现有的 Android 知识来使用 Flutter 构建移动应用的 Android 开发者。如果您理解 Android 框架的基础知识,那么可以使用本文件作为 Flutter 开发的入门指南。
在使用 Flutter 进行构建时,您的 Android 知识和技能非常宝贵,因为 Flutter 依赖于移动操作系统来实现众多功能和配置。Flutter 是一种构建移动 UI 的新方法,但它有一个插件系统,可与 Android(和 iOS)进行通信以执行非 UI 任务。如果您是 Android 方面的专家,则无需重新学习所有内容即可使用 Flutter。
本文件可用作食谱,您可以随意跳跃,找到与您的需求最相关的疑问。
视图
Flutter 中 View 的等效项是什么?
在 Android 中,View
是屏幕上显示所有内容的基础。按钮、工具栏和输入,所有内容都是 View。在 Flutter 中,View
的近似等效项是 Widget
。Widget 无法完全映射到 Android 视图,但在熟悉 Flutter 的工作原理时,可以将它们视为“声明和构建 UI 的方式”。
但是,它们与 View
有几个不同之处。首先,Widget 具有不同的生命周期:它们是不可变的,并且仅在需要更改时才存在。每当 Widget 或其状态发生更改时,Flutter 的框架都会创建一个新的 Widget 实例树。相比之下,Android 视图绘制一次,直到调用 invalidate
才重新绘制。
Flutter 的 Widget 很轻量,部分原因是它们不可变。因为它们本身不是视图,并且不会直接绘制任何内容,而只是 UI 及其语义的描述,这些描述在后台“膨胀”为实际的视图对象。
Flutter 包含 Material Components 库。这些是实现 Material Design 指南 的 Widget。Material Design 是一个灵活的设计系统,针对所有平台进行了优化,包括 iOS。
但是,Flutter 足够灵活和富有表现力,可以实现任何设计语言。例如,在 iOS 上,可以使用 Cupertino Widget 来生成看起来像 Apple 的 iOS 设计语言 的界面。
如何更新 Widget?
在 Android 中,通过直接修改视图来更新视图。但是,在 Flutter 中,Widget
是不可变的,不会直接更新,而必须使用 Widget 的状态。
这就是 Stateful
和 Stateless
Widget 概念的由来。StatelessWidget
正如其名——没有状态信息的 Widget。
StatelessWidget
在描述的用户界面部分不依赖于对象中的配置信息以外的任何内容时很有用。
例如,在 Android 中,这类似于放置带有徽标的 ImageView
。徽标在运行时不会更改,因此在 Flutter 中使用 StatelessWidget
。
如果您希望根据在进行 HTTP 调用或用户交互后收到的数据动态更改 UI,则必须使用 StatefulWidget
并告知 Flutter 框架小部件的 State
已更新,以便它可以更新该小部件。
这里需要注意的重要一点是,从本质上来说,无状态小部件和有状态小部件的行为是相同的。它们每帧都会重建,不同之处在于 StatefulWidget
具有一个 State
对象,该对象跨帧存储状态数据并对其进行恢复。
如果您有疑问,请始终记住此规则:如果小部件发生更改(例如,由于用户交互),则它是有状态的。但是,如果小部件对更改做出反应,则包含它的父小部件仍然可以是无状态的,如果它本身不对更改做出反应。
以下示例演示如何使用 StatelessWidget
。常见的 StatelessWidget
是 Text
小部件。如果您查看 Text
小部件的实现,您会发现它继承了 StatelessWidget
。
Text(
'I like Flutter!',
style: TextStyle(fontWeight: FontWeight.bold),
);
如您所见,Text
小部件没有与其关联的状态信息,它呈现其构造函数中传递的内容,仅此而已。
但是,如果您希望使“I Like Flutter”动态更改,例如在单击 FloatingActionButton
时,该怎么办?
要实现此目的,请将 Text
小部件包装在 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),
),
);
}
}
如何布局我的小部件?我的 XML 布局文件在哪里?
在 Android 中,您在 XML 中编写布局,但在 Flutter 中,您使用小部件树编写布局。
以下示例演示如何显示带内边距的简单小部件
@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 提供的一些布局。
如何从我的布局中添加或移除组件?
在 Android 中,您在父级上调用 addChild()
或 removeChild()
来动态添加或移除子视图。在 Flutter 中,由于小部件是不可变的,因此没有 addChild()
的直接等效项。相反,您可以将函数传递给返回小部件的父级,并使用布尔标志控制该子级的创建。
例如,以下是如何在单击 FloatingActionButton
时在两个小部件之间切换
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),
),
);
}
}
如何对小部件进行动画处理?
在 Android 中,您可以使用 XML 创建动画,或对视图调用 animate()
方法。在 Flutter 中,通过将小组件包装在动画小组件内,使用动画库对小组件进行动画处理。
在 Flutter 中,使用 AnimationController
,它是一个 Animation<double>
,可以暂停、查找、停止和反转动画。它需要一个 Ticker
,该 Ticker 会在发生 vsync 时发出信号,并在运行时在每帧上生成 0 到 1 之间的线性插值。然后,您可以创建一个或多个 Animation
,并将它们附加到控制器。
例如,您可以使用 CurvedAnimation
来沿插值曲线实现动画。从这个意义上说,控制器是动画进度的“主”源,而 CurvedAnimation
计算出替换控制器的默认线性运动的曲线。与小组件一样,Flutter 中的动画使用组合。
在构建小组件树时,您将 Animation
分配给小组件的动画属性,例如 FadeTransition
的不透明度,并告诉控制器启动动画。
以下示例展示了如何在按下 FloatingActionButton
时将小组件淡化为徽标的 FadeTransition
的编写方法
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),
),
);
}
}
有关详细信息,请参阅 动画和动态小组件、动画教程和 动画概览。
如何使用 Canvas 进行绘制/绘画?
在 Android 中,您将使用 Canvas
和 Drawable
将图像和形状绘制到屏幕上。Flutter 也具有类似的 Canvas
API,因为它基于同一底层渲染引擎 Skia。因此,对 Flutter 中的画布进行绘画对于 Android 开发人员来说是一项非常熟悉的任务。
Flutter 有两个类可帮助您在画布上进行绘制:CustomPaint
和 CustomPainter
,后者实现了在画布上进行绘制的算法。
要了解如何在 Flutter 中实现签名绘画器,请参阅 Collin 在 自定义绘画上的回答。
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 中,通过组合较小的组件(而不是扩展它们)来构建自定义组件。这与在 Android 中实现自定义ViewGroup
有些类似,其中所有构建模块都已存在,但您提供了不同的行为,例如自定义布局逻辑。
例如,如何构建一个在构造函数中采用标签的CustomButton
?创建一个 CustomButton,它通过标签组合一个ElevatedButton
,而不是通过扩展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 组件一样
@override
Widget build(BuildContext context) {
return const Center(
child: CustomButton('Hello'),
);
}
意图
Flutter 中的意图等效项是什么?
在 Android 中,Intent
有两种主要用例:在活动之间导航和与组件通信。另一方面,Flutter 没有意图的概念,尽管您仍然可以通过本机集成(使用插件)启动意图。
Flutter 实际上没有与活动和片段直接等效的内容;相反,在 Flutter 中,您使用Navigator
和Route
在屏幕之间导航,所有这些都在同一个Activity
中。
一个 Route
是一个应用程序的“屏幕”或“页面”的抽象,而 Navigator
是一个管理路由的小部件。一个路由大致映射到一个 Activity
,但它并不具有相同的含义。导航器可以推送和弹出路由以在屏幕之间移动。导航器的工作原理就像一个堆栈,您可以在其中 push()
您想要导航到的新路由,并且可以从中 pop()
路由,当您想要“返回”时。
在 Android 中,您在应用程序的 AndroidManifest.xml
中声明您的活动。
在 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 中处理来自外部应用程序的传入意图?
Flutter 可以通过直接与 Android 层通信并请求共享的数据来处理来自 Android 的传入意图。
以下示例在运行我们的 Flutter 代码的本机活动上注册了一个文本共享意图过滤器,以便其他应用程序可以与我们的 Flutter 应用程序共享文本。
基本流程意味着我们首先在 Android 本机端(在我们的 Activity
中)处理共享的文本数据,然后等待 Flutter 请求数据以使用 MethodChannel
提供数据。
首先,在 AndroidManifest.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
中,处理意图,从意图中提取共享的文本,并保留它。当 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);
}
}
最后,在小组件呈现时从 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;
});
}
}
}
startActivityForResult() 的等效项是什么?
Navigator
类处理 Flutter 中的路由,用于从已推送到堆栈的路由中获取结果。这是通过 await
ing 在 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 不同,在 Flutter 中,使用 Dart 语言提供的异步功能(例如 async
/await
)来执行异步工作。如果您在 C#、Javascript 中使用过 async
/await
范例,或者使用过 Kotlin 的协程,那么您可能熟悉该范例。
例如,您可以使用 async
/await
运行网络代码,而不会导致 UI 挂起,并让 Dart 完成繁重的工作
Future<void> loadData() async {
var dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
http.Response response = await http.get(dataURL);
setState(() {
widgets = jsonDecode(response.body);
});
}
一旦 await
ed 网络调用完成,通过调用 setState()
更新 UI,这将触发小组件子树的重建并更新数据。
以下示例异步加载数据并将其显示在 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 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 {
var dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
http.Response response = await http.get(dataURL);
setState(() {
widgets = jsonDecode(response.body);
});
}
}
有关在后台执行工作以及 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 {
var dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
http.Response response = await http.get(dataURL);
setState(() {
widgets = jsonDecode(response.body);
});
}
这是您通常执行网络或数据库调用的方式,它们都是 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;
List msg = await sendReceive(
sendPort,
'https://jsonplaceholder.typicode.com/posts',
);
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];
SendPort replyTo = msg[1];
String dataURL = data;
http.Response response = await http.get(Uri.parse(dataURL));
// Lots of JSON to parse
replyTo.send(jsonDecode(response.body));
}
}
Future sendReceive(SendPort port, 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;
List msg = await sendReceive(
sendPort,
'https://jsonplaceholder.typicode.com/posts',
);
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];
SendPort replyTo = msg[1];
String dataURL = data;
http.Response response = await http.get(Uri.parse(dataURL));
// Lots of JSON to parse
replyTo.send(jsonDecode(response.body));
}
}
Future sendReceive(SendPort port, 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
小组件。通过控制何时通过布尔标志呈现进度指示器以以编程方式显示进度。在长时间运行的任务开始之前告诉 Flutter 更新其状态,并在任务结束后将其隐藏。
在以下示例中,构建函数被分成三个不同的函数。如果 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 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 {
var dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
http.Response response = await http.get(dataURL);
setState(() {
widgets = jsonDecode(response.body);
});
}
}
项目结构和资源
我应该将分辨率相关的图像文件存储在哪里?
尽管 Android 将资源和资产视为不同的项,但 Flutter 应用只有资产。所有在 Android 上位于 res/drawable-*
文件夹中的资源都放置在 Flutter 的资产文件夹中。
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.jpeg
然后,你可以使用 AssetImage
访问你的图像
AssetImage('images/my_icon.jpeg')
或直接在 Image
小组件中
@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。
活动和片段
Flutter 中活动和片段的等效项是什么?
在 Android 中,Activity
表示用户可以执行的单个重点事务。Fragment
表示行为或用户界面的一部分。片段是模块化代码、为大屏幕编写复杂的用户界面以及帮助扩展应用程序 UI 的一种方式。在 Flutter 中,这两个概念都属于 Widget
的范畴。
要了解有关构建活动和片段的 UI 的更多信息,请参阅社区贡献的 Medium 文章,面向 Android 开发人员的 Flutter:如何在 Flutter 中设计活动 UI。
正如 “意图” 部分中所述,在 Flutter 中,屏幕由 Widget
表示,因为在 Flutter 中,所有内容都是小组件。使用 Navigator
在表示不同屏幕或页面的不同 Route
之间移动,或者可能是同一数据的不同状态或渲染。
如何监听 Android 活动生命周期事件?
在 Android 中,你可以覆盖 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 用于以线性方式排列小部件,可以是水平或垂直方式。在 Flutter 中,使用 Row 或 Column 小部件来实现相同的结果。
如果您注意到,除了“Row”和“Column”小部件外,两个代码示例是相同的。子项相同,并且可以利用此功能来开发随着时间的推移使用相同子项而发生变化的丰富布局。
@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 相对于彼此排列小部件。在 Flutter 中,有几种方法可以实现相同的结果。
您可以通过结合使用 Column、Row 和 Stack 小部件来实现 RelativeLayout 的结果。您可以为小部件构造函数指定规则,说明子项相对于父项的排列方式。
有关在 Flutter 中构建 RelativeLayout 的一个很好的示例,请参阅 Collin 在 StackOverflow 上的回答。
ScrollView 的等效项是什么?
在 Android 中,使用 ScrollView 来排列小部件,如果用户的设备屏幕比您的内容小,它会滚动。
在 Flutter 中,最简单的方法是使用 ListView 小部件。对于来自 Android 的人来说,这可能看起来有些过分,但在 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 中处理横向过渡?
如果 AndroidManifest.xml 包含,则 FlutterView 会处理配置更改
android:configChanges="orientation|screenSize"
手势检测和触摸事件处理
如何在 Flutter 中向小部件添加 onClick 侦听器?
在 Android 中,你可以通过调用方法“setOnClickListener”将 onClick 附加到按钮等视图。
在 Flutter 中,有两种方法可以添加触摸侦听器
- 如果小部件支持事件检测,则向其传递一个函数并在函数中处理它。例如,ElevatedButton 有一个
onPressed
参数
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
developer.log('click');
},
child: const Text('Button'),
);
}
- 如果小部件不支持事件检测,则将小部件包装在 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,
),
),
),
);
}
}
如何在小部件上处理其他手势?
使用 GestureDetector,你可以侦听各种手势,例如
-
轻触
-
onTapDown
- 可能导致轻触的指针已在特定位置接触屏幕。 -
onTapUp
- 触发轻触的指针已停止在特定位置接触屏幕。 -
onTap
- 已发生轻触。 -
onTapCancel
- 之前触发onTapDown
的指针不会导致轻触。
-
-
双击
-
onDoubleTap
- 用户在同一位置快速连续轻触屏幕两次。
-
-
长按
-
onLongPress
- 指针在同一位置长时间与屏幕保持接触。
-
-
垂直拖动
-
onVerticalDragStart
- 指针已接触屏幕,可能开始垂直移动。 -
onVerticalDragUpdate
- 与屏幕接触的指针在垂直方向进一步移动。 -
onVerticalDragEnd
- 之前与屏幕接触并垂直移动的指针不再接触屏幕,且在停止接触屏幕时以特定速度移动。
-
-
水平拖动
-
onHorizontalDragStart
- 指针已接触屏幕,可能开始水平移动。 -
onHorizontalDragUpdate
- 与屏幕接触的指针在水平方向进一步移动。 -
onHorizontalDragEnd
- 之前与屏幕接触并水平移动的指针不再接触屏幕,且在停止接触屏幕时以特定速度移动。
-
以下示例显示了在双击时旋转 Flutter 徽标的 GestureDetector
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,
),
),
),
),
);
}
}
列表视图和适配器
Flutter 中 ListView 的替代方案是什么?
Flutter 中 ListView 的等效项是……ListView!
在 Android ListView 中,您创建一个适配器并将其传递到 ListView,ListView 使用适配器返回的内容呈现每一行。但是,您必须确保回收行,否则,您会遇到各种疯狂的视觉故障和内存问题。
由于 Flutter 的不可变小部件模式,您将小部件列表传递到 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 中,使用传入小部件提供的触摸处理。
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()
中的部件列表,你会很快发现你的数据在视觉上没有发生变化。这是因为当调用 setState()
时,Flutter 渲染引擎会查看部件树以查看是否有任何变化。当它到达你的 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
到其中。
使用文本
如何在我的文本部件上设置自定义字体?
在 Android SDK(从 Android O 开始),你创建一个字体资源文件,并将其传递给 TextView 的 FontFamily 参数。
在 Flutter 中,将字体文件放在一个文件夹中,并在 pubspec.yaml
文件中引用它,类似于你导入图像的方式。
fonts:
- family: MyCustomFont
fonts:
- asset: fonts/MyCustomFont.ttf
- style: italic
然后将字体分配给你的 Text
部件
@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
部件的 style 参数采用一个 TextStyle
对象,你可以在其中自定义许多参数,例如
- 颜色
- 装饰
- 装饰颜色
- 装饰样式
- 字体系列
- 字体大小
- 字体样式
- 字体粗细
- 哈希码
- 高度
- 继承
- 字母间距
- 文本基线
- 字间距
表单输入
有关使用表单的更多信息,请参阅 从 Flutter 烹饪手册 中检索文本字段的值。
输入的“提示”等效项是什么?
在 Flutter 中,你可以通过向文本小组件的 decoration 构造函数参数添加 InputDecoration 对象,轻松地为输入显示“提示”或占位符文本。
Center(
child: TextField(
decoration: InputDecoration(hintText: 'This is a hint'),
),
)
如何显示验证错误?
就像使用“提示”一样,将 InputDecoration 对象传递给文本小组件的 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 团队维护
-
google_mobile_ads
用于 Google 移动广告的 Flutter -
firebase_analytics
适用于 Firebase Analytics -
firebase_auth
适用于 Firebase Auth -
firebase_database
适用于 Firebase RTDB -
firebase_storage
适用于 Firebase Cloud Storage -
firebase_messaging
适用于 Firebase Messaging (FCM) -
flutter_firebase_ui
适用于快速 Firebase Auth 集成(Facebook、Google、Twitter 和电子邮件) -
cloud_firestore
适用于 Firebase Cloud Firestore
您还可以在 pub.dev 上找到一些第三方 Firebase 插件,它们涵盖了第一方插件未直接涵盖的领域。
如何构建我自己的自定义原生集成?
如果 Flutter 或其社区插件缺少特定于平台的功能,您可以按照开发包和插件页面构建自己的插件。
简而言之,Flutter 的插件架构非常类似于在 Android 中使用事件总线:您发送一条消息,让接收器处理并向您发回结果。在这种情况下,接收器是在 Android 或 iOS 上的原生端运行的代码。
如何在 Flutter 应用程序中使用 NDK?
如果您在当前的 Android 应用程序中使用 NDK,并且希望您的 Flutter 应用程序利用您的原生库,那么可以通过构建自定义插件来实现。
您的自定义插件首先与您的 Android 应用程序通信,您可以在其中通过 JNI 调用您的native
函数。一旦响应准备就绪,就向 Flutter 发送一条消息并呈现结果。
目前不支持直接从 Flutter 调用原生代码。
主题
如何为我的应用程序设置主题?
开箱即用,Flutter 带有一个 Material Design 的漂亮实现,它满足了您通常需要的大量样式和主题需求。与在 XML 中声明主题然后使用 AndroidManifest.xml 将其分配给应用程序的 Android 不同,在 Flutter 中,您在顶级小部件中声明主题。
要充分利用应用程序中的 Material Components,您可以将顶级小部件MaterialApp
声明为应用程序的入口点。MaterialApp 是一个便捷小部件,它包装了实现 Material Design 的应用程序通常需要的许多小部件。它通过添加 Material 特定的功能来构建 WidgetsApp。
您还可以使用WidgetsApp
作为您的应用程序小部件,它提供了一些相同的功能,但不如MaterialApp
丰富。
要自定义任何子组件的颜色和样式,请将 ThemeData
对象传递给 MaterialApp
小组件。例如,在下面的代码中,种子中的配色方案设置为 deepPurple,文本选择颜色为红色。
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(),
);
}
}
数据库和本地存储
如何访问共享首选项?
在 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 包括对分析、检查堆、检查小组件树、记录诊断、调试、观察已执行代码行、调试内存泄漏和内存碎片的支持。有关更多信息,请参阅 DevTools 文档。
通知
如何设置推送通知?
在 Android 中,使用 Firebase Cloud Messaging 为应用设置推送通知。
在 Flutter 中,使用 Firebase Messaging 插件访问此功能。有关使用 Firebase Cloud Messaging API 的更多信息,请参阅 firebase_messaging
插件文档。