snovaのブログ

主にプログラミングやデジタルコンテンツについて書きます。最近はPython, Flutter, VRに興味があります。

Flutterから利用できるFirebaseサービスをカウンターアプリで実践(Crashlytics編)

はじめに

Flutterから利用できるFirebaseの機能をカウンターアプリに実装し、内容をまとめたシリーズの第2回目です。

今回は、アプリの問題を追跡するクラッシュレポートツールのFirebase Crashlyticsについてです。

Crashlyticsはweb以外の端末で利用できます。

目次

シリーズの内容

可能な限り毎日連載していきます。 また、順番は前後する可能性があります。

回数 内容 リンク
第0回 準備編 ブログ
第1回 Analytics ブログ
第2回 Firebase Crashlytics イマココ
第3回 Firebase Performance Monitoring ブログ
第4回 Firebase Remote Config ブログ
第5回 Firebase Authentication ブログ
第6回 Cloud Firestore ブログ
第7回 Firebase Realtime Database ブログ
第8回 Cloud Storage for Firebase ブログ
第9回 Firebase Cloud Messaging ブログ
第10回 Firebase In-App Messaging ブログ
第11回 Firebase ML ブログ
第12回 Cloud Functions for Firebase ブログ
第13回 Firebase Hosting ブログ
第14回 Firebaseのその他のサービス ブログ

開発環境

項目 内容
PC Macbook Air(M1)
Flutter 3.0.1
Firebase 11.0.1
FlutterFire 0.2.2+2

準備

準備編が完了出来ているものとします。

導入方法

プロジェクトにfirebase_crashlyticsを導入するため、pubspec.yamlに追記しインポートします。

dependencies:
  firebase_crashlytics: ^2.8.0

Firebaseの構成が最新の状態にするため、プロジェクトのルートディレクトリでターミナルを開き、flutterfire configureを実行します。

flutterfire configure

準備ができたら、クラッシュハンドラを構成します。

FirebaseCrashlytics.instance.recordFlutterFatalErrorFlutterフレームワーク内でスローされたすべてのエラーを自動的にキャッチします。 また、runZonedGuarded(dart:asyncに入っている)を使用することで、Flutterフレームワークでキャッチされないエラーをキャッチできます。

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));
}

構成が出来たら、テストのためWeb以外の端末で強制的にクラッシュさせます。 FirebaseCrashlytics.instance.recordError(error, stack, fatal: true)を呼び出すエラーハンドラを追加している場合は、ボタンのonPressedthrow Exception()を使用することで、クラッシュさせることができます。

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

今回は新しくクラッシュページを追加し、クラッシュボタンを作成しました。

クラッシュしたら、Firebase ConsoleCrashlyticsがレポートを表示するようになります。 これで、Crashlyticsがアプリのクラッシュをモニタリングするようになりました。

なお、クラッシュレポートはカスタマイズも可能です。

コード全文

前回からの変更点です。

  • firebase_crashlyticsを実装
  • テストクラッシュページを追加
  • その他、コードの変更

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/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(context, 'ノーマルカウンター', const NormalCounterPage()),
          _PagePushButton(context, 'クラッシュページ', const CrashPage()),
        ],
      ),
    );
  }
}

/// ページ遷移ボタン
class _PagePushButton extends Container {
  _PagePushButton(BuildContext context, String buttonTitle, pagename)
      : super(
          padding: const EdgeInsets.all(10),
          child: ElevatedButton(
            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("Throw Test Exception"),
          ),
        ],
      ),
    );
  }
}

GitHubのページを貼ります。

github.com

Google Play and the Google Play logo are trademarks of Google LLC.