【Flutter/Firebase】メンテナンスモードをremote configで切り替えられるようにした話

Devlog
Devlog

やりたいこと

メンテナンスモードを遠隔で発動し、ユーザーがアプリを使用できない状態にすることを可能にしておきたい。

想定しているのはデータベース側のルールや関数のメンテ、あるいは配信用サーバーのコンテンツ入れ替えなど。ただ強制アップデートは仕込まないので、接続先やクライアントも含む処理を変えるメンテナンスはできない。配信用サーバのコンテンツも現行はお知らせ文くらいなので、正直クライアント側に一斉にストップをかけるほどのケースはないと思う。

 

まあ、一応やっておくか。くらいの感じ。

remote configに値を格納

bool値のフラグと、お知らせ文も遠隔で設定したい。

文章を分ける基準は、画面上での表示箇所ごと。

画面はできたが

画面構築はなんということもないので割愛。

remote configで取得した文章を、表示箇所ごとに埋め込みます。

ちなみにウィジェットのサイズやパディングなどの数字は、各コードに埋め込まずに一箇所にまとめています。カラーやスタイルなど共通で使うものも同様。参考に記事の末尾にコードを載せておきますのでご参考ください。

Dart
// lib/presentation/widgets/mainte_page.dart

import 'package:flutter/material.dart';
import 'package:electricorange/infrastructure/remote_config/remote_config.dart';
import 'package:electricorange/presentation/styles/sizes.dart';
import 'package:electricorange/presentation/styles/texts.dart';
import 'package:electricorange/presentation/styles/theme.dart';
import 'package:electricorange/presentation/widgets/widget_styles.dart';

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

  @override
  Widget build(BuildContext context) {
    final String top = RemoteConfigService().mainteTop();
    final String time = RemoteConfigService().mainteTime();
    final String msg = RemoteConfigService().mainteMessage();

    return Scaffold(
      body: Center(
        child: Container(
          padding: MyPad().ega16,
          decoration: myBoxDecoration,
          height: MySize.maintePageHeight(context),
          width: MySize.maintePageWidth(context),
          alignment: Alignment.center,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              Text(
                top,
                style: MyText().xxlBold,
                textAlign: TextAlign.center,
              ),
              Container(
                alignment: Alignment.center,
                color: MyTheme.green,
                padding: MyPad().ega8,
                child: Text(
                  time,
                  style: MyText().whlBold,
                  textAlign: TextAlign.center,
                ),
              ),
              Text(
                msg,
                style: MyText().lBold,
                textAlign: TextAlign.center,
              ),
            ],
          ),
        ),
      ),
    );
  }
}

メンテナンスモードのbool値がtrueであればこの画面へ遷移し、falseであれば通常通りの画面へ遷移するようにmain()から呼び出すMyApp内で出しわけの設定をしています。

Dart
// lib/main.dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:electricorange/infrastructure/remote_config/remote_config.dart';
import 'package:electricorange/presentation/styles/sizes.dart';
import 'package:electricorange/presentation/styles/texts.dart';
import 'package:electricorange/presentation/styles/theme.dart';
import 'package:electricorange/presentation/widgets/mainte_page.dart';
import 'package:electricorange/firebase_options.dart';

void main() {
  runZonedGuarded<Future<void>>(
    () async {
      // Firebaseの初期化
      WidgetsFlutterBinding.ensureInitialized();
      await MobileAds.instance.initialize();
      await Firebase.initializeApp(
        options: DefaultFirebaseOptions.currentPlatform,
      );
      await RemoteConfigService().setRemoteConfig();
      // 画面の向き
      await SystemChrome.setPreferredOrientations([
        // 縦向き
        DeviceOrientation.portraitUp,
        DeviceOrientation.portraitDown,
      ]);
      // Flutterフレームワーク内でスローされたすべてのエラーを自動的にキャッチ
      FlutterError.onError =
          FirebaseCrashlytics.instance.recordFlutterFatalError;
      runApp(ProviderScope(child: MyApp()));
    },
    // Flutterフレームワーク内でキャッチされないエラー
    (error, stack) async => await FirebaseCrashlytics.instance.recordError(
      error,
      stack,
      fatal: true,
    ),
  );
}

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // myRouterの継承は省略

    return ScreenUtilInit(
      designSize: const Size(411, 890), // 基準サイズ
      minTextAdapt: true, // テキストサイズの適応を有効化
      splitScreenMode: true, // マルチウィンドウ対応
      builder: (context, child) {
        return MaterialApp.router(
          routeInformationProvider: myRouter.routeInformationProvider,
          routeInformationParser: myRouter.routeInformationParser,
          routerDelegate: myRouter.routerDelegate,
          localizationsDelegates: const [
            GlobalMaterialLocalizations.delegate,
            GlobalWidgetsLocalizations.delegate,
            GlobalCupertinoLocalizations.delegate,
          ],
          // routerの各種設定は割愛   
          builder: (context, child) => MediaQuery(
            data: MediaQuery.of(context).copyWith(
              textScaler: TextScaler.linear(1),
              boldText: false,
            ), // 文字サイズを固定
            child: (RemoteConfigService().isMaintenance()) ? MaintePage() : child!,
          ),
        );
      },
    );
  }
}

remote configでのフラグ管理ってどうなの?

remote configの取得コードはこちら。フェッチ間隔は、公式の推奨値の12時間にしているため、一度フェッチすると12時間経過までは最新フェッチ結果のキャッシュから値が取得され、サーバー側の更新は受け取ってもらえません。

Dart
// electricorange/infrastructure/remote_config/remote_config.dart

import 'package:firebase_remote_config/firebase_remote_config.dart';

class RemoteConfigService {
  final config = FirebaseRemoteConfig.instance;

  // 初期設定
  Future<void> setRemoteConfig() async {
    await config.setConfigSettings(RemoteConfigSettings(
      fetchTimeout: const Duration(minutes: 10),
      minimumFetchInterval: const Duration(hours: 12),
    ));

    await config.setDefaults(const {
      // メンテナンスモード関係以外は割愛
      'statement_maintenance_message': "ご迷惑をおかけし大変申し訳ございません。メンテナンス期間の終了後にご利用ください。",
      'statement_maintenance_top': "ただいまメンテナンス中です",
      'doing_maintenance': false,
    });

    await config.fetchAndActivate();
  }

  // メンテナンスモード関係以外は割愛
  bool isMaintenance() => config.getBool('doing_maintenance');
  String mainteTop() => config.getString('statement_maintenance_top');
  String mainteTime() => config.getString('statement_maintenance_time');
  String mainteMessage() => config.getString('statement_maintenance_message');
}

となると、メンテナンス期間は最低でも12時間以上に設定し、サーバー側でメンテナンスモードのbool値をtrueに設定変更してから12時間経過してからメンテナンスを開始することになるため、ユーザーが半日以上アプリを使えないこととなってしまうと思うのです。

クライアント側のメンテナンスモードが必要なメンテナンスなど、今の所発生するかもわからないので、そのためにいたずらにフェッチ間隔を頻繁にしたくもない。

メンテナンスモードをremote configのフラグで切り替えるケースはいくつか記事も見かけたのですが、みなさんこの課題はどうしているんでしょう•••

 

誰かベスプラ教えて欲しい•••

このアプリに関しては、利用する可能性がかなり低いので(実装したくせに•••)、割り切って使わないつもりでこのままにしておきます。

参考

実際はあらゆるサイズをまとめているので長いリストになってますが、該当部分だけ抜粋

Dart
// electricorange/presentation/styles/sizes.dart

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

class MySize {
  static double maintePageHeight(BuildContext context) => fieldHeight(context) * 0.5;
  static double maintePageWidth(BuildContext context) => fieldWidth(context) * 0.9;
}

class MyPad {
  final ega8 = EdgeInsets.all(8 * sz);
  final ega16 = EdgeInsets.all(16 * sz);
}

// デバイスサイズに合わせて調整
final double sz = 1.w;

double fieldWidth(BuildContext context) => MediaQuery.of(context).size.width;
double fieldHeight(BuildContext context) => MediaQuery.of(context).size.height;


こちらも実際はSサイズまで、色付きやリンク用など様々設定してます。

Dart
// package:electricorange/presentation/styles/texts.dart

import 'package:flutter/material.dart';
import 'package:electricorange/presentation/styles/sizes.dart';
import 'package:electricorange/presentation/styles/theme.dart';

class MyText {
  // <---------------- Normal ----------------->
  // XXLサイズ 太字
  final xxlBold = TextStyle(
    color: MyTheme.text,
    fontSize: myTextXXl,
    fontWeight: myBold,
  );

  // XLサイズ 太字
  final xlBold = TextStyle(
    color: MyTheme.text,
    fontSize: myTextXl,
    fontWeight: myBold,
  );
  
  // XLサイズ
  final xl = TextStyle(
    color: MyTheme.text,
    fontSize: myTextXl,
    fontWeight: myLight,
  );
  
  // Lサイズ 太字
  final lBold = TextStyle(
    color: MyTheme.text,
    fontSize: myTextL,
    fontWeight: myBold,
  );

  // Lサイズ
  final l = TextStyle(
    color: MyTheme.text,
    fontSize: myTextL,
    fontWeight: myLight,
  );
  
  // <---------------- OnColor ----------------->
  // XLサイズ 太字
  final whxlBold = TextStyle(
    color: MyTheme.onGreen,
    fontSize: myTextXl,
    fontWeight: myBold,
  );

  // XLサイズ
  final whxl = TextStyle(
    color: MyTheme.onGreen,
    fontSize: myTextXl,
    fontWeight: myLight,
  );

}

final double myTextXXl = 20 * sz;
final double myTextXl = 16 * sz;
final double myTextL = 15 * sz;

const FontWeight myLight = FontWeight.w400;
const FontWeight myBold = FontWeight.w500;


以下も該当部分のみ

Dart
// package:electricorange/presentation/styles/theme.dart

import 'package:flutter/material.dart';

class MyTheme {
  static const green = Color(0xFF94CDC7);
  static const onGreen = Color(0xFFFFFFFF);
  static const onBackground = Color(0xFFFFFFFF);
  static const text = Colors.black87;
}

 

Dart
// package:electricorange/presentation/widgets/widget_styles.dart

import 'package:flutter/material.dart';
import 'package:electricorange/presentation/styles/sizes.dart';
import 'package:electricorange/presentation/styles/texts.dart';
import 'package:electricorange/presentation/styles/theme.dart';

BoxDecoration myBoxDecoration = BoxDecoration(
  color: MyTheme.onBackground,
  boxShadow: myBoxShadow,
);

コメント

タイトルとURLをコピーしました