Chapter 25

[v0.14.0以下版] RemoteConfigを使用した強制アップデート機能 後編「バージョンチェックを行うProvider」

村松龍之介
村松龍之介
2023.02.12に更新

強制アップデートが必要かどうかの結果をEnumで定義

ここでは一例として、以下の3種類を定義します。

  • not : アップデートなし
  • cancelable : 後回しを許容するアップデートがある
  • forcibly : 強制的なアップデートがある

最新バージョンへのアップデートへのお願いは cancelable
古いバージョンで発生した不具合を避けたり、互換性が無くなった時に強制的にアップデートさせたいときは forcibly を使用する想定です。

enum UpdateRequestType {
  /// アップデートなし
  not,

  /// 後回しを許容するアップデートあり
  cancelable,

  /// 強制的なアップデートあり
  forcibly,
}

RemoteConfigから受け取るデータをアプリ内で定義

今回は以下の3つのパラメータを定義しました。

  • requiredVersion: 要求するバージョン文字列
  • canCancel: アップデートをキャンセルしてアプリを使用できるようにするか
  • enabledAt: 強制アップデートを有効にする開始日

freezedjson_serializable を使用して、JSONから変換できる fromJson ファクトリーコンストラクタを持ったクラスを以下のように定義します。

update_info.dart
import 'package:freezed_annotation/freezed_annotation.dart';

part 'update_info_entity.freezed.dart';
part 'update_info_entity.g.dart';


class UpdateInfoEntity with _$UpdateInfoEntity {
  const factory UpdateInfoEntity({
    /// 要求バージョン e.g., '1.0.0'
    required String requiredVersion,

    /// アップデートを後回し可能にするかどうか
    (false) bool canCancel,

    /// 有効日(この日時以降のみ有効とする)
    required DateTime enabledAt,
  }) = _UpdateInfoEntity;

  factory UpdateInfoEntity.fromJson(Map<String, dynamic> json) =>
      _$UpdateInfoEntityFromJson(json);
}

flutter pub pub run build_runner コマンドを実行すると、
update_info_entity.freezed.dart , update_info_entity.g.dart が生成されます。

アップデート情報を提供するFutureProviderを作成

詳しくはコメントで解説していますが、

  1. Remote Configのインスタンスを取得して最新の値をフェッチ
  2. 現在のバージョンと要求バージョンを比較
  3. 有効期間内かどうかチェック

を行い、 UpdateRequestType として結果を返しています。

final updateRequesterProvider = FutureProvider<UpdateRequestType>((ref) async {
  // 初期化・アクティベート済みのRemoteConfigインスタンス
  final remoteConfig = ref.watch(remoteConfigProvider);
  await remoteConfig.fetchAndActivate();
  // 現在のアプリバージョンを取得するためにPackageInfoを利用
  final appPackageInfo = await PackageInfo.fromPlatform();
  // Firebase の RemoteConfigコンソールで指定したキーを使って値を取得
  final string = remoteConfig.getString('update_info');
  if (string.isEmpty) {
    return UpdateRequestType.not;
  }
  // JSON to Map
  final map = json.decode(string) as Map<String, Object?>;
  // Map(JSON) to Entity
  final entity = UpdateInfoEntity.fromJson(map);

  final enabledAt = entity.enabledAt;
  final requiredVersion = Version.parse(entity.requiredVersion);
  final currentVersion = Version.parse(appPackageInfo.version);
  // 現在のバージョンより新しいバージョンが指定されているか
  final hasNewVersion = requiredVersion > currentVersion;
  // 強制アップデート有効期間内かどうか
  final isEnabled = enabledAt.compareTo(DateTime.now()) < 0;

  if (!isEnabled || !hasNewVersion) {
    // 有効期間外、もしくは新しいバージョンは無い
    return UpdateRequestType.not;
  }
  return entity.canCancel
      ? UpdateRequestType.cancelable
      : UpdateRequestType.forcibly;
});

ダイアログを表示するページの作成(デモページ)

前項で作成した updateRequesterProvider を使用して、アップデート情報を取得します。

loading パラメータでは、情報取得中の表示なので、インジケーターを表示しています。
非同期処理が終わり、アップデート情報を取得できると、 data パラメータで情報を取得できます。

_ContentAndDialog は、次項でStatelessWidgetとして作成します。
(Widgetを分けることは必須ではありません。 data にそのまま続きを書いても問題ありません)

version_check_page.dart
/// 強制アップデート情報を取得してダイアログを表示するデモページ
class VersionCheckPage extends ConsumerWidget {
  const VersionCheckPage({
    Key? key,
  }) : super(key: key);

  
  Widget build(BuildContext context, ScopedReader watch) {
    return Scaffold(
      appBar: AppBar(title: const Text('バージョンアップダイアログ')),
      body: SafeArea(
        child: Center(
          // FutureProviderなので、 `when` で loading, error, data のハンドリングができる
          child: watch(updateRequesterProvider).when(
            loading: () => const CircularProgressIndicator(),
            error: (error, stack) => ErrorWidget(error),
            data: (requestType) => _ContentAndDialog(requestType: requestType),
          ),
        ),
      ),
    );
  }
}

ダイアログを表示する

引数として、 VersionCheckPage からアップデート要求タイプを受け取っています。

showDialog を使用して、アップデートをお願いするダイアログを表示しています。
「アップデート」ボタンを押した時に、アプリのストアURLを開くか、パッケージ[1]を使用して、App Store or Goole Play に遷移させましょう。

version_check_page.dart
class _ContentAndDialog extends StatelessWidget {
  const _ContentAndDialog({
    Key? key,
    required this.requestType,
  }) : super(key: key);

  final UpdateRequestType requestType;

  
  Widget build(BuildContext context) {
    // ビルド後にダイアログを表示させるための記法
    WidgetsBinding.instance!.addPostFrameCallback((_) {
      if (requestType != UpdateRequestType.not) {
        // 新しいアプリバージョンがある場合はダイアログを表示する
        showDialog<void>(
          context: context,
          // ダイアログの外をタップしても閉じられないようにする
          barrierDismissible: false,
          builder: (context) {
            return WillPopScope(
              // AndroidのBackボタンで閉じられないようにする
              onWillPop: () async => false,
              child: AlertDialog(
                title: const Text('最新の更新があります。\nアップデートをお願いします。'),
                actions: [
                  if (requestType == UpdateRequestType.cancelable)
                    TextButton(
                      onPressed: () => Navigator.of(context).pop(),
                      child: const Text('キャンセル'),
                    ),
                  TextButton(
                    onPressed: () {
                      // 要追加: App Store or Google Play に飛ばす処理
                      ScaffoldMessenger.of(context).showSnackBar(
                        const SnackBar(content: Text('Jump to Store.')),
                      );
                      Navigator.of(context).pop();
                    },
                    child: const Text('アップデート'),
                  ),
                ],
              ),
            );
          },
        );
      }
    });
    return const Text('新しいバージョンがある場合はダイアログが表示されます。');
  }
}
脚注
  1. 自動でiOSかAndroidか判別してStoreにジャンプさせてくれるメソッドを持ったパッケージがいくつか存在します。
    app_review
    https://pub.dev/packages/app_review ↩︎