使用 Flutter 和 Firebase 制作!计数器应用程序

使用 Flutter 和 Firebase 制作!计数器应用程序

目录

<div id="01">1️⃣Flutter 概述和特点</div>

什么是Flutter?

Flutter是一个由谷歌开发的开源应用程序框架。

Flutter官网

Flutter的特点

  • <span>只需一段代码就能为多个平台创建应用程序,包括Android, iOS, Web, Windows, MacOS和Linux。</span>
  • <span>轻松访问 Material Design</span>
  • <span>UI是使用小工具的组合来构建的</span>
  • <span>使用Dart作为开发语言</span>
  • <span>热重载功能实现了快速开发</span>

Flutter的文档

Flutter/Dart有一套完整的官方文档。
这里有一些例子

关于如何开发Flutter应用程序的文档。
Flutter官方文档

Flutter API参考

FlutterAPI参考

Dart 包搜索站点

Dart 包搜索站点


此外,Flutter一年比一年受欢迎,除了官方文档外,许多开发者在其他网站上整理了一些通俗易懂的文章,可以作为开发的参考。

<div id="02">2️⃣Firebase 概览和服务列表</div>

什么是Firebase?

Firebase是谷歌提供的一个移动后台服务(mBaaS)。

Firebase可以很容易地将数据存储和通过云同步、应用认证、消息通知、应用分析和性能测量等功能添加到移动应用。

Firebase 服务列表

名称 内容
A/B Testing 轻松运行并分析产品和营销测试
Analytics 应用分析功能
App Check 为应用程序数据提供保护
App Distribution 将应用程序分发到测试人员
Firebase Authentication 易于建立的用户认证
Cloud Firestore NoSQL 数据库构建无服务器
Cloud Functions for Firebase 无服务器运行后端代码
Firebase Cloud Messaging 发送和接收推送消息
Firebase Crashlytics 跟踪应用稳定性问题
Dynamic Links 提供对本机应用程序链接内容的直接导航
Firebase Extensions Firebase 扩展
Firebase Hosting 网站部署
Firebase In-App Messaging 发送有针对性的上下文消息
Firebase ML 为应用程序提供机器学习功能
Firebase Performance Monitoring 获取性能分析
Firebase Realtime Database 可以保存为 JSON 格式的数据库
Firebase Remote Config 允许功能的动态变化
Cloud Storage for Firebase 保存用户创建的内容
Test lab 在虚拟设备上验证您的应用

Firebase的费用

有两种收费方案

产品 价格 备注
Spark 方案 免费 由于是小规模的产品,所以受到限制
Blaze 方案 随用随付 用于大规模的产品

有关每个方案的限制和详细价格,请参见官方网站

<div id="03">3️⃣开发环境</div>

关于开发此计数器应用程序的环境。

对于与以下不同的环境,代码可能会有所不同。

项目 内容
PC Macbook Air(M1)
Flutter 3.0.4
Firebase CLI 11.2.2
FlutterFire 0.2.4
模拟器 Android 12(API 31), Chrome

<div id="04">4️⃣准备编码</div>

安装Flutter

要安装Flutter,请参考官方网站

创建计数器应用程序

首先,初始化Flutter应用程序并创建一个计数器应用程序。

flutter create counter_firebase

Firebase CLI的设置

参照官方文档,安装Firebase CLI


这里有几种安装方法,但你也可以实用npm来进行安装

npm install -g firebase-tools

此后,按照官方文件进行

首先,登录到firebase,全局启用flutterfire_cli

firebase login
dart pub global activate flutterfire_cli

Firebase Console创建一个项目

此时应启用Google Analytics


将你的应用程序连接到Firebase

flutterfire configure

选择如下

# 选择项目
? Select a Firebase project to configure your Flutter application with ›
❯ counterfirebase-*** (counterFirebase)

# 平台选择。 检查是否都打了勾
? Which platforms should your configuration support (use arrow keys & space to select)? ›
✔ android
✔ ios
✔ macos
✔ web

# android/build.gradle是否更新
? The files android/build.gradle & android/app/build.gradle will be updated to apply Firebase configuration and gradle build plugins. Do you want to continue? (y/n) › yes

pubspe.yaml中加入firebase_core

dependencies:
  firebase_core: ^1.19.2

确保Firebase的配置是最新的

flutterfire configure

在main.dart中安装并初始化Firebase包

import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const MyApp());
}

以下是已完成的应用程序的屏幕截图

小结总结

以下部分已从最初创建的计数器应用程序中更改

  • 数目增加是由Riverpod完成的
  • 从主页屏幕过渡到计数器屏幕

main.dart

/// Flutter导入
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

/// Firebase导入
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';

/// 导入其他页面
import 'package:counter_firebase/normal_counter_page.dart';

/// 主
void main() async {
  /// Firebase初始化
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  /// runApp w/ Riverpod
  runApp(const ProviderScope(child: MyApp()));
}

/// Provider初始化
final counterProvider = StateNotifierProvider<Counter, int>((ref) {
  return Counter();
});

class Counter extends StateNotifier<int> {
  Counter() : super(0);

  /// 
  void increment() => state++;
}

/// MaterialApp的配置
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Counter Firebase',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
      debugShowCheckedModeBanner: false,
    );
  }
}

/// 首页屏幕
class MyHomePage extends ConsumerWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('My Homepage'),
      ),
      body: ListView(
        padding: const EdgeInsets.all(10),
        children: const <Widget>[
          _PagePushButton(
            buttonTitle: '计数器',
            pagename: NormalCounterPage(),
          ),
        ],
      ),
    );
  }
}

class _PagePushButton extends StatelessWidget {
  const _PagePushButton({
    Key? key,
    required this.buttonTitle,
    required this.pagename,
  }) : super(key: key);

  final String buttonTitle;
  final dynamic pagename;

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      child: Container(
        padding: const EdgeInsets.all(10),
        child: Text(buttonTitle),
      ),
      onPressed: () {
        Navigator.push(
          context,
          MaterialPageRoute(builder: (context) => pagename),
        );
      },
    );
  }
}

normal_counter_page.dart

/// Flutter
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

/// 其他页面
import 'package:counter_firebase/main.dart';

class NormalCounterPage extends ConsumerStatefulWidget {
  const NormalCounterPage({Key? key}) : super(key: key);

  @override
  NormalCounterPageState createState() => NormalCounterPageState();
}

class NormalCounterPageState extends ConsumerState<NormalCounterPage> {
  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    final counter = ref.watch(counterProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Homepage'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          ref.read(counterProvider.notifier).increment();
        },
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

<div id="05">5️⃣Firebase Analytics编</div>

Firebase Analytics概述

Firebase Analytics是一项服务,允许你使用Firebase将Google Analytics应用到你的应用程序中

分析允许你记录应用程序的事件,并找出应用程序的使用情况

以下是关于在Flutter中使用分析的官方文档

准备

准备工作和前几章都已完成方可开始

使用方法

要在项目中引入firebase_analytics,将其添加到pubspec.yaml中并导入

pubspec.yaml

dependencies:
  firebase_analytics: ^9.2.0

可以记录的事件在firebase_analytics_package网页上列出

安装

这一次,logEvent记录了屏幕转换事件

import 'package:firebase_analytics/firebase_analytics.dart';

class AnalyticsService {
  Future<void> logPage(String screenName) async {
    await FirebaseAnalytics.instance.logEvent(
      name: 'screen_view',
      parameters: {
        'firebase_screen': screenName,
      },
    );
  }
}

Widget端的设置如下

ElevatedButton{
  child: Text(buttonTitle),
  onPressed: () {
    AnalyticsService().logPage(buttonTitle);
    Navigator.push(
        context, MaterialPageRoute(builder: (context) => pagename));
  },
},

日志信息可以在Firebase控制台找到

在分析关系中,显示了实时分析、事件分析和转换分析

小结总结

这是与上次相比的变化

  • Analytics实现页面转换记录
  • 其他代码更改

main.dart

/// Flutter导入
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

/// Firebase导入
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
import 'package:firebase_analytics/firebase_analytics.dart';

/// 导入其他页面
import 'package:counter_firebase/normal_counter_page.dart';

/// 主
void main() async {
  /// Firebase初始化
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  /// runApp w/ Riverpod
  runApp(const ProviderScope(child: MyApp()));
}

/// Provider初始化
final counterProvider = StateNotifierProvider<Counter, int>((ref) {
  return Counter();
});

class Counter extends StateNotifier<int> {
  Counter() : super(0);

  void increment() => state++;
}

/// MaterialApp的配置
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Counter Firebase',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
      debugShowCheckedModeBanner: false,
    );
  }
}

/// 主屏幕
class MyHomePage extends ConsumerWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('My Homepage'),
      ),
      body: ListView(
        padding: const EdgeInsets.all(10),
        children: const <Widget>[
          _PagePushButton(
            buttonTitle: '计数器',
            pagename: NormalCounterPage(),
          ),
        ],
      ),
    );
  }
}

/// 页面过渡按钮
class _PagePushButton extends StatelessWidget {
  const _PagePushButton({
    Key? key,
    required this.buttonTitle,
    required this.pagename,
  }) : super(key: key);

  final String buttonTitle;
  final dynamic pagename;

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      child: Container(
        padding: const EdgeInsets.all(10),
        child: Text(buttonTitle),
      ),
      onPressed: () {
        AnalyticsService().logPage(buttonTitle);
        Navigator.push(
          context,
          MaterialPageRoute(builder: (context) => pagename),
        );
      },
    );
  }
}

/// Analytics
class AnalyticsService {
  /// 页面转换的日志
  Future<void> logPage(String screenName) async {
    await FirebaseAnalytics.instance.logEvent(
      name: 'screen_view',
      parameters: {
        'firebase_screen': screenName,
      },
    );
  }
}

检查Firebase Console中的内容

<div id="06">6️⃣Firebase Crashlytics编</div>

Firebase Crashlytics概述

Firebase Crashlytics是一个跟踪应用问题的崩溃报告工具

Firebase Crashlytics可用于AndroidiOS设备

Firebase Crashlytics官方文档

准备

准备工作和前几章都已完成方可开始

使用方法

要在项目中引入firebase_analytics,将其添加到pubspec.yaml中并导入

pubspec.yaml

dependencies:
  firebase_crashlytics: ^2.8.5

为了确保Firebase配置是最新的,在项目根目录下打开一个终端,运行flutterfire configure

flutterfire configure

崩溃处理程序配置

准备好后,配置崩溃处理程序

FirebaseCrashlytics.instance.recordFlutterFatalError会自动抓取Flutter框架内抛出的所有错误

您还可以使用runZonedGuarded(需要导入dart:async)来捕捉Flutter框架没有捕捉到的错误

import 'dart:async';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';

void main() async {
  /// 崩溃处理程序
  runZonedGuarded<Future<void>>(() async {
    /// Firebase初始化
    WidgetsFlutterBinding.ensureInitialized();
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );

    /// 崩溃处理程序(Flutter框架内抛出的所有错误)
    FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;

    /// runApp w/ Riverpod
    runApp(const ProviderScope(child: MyApp()));
  },

      /// 崩溃处理程序(Flutter框架内未捕获的错误)
      (error, stack) =>
          FirebaseCrashlytics.instance.recordError(error, stack, fatal: true));
}

测试碰撞

一旦配置好,在安卓或iOS设备上强制崩溃,进行测试

如果你已经添加了一个错误处理程序,调用FirebaseCrashlytics.instance.recordError(error, stack, fatal: true),可以在按钮的 onPressed 上使用 throw Exception () 使其崩溃

TextButton(
  onPressed: () => throw Exception(),
  child: const Text("Throw Test Exception"),
),

这一次,我们增加了一个新的崩溃页面,并创建了一个崩溃按钮

当崩溃发生时,Firebase Console的Crashlytics会显示一份报告。

Crashlytics现在将监测应用程序崩溃的情况

碰撞报告也可以自定义

小结总结

这是与上次相比的变化

  • 增加了测试碰撞页面
  • 其他代码修改

main.dart

/// Flutter导入
import 'package:flutter/material.dart';
import 'package:flutter_river:pod/flutter_riverpod.dart';
import 'dart:async';

/// Firebase导入
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';

/// 导入其他页面
import 'package:counter_firebase/normal_counter_page.dart';
import 'package:counter_firebase/crash_page.dart';

void main() async {
  /// 崩溃处理程序
  runZonedGuarded<Future<void>>(() async {
    /// Firebase初始化
    WidgetsFlutterBinding.ensureInitialized();
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );

    /// 崩溃处理程序(Flutter框架内抛出的所有错误)
    FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;

    /// runApp w/ Riverpod
    runApp(const ProviderScope(child: MyApp()));
  },

      /// 崩溃处理程序(Flutter框架内未捕获的错误)
      (error, stack) =>
          FirebaseCrashlytics.instance.recordError(error, stack, fatal: true));
}

/// Provider初始化
final counterProvider = StateNotifierProvider<Counter, int>((ref) {
  return Counter();
});

class Counter extends StateNotifier<int> {
  Counter() : super(0);

  void increment() => state++;
}

/// MaterialApp设置
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Counter Firebase',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
      debugShowCheckedModeBanner: false,
    );
  }
}

/// 主屏幕
class MyHomePage extends ConsumerWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('My Homepage'),
      ),
      body: ListView(
        padding: const EdgeInsets.all(10),
        children: <Widget>[
          _PagePushButton(
            buttonTitle: '计数器',
            pagename: NormalCounterPage(),
          ),
          _PagePushButton(
            buttonTitle: '崩溃页面',
            pagename: CrashPage(),
          ),
        ],
      ),
    );
  }
}

/// 页面过渡按钮
class _PagePushButton extends StatelessWidget {
  const _PagePushButton({
    Key? key,
    required this.buttonTitle,
    required this.pagename,
  }) : super(key: key);

  final String buttonTitle;
  final dynamic pagename;

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      child: Container(
        padding: const EdgeInsets.all(10),
        child: Text(buttonTitle),
      ),
      onPressed: () {
        AnalyticsService().logPage(buttonTitle);
        Navigator.push(
          context,
          MaterialPageRoute(builder: (context) => pagename),
        );
      },
    );
  }
}

/// Analytics
class AnalyticsService {
  /// 页面转换的日志
  Future<void> logPage(String screenName) async {
    await FirebaseAnalytics.instance.logEvent(
      name: 'screen_view',
      parameters: {
        'firebase_screen': screenName,
      },
    );
  }
}

crash_page.dart

/// Flutter
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class CrashPage extends ConsumerWidget {
  const CrashPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('崩溃页面'),
      ),
      body: ListView(
        padding: const EdgeInsets.all(10),
        children: <Widget>[
          TextButton(
            onPressed: () => throw Exception(),
            child: const Text("抛出测试异常"),
          ),
        ],
      ),
    );
  }
}

<div id="07">7️⃣Firebase Remote Config</div>

Firebase Remote Config概述

Firebase Remote Config是一项服务,它允许你改变你的应用程序的行为和外观,而不需要发布更新和远程改变配置值。

Firebase Remote Config官方文档

应用案例

官方介绍了以下 Remote Config 用例。

  • 通过百分比推出发布新功能
  • 为您的应用定义针对具体平台和针对具体语言区域的促销横幅

准备

准备工作和前几章都已完成方可开始

使用方法

要在项目中引入firebase_remote_config,将其添加到pubspec.yaml中并导入

pubspec.yaml

dependencies:
  firebase_remote_config: ^2.0.12

创建并执行一个方法来初始化和设置参数

检索单例对象时,控制最小获取间隔以获得最佳更新时间

使用getString()getBool()等方法获取app中使用的参数

import 'package:firebase_remote_config/firebase_remote_config.dart';

/// Firebase Remote Config的初始化
class FirebaseRemoteConfigService {
  void initRemoteConfig() async {
    /// 实例创建
    final remoteConfig = FirebaseRemoteConfig.instance;

    /// 获得一个单例对象
    await remoteConfig.setConfigSettings(RemoteConfigSettings(
      fetchTimeout: const Duration(minutes: 1),
      minimumFetchInterval: const Duration(minutes: 5),
    ));

    /// 在应用程序中设置默认参数值
    await remoteConfig.setDefaults(const {
      "example_param": "Hello, world!",
    });

    /// 取值
    await remoteConfig.fetchAndActivate();
  }
}

加载

初始化

remote_config_page.dart

@override
void initState() {
  super.initState();

  /// Firebase Remote Config初始化
  FirebaseRemoteConfigService().initRemoteConfig();
}

导出到Text Widget时,可以看到输出的是设定值(0)

remote_config_page.dart

Text(FirebaseRemoteConfig.instance.getString("example_param")),

更改值

然后从 Firebase Console的Remote Config设置后端配置以更改值

在参数键中输入由setDefaults确定的键,在默认值中输入新的值,然后'发布更改'


过了一会儿,我能够确认文本已更改为 8

小结总结

这是与上次相比的变化

  • 增加了Remote Config页面
  • 其他代码修改

main.dart

/// Flutter导入
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'dart:async';

/// Firebase导入
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:counter_firebase/remote_config_page.dart';

/// 导入其他页面
import 'package:counter_firebase/normal_counter_page.dart';
import 'package:counter_firebase/crash_page.dart';

void main() async {
  /// 崩溃处理程序
  runZonedGuarded<Future<void>>(() async {
    /// Firebase初始化
    WidgetsFlutterBinding.ensureInitialized();
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );

    /// 崩溃处理程序(Flutter框架内抛出的所有错误)
    FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;

    /// runApp w/ Riverpod
    runApp(const ProviderScope(child: MyApp()));
  },

      /// 崩溃处理程序(Flutter框架内未捕获的错误)
      (error, stack) =>
          FirebaseCrashlytics.instance.recordError(error, stack, fatal: true));
}

/// Provider初始化
final counterProvider = StateNotifierProvider<Counter, int>((ref) {
  return Counter();
});

class Counter extends StateNotifier<int> {
  Counter() : super(0);

  void increment() => state++;
}

/// MaterialApp设置
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Counter Firebase',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
      debugShowCheckedModeBanner: false,
    );
  }
}

/// 主屏幕
class MyHomePage extends ConsumerWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('My Homepage'),
      ),
      body: ListView(
        padding: const EdgeInsets.all(10),
        children: <Widget>[
          _PagePushButton(
            buttonTitle: '计数器',
            pagename: NormalCounterPage(),
          ),
          _PagePushButton(
            buttonTitle: '计数器',
            pagename: CrashPage(),
          ),
          _PagePushButton(
            buttonTitle: 'Remote Config计数器',
            pagename: RemoteConfigPage(),
          ),
        ],
      ),
    );
  }
}

/// 页面过渡按钮
class _PagePushButton extends StatelessWidget {
  const _PagePushButton({
    Key? key,
    required this.buttonTitle,
    required this.pagename,
  }) : super(key: key);

  final String buttonTitle;
  final dynamic pagename;

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      child: Container(
        padding: const EdgeInsets.all(10),
        child: Text(buttonTitle),
      ),
      onPressed: () {
        AnalyticsService().logPage(buttonTitle);
        Navigator.push(
          context,
          MaterialPageRoute(builder: (context) => pagename),
        );
      },
    );
  }
}

class AnalyticsService {
  /// 页面转换的日志
  Future<void> logPage(String screenName) async {
    await FirebaseAnalytics.instance.logEvent(
      name: 'screen_view',
      parameters: {
        'firebase_screen': screenName,
      },
    );
  }
}

remote_config_page.dart

/// Flutter
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

/// Firebase导入
import 'package:firebase_remote_config/firebase_remote_config.dart';

/// 其他页面
import 'package:counter_firebase/main.dart';

class RemoteConfigPage extends ConsumerStatefulWidget {
  const RemoteConfigPage({Key? key}) : super(key: key);

  @override
  RemoteConfigPageState createState() => RemoteConfigPageState();
}

class RemoteConfigPageState extends ConsumerState<RemoteConfigPage> {
  @override
  void initState() {
    super.initState();

    /// Firebase Remote Config初始化
    FirebaseRemoteConfigService().initRemoteConfig();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Homepage'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            /// Remote Config数据采集
            Text(
          FirebaseRemoteConfig.instance.getString("example_param"),
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
    );
  }
}

/// Firebase Remote Config的初始设置
class FirebaseRemoteConfigService {
  void initRemoteConfig() async {
    /// 实例创建
    final remoteConfig = FirebaseRemoteConfig.instance;

    /// 获得一个单例对象
    await remoteConfig.setConfigSettings(RemoteConfigSettings(
      fetchTimeout: const Duration(minutes: 1),
      minimumFetchInterval: const Duration(minutes: 5),
    ));

    /// 在应用程序中设置默认参数值
    await remoteConfig.setDefaults(const {
      "example_param": "0",
    });

    /// 获取数值
    await remoteConfig.fetchAndActivate();
  }
}

<div id="08">8️⃣Firebase Authentication</div>

Firebase Authentication概述

Firebase认证是一项能够使用用户认证功能的服务

官方网站

准备

准备工作和前几章都已完成方可开始

使用方法

要在项目中引入firebase_auth,请在pubspec.yaml中添加以下内容,并导入它

pubspec.yaml

dependencies:
  firebase_auth: ^3.4.2

要选择你的登录方式(电子邮件地址、电话号码等),请从Firebase Console进入认证,在登录方式下选择你喜欢的登录方式。

在这种情况下,我们将使用一个电子邮件地址和密码


在Firebase Console中设置好配置后,你就可以实施了

输入TextField中输入的电子邮件地址和密码。

通过将obscureText设置为 "true "使密码不可见。

/// 输入你的电子邮件地址
TextField(
  decoration: const InputDecoration(
    label: Text('E-mail'),
  ),
  controller: _idController,
),

/// 输入密码
TextField(
  decoration: const InputDecoration(
    label: Text('Password'),
  ),
  controller: _passController,
  obscureText: true,
),

创建一个执行按钮并调用一个允许你创建账户或登录的函数

/// 用于创建账户
Container(
  margin: const EdgeInsets.all(10),
  child: ElevatedButton(
    onPressed: () {
      _createAccount(ref, idController.text, passController.text);
    },
    child: const Text('创建账户'),
  ),
),

使用FirebaseAuth.instance.createUserWithEmailAndPassword来处理账户创建。

电子邮件地址和密码被传递,如果发生错误,会产生一个错误信息。

import 'package:firebase_auth/firebase_auth.dart';

void _createAccount(String id, String pass) async {
  try {
    /// credential 帐户信息记录
    final credential =
        await FirebaseAuth.instance.createUserWithEmailAndPassword(
      email: id,
      password: pass,
    );
  }

  /// 在账户失败的情况下进行错误处理
  on FirebaseAuthException catch (e) {
    /// 如果密码很弱的话
    if (e.code == 'weak-password') {
      print('请设置包含大小写字母和数字的6-18位密码');

      /// 如果该电子邮件地址已经在使用中
    } else if (e.code == 'email-already-in-use') {
      print('该电子邮件以注册');
    }

    ///其他错误
    else {
      print('账户创建错误');
    }
  } catch (e) {
    print(e);
  }
}

登录过程是使用FirebaseAuth.instance.signInWithEmailAndPassword来处理。

它传递电子邮件地址和密码,如果发生错误,会产生一个错误信息

void _signIn(String id, String pass) async {
  try {
    /// credential 帐户信息记录
    final credential = await FirebaseAuth.instance.signInWithEmailAndPassword(
      email: id,
      password: pass,
    );
  }

  /// 登录失败时的错误处理
  on FirebaseAuthException catch (e) {
    /// 无效的电子邮件地址
    if (e.code == 'invalid-email') {
      print('无效的电子邮件地址');
    }

    /// 如果该用户不存在
    else if (e.code == 'user-not-found') {
      print('用户不存在');
    }

    /// 如果密码不正确
    else if (e.code == 'wrong-password') {
      print('密码不正确');
    }

    /// 其他错误
    else {
      print('登录错误');
    }
  }
}

用于登出FirebaseAuth.instance.signOut()
auth_page.dart

void _signOut() async {
  await FirebaseAuth.instance.signOut();
}

获取用户信息的三种方式。

/// 使用authStateChanges、idTokenChanges和userChanges流
FirebaseAuth.instance
  .authStateChanges()
  .listen((User? user) {
    if (user != null) {
      print(user.uid);
    }
  });

/// 使用由认证(signIn)方法返回的UserCredential对象
final userCredential =
    await FirebaseAuth.instance.signInWithCredential(credential);
final user = userCredential.user;
print(user?.uid);

/// 使用FirebaseAuth实例的currentUser属性
if (FirebaseAuth.instance.currentUser != null) {
  print(FirebaseAuth.instance.currentUser?.uid);
}

使用.update***来更新用户资料和电子邮件地址

final userCredential =
    await FirebaseAuth.instance.signInWithCredential(credential);
final user = userCredential.user;

await user?.updateDisplayName("Jane Q. User");
await user?.updateEmail("janeq@example.com");

通过电子邮件地址进行认证,但也可以通过电话号码和OAuth进行认证

登录前的主屏幕


登录页面


登录后的主屏幕

小结总结

这是与上次相比的变化

  • 添加 Firebase 身份验证页面
  • 其他代码修改

main.dart

/// Flutter导入
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'dart:async';

/// Firebase导入
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:firebase_auth/firebase_auth.dart';

/// 导入其他页面
import 'package:counter_firebase/normal_counter_page.dart';
import 'package:counter_firebase/crash_page.dart';
import 'package:counter_firebase/auth_page.dart';
import 'package:counter_firebase/remote_config_page.dart';

void main() async {
  /// 崩溃处理程序
  runZonedGuarded<Future<void>>(() async {
    /// Firebase初始化
    WidgetsFlutterBinding.ensureInitialized();
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );

    /// 崩溃处理程序(Flutter框架内抛出的所有错误)
    FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;

    /// runApp w/ Riverpod
    runApp(const ProviderScope(child: MyApp()));
  },

      /// 崩溃处理程序(Flutter框架内未捕获的错误)
      (error, stack) =>
          FirebaseCrashlytics.instance.recordError(error, stack, fatal: true));
}

/// Provider初始化
final counterProvider = StateNotifierProvider.autoDispose<Counter, int>((ref) {
  return Counter();
});

class Counter extends StateNotifier<int> {
  Counter() : super(0);
  
  void increment() => state++;
}

/// MaterialApp设置
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Counter Firebase',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
      debugShowCheckedModeBanner: false,
    );
  }
}

/// 主屏幕
class MyHomePage extends ConsumerWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
      /// 获取用户信息
    FirebaseAuth.instance.authStateChanges().listen((User? user) {
      if (user == null) {
        ref.watch(userEmailProvider.state).state = '未登录';
      } else {
        ref.watch(userEmailProvider.state).state = user.email!;
      }
    });

    return Scaffold(
      appBar: AppBar(
        title: const Text('My Homepage'),
      ),
      body: ListView(
        padding: const EdgeInsets.all(10),
        children: <Widget>[
          /// 显示用户信息
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const Icon(Icons.person),
              Text(ref.watch(userEmailProvider)),
            ],
          ),

          /// 页面过渡
          const _PagePushButton(
            buttonTitle: '普通计数器',
            pagename: NormalCounterPage(),
          ),
          const _PagePushButton(
            buttonTitle: '崩溃页面',
            pagename: CrashPage(),
          ),
          const _PagePushButton(
            buttonTitle: '远程配置计数器',
            pagename: RemoteConfigPage(),
          ),
          const _PagePushButton(
            buttonTitle: '认证页面',
            pagename: AuthPage(),
          ),
        ],
      ),
    );
  }
}

/// 页面过渡按钮
class _PagePushButton extends StatelessWidget {
  const _PagePushButton({
    Key? key,
    required this.buttonTitle,
    required this.pagename,
  }) : super(key: key);

  final String buttonTitle;
  final dynamic pagename;

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      child: Container(
        padding: const EdgeInsets.all(10),
        child: Text(buttonTitle),
      ),
      onPressed: () {
        AnalyticsService().logPage(buttonTitle);
        Navigator.push(
          context,
          MaterialPageRoute(builder: (context) => pagename),
        );
      },
    );
  }
}

class AnalyticsService {
  /// 页面转换的日志
  Future<void> logPage(String screenName) async {
    await FirebaseAnalytics.instance.logEvent(
      name: 'screen_view',
      parameters: {
        'firebase_screen': screenName,
      },
    );
  }
}

auth_page.dart

/// Flutter
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

/// Firebase导入
import 'package:firebase_auth/firebase_auth.dart';

/// Auth签入状态提供者
final signInStateProvider = StateProvider((ref) => '登录或创建一个账户');

/// 登录用户的信息提供
final userProvider = StateProvider<User?>((ref) => null);
final userEmailProvider = StateProvider<String>((ref) => '未登录');

/// 页面设置
class AuthPage extends ConsumerStatefulWidget {
  const AuthPage({Key? key}) : super(key: key);

  @override
  AuthPageState createState() => AuthPageState();
}

class AuthPageState extends ConsumerState<AuthPage> {
  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    final singInStatus = ref.watch(signInStateProvider);
    final idController = TextEditingController();
    final passController = TextEditingController();

    return Scaffold(
      appBar: AppBar(
        title: const Text('Auth Page'),
      ),
      body: ListView(
        padding: const EdgeInsets.all(10),
        children: <Widget>[
          /// 输入你的电子邮件地址
          TextField(
            decoration: const InputDecoration(
              label: Text('E-mail'),
              icon: Icon(Icons.mail),
            ),
            controller: idController,
          ),

          /// 输入密码
          TextField(
            decoration: const InputDecoration(
              label: Text('Password'),
              icon: Icon(Icons.key),
            ),
            controller: passController,
            obscureText: true,
          ),

          /// 登录
          Container(
            margin: const EdgeInsets.all(10),
            child: ElevatedButton(
              onPressed: () {
                /// 用于登录
                _signIn(ref, idController.text, passController.text);
              },
              style: ButtonStyle(
                  backgroundColor: MaterialStateProperty.all(Colors.grey)),
              child: const Text('登录'),
            ),
          ),

          /// 创建账户
          Container(
            margin: const EdgeInsets.all(10),
            child: ElevatedButton(
              onPressed: () {
                /// 用于创建账户
                _createAccount(ref, idController.text, passController.text);
              },
              child: const Text('创建账户'),
            ),
          ),

          /// 登录信息显示
          Container(
            padding: const EdgeInsets.all(10),
            child: Text('信息 : $singInStatus'),
          ),

          /// 登出
          TextButton(
              onPressed: () {
                _signOut(ref);
              },
              child: const Text('SIGN OUT'))
        ],
      ),
    );
  }
}

/// 登录处理
void _signIn(WidgetRef ref, String id, String pass) async {
  try {
    /// 帐户信息被记录在credential 
    final credential = await FirebaseAuth.instance.signInWithEmailAndPassword(
      email: id,
      password: pass,
    );

    /// 更新用户信息
    ref.watch(userProvider.state).state = credential.user;

    /// 在屏幕上显示
    ref.read(signInStateProvider.state).state = '我已经能够登录了!';
  }

  /// 登录失败时的错误处理
  on FirebaseAuthException catch (e) {
    /// 无效的电子邮件地址
    if (e.code == 'invalid-email') {
      ref.read(signInStateProvider.state).state = '无效的电子邮件地址';
    }

    /// 该用户不存在
    else if (e.code == 'user-not-found') {
      ref.read(signInStateProvider.state).state = '该用户不存在';
    }

    /// 密码不正确
    else if (e.code == 'wrong-password') {
      ref.read(signInStateProvider.state).state = '密码不正确';
    }

    /// 其他错误
    else {
      ref.read(signInStateProvider.state).state = '登录错误';
    }
  }
}

/// 创建账户
void _createAccount(WidgetRef ref, String id, String pass) async {
  try {
    /// 帐户信息被记录在credential 
    final credential =
        await FirebaseAuth.instance.createUserWithEmailAndPassword(
      email: id,
      password: pass,
    );

    /// 更新用户信息
    ref.watch(userProvider.state).state = credential.user;

    /// 在屏幕上显示
    ref.read(signInStateProvider.state).state = '账户创建成功!';
  }

  /// 在账户失败的情况下进行错误处理
  on FirebaseAuthException catch (e) {
    /// 如果密码很弱
    if (e.code == 'weak-password') {
      ref.read(signInStateProvider.state).state = '请设置包含大小写字母和数字的6-18位密码');

      /// 如果该电子邮件地址已经在使用中
    } else if (e.code == 'email-already-in-use') {
      print('该电子邮件以注册');
    }

   ///其他错误
    else {
      print('账户创建错误');
    }
  } catch (e) {
    print(e);
  }
}

/// 登出
void _signOut(WidgetRef ref) async {
  await FirebaseAuth.instance.signOut();
  ref.read(signInStateProvider.state).state = '登录或创建一个账户';
}

<div id="09">9️⃣Cloud Firestore</div>

Cloud Firestore概述

Cloud Firestore是一个用于无服务器数据存储的NoSQL数据库

官方网站

类似的服务

除了Firestore之外,Firebase也有一个类似的数据库。

云存储,用于存储用户生成的数据,如照片和视频

实时数据库,用于客户与客户之间的实时通信

官方网站上有一个与实时数据库的比较,以帮助你选择哪种数据库

准备

准备工作和前几章都已完成方可开始

使用方法

从Firebase Console,选择Firestore数据库并创建数据库。

Firestore的安全规则已被设置为记录用户ID中的计数,具体如下。
请注意,安全规则在另一章中描述。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId}/{documents=**} {
      allow read, write: if request.auth != null && request.auth.uid == userId
    }
  }
}

Firestore的数据模型由文档、集合等组成,支持的数据类型包括boolintMap类型,以及日期和地理坐标

Firebase Console设置完毕后,在pubspec.yaml中添加以下内容,将cloud_firestore引入项目并导入

pubspec.yaml

dependencies:
  cloud_firestore: ^3.3.0

Firestore数据处理

显示了向Firestore添加、读取和删除数据的例子。
Riverpod用于写入和读取数据

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';

/// Firestore数据库的定义
final db = FirebaseFirestore.instance;

/// 获取UserID
final userID = FirebaseAuth.instance.currentUser?.uid ?? 'test';

/// 数据添加
void add(WidgetRef ref) {

  final Map<String, dynamic> counterMap = {
    'count': ref.read(counterProvider),
  };

  /// 将数据添加到Firestore
  try {
    db.collection('users').doc(userID).set(counterMap);
  } catch (e) {
    print('Error : $e');
  }
}

/// 数据采集
void get(WidgetRef ref) async {
  try {
    await db.collection('users').doc(userID).get().then(
      (event) {
        ref.read(counterProvider.notifier).state = event.get('count');
      },
    );
  } catch (e) {
    print('Error : $e');
  }
}

/// 数据删除
void delete() async {
  try {
    db.collection('users').doc(userID).delete().then((doc) => null);
  } catch (e) {
    print('Error : $e');
  }
}

实际的计数

检查Firebase Console,看看数据是否在Firestore中

小结总结

在这一小结中我们完成了一下功能

  • 增加了Firestore页面
  • 改变页面转换的按钮的颜色
  • 其他代码修改

<div id="10">🔟Firebase Realtime Database</div>

Firebase Realtime Database概述

Firebase实时数据库是一个NoSQL数据库服务,它能够在所有的客户端进行数据存储和实时同步

数据以json格式存储,并根据数据量收费,因此它适合存储比Firestore更频繁更新的小数据

类似的服务

除了Firestore之外,Firebase也有一个类似的数据库。

云存储,用于存储用户生成的数据,如照片和视频

准备

准备工作和前几章都已完成方可开始

使用方法

从Firebase Console,选择实时数据库并创建数据库。

安全规则设置如下,因此,只有内容所有者可以访问数据库

{
  "rules": {
    "users": {
      "$uid": {
        ".read": "auth != null && auth.uid == $uid",
        ".write": "auth != null && auth.uid == $uid"
      }
    }
  }
}

在设置了Firebase Console之后,在pubspec.yaml中添加以下内容,将firebase_database导入项目中

dependencies:
  firebase_database: ^9.0.19

数据操作

数据库定义

检索了用户ID,并定义了数据库

Riverpod用于阅读和写作

import 'package:firebase_database/firebase_database.dart';
import 'package:firebase_auth/firebase_auth.dart';

final userID = FirebaseAuth.instance.currentUser?.uid ?? '';

DatabaseReference dbRef = FirebaseDatabase.instance.ref('users');

写入实时数据库

有两种方法可以向实时数据库写入数据:使用设置和使用更新

这一次是由UPDATE实施的

void write(WidgetRef ref) async {
  try {
    await dbRef.update({
      '$userID/count': ref.read(counterProvider),
    });
  } catch (e) {
    print('Error : $e');
  }
}

读取实时数据库数据

有两种读取实时数据库数据的方法:监听DatabaseReference并调用DatabaseEvent,或使用get()。
前者在每次改变数据时都会被触发,而后者只读取一次数据。

这次是用get()实现的。

void read(WidgetRef ref) async {
  try {
    final snapshot = await dbRef.child(userID).get();
    if (snapshot.exists) {
      ref.read(counterProvider.notifier).state =
          snapshot.child('count').value as int;
    }
  } catch (e) {
    print('Error : $e');
  }
}

删除实时数据库数据

可以使用remove()删除数据

void remove() async {
  try {
    await dbRef.child(userID).remove();
  } catch (e) {
    print('Error : $e');
  }
}

Realtime Database的计数器画面


运行后,在Firebase Console检查数据库是否已经被改变

小结总结

在这一小结中我们完成了一下功能

  • 增加了实时数据库页面。
  • 其他代码修改

<div id="11">▶️Cloud Storage for Firebase</div>

Cloud Firestore概述

Firebase的云存储是一项用于存储用户生成的内容的服务,如照片和视频

类似的服务

除了Firestore之外,Firebase也有一个类似的数据库。

云存储,用于存储用户生成的数据,如照片和视频。

准备

准备工作和前几章都已完成方可开始

使用方法

从Firebase控制台中选择存储来开始。

安全规则已经设置好了,只有内容所有者才能访问,具体如下

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /users/{userId}/{allPaths=**} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }
  }
}

在设置好Firebase控制台后,添加到pubspec.yaml中,并将firebase_storage导入项目中。

这一次,为了在Android上上传图片,image_picker也被一起导入。

注意,Web不支持dart:io包,所以不能使用下面的代码。

dependencies:
  image_picker: ^0.8.5
  firebase_storage: ^10.3.2

数据操作

上传至云存储

要将图片上传到云存储,用image_picker选择图片并使用putFile

import 'package:image_picker/image_picker.dart';
import 'package:firebase_storage/firebase_storage.dart';

final userID = FirebaseAuth.instance.currentUser?.uid ?? '';

void uploadPic() async {
  try {
    final ImagePicker picker = ImagePicker();
    final XFile? image = await picker.pickImage(source: ImageSource.gallery);
    File file = File(image!.path);

    String uploadName = 'image.png';
    final storageRef =
        FirebaseStorage.instance.ref().child('users/$userID/$uploadName');
    final task = await storageRef.putFile(file);
  } catch (e) {
    print(e);
  }

管理上传

作为管理上传的一部分,你可以暂停、恢复和取消上传。

此外,你还可以监控上传的进度。

bool paused = await task.pause();
print('paused, $paused');

bool resumed = await task.resume();
print('resumed, $resumed');

bool canceled = await task.cancel();
print('canceled, $canceled');

可以从Firebase控制台检查图片是否已经上传

从云存储下载

有两种方法可以从云存储下载图像:下载到内存或直接下载到本地文件夹。

这一次,图像被下载到内存中并显示在应用程序中。

此外,Riverpod还用于阅读和写作。

final imageStateProvider = StateProvider<Uint8List?>((ref) => null);

void downloadPic(WidgetRef ref) async {
  try {
    String downloadName = 'image.png';
    final storageRef =
        FirebaseStorage.instance.ref().child('users/$userID/$downloadName');

    const oneMegabyte = 1024 * 1024;
    ref.read(imageStateProvider.state).state =
        await storageRef.getData(oneMegabyte);
  } catch (e) {
    print(e);
  }
}

如果显示出来,说明下载的实施是OK的。


删除云存储数据

使用delete()来删除云存储数据。

void deletePic() async {
  String deleteName = 'image.png';
  final storageRef =
      FirebaseStorage.instance.ref().child('users/$userID/$deleteName');

  await storageRef.delete();
}

小结总结

在这一小结中我们完成了一下功能

  • 增加了云存储页面
  • 其他代码修改

<div id="12">⬇️Firebase Cloud Messaging</div>

Firebase Cloud Messaging概述

Firebase Cloud Messaging(FCM)是一项允许向客户端应用程序发送推送通知的服务。

官方网站

FCM的架构也在官方文件中作了介绍

准备

准备工作和前几章都已完成方可开始

使用方法

iOS、Android和Web的准备工作和使用条件有所不同

Android。

Android可以在运行Android4.4或更高版本的设备上运行。

iOS

查看设置说明,为在iOS上使用做准备

web

在web上使用时,有必要在Firebase控制台为 "网络推送证书 "生成一对密钥,创建并注册firebase-messaging-sw.js文件,等等

添加到导入到firebase_messaging项目中。pubspec.yaml

dependencies:
  firebase_messaging: ^11.4.0

接待设置

获取令牌的ID。

测试时打印出令牌。

import 'package:firebase_messaging/firebase_messaging.dart';

// final fcmToken = await FirebaseMessaging.instance.getToken(vapidKey: 'BDdcxJZSBD...');

final fcmToken = await FirebaseMessaging.instance.getToken();

print(fcmToken);

如果你也想在后台接收信息,不管是什么平台,添加以下代码。

_firebaseMessagingBackgroundHandler函数不能是一个匿名函数,必须被当作一个顶级函数,否则会发生错误。

Future<void> main() async {
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

  runApp(...
}

Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp();
  print("Handling a background message: ${message.messageId}");
}

此外,还为web和iOS设备设置了权限

NotificationSettings settings = await messaging.requestPermission(
  alert: true,
  announcement: false,
  badge: true,
  carPlay: false,
  criticalAlert: false,
  provisional: false,
  sound: true,
);

实际分配

测试交付给一个安卓模拟器。

在Firebase控制台的Messaging中选择一个新的活动。

从 "在设备上测试",复制并粘贴你刚刚打印出来的令牌到FCM注册令牌中。

如果你的设备上收到推送通知,你就成功了

小结总结

在这一小结中我们完成了一下功能

  • 其他代码修改
  • 例如,有些键是用类来隐藏的

<div id="13">⬅️Firebase In-App Messaging</div>

Firebase In-App Messaging概述

Firebase In-App Messaging是一项允许你发送目标信息的服务

这项服务只在移动端(iOS、Android)提供

准备

准备工作和前几章都已完成方可开始

使用方法

要在项目中引入firebase_in_app_messaging,请将其加入pubspec.yaml

由于In-App Messaging每天只从服务器检索一次信息,我们将在测试中尝试使用Firebase安装ID(FID)

dependencies:
  firebase_in_app_messaging: ^0.6.0+14
  firebase_app_installations: ^0.1.0+14

在Flutter中可以通过加载firebase_in_app_messaging来使用应用内消息

import 'package:firebase_in_app_messaging/firebase_in_app_messaging.dart';

使用FirebaseInstallations来获取FID

import 'package:firebase_app_installations/firebase_app_installations.dart';

void getFID() async {
  String id = await FirebaseInstallations.instance.getId();
  print('id : $id');
}

交付测试

从Firebase控制台测试交付。

当你准备好了,输入你刚刚克制的FID,进行设备交付测试

交付后,在调试设备上返回主屏幕一次,并再次打开应用,查看应用内信息

请注意,如果出现以下错误信息,说明Firebase In-App Messaging API被禁用了,你需要访问谷歌云平台的Firebase In-App Messaging API并启用该API,如错误文本所示

PERMISSION_DENIED: Firebase In-App Messaging API has not been used in project *** before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/firebaseinappmessaging.googleapis.com/overview?project=*** then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.

小结总结

在这一小结中我们完成了一下功能

  • In-App Messaging的实施

<div id="14">◀️Firebase ML</div>

Firebase ML概述

Firebase ML是一个使用Firebase的机器学习模型推理

Firebase x Flutter中提供的机器学习推理

使用Flutter和Firebase的机器学习推理可以在设备上或在云端完成

设备上的推理

使用Firebase进行设备上的推理意味着用Firebase ML提供自定义的TensorFlowLite模型进行本地推理

对于实际的推断,使用了tflite_flutter、ML Kit等

用Firebase提供定制的TF模型的好处是,用户可以使用最新的模型,而不需要更新他们的应用程序

云端推理

使用Firebase(谷歌云)进行云推理,意味着用云视觉AI或云自然语言进行推理。
目前,没有为Flutter提供API,所以你需要为每个操作系统组合API

除上述之外,还有其他方法可以使用来自其他服务的机器学习模型

如何用Firebase部署自定义的TensorflowLite模型分布

准备一个你自己训练的自定义TF模型的TFLite文件

在这种情况下,为了执行图像识别任务,我从TensorFlow Hub获得了Imagenet图像分类训练模型

请注意,当从TensorFlow Hub下载模型时,要注意许可证和下载的文件类型

一旦文件准备好了,从Firebase控制台的机器学习中部署该模型

Firebase官方文档推荐使用tflite_flutter和tflite进行推理,但我在开发环境中无法用导入的tflite_flutter等构建应用,所以我用ML Kit做了实验

可以使用ML工具包中的TFLite自定义模型的任务包括图像标签或物体检测和跟踪

由于我们将对图像标签进行推理,我们也将介绍google_ml_kitgoogle_mlkit_image_labelling

dependencies:
  google_ml_kit: ^0.11.0
  google_mlkit_image_labeling: ^0.3.0

使用google_mlkit_image_labelling包中的FirebaseImageLabelerModelManager从Firebase ML下载模型

final bool response =
    await FirebaseImageLabelerModelManager().downloadModel(modelname);
final options = FirebaseLabelerOption(
        confidenceThreshold: 0.5, modelName: modelname, maxCount: 3);
_imageLabeler = ImageLabeler(options: options);

如果你知道照片的路径,例如image_picker,你只需要两行代码进行基本的标签推理

final InputImage inputImage = InputImage.fromFilePath(path);
final List labels = await _imageLabeler.processImage(inputImage);

从推断出的结果中提取标签

String labelText = '';
for (final label in labels) {
  labelText += '\nLabel: ${label.label}';
}

用应用程序检查。

它似乎能够进行推论,但结果与我预期的不同,所以还有改进的余地

小结总结

在这一小结中我们完成了一下功能

  • 添加ml_page页面
  • 其他代码修改

<div id="15">⏬Cloud Functions for Firebase</div>

Cloud Functions for Firebase概述

Cloud Functions for Firebase是一项服务,它可以对触发的事件自动执行后端代码

Firebase的云功能支持用javascript和typescript编写,允许在不管理或扩展服务器的情况下实现后端

应用案例

准备

准备工作和前几章都已完成方可开始

在JavaScript环境中运行Cloud Functions需要一个Node.js环境,如果你还没有这样做,请使用nvm安装它

使用方法

安装firebase-tools

npm install -g firebase-tools

初始化项目

在Firebase登录准备好的环境中,初始化函数和其他必要的工具。

在这种情况下,这次我选择了语言JavaScript

firebase init functions

一旦初始化,项目中就会创建一个新的函数文件夹

创建功能

在function/index.js中编写执行Cloud Functions的函数。

导入必要的模块

const functions = require("firebase-functions");

功能定义。

有三种主要的方法来调用一个函数

  • 如何直接从应用程序调用
  • 如何通过 HTTP 请求调用函数
  • 如何调入日程设置

这一次,计数功能是直接从应用程序中调用的,所以在后端使用了onCall触发器。
当我们在做这件事的时候,我们还将试验能够调用UID

exports.functionsTest = functions.https.onCall((data, context) => {

  const firstNumber = data.firstNumber;
  const secondNumber = data.secondNumber;
  const addNumber = firstNumber + secondNumber;
  const contextUid = context.auth.uid;
  return { addNumber:addNumber,  contextUid:contextUid }
});

在模拟器上测试

在部署之前在本地模拟器上进行测试以检查无限循环等。

如果App Check使用,则无法运行模拟器,需要使用 App Check 调试提供程序,每个执行环境都必须使用调试提供程序

Java安装 Open JDK,因为它需要启动模拟器

安装并初始化本地模拟器,必要时用Firebase init ***安装每个插件

firebase init emulators

当使用本地模拟器时,在Flutter侧的主函数中设置useFunctionsEmulator

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();

  // Ideal time to initialize
  FirebaseFunctions.instance.useFunctionsEmulator('localhost', 5001);

...

安装后,用emulators:start启动模拟器,并在浏览器中打开http://localhost:4000/(默认情况下)

firebase emulators:start

部署

如果一切顺利,就部署到生产环境

在Firebase控制台,进入功能,选择 "开始"

firebase deploy --only functions:functionsTest

从Flutter应用程序调用

要在你的Flutter项目中应用云函数,请在pubspec.yaml中引入cloud_functions

dependencies:
  cloud_functions: ^3.3.2

functions编写执行的代码

import 'package:cloud_functions/cloud_functions.dart';

void addNumber() async {
  try {
  
    final result = await FirebaseFunctions.instance
        .httpsCallable('functionsTest')
        .call({'firstNumber': _number, 'secondNumber': 1});
    _number = result.data['addNumber'];
    print(result.data['contextUid']);
  } on FirebaseFunctionsException catch (error) {
    print(error.code);
    print(error.details);
    print(error.message);
  }
}****

一款使用 Cloud Functions 进行计数的豪华应用程序已经完成

小结总结

在这一小结中我们完成了一下功能

  • 添加了Cloud Functions
  • 添加了cloud_functions
  • 其他代码修改
const functions = require("firebase-functions");

exports.functionsTest = functions.https.onCall(async(data, context) => {
  const firstNumber = data.firstNumber;
  const secondNumber = data.secondNumber;

  const addNumber = firstNumber + secondNumber;

  const contextUid = context.auth.uid;

  return { addNumber:addNumber,  contextUid:contextUid }
});
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:counter_firebase/remote_config_page.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:firebase_app_installations/firebase_app_installations.dart';
import 'package:firebase_in_app_messaging/firebase_in_app_messaging.dart';

import 'package:counter_firebase/normal_counter_page.dart';
import 'package:counter_firebase/crash_page.dart';
import 'package:counter_firebase/auth_page.dart';
import 'package:counter_firebase/firestore_page.dart';
import 'package:counter_firebase/realtime_database_page.dart';
import 'package:counter_firebase/cloud_storage.dart';
import 'package:counter_firebase/cloud_functions_page.dart';
import 'package:counter_firebase/ml_page.dart';

final isAndroid =
    defaultTargetPlatform == TargetPlatform.android ? true : false;
final isIOS = defaultTargetPlatform == TargetPlatform.iOS ? true : false;

Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp();
  print('Handling a background message: ${message.messageId}');
}

void main() async {
  runZonedGuarded<Future<void>>(() async {
    WidgetsFlutterBinding.ensureInitialized();
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );

    FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

    FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;

    // FirebaseFunctions.instance.useFunctionsEmulator('localhost', 5001);

    /// runApp w/ Riverpod
    runApp(const ProviderScope(child: MyApp()));
  },

      (error, stack) =>
          FirebaseCrashlytics.instance.recordError(error, stack, fatal: true));
}

final counterProvider = StateNotifierProvider<Counter, int>((ref) {
  return Counter();
});

class Counter extends StateNotifier<int> {
  Counter() : super(0);

  void increment() => state++;
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Counter Firebase',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
      debugShowCheckedModeBanner: false,
    );
  }
}

class MyHomePage extends ConsumerStatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  MyHomePageState createState() => MyHomePageState();
}

class MyHomePageState extends ConsumerState<MyHomePage> {
  @override
  void initState() {
    super.initState();

    FirebaseMessagingService().setting();

    FirebaseMessagingService().fcmGetToken();

    FirebaseInAppMessagingService().getFID();
  }

  @override
  Widget build(BuildContext context) {
    FirebaseAuth.instance.authStateChanges().listen((User? user) {
      if (user == null) {
        ref.watch(userEmailProvider.state).state = '未登录';
      } else {
        ref.watch(userEmailProvider.state).state = user.email!;
      }
    });

    return Scaffold(
      appBar: AppBar(
        title: const Text('My Homepage'),
      ),
      body: ListView(
        padding: const EdgeInsets.all(10),
        children: <Widget>[
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const Icon(Icons.person),
              Text(ref.watch(userEmailProvider)),
            ],
          ),

          const _PagePushButton(
            buttonTitle: '计数器',
            pagename: NormalCounterPage(),
          ),
          const _PagePushButton(
            buttonTitle: '崩溃页面',
            pagename: CrashPage(),
          ),
          const _PagePushButton(
            buttonTitle: 'Remote Config计数器',
            pagename: RemoteConfigPage(),
          ),
          const _PagePushButton(
            buttonTitle: '机器学习页面',
            pagename: MLPage(),
          ),
          const _PagePushButton(
            buttonTitle: '验证页面',
            pagename: AuthPage(),
            bgColor: Colors.red,
          ),

          /// 过渡到每个页面(认证后可用)
          /// 让未经授权的人无法按下按钮
          FirebaseAuth.instance.currentUser?.uid != null
              ? const _PagePushButton(
                  buttonTitle: 'Firestore计数器',
                  pagename: FirestorePage(),
                  bgColor: Colors.green,
                )
              : const Text('Firestore验证后,即可打开柜台'),
          FirebaseAuth.instance.currentUser?.uid != null
              ? const _PagePushButton(
                  buttonTitle: 'Realtime Database计数器',
                  pagename: RealtimeDatabasePage(),
                  bgColor: Colors.green,
                )
              : const Text('Realtime Database认证,以打开计数器'),
          FirebaseAuth.instance.currentUser?.uid != null
              ? const _PagePushButton(
                  buttonTitle: 'Cloud Storage页',
                  pagename: CloudStoragePage(),
                  bgColor: Colors.green,
                )
              : const Text('Cloud Storage请认证以打开该页面'),
          FirebaseAuth.instance.currentUser?.uid != null
              ? const _PagePushButton(
                  buttonTitle: 'Cloud Functions页',
                  pagename: CloudFunctionsPage(),
                  bgColor: Colors.green,
                )
              : const Text('Cloud Functions请认证以打开该页面'),
        ],
      ),
    );
  }
}

class _PagePushButton extends StatelessWidget {
  const _PagePushButton({
    Key? key,
    required this.buttonTitle,
    required this.pagename,
    this.bgColor = Colors.blue,
  }) : super(key: key);

  final String buttonTitle;
  final dynamic pagename;
  final Color bgColor;

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      style: ButtonStyle(
        backgroundColor: MaterialStateProperty.all(bgColor),
      ),
      child: Container(
        padding: const EdgeInsets.all(10),
        child: Text(buttonTitle),
      ),
      onPressed: () {
        AnalyticsService().logPage(buttonTitle);
        Navigator.push(
          context,
          MaterialPageRoute(builder: (context) => pagename),
        );
      },
    );
  }
}


class AnalyticsService {

  Future<void> logPage(String screenName) async {
    await FirebaseAnalytics.instance.logEvent(
      name: 'screen_view',
      parameters: {
        'firebase_screen': screenName,
      },
    );
  }
}


class FirebaseMessagingService {
  FirebaseMessaging messaging = FirebaseMessaging.instance;


  void setting() async {
    NotificationSettings settings = await messaging.requestPermission(
      alert: true,
      announcement: false,
      badge: true,
      carPlay: false,
      criticalAlert: false,
      provisional: false,
      sound: true,
    );

    print('User granted permission: ${settings.authorizationStatus}');
  }

  void fcmGetToken() async {

    if (isAndroid || isIOS) {
      final fcmToken = await messaging.getToken();
      print(fcmToken);
    }

    else {
      final fcmToken = await messaging.getToken(
          vapidKey: FirebaseOptionMessaging().webPushKeyPair);
      print('web : $fcmToken');
    }
  }
}

class FirebaseInAppMessagingService {
  void getFID() async {
    String id = await FirebaseInstallations.instance.getId();
    print('id : $id');
  }
}
/// Flutter
import 'package:flutter/material.dart';

/// Firebase
import 'package:cloud_functions/cloud_functions.dart';

class CloudFunctionsPage extends StatefulWidget {
  const CloudFunctionsPage({Key? key}) : super(key: key);

  @override
  CloudFunctionsPageState createState() => CloudFunctionsPageState();
}

class CloudFunctionsPageState extends State<CloudFunctionsPage> {

  int _number = 0;


  void addNumber() async {
    try {

      final result = await FirebaseFunctions.instance
          .httpsCallable('functionsTest')
          .call({'firstNumber': _number, 'secondNumber': 1});
      _number = result.data['addNumber'];
    } on FirebaseFunctionsException catch (error) {
      print(error.code);
      print(error.details);
      print(error.message);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Cloud Functions页'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('You have pushed the button this many times:'),
            Text(
              '$_number',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            addNumber();
          });
        },
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

<div id="16">⏫Firebase Hosting</div>

Firebase Hosting概述

Firebase Hosting是一个为网络应用、静态和动态内容以及微服务提供的托管服务

如果您想将您的Flutter应用作为一个网络应用托管,可以使用

应用案例

准备

准备工作和前几章都已完成方可开始

使用方法

有两种方法来部署到Firebase Hosting

  • 通过输入命令进行部署
  • 使用Github Actions进行部署

在这种情况下,我们选择通过输入命令进行部署

创建一个Firebase项目,安装Firebase CLI并初始化项目

firebase init hosting

问题和答案示例如下

? What do you want to use as your public directory? (public)
build/web

? Configure as a single-page app (rewrite all urls to /index.html)? (y/N)
No

? Set up automatic builds and deploys with GitHub? (y/N)
No

为网络配置设置

web/index.html中,配置html <head>设置

web/manifest.json中,配置网络应用的行为和图标

可以通过覆盖web/icons目录下的文件(如Icon-192.png)来改变应用程序的图标。
在这一点上要注意不要弄错尺寸。

调试完应用程序后,在Flutter端构建应用程序,然后用Firebase部署它

flutter build web
firebase deploy --only hosting

访问部署后出现的URL,如果应用程序被验证,就可以使用了

注意,如果你想使用一个自定义的域名,你可以从Firebase Console进行设置

<div id="17">↩️Firebase Performance Monitoring</div>

Firebase Performance Monitoring概述

Firebase Performance Monitoring是一项允许您衡量您的Flutter应用性能的服务

点击这里查看Performance Monitoring官方文件

使用方法

要在项目中引入firebase_performance,请在pubspec.yaml中添加以下内容

dependencies:
  firebase_performance: ^0.8.2

为了确保Firebase的配置是最新的,在项目根目录下打开一个终端,运行flutterfire configure

为了显示最初的性能数据,运行该项目并检查它是否显示在Firebase控制台。

如果显示了图表,你就成功了。

自定义性能测量允许你添加自定义属性以及通用属性。

<div id="18">🔽Firebase 其他服务</div>

本章概述

本章总结了前几章中没有介绍的Firebase服务

Firebase Dynamic Links(仅限移动端)

Firebase Dynamic Links是一项提供 "动态链接 "的服务,可以让你直接进入移动原生应用中的链接内容

要在Flutter中构建,请将Firebase_dynamic_links导入你的项目,并从Firebase控制台创建链接

Firebase App Check

App Check是一项保护后端资源不被滥用的服务,如计费欺诈和网络钓鱼

使用reCAPTCHA或其他方式检查设备是否被信任。

所使用的认证供应商将因平台而异

平台 提供者
Apple platforms DeviceCheck, App Attest
Android Play Integrity, SafetyNet
web reCAPTCHA v3, reCAPTCHA Enterprise

它也可以与 Flutter 一起使用

如果flutterfire安装在你的环境中,从Firebase Console启用并初步配置App Check,并安装App Check库

flutter pub add firebase_app_check

调用执行 App Check 的代码,运行它,你就大功告成了

import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';

// Import the firebase_app_check plugin
import 'package:firebase_app_check/firebase_app_check.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  await FirebaseAppCheck.instance.activate(
    webRecaptchaSiteKey: 'recaptcha-v3-site-key',
  );
  runApp(App());
}

Firebase installations service

Firebase installations service负责管理Firebase的安装

用来检查Firebase的安装ID,用于Messaging、Analytics等

Firebase Google AdMob

Firebase Google Admob是一项移动广告服务


如果你想在Flutter中启动AdMob广告,你不能使用Firebase的Admob,必须使用移动广告SDK(Flutter)(beta)

Test Lab

Test Lab是一项允许你在云端托管的设备上测试你的应用程序的服务,当你想在各种配置上测试你的应用程序时可以使用。

你可以通过将你的应用程序部署到Test Lab,并从Firebase Console将文件上传到Robo Test,来测试你的移动应用程序。

官方文档

Firebase App Distribution

Firebase App Distribution是一项促进向测试人员分发应用程序的服务

与Google Play和App Store的链接使分发应用程序变得容易

你可以从Firebase Console分发应用程序并管理测试人员

官方文档

Firebase Extensions

Firebase Extensions是一项服务,它允许你使用打包的解决方案快速为你的应用程序添加功能。

在官方的Firebase Extensions中可以找到许多扩展,这些扩展是使用Cloud Functions for Firebase编写的


请注意,Firebase Extensions的安装只适用于Blaze计划(按需付费)

<div id="19">🔀Firebase 安全规则</div>

什么是Firebase的安全规则?

Firebase安全规则是定义如何允许访问存储数据的语法

Firestore、Realtime Database、Cloud Storage下列各项的语法是不同的

Firestore的语法

Firestore安全规则的语法是基于Common Expression Language(CEL)语言

作为基本的安全规则,在所有经过身份验证的用户都可以访问的测试环境中,编写

service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if request.auth != null;
    }
  }
}

在只有内容所有者有权访问的生产环境中,编写

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow only authenticated content owners access
    match /some_collection/{userId}/{documents=**} {
      allow read, write: if request.auth != null && request.auth.uid == userId
    }
  }
}

Realtime Database

Realtime Database使用json格式的安全规则的语法。

为确保只有内容所有者才能访问,请按以下方式进行配置。

{
  "rules": {
    "users": {
      "$uid": {
        ".read": "auth != null && auth.uid == $uid",
        ".write": "auth != null && auth.uid == $uid"
      }
    }
  }
}

Cloud Storage

云存储安全规则的语法是基于Common Expression Language(CEL)

只能由内容所有者访问的安全规则包括

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /users/{userId}/{allPaths=**} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }
  }
}

<div id="20">🆗 参考网站</div>

在编写本文时参考了一些网站

在此表示感谢

本文由mdnice多平台发布

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容