给Android开发者的Flutter指南 (下) [翻译]

官方英文原文: https://flutter.io/flutter-for-android/

说明:此文上接 给Android开发者的Flutter指南(上)

四、工程结构与资源

1. 在哪放置不同分辨率(resolution-dependent)的图片文件?

Android中,resourcesassets是两个独立的文件夹,而在Flutter中,只存在assets,所有放在Androidres/drawable-*文件夹中的文件全都放在Flutter中的assets文件夹中。

Flutterios一样遵循简单的基于密度(density-base)的格式,assets包含1.0x2.0x3.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层)访问,同理,本地层的assetsresources文件也不能被Flutter访问,因为它们存在于分立的文件夹中。

而从Flutter 1.0 beta2开始,assets存储于本地层的assets文件夹中,且可以被本地层通过AssetManager访问,但是Flutter依然不能访问本地层的resourcesassets

val flutterAssetStream = assetManager.open("flutter_assets/assets/my_flutter_asset.png")

如果向Flutter工程中添加一个叫做my_icon.png的图片资源,比方说,把它放到一个叫做Images的文件夹中(名字是任意的),那么我们应该把1.0x的基础图片放到Images的根目录,而其他大小,比如2.0x3.0x等大小的图片分别放在Images中名字为2.0x3.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)

FlutterAndroid中的辅助功能有了基础支持,目前工作还在进行中。开发者可以通过查看intl package来获取关于国际化和本地化的信息。

3. 对应于Gradle文件的是啥?怎么添加依赖?

Android中,依赖添加在gradle构建脚本中,而Flutter则是使用Dart自己的构建系统和Pub包管理器,Flutter的构建工具会将本地AndroidIOS的构建工作委托到它们各自的构建系统。

gradle文件在Flutter工程目录的android文件夹下,只有需要针对单个平台添加本地依赖时才添加到gradle,其他普通场景直接在pubspec.yaml添加外部依赖就好了。找包?上 Pub

五、 Activity 与 fragment

Note: 你几乎不会想让Android因为Flutter应用而重启activity,因为它违背了Android文档中的提出的建议,因此例如需要支持分屏,那么也需要添加screenLayoutdensity

1. Flutter中与activityfragment相对应的是啥?

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中则是使用RowColumn控件来实现与之相同的行为。

如下示例,当布局中子控件重复利用率比较高时,使用这种容器控件就很方便了:

@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实现与之相同效果的方法很少。可以通过组合ColumnRowStack控件来实现类似效果,也可以通过控件的构造器指定其子控件在它内部的布局规则来实现。

关于如何构建一个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中,是通过调用ViewsetOnClickListener方法绑定监听器的,而在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

对于AndroidListView,我们会创建一个适配器(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函数(构建列表条目的函数)。

itemBuildAndroid中的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?

使用 flutter_facebook_login

4. 如何使用Firebase ?
大部分Firebase函数转换自 first party plugins,以下是第一批集成的插件,由Flutter团队维护:

4. 如何构建自定义的 Native integration(本地集成)

如果没有找到我们想要的插件,那么可以查看the developing packages and plugins,学习如何构建我们自己的插件。

Flutter的插件架构,类似于EventBus:发出消息给接收器处理,接收器处理后将结果返回过来。在这里,接收器代码运行在本地层,也就是AndroidIOS.

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 PreferencesNSUserDefaultsios平台)

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插件文档。

推荐阅读更多精彩内容