【Flutter】強制アップデートを簡単に安全に実装する（テストコード付き）

![スクリーンショット 2025-04-13 10.39.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/672609/bb429a6a-d4a9-4aea-9f6b-eb813b1e5772.png)



# はじめに
こんにちは。いせりゅーです。久々の記事執筆です。色々と情報が溜まってきたので少しずつ出していくために投稿しようと思いました。

ということで早速一つ目の記事は、「強制アップデート」についてです。これらについてはもっと早く対応すればよかったと後悔しています。


# これからの話しにおいてよりわかりやすくなるために
- これ以降の話は僕が作っているアプリである「FoodGram」というアプリを元に記載しています
- ぜひ、使っていただけると幸いです...🙇（投稿待ってるよぉぉぉ）

https://apps.apple.com/sa/app/foodgram/id6474065183



# 強制アップデートの必要性
- 最新版に意図しない不具合があり、アップデートを直さないと一部の機能が使えない状態にある

- 機能改善によって検索のしやすさやマップのスタイルの改善を統一して使えるようにしたい

- 知り合いがアプリを触ってみて、最新ではなかったことにより、使いづらいアプリのままになっていた

# 強制アップデートの懸念点

- 投稿しようと思ったときに、「アップデートしてください」と強制ダイアログが出ると投稿をするのをやめてしまう気がする

- アップデートしなくてもちゃんと使えることが一番いいアプリではないかと個人的には考えている


# 強制アップデートの実装の前に考えること
- バージョンアップの規模を考慮しておく
  - メジャーバージョン
      - 大部分のデザインや機能が変更されたとき（変更内容「大」）
  - マイナーバージョン
      - 一部機能の改善や、細かい追加機能が実装されたとき（変更内容「中」）
  - パッチバージョン
      - 軽微なバグの修正や誤字や注釈表現を変更したとき（変更内容「小」）

今回は、メジャーバージョンとマイナーバージョンの場合に強制アップデートをすることにしました。


# 強制アップデートの実装

- package_info_plus
端末にダウンロードされているバージョンの取得

https://pub.dev/packages/package_info_plus


- new_version_plus
AppleStore/Google Play Storeにある最新のバージョンの取得

https://pub.dev/packages/new_version_plus

（iOS/Androidそれぞれの準備についてはPackagesを見て、適宜やってください。）


# 実装していく

## アップデートが必要かどうかを判定するProvider

riverpod_anotationを使っているので、必要とあれば追加をお願いします。
流れとしては、以下の通りです
1. 現在のバージョンを取得
1. Apple Store/Google Play Storeの最新版のバージョンを取得
1. メジャー・マイナーバージョンの変化を確認
1. 強制アップデートが必要であればダイアログを開くようにする


```dart
import 'package:new_version_plus/new_version_plus.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'app_update_checker.g.dart';

@riverpod
class AppUpdateChecker extends _$AppUpdateChecker {
  @override
  void build() {}

  Future<void> checkForceUpdate({required Function() openDialog}) async {
    // 現在のバージョンを取得
    final packageInfo = await PackageInfo.fromPlatform();
    final currentVersion = packageInfo.version;

    // Apple Store/Google Play Storeの最新版のバージョンを取得
    final newVersion = NewVersionPlus();
    final status = await newVersion.getVersionStatus();

    if (status == null) {
      return; // ストア情報が取得できない場合、強制アップデートはしない
    }
    final storeVersion = status.storeVersion;
    // メジャー・マイナーバージョンの変化を確認
    final result = shouldForceUpdate(currentVersion, storeVersion);
    if (result) {
      openDialog();
    }
  }
}
```



## メジャーバージョンとマイナーバージョンの場合にtrueを返す関数

```dart
  /// メジャーまたはマイナーバージョンに変化があるかを確認
  bool shouldForceUpdate(String currentVersion, String storeVersion) {
    try {
      // バージョンを分割して取得
      final currentParts = currentVersion.split('.');
      final storeParts = storeVersion.split('.');

      if (currentParts.length < 2 || storeParts.length < 2) {
        return false; // バージョン形式が不正の場合
      }

      // メジャーバージョンとマイナーバージョンを比較
      final currentMajor = int.parse(currentParts[0]);
      final currentMinor = int.parse(currentParts[1]);
      final storeMajor = int.parse(storeParts[0]);
      final storeMinor = int.parse(storeParts[1]);

      // メジャーまたはマイナーバージョンが異なる場合は強制アップデート
      return currentMajor < storeMajor ||
          (currentMajor == storeMajor && currentMinor < storeMinor);
    } on Exception catch (_) {
      // エラーが発生した場合はアップデートを要求しない
      return false;
    }
  }
}
```



## テストコード

自分の中で必要そうなテストコードを記載してみました。参考になればなと思っています..
ある程度これで問題ないと思い、マージしました。

```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:food_gram_app/core/config/app_update_checker.dart';

void main() {
  late AppUpdateChecker appUpdateChecker;

  setUp(() {
    appUpdateChecker = AppUpdateChecker();
  });

  group('shouldForceUpdate Tests', () {
    test('should return true when major version is different', () {
      // メジャーバージョンが異なる場合
      final result = appUpdateChecker.shouldForceUpdate('1.0.0', '2.0.0');
      expect(result, isTrue); // 強制アップデートが必要
    });

    test('should return true when minor version is different', () {
      // メジャーバージョンは同じで、マイナーバージョンが異なる場合
      final result = appUpdateChecker.shouldForceUpdate('1.1.0', '1.2.0');
      expect(result, isTrue); // 強制アップデートが必要
    });

    test('should return true when patch version is different', () {
      // メジャーバージョン、マイナーバージョンは同じで、パッチバージョンが異なる場合
      final result = appUpdateChecker.shouldForceUpdate('1.0.1', '1.0.2');
      expect(result, isFalse); // アップデートは不要
    });

    test('should return false when versions are identical', () {
      // バージョンが同じ場合
      final result = appUpdateChecker.shouldForceUpdate('1.0.0', '1.0.0');
      expect(result, isFalse); // アップデートは不要
    });

    test('should return false when current version is greater', () {
      // 現在のバージョンがストアのバージョンより大きい場合
      final result = appUpdateChecker.shouldForceUpdate('2.0.0', '1.0.0');
      expect(result, isFalse); // 強制アップデートは不要
    });

    test('should return false when version format is incorrect', () {
      // バージョン形式が不正な場合
      final result = appUpdateChecker.shouldForceUpdate('1.0', '1.0.0');
      expect(result, isFalse); // アップデートは不要
    });
  });
}
```


# 実際にやってみて
実際に自分のアプリに取り入れてみました。こんな感じでダイアログを出して、アップデートさせることでユーザー間での差を統一することができます。とても大切ですね。

| TH | TH |
| ---- | ---- |
| ![IMG_9893](https://github.com/user-attachments/assets/3b3fbd74-d72c-4139-b2f1-6f32f1b1f137) | ![IMG_9894](https://github.com/user-attachments/assets/ef01237a-b61b-4328-8ef1-bc8bd07791ac) |



# 最後に

こちらのPRで作業した内容があるので詳しい詳細やコードはこちらです。

https://github.com/iseruuuuu/food_gram_app/pull/499