Flutter Firebase Remote Config Edit

# 📕Overview
[Firebase Remote Config](https://firebase.google.com/docs/remote-config?hl=ja)
アプリのアップデートを公開しなくても、アプリの動作と外観を変更できます。コストはかからず、1 日あたりのアクティブ ユーザー数の制限もありません。

Firebase Remote Config は、ユーザーにアプリのアップデートをダウンロードしてもらわなくても、アプリの動作や外観を変更できるクラウド サービスです。Remote Config を使用して、アプリの動作や外観を制御するためのアプリ内デフォルト値を作成できます。その後、Firebase コンソールか Remote Config バックエンド API を使用して、すべてのアプリユーザーまたはユーザーベースの特定セグメントに対して、アプリ内デフォルト値をオーバーライドできます。アップデートを適用するタイミングはアプリ側で制御できます。アプリはアップデートの有無を頻繁にチェックし、パフォーマンスにほとんど影響をおよぼすことなくアップデートを適用できます。

## 🔥公式の解説を参考に設定していきましょう！
[Flutter用の解説はこちら](https://firebase.google.com/docs/remote-config/get-started?platform=flutter&hl=ja)

こちらのパッケージを追加してください
https://pub.dev/packages/firebase_core
https://pub.dev/packages/firebase_remote_config

## Remote Config シングルトン オブジェクトを取得する
Remote Config オブジェクトのインスタンスを取得し、最小フェッチ間隔を設定して更新の頻度を増やせるようにします。

```dart
final remoteConfig = FirebaseRemoteConfig.instance;
await remoteConfig.setConfigSettings(RemoteConfigSettings(
    fetchTimeout: const Duration(minutes: 1),
    minimumFetchInterval: const Duration(hours: 1),
));
```

このシングルトン オブジェクトは、アプリ内デフォルト パラメータ値の保存、更新されたパラメータ値のバックエンドからのフェッチ、フェッチされた値がアプリで使用できるようになるタイミングの制御に使用されます。

## アプリ内デフォルト パラメータ値を設定する
>重要: 機密データを Remote Config のパラメータキーまたはパラメータ値に保存しないでください。Remote Config データは転送中に暗号化されますが、エンドユーザーはクライアント アプリ インスタンスで利用可能なデフォルトの Remote Config パラメータまたはフェッチされた Remote Config パラメータにアクセスできます。

```dart
await remoteConfig.setDefaults(const {
    "example_param_1": 42,
    "example_param_2": 3.14159,
    "example_param_3": true,
    "example_param_4": "Hello, world!",
});
```

## Remote Config バックエンドでパラメータ値を設定する
Firebase コンソールまたは Remote Config バックエンド API を使用して、サーバーサイドの新しいデフォルト値を作成できます。このデフォルト値は、目的の条件付きロジックまたはユーザー ターゲティングに従って、アプリ内の値をオーバーライドします。このセクションでは、Firebase コンソールでこれらの値を作成するための手順を説明します。

1. Firebase コンソールでプロジェクトを開きます。
2. メニューから [Remote Config] を選択して Remote Config ダッシュボードを表示します。
3. アプリで定義したパラメータと同じ名前のパラメータを定義します。それぞれのパラメータにデフォルト値を設定できます（最終的には対応するアプリ内デフォルト値をオーバーライドします）。また、条件値を設定することもできます。詳しくは Remote Config のパラメータと条件をご覧ください。

## 値をフェッチして有効にする
1. Remote Config バックエンドからパラメータ値をフェッチするには、fetch() メソッドを呼び出します。バックエンドに設定したすべての値がフェッチされ、Remote Config オブジェクトに保存されます。

2. フェッチ済みのパラメータ値をアプリで使用できるようにするには、activate() メソッドを呼び出します。

1 回の呼び出しで値をフェッチして有効化する場合は、fetchAndActivate() リクエストを使用して Remote Config バックエンドから値をフェッチし、それらをアプリで利用できるようにします。

```dart
await remoteConfig.fetchAndActivate();
```

これらの更新されたパラメータ値はアプリの動作と外観に影響するので、スムーズなユーザー エクスペリエンスを実現するタイミング（次にユーザーがアプリを開いたときなど）でフェッチ済みの値を有効化する必要があります。詳細と例については、Remote Config の読み込み方法をご覧ください。

## リアルタイムで更新をリッスンする
バックエンドからの更新をリッスンできます。更新が利用可能になると、リアルタイム Remote Config が接続済みデバイスに通知し、新しいバージョンの Remote Config を公開すると変更が自動的にフェッチされます。

なお、リアルタイム Remote Config はウェブで利用できません。

1. アプリで onConfigUpdated を使用して更新のリッスンを開始し、新しいパラメータ値を自動的にフェッチします。
```dart
 remoteConfig.onConfigUpdated.listen((event) async {
   await remoteConfig.activate();

   // Use the new config values here.
 });
```
2. 新しいバージョンの Remote Config が公開されると、アプリを実行して変更をリッスンしているデバイスが新しい構成ファイルを有効にします。

今回は、こちらの動画を参考に進めていこうと思います。
https://www.youtube.com/watch?v=z0datxJ6ks4

## 🧷summary
それでは、海外の動画を参考にやってみましょう！
設定はこれだけ。RemoteConfigに、値が無ければデフォルト値が表示される。`initState()`メソッドで、ページが呼ばれると、RemoteConfigからデータをfetchする。

```dart
final _remoteConfig = FirebaseRemoteConfig.instance;
  bool isLoading = false;

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

_initRemoteConfig() async {
    setState(() {
      isLoading = false;
    });
    // Default Setup
    await _remoteConfig.setDefaults(
      // データがないときは、このデフォルト値が使われる
      {"name": "Shah", "age": "28", "hobby": "Coding"},
    );

    // Configurations
    // 10 seconds fetch timeout
    await _remoteConfig.setConfigSettings(RemoteConfigSettings(
      fetchTimeout: const Duration(seconds: 10),// レスポンスを待つ最大時間
      minimumFetchInterval: const Duration(seconds: 10),// キャッシュされたコンフィグが古いとみなされるまでの最大時間
      ));

      // remote activate
      await _remoteConfig.fetchAndActivate();

      setState(() {
        isLoading = false;
      });
  }
```

### 📱こちらが全体のコード
```dart:main.dart
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_remote_config/firebase_remote_config.dart';
import 'package:flutter/material.dart';
import 'package:remote_config_app/firebase_options.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Remote Config App'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final _remoteConfig = FirebaseRemoteConfig.instance;
  bool isLoading = false;

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

  _initRemoteConfig() async {
    setState(() {
      isLoading = false;
    });
    // Default Setup
    await _remoteConfig.setDefaults(
      // データがないときは、このデフォルト値が使われる
      {"name": "Shah", "age": "28", "hobby": "Coding"},
    );

    // Configurations
    // 10 seconds fetch timeout
    await _remoteConfig.setConfigSettings(RemoteConfigSettings(
      fetchTimeout: const Duration(seconds: 10),// レスポンスを待つ最大時間
      minimumFetchInterval: const Duration(seconds: 10),// キャッシュされたコンフィグが古いとみなされるまでの最大時間
      ));

      // remote activate
      await _remoteConfig.fetchAndActivate();

      setState(() {
        isLoading = false;
      });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: isLoading
            ? const CircularProgressIndicator()
            :  Padding(
          padding: const EdgeInsets.symmetric(horizontal: 23.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              const Text('Welcome to the Remote Config App'),
              Text('Name: ${_remoteConfig.getString('name')}'),
              Text('Age: ${_remoteConfig.getString('age')}'),
              Text('Hobby: ${_remoteConfig.getString('hobby')}'),
            ],
          ),
        ),
      ),
    );
  }
}
```


値を入れてみたいので、動画と同じように進めていきましょう！
データ型を文字、数字に合わせてセットして公開すればOK!

![](https://storage.googleapis.com/zenn-user-upload/214d3e86a46a-20240727.png)
![](https://storage.googleapis.com/zenn-user-upload/50ec207b9668-20240727.png)

![](https://storage.googleapis.com/zenn-user-upload/64808281aa5c-20240727.png)
![](https://storage.googleapis.com/zenn-user-upload/a9c3bc1b8dab-20240727.png)
![](https://storage.googleapis.com/zenn-user-upload/94a88f5a01eb-20240727.png)
![](https://storage.googleapis.com/zenn-user-upload/8b6a62457cc9-20240727.png)
![](https://storage.googleapis.com/zenn-user-upload/600644127b25-20240727.png)
![](https://storage.googleapis.com/zenn-user-upload/ba0a87d38bb5-20240727.png)

アプリをリビルドすると、値が変わるはず。

### 👂リアルタイムに反映させたい
`onConfigChanged`を使うとできるようだ。

```dart
// リアルタイムに変更を反映させる方法には、onConfigChanged.listenを使う方法もある
      _remoteConfig.onConfigUpdated.listen((event) async {
        // 変更があると、callbackが呼ばれる
        await _remoteConfig.activate();
        setState(() {});
      });

      setState(() {
        isLoading = false;
      });
```

📝保存されている値を編集すると、おお変わった😆
![](https://storage.googleapis.com/zenn-user-upload/3ef76a2a8eb1-20240727.png)
![](https://storage.googleapis.com/zenn-user-upload/58f9fb508a7f-20240727.png)
![](https://storage.googleapis.com/zenn-user-upload/7f3f455c74af-20240727.png)
![](https://storage.googleapis.com/zenn-user-upload/ddbc30263119-20240727.png)


## 🧑‍🎓thoughts
🤔これってどんなメリットがあるのか？

調べて出てきた情報:
>Firebase Remote Config（FRC）は、Googleが提供するクラウドサービスで、アプリの動作や外観を変更したり、A/Bテストを行ったりすることができます。アプリをアップデートすることなく、アプリがローカルで持つ文言や画像データをリモート（サーバー）側に持たせることで、簡単に変更できます。

A/Bテストとは、バナーや広告文、Webサイトなどを最適化するために実施するテストの一つです。 特定の要素を変更したAパターン、Bパターンを作成し、それをランダムにユーザーに表示し、それぞれの成果を比較することで、より高い成果を得られるパターンを見つけることができます。

[A/Bテストとは？](https://www.oracle.com/jp/cx/marketing/what-is-ab-testing/)

## おまけ
私は、flutter_hooksを使うことが多いので試しにやってみました。ご興味ある方はやってみてください。

```shell
flutter pub add flutter_hooks
```

`build()`メソッドの中に処理を書くと無限にビルドされる。Reactだと、何度もレンダリングされる。防止策として、`useMemoized()`メソッドを使っています。

lutter_hooksを使用してリファクタリングし、useMemoizedでキャッシュを行うRemoteConfigHooksクラスを作成します。以下がリファクタリングされたコードです：

```dart:my_home_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:firebase_remote_config/firebase_remote_config.dart';

class RemoteConfigHooks {
  final FirebaseRemoteConfig _remoteConfig;

  RemoteConfigHooks(this._remoteConfig);

  Future<void> initRemoteConfig() async {
    await _remoteConfig.setDefaults({
      "name": "Shah",
      "age": "28",
      "hobby": "Coding",
    });

    await _remoteConfig.setConfigSettings(RemoteConfigSettings(
      fetchTimeout: const Duration(seconds: 10),
      minimumFetchInterval: const Duration(seconds: 10),
    ));

    await _remoteConfig.fetchAndActivate();
  }

  String getString(String key) => _remoteConfig.getString(key);

  Stream<RemoteConfigUpdate> get onConfigUpdated => _remoteConfig.onConfigUpdated;
}

class MyHomePage extends HookWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    final remoteConfigHooks = useMemoized(() => RemoteConfigHooks(FirebaseRemoteConfig.instance));
    final isLoading = useState(true);
    final updateTrigger = useState(0);

    useEffect(() {
      remoteConfigHooks.initRemoteConfig().then((_) {
        isLoading.value = false;
      });

      final subscription = remoteConfigHooks.onConfigUpdated.listen((_) async {
        await remoteConfigHooks._remoteConfig.activate();
        updateTrigger.value++;
      });

      return subscription.cancel;
    }, []);

    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.amberAccent,
        title: const Text('Remote Config Hooks'),
      ),
      body: Center(
        child: isLoading.value
            ? const CircularProgressIndicator()
            : Padding(
                padding: const EdgeInsets.symmetric(horizontal: 23.0),
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    const Text('Welcome to the Remote Config App'),
                    Text('Name: ${remoteConfigHooks.getString('name')}'),
                    Text('Age: ${remoteConfigHooks.getString('age')}'),
                    Text('Hobby: ${remoteConfigHooks.getString('hobby')}'),
                  ],
                ),
              ),
      ),
    );
  }
}
```

このリファクタリングでは以下の変更を行いました：

1. RemoteConfigHooksクラスを作成し、Remote Configに関連する操作をカプセル化しました。
2. MyHomePageをStatefulWidgetからHookWidgetに変更しました。
3. useMemoizedを使用してRemoteConfigHooksのインスタンスをキャッシュしています。これにより、ウィジェットが再構築されても同じインスタンスが使用されます。
4. useStateフックを使用してisLoadingの状態を管理しています。
5. useEffectフックを使用して初期化処理と設定変更のリスナーを設定しています。このフックは、コンポーネントのマウント時に一度だけ実行されます。
6. 設定が変更されたときに、updateTriggerの値を更新することで、ウィジェットの再構築をトリガーしています。
7. RemoteConfigHooksクラスのメソッドを使用して、Remote Configの値を取得しています。

このリファクタリングにより、コードの再利用性が向上し、状態管理がよりシンプルになりました。また、useMemoizedを使用することで、パフォーマンスの最適化も行っています。