🤩

【Flutter】FirebaseRemoteConfigの更新を全画面で対応させる(強制アップデート機能)(備忘録)

2023/08/21に公開

はじめに

強制アップデート機能なのでホーム画面だけでなく、スプラッシュ画面、ログイン画面など全画面対応することが必要です。
そこで今回は全画面に対応するために行った対応策を備忘録として残しておきたと思います。

開発環境

Flutter 3.7.12 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 4d9e56e694 (4 months ago) • 2023-04-17 21:47:46 -0400
Engine • revision 1a65d409c7
Tools • Dart 2.19.6 • DevTools 2.20.1

本題

remote_configのデータ更新を全画面対応させる

構造はこのようなイメージです。(Stackで画面を作成する)

  • 一番上に大きさ0のRemoteConfigのデータをfetchを目的とした画面
  • その裏にUserが見る画面

実際のコード

下記のようにStackで作成します。
child:Userが見える画面
ForceUpdateMonitor:強制アップデートをするか判断する画面

import 'package:flutter/material.dart';
import 'package:tomopiia_app/ui/layouts/app_layout/widgets/force_update_monitor.dart';

class AppLayout extends StatelessWidget {
  const AppLayout({
    super.key,
    required this.child,
  });

  final Widget child;

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          child,
          const ForceUpdateMonitor(),
        ],
      ),
    );
  }
}

またForceUpdateMonitorは下記のように実装しました

import 'package:firebase_remote_config/firebase_remote_config.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:tomopiia_app/controllers/remote_config_controller.dart';
import 'package:tomopiia_app/ui/components/show_update_dialog.dart';

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

  
  Widget build(BuildContext context, WidgetRef ref) {
    ref.watch(remoteConfigProvider);
    final remoteConfig = FirebaseRemoteConfig.instance;
    if (!kIsWeb) {
      remoteConfig.onConfigUpdated.listen((e) async {
        await remoteConfig.activate();
        await ref.read(remoteConfigProvider.notifier).updateMinBuildNumber();
      });
      ref.listen(remoteConfigProvider.select((s) => s.isForceUpdateRequired),
          (_, isForceUpdateRequired) async {
        final isUpdateDialogShowing = ref.watch(
          remoteConfigProvider.select((s) => s.isUpdateDialogShowing),
        );
        if (isForceUpdateRequired == true) {
          if (isUpdateDialogShowing == false) {
            showUpdateDialog(context, ref);
            ref.read(remoteConfigProvider.notifier).setIsUpdateShowing(true);
          }
        }
      });
    }
    return const SizedBox();
  }
}

remoteConfigProviderは下記のように実装しました。

import 'dart:io';

import 'package:firebase_remote_config/firebase_remote_config.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:package_info_plus/package_info_plus.dart';

part 'remote_config_controller.freezed.dart';


class RemoteConfigState with _$RemoteConfigState {
  factory RemoteConfigState({
    (false) bool isForceUpdateRequired,
    (false) bool isUpdateDialogShowing,
    (0) int minBuildNumber,
  }) = _RemoteConfigState;
}

final remoteConfigProvider =
    StateNotifierProvider<RemoteConfigController, RemoteConfigState>(
  (ref) => RemoteConfigController(),
);

class RemoteConfigController extends StateNotifier<RemoteConfigState> {
  RemoteConfigController() : super(RemoteConfigState());
  final remoteConfig = FirebaseRemoteConfig.instance;
  //remote_configの値が更新(公開)された時に発火している関数
  Future<void> updateMinBuildNumber() async {
    final newMinBuildNumber = Platform.isIOS
        ? remoteConfig.getInt('minBuildNumberIos')
        : remoteConfig.getInt('minBuildNumberAndroid');

    if (state.minBuildNumber != newMinBuildNumber) {
      state = state.copyWith(minBuildNumber: newMinBuildNumber);
      final info = await PackageInfo.fromPlatform();
      final buildNumber = int.parse(info.buildNumber);

      if (buildNumber > newMinBuildNumber) {
        setIsForceUpdateRequired(false);
      }

      if (buildNumber < newMinBuildNumber) {
        setIsForceUpdateRequired(true);
      }
    }
  }

  //ダイアログが表示されているかどうか
  void setIsUpdateShowing(bool value) =>
      state = state.copyWith(isUpdateDialogShowing: value);
  //アップデートが必要かどうか
  void setIsForceUpdateRequired(bool value) =>
      state = state.copyWith(isForceUpdateRequired: value);
}

AppLayoutの使い方

私はMaterialAppのhome部分をAppLayout()で実装しています。
※MyAppはmain.dartに初めから記載されているものです。

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primarySwatch: Colors.teal,
        scaffoldBackgroundColor: AppColors.white,
        appBarTheme: const AppBarTheme(
          color: AppColors.white,
          elevation: 0,
          iconTheme: IconThemeData(
            color: AppColors.black1,
          ),
        ),
      ),
      home: const AppLayout(child: SplashPage()),
      builder: EasyLoading.init(),
    );
  }
}

最後に

全部のページでデータが更新された時の処理を最初は記述していましたがこの方法でかなり楽に実装ができたと思います。

RemoteConfigを使用するにあたって参考にした資料等も同時に添付しておきます。
https://firebase.google.com/docs/remote-config/get-started?platform=flutter&hl=ja

https://zenn.dev/snova301/books/6df29a230d681f/viewer/fd6deb

Discussion