Firebase Remote Config で FormatException: Invalid envelope のエラーが出る

Firebase Remote Config を Android で利用しようとしたら以下のようなエラーが出た。

I/flutter ( 6164): ----------------FIREBASE CRASHLYTICS----------------
I/flutter ( 6164): FormatException: Invalid envelope
I/flutter ( 6164): #0      StandardMethodCodec.decodeEnvelope (package:flutter/src/services/message_codecs.dart:612:7)
I/flutter ( 6164): #1      MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:167:18)
I/flutter ( 6164): <asynchronous suspension>
I/flutter ( 6164): #2      MethodChannelFirebaseRemoteConfig.fetchAndActivate (package:firebase_remote_config_platform_interface/src/method_channel/method_channel_firebase_remote_config.dart:147:29)
I/flutter ( 6164): <asynchronous suspension>
I/flutter ( 6164): #3      FirebaseRemoteConfig.fetchAndActivate (package:firebase_remote_config/src/firebase_remote_config.dart:87:26)
I/flutter ( 6164): <asynchronous suspension>
I/flutter ( 6164): ----------------------------------------------------

これはエラーメッセージが不親切で、message_codecs.dartデバッグコードを仕込んでみると以下のエラーが発生していることがわかる。

I/flutter (10863): Firebase Installations failed to get installation auth token for fetch.
I/flutter (10863): {code: internal, message: internal remote config fetch error}

というわけで、権限周りでエラーが発生していることがわかる。

解決策

解決策として、Android の keystore の SHA1/SHA256 を Firebase/GCP に登録すると解決する。

また、deploygate などを利用している場合は deploygate の SHA1/SHA256 を登録する必要がある。

Flutter 2.8.4 -> 3.0.0 にアップグレードする

fvm を利用したプロジェクトをアップグレードする。

$ fvm releases
--------------------------------------
May 11 22  │ 3.0.0             stable
--------------------------------------

$ fvm install 3.0.0
$ fvm use 3.0.0
$ fvm flutter --version
Flutter 3.0.0 • channel stable • https://github.com/flutter/flutter.git
Framework • revision ee4e09cce0 (6 days ago) • 2022-05-09 16:45:18 -0700
Engine • revision d1b9a6938a
Tools • Dart 2.17.0 • DevTools 2.12.2

$ fvm flutter pub upgrade
$ fvm flutter pub upgrade --major-versions
$ fvm flutter pub outdated   
Showing outdated packages.
[*] indicates versions that are not the latest available.

Package Name              Current  Upgradable  Resolvable  Latest  

direct dependencies: all up-to-date.

dev_dependencies: all up-to-date.


$ fvm flutter run
...
    [!] CocoaPods could not find compatible versions for pod "Firebase/CoreOnly":
      In snapshot (Podfile.lock):
        Firebase/CoreOnly (= 8.9.0)

      In Podfile:
        firebase_core (from `.symlinks/plugins/firebase_core/ios`) was resolved to 1.17.0, which depends on
          Firebase/CoreOnly (= 8.15.0)
...

# Cocoapods でエラーが出ている

$ pod repo update
$ fvm flutter run

これでひとまず Flutter の環境の更新は完了。

手元のプロジェクトでは以下のエラー、ワーニングを修正。

warning

dart fix

$ fvm dart fix --apply

大体が Sort child properties last in widget instance creations. の warning だった。あとWidgetsBinding.instance。

手動で warning 修正

  • use_build_context_synchronously -context を利用する前に if (!mounted) return;を入れるか then を利用する
  • depend_on_referenced_packages
    • flutter_hooks などが明示的に pubspec.yml になかったので追記

エラー修正

List の firstOrNull がなくなった?

- hogeList.firstOrNull;
+ hogeList.isEmpty ? null : splitNames.first;

おわり

コマンドだけ見ると単純なんだけど、 Cocoapods のエラーとかで pod repo update が思い浮かず fvm flutter clean やったり derived data 消したり半日かかった。

楽天モバイル0円プラン廃止は俺にとって改善

www.itmedia.co.jp

楽天モバイルの0円プランが終了し最低1078円になる。

今までは楽天モバイル0円を電話として利用し、デュアルSIMでIIJmio eSIM 2GBの440円、合計で440円で音声通話+データ通信を持つことができたがそれができなくなった。

しかし楽天経済圏ダイヤモンド会員なら従来+2倍のポイントを獲得できるため、人によっては改善と言える。

楽天モバイル1078円 & SPU &通話無料

  • 楽天モバイルはなんと通話無料
  • 回線は楽天品質
  • 3GBまでは1078円だが、それを超えてしまうと通信制限ではなく2178円になってしまうリスクがある
  • 楽天SPUがあるため、楽天経済圏に入っていたら実質のキャッシュバックとなる
    • 私は先月SPU参加して楽天モバイルご契約特典分として914ポイント得ている
    • 今回のプラン変更で楽天モバイル契約で3倍のポイント獲得となる (ダイヤモンド会員の必要がある)
    • つまり前回と同じ買い物するとSPUで2742ポイントもらえることになる
    • 年4回SPUに参加すると仮定し、合計約 10800 ポイント得るとする → 毎月900ポイント還元となる
  • SPU を考慮すると月額178円+通話無料での運用が可能となる

SPU 最強!!!!

楽天経済圏でないなら povo 2.0 + IIJ 月額約477円プランが最安

www.itmedia.co.jp

  • povo 2.0 は基本無料で電話回線を維持することができる
  • が、180日条件を満たさないと利用停止されてしまうよう
  • そのため、180日の間に「有料トッピングを購入する」か「通話で660円(15~30分?)」が必須となる
  • ここでは電話をしないと仮定すると、最安トッピングが220円
    • つまり半年で220円で運用が可能となる
    • (ただし楽天モバイルは通話基本無料なんだよなあ)
  • 半年で220、1ヶ月約37円となる
  • これに従来の IIJmio eSIM 440円(docomo回線)を合わせると月額約477円での運用が可能となる

結論

SPU考慮すると楽天モバイル最強となった。今までの440円運用+SPUより安くなる可能性があり、俺にとって改善と言える。

間違った情報あったら教えてください。

Flutter でアプリの画面を Portrait 固定にする

Flutter で画面回転を無効にし縦固定にしたい、としてググるSystemChrome.setPreferredOrientations を設定しろと出てきたりする。

しかし、これだと splash screen は横で表示されてしまうので良くない。AndroidManifest.xml や Info.plist で設定する方が良い。

Android

AndroidManifest.xmlタグに android:screenOrientation="portrait" を追加。

esthersoftware.hatenablog.com

iOS

Xcode > Runner > General > TARGETS > Runner の Device Orientation で Portrait 以外のチェックを外す。これで iPhone の画面回転を抑止できる。

しかしこれだと iPad の画面回転は抑止できない。iPad も Portrait だけにしたければ Info.plist の ~ipad のをいじってあげれば良い。
(iPadは画面回転できるべきなのでほとんどの場合でその必要はないが)

Dart でテンプレートからファイル生成を行う

$ dart ./scripts/page_generator.dart sign_in

みたいにしたら sign_in_page.dart, sign_in_view_mode.dart, sign_in_view_state.dart みたいに必要なファイル群をガッっと生成してくれるやつを作る。iOS でいう Generamba とかそういう系のやつ。

既存のコードをロードして何かをするわけではなく、単純なファイル生成なので source_gen などは利用しない。

page_generator.dart

仕組みは単純で、引数の値を利用してテンプレートのStringにclass名を埋め込んだりファイル名を生成して適当なフォルダにファイルを生成するだけ。

import 'dart:io';

import 'package:dart_style/dart_style.dart';

void main(List<String> args) async {
  // 引数チェック
  if (args.length != 1) throw ArgumentError('Must have only one argument');

  // 例: sign_in
  final directoryName = args.first;

  // lib/pages/sign_in ディレクトリを作成
  final directory =
      await Directory('lib/pages/$directoryName').create(recursive: true);

  // sign_in を SignIn に変換
  final classPrefix = directoryName
      .split('_')
      .map((v) => v[0].toUpperCase() + v.substring(1))
      .join();

  // ここまでに作成した値を使いコード生成
  await Future.wait([
    _writePageCode(classPrefix, directory, directoryName),
    _writeViewModelCode(classPrefix, directory, directoryName),
    _writeViewStateCode(classPrefix, directory, directoryName),
  ]);
}

Future<void> _writePageCode(
    String classPrefix, Directory directory, String directoryName) async {
  // sign_in_page.dart がファイル名となる
  final fileName = directoryName + '_page.dart';
  final code = '''
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

class ${classPrefix}Page extends HookConsumerWidget {
  const ${classPrefix}Page({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return const Scaffold();
  }
}
''';

  // lib/pages/sign_in/sign_in_page.dart が生成される
  final file = File('${directory.path}/$fileName');
  // フォーマットしてファイル作成
  await file.writeAsString(DartFormatter().format(code));

  stdout.writeln('${file.path} generated.');
}

Future<void> _writeViewModelCode(
    String classPrefix, Directory directory, String directoryName) async {
  final fileName = directoryName + '_view_model.dart';
  final providerNamePrefix =
      classPrefix[0].toLowerCase() + classPrefix.substring(1);
  final code = '''
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

import '${directoryName}_view_state.dart';

final ${providerNamePrefix}ViewModelProvider =
    StateNotifierProvider.autoDispose<${classPrefix}ViewModel, ${classPrefix}ViewState>(
  (ref) {
    return ${classPrefix}ViewModel();
  },
);

class ${classPrefix}ViewModel extends StateNotifier<${classPrefix}ViewState> {
  ${classPrefix}ViewModel() : super(const ${classPrefix}ViewState());
}
''';

  final file = File('${directory.path}/$fileName');
  await file.writeAsString(DartFormatter().format(code));

  stdout.writeln('${file.path} generated.');
}

Future<void> _writeViewStateCode(
    String classPrefix, Directory directory, String directoryName) async {
  final fileName = directoryName + '_view_state.dart';
  final code = '''
import 'package:freezed_annotation/freezed_annotation.dart';

part '${directoryName}_view_state.freezed.dart';

@freezed
class ${classPrefix}ViewState with _\$${classPrefix}ViewState {
  const factory ${classPrefix}ViewState({
    @Default(false) loading,
  }) = _${classPrefix}ViewState;
}
''';

  final file = File('${directory.path}/$fileName');
  await file.writeAsString(DartFormatter().format(code));

  stdout.writeln('${file.path} generated.');
}

生成されたコード

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

class SignInPage extends HookConsumerWidget {
  const SignInPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return const Scaffold();
  }
}
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

import 'sign_in_view_state.dart';

final signInViewModelProvider =
    StateNotifierProvider.autoDispose<SignInViewModel, SignInViewState>(
  (ref) {
    return SignInViewModel();
  },
);

class SignInViewModel extends StateNotifier<SignInViewState> {
  SignInViewModel() : super(const SignInViewState());
}
import 'package:freezed_annotation/freezed_annotation.dart';

part 'sign_in_view_state.freezed.dart';

@freezed
class SignInViewState with _$SignInViewState {
  const factory SignInViewState({
    @Default(false) loading,
  }) = _SignInViewState;
}

おわり

最初はコード生成などで 自動コード生成を駆使してFlutter開発を楽にする - Qiita の記事などを読んでいたが、今回は既存のコードをロードしてそこからコードを生成せず、単純なファイル生成だけなので source_gen | Dart Package などは利用する必要がなくシンプルにStringに対し文字列を埋め込むだけで実装できた。

GCP のコンソールでFirebaseが自動生成したキーを誤って削除してしまった際の対処法

GCP の管理画面見てたら iOS key (auto created by Firebase) というのが生えてて、なんだこれいらんやろと思って消したら Firebase Remote Config でエラーが出てしまった。

f:id:star__hoshi:20220414223251p:plain
GCP管理画面にあるFirebaseのKey

Firebase Remote Config のエラー

[VERBOSE-2:ui_dart_state.cc(209)] Unhandled Exception: [firebase_remote_config/internal] Failed to get installations token. Error : Error Domain=com.firebase.installations Code=2 "Too many server requests." UserInfo={NSLocalizedFailureReason=Too many server requests.}.

対処法

Key を再作成して、GoogleService-Info.plistAPI_KEY を再作成したものに変更する。
GCP 上で Key の再作成も可能だが、Firebase の画面で プロジェクトの設定 -> アプリを追加 から適当にアプリを作成すると Key が作成されるので、GoogleService-Info.plistAPI_KEY を新しく作成したものに変更するのが楽。適当に作成したアプリは消してしまって問題ない。

これで Firebase Remote Config も無事利用できるようになった。

参考

stackoverflow.com

auto_route で WillPopScope の onWillPop で false を返している時は popForced を使って閉じる

pub.dev

画面遷移の際に type safe にできるので auto_route を使っているが、WillPopScope で囲んだ AlertDialog を閉じれずにハマった。

Android の戻るボタンでダイアログが閉じれないように WillPopScope で囲んだダイアログを作る。Android back ボタンは無効になるが、closeボタンを押してもダイアログが閉じない。

    return showDialog<void>(
      context: context,
      barrierDismissible: false,
      builder: (BuildContext context) {
        return WillPopScope(
          onWillPop: () async => false,
          child: AlertDialog(
            content: Text('message'),
            actions: [
              TextButton(
                child: Text('close'),
                onPressed: () {
                  AutoRouter.of(context).pop();
                },
              ),
            ],
          ),
        );
      },
    );
  }

popForced

github.com

popForced を使えとのこと。AutoRouter.of(context).popForced(); にしたらダイアログが閉じれるようになった。