官方英文原文: flutter.io/flutter-for…html
說明:此文上接 給Android開發者的Flutter指南(上)。android
1. 在哪放置不一樣分辨率(resolution-dependent
)的圖片文件?ios
在Android
中,resources
與assets
是兩個獨立的文件夾,而在Flutter
中,只存在assets
,全部放在Android
的res/drawable-*
文件夾中的文件全都放在Flutter
中的assets
文件夾中。git
Flutter
跟ios
同樣遵循簡單的基於密度(density-base
)的格式,assets
包含1.0x
、2.0x
、3.0x
或者更高乘數,Flutter
中並無dp
這一說,而是使用與設備無關的邏輯像素,在devicePixelRatio 中描述了單個邏輯像素與物理像素的關係。github
對應於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
也能正確的讀取到。bash
在Flutter 1.0 beta2
之前,在Flutter
中定義的assets
不能被本地層(native
層)訪問,同理,本地層的assets
和resources
文件也不能被Flutter
訪問,由於它們存在於分立的文件夾中。架構
而從Flutter 1.0 beta2
開始,assets
存儲於本地層的assets
文件夾中,且能夠被本地層經過AssetManager
訪問,可是Flutter
依然不能訪問本地層的resources
和assets
:app
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
的子文件夾中,以下示例路徑:less
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
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();
}
},
),
));
}
}
複製代碼
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
對象,咱們能夠定義它的不少參數,例如:
有關表單的更多信息請查看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);
}
}
複製代碼
1. 如何訪問GPS傳感器?
使用geolocator社區插件
2. 如何訪問相機? 使用這個image_picker
3. 如何登陸Facebook?
4. 如何使用Firebase ? 大部分Firebase
函數轉換自 first party plugins,如下是第一批集成的插件,由Flutter
團隊維護:
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
代碼。
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插件文檔。