官方英文原文: https://flutter.io/flutter-for-android/
说明:此文上接 给Android开发者的Flutter指南(上)。
四、工程结构与资源
1. 在哪放置不同分辨率(resolution-dependent
)的图片文件?
在Android
中,resources
与assets
是两个独立的文件夹,而在Flutter
中,只存在assets
,所有放在Android
的res/drawable-*
文件夹中的文件全都放在Flutter
中的assets
文件夹中。
Flutter
跟ios
一样遵循简单的基于密度(density-base
)的格式,assets
包含1.0x
、2.0x
、3.0x
或者更高乘数,Flutter
中并没有dp
这一说,而是使用与设备无关的逻辑像素,在devicePixelRatio 中描述了单个逻辑像素与物理像素的关系。
对应于Android
密度的关系如下:
Android density qualifier | Flutter pixel ratio |
---|---|
ldpi | 0.75x |
mdpi | 1.0x |
hdpi | 1.5x |
xhdpi | 2.0x |
xxhdpi | 3.0x |
xxxhdpi | 4.0x |
assets
可以存在与任意文件夹中,Flutter
没有规定文件夹结构,因此即使你将assets
放在与pubspec.yaml
相同的位置,Flutter
也能正确的读取到。
在Flutter 1.0 beta2
以前,在Flutter
中定义的assets
不能被本地层(native
层)访问,同理,本地层的assets
和resources
文件也不能被Flutter
访问,因为它们存在于分立的文件夹中。
而从Flutter 1.0 beta2
开始,assets
存储于本地层的assets
文件夹中,且可以被本地层通过AssetManager
访问,但是Flutter
依然不能访问本地层的resources
和assets
:
val flutterAssetStream = assetManager.open("flutter_assets/assets/my_flutter_asset.png")
如果向Flutter
工程中添加一个叫做my_icon.png
的图片资源,比方说,把它放到一个叫做Images
的文件夹中(名字是任意的),那么我们应该把1.0x
的基础图片放到Images
的根目录,而其他大小,比如2.0x
、3.0x
等大小的图片分别放在Images
中名字为2.0x
、3.0x
的子文件夹中,如下示例路径:
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
访问图片资源了:
return AssetImage("images/a_dot_burr.jpeg");
或者直接在Image
控件中使用:
@override
Widget build(BuildContext context) {
return Image.asset("images/my_image.png");
}
2. 在哪存储strings
字符串资源?怎样处理本地化?
目前Flutter
没有像系统声明字符串资源那样的形式,因此当前最佳方式就是将字符串声明成static
形式,然后存储在一个特定的类中,之后都从这个类中获取,如下示例:
class Strings {
static String welcomeMessage = "Welcome To Flutter";
}
然后在代码中这样调用这些字符串资源:
Text(Strings.welcomeMessage)
Flutter
对Android
中的辅助功能有了基础支持,目前工作还在进行中。开发者可以通过查看intl package来获取关于国际化和本地化的信息。
3. 对应于Gradle
文件的是啥?怎么添加依赖?
Android
中,依赖添加在gradle
构建脚本中,而Flutter
则是使用Dart
自己的构建系统和Pub
包管理器,Flutter
的构建工具会将本地Android
和IOS
的构建工作委托到它们各自的构建系统。
gradle
文件在Flutter
工程目录的android
文件夹下,只有需要针对单个平台添加本地依赖时才添加到gradle
,其他普通场景直接在pubspec.yaml
添加外部依赖就好了。找包?上 Pub
五、 Activity 与 fragment
Note: 你几乎不会想让Android
因为Flutter
应用而重启activity
,因为它违背了Android
文档中的提出的建议,因此例如需要支持分屏,那么也需要添加screenLayout
和density
。
1. Flutter中与activity
和fragment
相对应的是啥?
在Android
中,activity
代表了用户的单个焦点所在,而Fragment
则代表用户交互及接口的一个行为或者说一个部分。Fragment
可以模块化你的代码,用来为大屏设备组合出复杂的用户交互接口、以及比例化应用UI
。在Flutter
中,这两者的概念都汇集到在Widget
。
正如在Intent
部分所提到的,在Flutter
中,Widget
就代表着屏幕,因为在Flutter
中万物皆Widget
。我们使用Navigator
来切换Route
,而Route
代表着不同屏幕或页面、亦或只是不同状态、或是相同数据的渲染效果。
2. 如何监听Android中activity的生命周期事件?
在Android
中,我们会复写activity
中的生命周期方法,或者在Application
中注册ActivityLifecycleCallbacks
,而在Flutter
中,并没有这个概念,但是我们可以通过给WidgetBinding
观察者下个钩子(hook
)来监听生命周期事件,然后监听didChangeAppLifecycleState()
的变化事件,其生命周期事件如下:
-
inactive
: 应用处于非活动状态,此时不在接收用户输入。这个事件只在IOS
中有效,因为在Android
中没有与这个状态相映射的事件。 -
paused
: 当前应用对用户可见,但不在响应用户输入,且运行在后台。等同于Android
中的onPause
。 -
resumed
: 应用可见且正在响应用户输入。等同于Android
中的onPostResume()
-
suspending
: 此时应用挂起了。等同于Android
中的onStop
;不会触发IOS
上的事件,因为没有与之映射的状态事件。
关于这些状态的更多细节,请查看 AppLifecycleStatus documentation..
你可能注意到了,只有那么几个可用的Android
生命周期事件。这是因为FlutterActivity
已经捕获了几乎所有的生命周期事件,并将它们传送到了Flutter
引擎中,然后很多事件都被它屏蔽掉了。Flutter
会管理引擎的启动和关闭动作,因而大多数情况下我们都没多大必要在Flutter
层监听activity
生命周期事件。如果要监听或者释放本地层的资源,那么可以在本地层以任何频率进行。
以下示例描述了如何监听Activity
中的生命周期事件:
import 'package:flutter/widgets.dart';
class LifecycleWatcher extends StatefulWidget {
@override
_LifecycleWatcherState 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 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(Center(child: LifecycleWatcher()));
}
六、布局
1. 对应于LinearLayout的是啥?
在Android
中,LinearLayout
用于横向和纵向布局控件,而在Flutter
中则是使用Row
或Column
控件来实现与之相同的行为。
如下示例,当布局中子控件重复利用率比较高时,使用这种容器控件就很方便了:
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Row One'),
Text('Row Two'),
Text('Row Three'),
Text('Row Four'),
],
);
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Column One'),
Text('Column Two'),
Text('Column Three'),
Text('Column Four'),
],
);
}
更多线性布局细节,请看这里 Flutter For Android Developers : How to design LinearLayout in Flutter?.
2. 对应于RelativeLayout的是啥?
在Flutter
实现与之相同效果的方法很少。可以通过组合Column
、Row
和 Stack
控件来实现类似效果,也可以通过控件的构造器指定其子控件在它内部的布局规则来实现。
关于如何构建一个RelativeLayout
,可以查看这里 StackOverflow.
3. 对应于ScrollView的是啥?
在Flutter
中,实现ScrollVIew
的最简单方式就是使用LsitView
。这看起来好像有点夸张,但是在Flutter
中,ListView
控件既是Android
中的ScrollView
,也是ListView
。
@override
Widget build(BuildContext context) {
return ListView(
children: <Widget>[
Text('Row One'),
Text('Row Two'),
Text('Row Three'),
Text('Row Four'),
],
);
}
4. 在Flutter中如何处理屏幕旋转?
在AndroidManifest.xml
中添加如下配置即可:
android:configChanges="orientation|screenSize"
七、检测手势与触摸事件处理
1. 如何为控件添加 OnClick 事件监听器?
在Android
中,是通过调用View
的setOnClickListener
方法绑定监听器的,而在Flutter
中可以有以下两种添加触摸事件监听器的方式:
- 如果控件支持事件检测,那么可以给它传入一个函数用以处理这个事件。比如,
RaisedButton
包含一个onPressd
参数:
@override
Widget build(BuildContext context) {
return RaisedButton(
onPressed: () {
print("click");
},
child: Text("Button"));
}
- 如果控件并不支持事件检测,那么可以将这个控件包裹在
GestureDetector
中,然后传入一个函数给onTap
参数:
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: GestureDetector(
child: FlutterLogo(
size: 200.0,
),
onTap: () {
print("tap");
},
),
));
}
}
2. 如何处理控件中的其他手势?
使用GestureDetector
可以监听大量手势,例如:
-
单击(
Tab
)-
onTabDown
: 触发单击事件的指针已经开始与屏幕在特定点上进行联系 -
onTapUp
: 触发单击事件的指针停止与屏幕在特定点上的联系 -
onTap
: 形成了单击事件 -
onTapCancel
: 触发时会导致之前触发onTabDown
的指针无法形成单击事件
-
-
双击(
Double Tab
)-
onDoubleTab
: 用户在屏幕的同一个点上连续快速点击了两次
-
-
长按
Long Press
-
onLongPress
: 指针持续在同一个位置上与屏幕进行了一段事时间的联系。
-
-
垂直拖动(
Vertical drag
)-
onVerticalDragStart
: 指针已经和屏幕联系,并且可能开始垂直移动。 -
onVerticalDragUpdate
: 正在和屏幕联系的指针已经开始在垂直方向上进行移动。 -
onVerticalDragEnd
: 之前与屏幕进行联系且在垂直方向移动的指针,现在已经不需要再与屏幕联系了,并且在停止联系的瞬间,指针依然以一定的速度移动。
-
-
水平拖动(
Horizontal drag
)(请参考上面的垂直拖动)onHorizontalDragStart
onHorizontalDragUpdate
onHorizontalDragEnd
下面示例描述了在使用GestureDetector
双击Flutter logo
时,logo
旋转的效果:
AnimationController controller;
CurvedAnimation curve;
@override
void initState() {
controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
}
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: GestureDetector(
child: RotationTransition(
turns: curve,
child: FlutterLogo(
size: 200.0,
)),
onDoubleTap: () {
if (controller.isCompleted) {
controller.reverse();
} else {
controller.forward();
}
},
),
));
}
}
八、ListView 与 Adapter
1. 在Flutter中,替代ListView的是啥?
在Flutter
中,等效于ListView
的是...ListView
对于Android
的ListView
,我们会创建一个适配器(adapter
)传给ListView
,然后ListView
渲染这个适配器返回的每一行数据。而我们必须确保每行数据最后都被我们回收,否则就可能会导致显示错乱和内存问题。
而因为Flutter
的控件是不可变的,我们给ListView
传入一个集合的控件,然后Flutter
就会自己确保滑动的流畅、快速了。
import 'package:flutter/material.dart';
void main() {
runApp(SampleApp());
}
class SampleApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: ListView(children: _getListData()),
);
}
_getListData() {
List<Widget> widgets = [];
for (int i = 0; i < 100; i++) {
widgets.add(Padding(padding: EdgeInsets.all(10.0), child: Text("Row $i")));
}
return widgets;
}
}
2. 我怎么知道列表的哪个条目被点击了呢?
在Android
中,ListView
中包含了查找点击了哪个条目的onItemClickListener
监听器,而在Flutter
中,则是通过传入列表的控件来处理触摸动作。
import 'package:flutter/material.dart';
void main() {
runApp(SampleApp());
}
class SampleApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: ListView(children: _getListData()),
);
}
_getListData() {
List<Widget> widgets = [];
for (int i = 0; i < 100; i++) {
widgets.add(GestureDetector(
child: Padding(
padding: EdgeInsets.all(10.0),
child: Text("Row $i")),
onTap: () { // 给列表中的每个控件添加一个点击事件监听器
print('row tapped');
},
));
}
return widgets;
}
}
3. 如何动态更新ListView?
在Android
中是通过更新适配器,然后调用notifyDataSetChanged
来处理。
而在Flutter
中,如果你是在setState()
方法中更新控件集,那么你会很快看到你的数据并没有被更新,这是因为当setState()
被调用时,Flutter
渲染引擎会在控件树中搜索是否存在发生改变的东西,而当它找到了你的ListView
,它会进行==
检查,然后判定这两个ListView
是相同的,因而不会发生任何改变,也就不会请求更新。
一个简单更新ListView
的方法就是,在setState()
方法中创建一个新的List
,然后将旧集合的数据拷贝过来。这一解决方案是简单,但是不推荐在数据量大的时候使用,如下示例:
import 'package:flutter/material.dart';
void main() {
runApp(SampleApp());
}
class SampleApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List 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: Text("Sample App"),
),
body: ListView(children: widgets),
);
}
Widget getRow(int i) {
return GestureDetector(
child: Padding(
padding: EdgeInsets.all(10.0),
child: Text("Row $i")),
onTap: () {
setState(() {
widgets = List.from(widgets);
widgets.add(getRow(widgets.length + 1));
print('row $i');
});
},
);
}
}
一个高效且有效的方式是通过ListView.Builder
来建立ListView
,这种方式在你的数据量巨大或者需要动态改变数据的情况下非常有用,这实质上跟Android
中的RecyclerView
等价了,因为它会自动回收列表数据:
import 'package:flutter/material.dart';
void main() {
runApp(SampleApp());
}
class SampleApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List 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: Text("Sample App"),
),
body: ListView.builder(
itemCount: widgets.length,
itemBuilder: (BuildContext context, int position) {
return getRow(position);
}));
}
Widget getRow(int i) {
return GestureDetector(
child: Padding(
padding: EdgeInsets.all(10.0),
child: Text("Row $i")),
onTap: () {
setState(() {
widgets.add(getRow(widgets.length + 1));
print('row $i');
});
},
);
}
}
与创建ListView
不同的是,创建ListView.Build
需要传入两个主要参数,一个是初始列表的长度,一个是itemBuild
函数(构建列表条目的函数)。
itemBuild
与Android
中的adapter#getView()
方法类似,它给你一个位置,然后需要返回你需要在对应位置上渲染的行。
最后注意到, onTab
函数已经不需要在创新创建数据列表了,取而代之的是通过.add()
来添加到其中。
九、文字处理
1. 如何给文字控件设置字体?
在Android SDK(Android O)
,可以创建一个Font
资源,然后作为FontFamily
参数传入TextView
。而在Flutter
中,则是将字体文件放到一个文件夹中,然后在pubspec.yaml
中引用即可,跟引入图片是类似的。
fonts:
- family: MyCustomFont
fonts:
- asset: fonts/MyCustomFont.ttf
- style: italic
然后在Text
控件中使用:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: Center(
child: Text(
'This is a custom font text',
style: TextStyle(fontFamily: 'MyCustomFont'),
),
),
);
}
2. 如何给Text控件定义风格?
除了字体,我们还可以定义Text
控件的其他风格属性,Text
控件的风格参数中包含一个TextStyle
对象,我们可以定义它的很多参数,例如:
- color
- decoration
- decorationColor
- decorationStyle
- fontFamily
- fontSize
- fontStyle
- fontWeight
- hashCode
- height
- inherit
- letterSpacing
- textBaseline
- wordSpacing
十、表单输入(Form input)
有关表单的更多信息请查看Flutter cookbook 中的这里Retrieve the value of a text field。
1. 对应于输入框中的“hint”的是啥?
在Flutter
中,可以通过给Text
控件传入一个InputDecoration
对象来显示“hint
”或者占位字符,如下示例:
body: Center(
child: TextField(
decoration: InputDecoration(hintText: "This is a hint"),
)
)
2. 如何展示验证错误信息?
和显示hint
一样,传一个InputDecoration
对象给Text
控件的构造函数即可。
但是你肯定不想一开始就显示错误,而是在出现错误时才显示,因此发生错误时传入一个新的InputDecoration
对象即可。
import 'package:flutter/material.dart';
void main() {
runApp(SampleApp());
}
class SampleApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
String _errorText;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: Center(
child: TextField(
onSubmitted: (String text) {
setState(() {
if (!isEmail(text)) {
_errorText = 'Error: This is not an email';
} else {
_errorText = null;
}
});
},
decoration: InputDecoration(hintText: "This is a hint", errorText: _getErrorText()),
),
),
);
}
_getErrorText() {
return _errorText;
}
bool isEmail(String em) {
String emailRegexp =
r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$';
RegExp regExp = RegExp(emailRegexp);
return regExp.hasMatch(em);
}
}
十一、Flutter Plugins
1. 如何访问GPS传感器?
使用geolocator社区插件
2. 如何访问相机?
使用这个image_picker
3. 如何登录Facebook?
4. 如何使用Firebase ?
大部分Firebase
函数转换自 first party plugins,以下是第一批集成的插件,由Flutter
团队维护:
- firebase_admob for Firebase AdMob
- firebase_analytics for Firebase Analytics
- firebase_auth for Firebase Auth
- firebase_database for Firebase RTDB
- firebase_storage for Firebase Cloud Storage
- firebase_messaging for Firebase Messaging (FCM)
- flutter_firebase_ui for quick Firebase Auth integrations (Facebook, Google, Twitter and email)
- cloud_firestore for Firebase Cloud Firestore
4. 如何构建自定义的 Native integration(本地集成)
如果没有找到我们想要的插件,那么可以查看the developing packages and plugins,学习如何构建我们自己的插件。
Flutter
的插件架构,类似于EventBus
:发出消息给接收器处理,接收器处理后将结果返回过来。在这里,接收器代码运行在本地层,也就是Android
或IOS
.
5. 如何在Flutter应用中使用NDK?
如果你已经在Android
应用中使用了NDK
,并且想要让Flutter
也能使用到这些类库,那么就需要构建自定义插件了。
首先让自定义的插件能与Android
应用交互,即在Android
应用中通过JNI
调用native
函数,一旦拿到拿到结果了,就回送给Flutter
,然后渲染结果。
目前不支持直接从Flutter
中调用native
代码。
十二、 主题(themes)
1. 如何给应用定制主题?
Flutter
自带Material Design
风格的主题,不像Android
可以在XML
文件中声明主题,然后在AndroidManifest.xml
中使用。在Flutter
中是在顶层控件中声明主题的。
如果想要在应用中充分使用Material
组件,那么可以使用MaterialApp
作为应用的入口。MaterialApp
包含有大量的实现了Material Design
风格的通用控件,它建立在WidgetsApp
之上,只是添加了具有Material
特性的功能。
同样也可以使用WidgetsApp
作为应用控件,它提供了一些相同的功能,但不如MaterialApp
丰富。
想要在任意子组件上自定义颜色和风格的话,那么给MaterialApp
传入一个ThemeData
对象,例如,在下面示例中,初始样本显示为蓝色,而文字选择后显示为红色。
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
primarySwatch: Colors.blue,
textSelectionColor: Colors.red
),
home: SampleAppPage(),
);
}
}
十三、数据库与本地存储
1. 怎样访问Shared Preference?
在Flutter
中,可以通过Shared_Preferences plugin来实现这些功能,这个插件包含了Shared Preferences
和NSUserDefaults
(ios
平台)
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
runApp(
MaterialApp(
home: Scaffold(
body: Center(
child: RaisedButton(
onPressed: _incrementCounter,
child: Text('Increment Counter'),
),
),
),
),
);
}
_incrementCounter() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
int counter = (prefs.getInt('counter') ?? 0) + 1;
print('Pressed $counter times.');
prefs.setInt('counter', counter);
}
2. 如何访问SQLite数据库?
使用SQFlite插件
十五、通知
1. 如何设置推送通知?
在Android
中可以使用Firebase Cloud Messaging
给应用设置推送通知。
Flutter
中,通过Firebase_Messaging可以使用到这一功能,更多关于使用Firebase Cloud Messaging API
的信息,请查看firebase_messaging插件文档。