snovaのブログ

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

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

はじめに

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

今回はアプリのアップデートを公開しなくても、動作や外観を変更できるFirebase Remote Configについてです。

公式では、以下のようなRemote Configのユースケースが紹介されています。

目次

シリーズの内容

回数 内容 リンク
第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
デバッグバイス Android 12(APIレベル31), Chrome

準備

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

導入方法

プロジェクトにfirebase_remote_configを導入するため、pubspec.yamlに追記しインポートします。 Remote Configを使用するために、firebase_analyticsも導入します。

dependencies:
  firebase_remote_config: ^2.0.7
  firebase_analytics: ^9.1.8

初期化とパラメータの設定を行うため、メソッドを作って実行します。

シングルトンオブジェクトの取得では、最小フェッチ間隔を制御して、最適な時間を更新頻度にします。 アプリ内で使うパラメータの取得には、getString(), getBool()などのメソッドを使います。

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

初期化します。

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

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

Text Widgetに書き出すと、設定した値(Hello, world!)が出力されていることがわかります。

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

次に、値を変更するため、Firebase ConsoleRemote Configからバックエンド側の構成をセットアップします。

パラメータキーにsetDefaultsで決めたキーを入力、Default valueに新しい値を入力し、「変更を公開」します。

しばらくするとテキストがこんにちは、世界に変更されていることが確認できました。

コード全文

前回からの変更点です。

  • Remote Configを実装
  • Remote Configページを追加
  • その他、コードの変更

main.dart

/// Flutter関係のインポート
import 'package:counter_firebase/remote_config_page.dart';
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()),
          _PagePushButton(
              context, 'Remote Configカウンター', const RemoteConfigPage()),
        ],
      ),
    );
  }
}

/// ページ遷移ボタン
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,
      },
    );
  }
}

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';

/// Other Page
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) {
    final counter = ref.watch(counterProvider);

    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")),
            Text(
              '$counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          ref.read(counterProvider.notifier).increment();
        },
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

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

GitHubのページを貼ります。

github.com

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